Skip to content

feat: add optional folder field on schema artifact definitions#1020

Open
harikrishnan83 wants to merge 3 commits intoFission-AI:mainfrom
intent-driven-dev:add-artifact-target-folder
Open

feat: add optional folder field on schema artifact definitions#1020
harikrishnan83 wants to merge 3 commits intoFission-AI:mainfrom
intent-driven-dev:add-artifact-target-folder

Conversation

@harikrishnan83
Copy link
Copy Markdown
Contributor

@harikrishnan83 harikrishnan83 commented Apr 29, 2026

Summary

This PR is in the context of custom schemas and the artifacts they generate.

Adds an opt-in folder: field on artifact definitions in schema YAML. When set, the artifact's outputs are written to a project-root-relative directory instead of the change directory. This is useful for artifacts that should outlive a single change and don't belong inside the specs folder either (example: Architecture Decision Records).

When folder: is omitted, behavior is unchanged — every existing schema continues to work without modification.

Motivation

Certain artifacts fit the following criteria:

  • Need to outlive a single change lifecycle and influence future changes.
  • Do not belong inside openspec/ — once archived they fall out of view, and they are not part of the spec source-of-truth either.

Example: ADRs record decisions. If we only capture them in design.md, once design is archived the decision is gone. Keeping ADRs in a dedicated top-level folder lets us reference them from future changes — both to align on current design choices and to remember historical decisions we don't want to revisit.

Other examples: UI design choices, service boundaries, or changes where future contributors
need a persistent decision trail

Example schema

spec-driven-with-adr: https://github.com/intent-driven-dev/openspec-schemas/blob/main/openspec/schemas/spec-driven-with-adr/
exact line: https://github.com/intent-driven-dev/openspec-schemas/blob/f14d1d55ac484d3f2998d0f0449766676428cc3c/openspec/schemas/spec-driven-with-adr/schema.yaml#L206

Validation rules

The folder value must not start with the reserved openspec/ prefix, to avoid accidental collisions.

Strictly additive change

The folder field is optional. Existing schemas (including built-in spec-driven) and existing changes are unaffected.

Summary by CodeRabbit

  • New Features

    • Added optional folder property on artifacts to output files to project-root-relative directories instead of change directories
    • External artifacts are excluded from archiving, included in status tracking, and not overwritten on re-runs
    • Schema validation enforces path safety, rejecting absolute paths, project-root escape attempts, and reserved prefixes
  • Documentation

    • Added design specifications, proposal documentation, and implementation tasks for external folder artifact support
  • Tests

    • Added comprehensive test coverage for folder validation, path resolution, instruction generation, and external artifact workflows

harikrishnan83 and others added 3 commits April 29, 2026 11:41
Lets schema authors re-parent artifacts (e.g. ADRs) to repo-root-relative
locations outside openspec/changes/, with no archival and no delta format.

Includes proposal, design, four delta specs (artifact-graph,
cli-artifact-workflow, cli-archive, schema-validate-command), and a tasks
checklist for implementation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reduce surface based on review: collapse path resolution into a single
helper used by the existing resolveArtifactOutputs() chokepoint (no
projectRoot threading), drop the archive preflight warning entirely
(moveDirectory already cannot reach external paths), and rely on Node's
stdlib path module instead of bespoke cross-platform handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds opt-in `folder:` on artifact schemas that re-parents an artifact's
outputs to a project-root-relative directory (e.g., ADRs persisting past
a single change). Implementation funnels through a single new
`resolveArtifactBaseDir` helper inside `resolveArtifactOutputs()`, so
state detection, instruction rendering, and archive behavior inherit
the new resolution transitively. Validation runs in the Zod refinement
layer (rejects empty/absolute/parent-traversal/`openspec/`-prefixed
values) so every consumer sees the same errors.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

This PR adds support for an optional folder property on artifacts, enabling outputs to be written to project-root-relative directories instead of change-local directories. It includes schema validation, path-resolution helpers, updated output handling, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Design & Documentation
docs/customization.md, openspec/changes/add-artifact-target-folder/design.md, openspec/changes/add-artifact-target-folder/proposal.md, openspec/changes/add-artifact-target-folder/tasks.md
Specifications and documentation defining the external folder artifact feature, validation constraints (non-empty, non-absolute, within project root, reserved openspec/ prefix rejection), and implementation plan.
Schema & Artifact Graph Specs
openspec/changes/add-artifact-target-folder/specs/artifact-graph/spec.md, openspec/changes/add-artifact-target-folder/specs/cli-artifact-workflow/spec.md, openspec/changes/add-artifact-target-folder/specs/schema-validate-command/spec.md
Formal specifications for artifact graph support, CLI behavior with external folders, and Zod validation requirements for path safety.
Schema & Type Validation
src/core/artifact-graph/types.ts, src/core/artifact-graph/schema.ts
Adds optional folder field to ArtifactSchema with superRefine validation checks; improves error reporting with artifact ID substitution in validation messages.
Path Resolution
src/core/artifact-graph/paths.ts, src/core/artifact-graph/index.ts
New resolveArtifactBaseDir helper function that computes output base directory from artifact and changeDir, deriving project root from the known directory structure; re-exported via barrel.
Output & Completion Detection
src/core/artifact-graph/outputs.ts, src/core/artifact-graph/state.ts
Updated resolveArtifactOutputs and artifactOutputExists to accept full artifact object instead of generates string; completion detection now honors folder through the revised base-directory resolution.
Instruction Generation
src/core/artifact-graph/instruction-loader.ts, src/commands/workflow/instructions.ts
Updates artifact output path and dependency metadata resolution to use resolveArtifactBaseDir for external folders; removes changeDir prefixing from path rendering in CLI output.
Tests
test/core/artifact-graph/paths.test.ts, test/core/artifact-graph/outputs.test.ts, test/core/artifact-graph/schema.test.ts, test/core/artifact-graph/state.test.ts, test/core/artifact-graph/instruction-loader.test.ts, test/commands/artifact-workflow.test.ts
Comprehensive test coverage for path resolution with nested folders, artifact output detection, validation error messages, completion state for external artifacts, instruction rendering, and CLI non-mutation of pre-existing external files.
Configuration
openspec/changes/add-artifact-target-folder/.openspec.yaml
New change metadata file declaring spec-driven configuration.

Sequence Diagram

sequenceDiagram
    participant SchemaAuthor as Schema Author
    participant ParseLayer as Schema Parser
    participant ValidateLayer as Folder Validator
    participant ArtifactGraph as Artifact Graph
    participant PathResolver as Path Resolver
    participant InstructionGen as Instruction Generator
    participant CliOutput as CLI Output

    SchemaAuthor->>ParseLayer: Define artifact with folder: "docs/adr"
    ParseLayer->>ValidateLayer: Pass folder value for validation
    ValidateLayer->>ValidateLayer: Check non-empty, non-absolute, no traversal, no openspec/ prefix
    ValidateLayer->>ArtifactGraph: Folder field stored in Artifact object
    
    ArtifactGraph->>PathResolver: Request base directory for artifact
    PathResolver->>PathResolver: Walk up changeDir to find projectRoot
    PathResolver->>PathResolver: Resolve folder relative to projectRoot
    PathResolver->>ArtifactGraph: Return external base directory
    
    ArtifactGraph->>ArtifactGraph: Resolve generates glob against base directory
    ArtifactGraph->>ArtifactGraph: Detect completion using external paths
    
    InstructionGen->>PathResolver: Compute output directory
    PathResolver->>InstructionGen: Return absolute external path
    InstructionGen->>CliOutput: Render "Write to: /repo/docs/adr/..."
    CliOutput->>SchemaAuthor: Display resolved external target location
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

Suggested reviewers

  • TabishB

Poem

🐰 A folder-path whisper through the openspec warren...

External folders bloom where changeDir dared not tread,
A project-root horizon, the schema author said,
Paths are validated, preserved through and through—
No archive, no clobber, just truth tried and true!
Thump thump 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title 'feat: add optional folder field on schema artifact definitions' is clear, specific, and directly summarizes the main change: adding an optional folder field to artifact definitions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@harikrishnan83 harikrishnan83 marked this pull request as ready for review April 29, 2026 06:48
@harikrishnan83 harikrishnan83 requested a review from TabishB as a code owner April 29, 2026 06:48
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
test/core/artifact-graph/outputs.test.ts (1)

22-175: Add direct folder cases in outputs unit tests.

The refactor to artifact-shaped inputs is covered, but this file still lacks explicit assertions for artifact.folder behavior at the resolveArtifactOutputs/artifactOutputExists level.

Diff suggestion
+  it('resolves outputs relative to project root when folder is set', () => {
+    const projectRoot = tempDir;
+    const changeDir = path.join(projectRoot, 'openspec', 'changes', 'my-change');
+    const adrDir = path.join(projectRoot, 'ADR');
+    const adrFile = path.join(adrDir, '0001.md');
+    fs.mkdirSync(changeDir, { recursive: true });
+    fs.mkdirSync(adrDir, { recursive: true });
+    fs.writeFileSync(adrFile, 'content');
+
+    expect(resolveArtifactOutputs(artifactWith('*.md', 'ADR'), changeDir)).toEqual([canonical(adrFile)]);
+    expect(artifactOutputExists(artifactWith('*.md', 'ADR'), changeDir)).toBe(true);
+  });
+
+  it('returns false for folder-based artifact when external folder has no matches', () => {
+    const projectRoot = tempDir;
+    const changeDir = path.join(projectRoot, 'openspec', 'changes', 'my-change');
+    fs.mkdirSync(changeDir, { recursive: true });
+    fs.mkdirSync(path.join(projectRoot, 'ADR'), { recursive: true });
+
+    expect(resolveArtifactOutputs(artifactWith('*.md', 'ADR'), changeDir)).toEqual([]);
+    expect(artifactOutputExists(artifactWith('*.md', 'ADR'), changeDir)).toBe(false);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/core/artifact-graph/outputs.test.ts` around lines 22 - 175, The tests
are missing cases that pass artifact.folder to resolveArtifactOutputs and
artifactOutputExists; update the test suite to call artifactWith('proposal.md',
'<folder>') (and similar glob cases) and assert behavior when folder is provided
(e.g., a file exists in that folder resolves to canonical path, directories are
not treated as files, and glob patterns resolve relative to the supplied folder)
so that resolveArtifactOutputs and artifactOutputExists are validated with
artifact.folder populated.
test/core/artifact-graph/instruction-loader.test.ts (1)

211-232: Add one folder-aware dependency path assertion.

This block validates outputPath, but not dependencies[].path, which also changed to resolved absolute paths via resolveArtifactBaseDir.

Diff suggestion
+      it('resolves dependency path with external folder when dependency uses folder', () => {
+        createCustomSchema(
+          'with-folder-deps',
+          `name: with-folder-deps
+version: 1
+artifacts:
+  - id: adr
+    generates: "*.md"
+    folder: ADR
+    description: ADR
+    template: adr.md
+    requires: []
+  - id: index
+    generates: index.md
+    description: Index
+    template: index.md
+    requires: [adr]
+`,
+          { 'adr.md': '# ADR\n', 'index.md': '# Index\n' }
+        );
+
+        const context = loadChangeContext(tempDir, 'my-change', 'with-folder-deps');
+        const instructions = generateInstructions(context, 'index', tempDir);
+        expect(instructions.dependencies[0].path).toBe(path.join(tempDir, 'ADR', '*.md'));
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/core/artifact-graph/instruction-loader.test.ts` around lines 211 - 232,
The test currently asserts only instructions.outputPath but must also verify
that dependency paths were resolved with the artifact folder; add an assertion
that checks instructions.dependencies[0].path equals the resolved absolute path
for the dependency (use path.join(tempDir, 'ADR', <dependency filename or glob>)
to match how resolveArtifactBaseDir is expected to resolve paths). Locate the
test around the generateInstructions(context, 'adr', tempDir) call and add a
folder-aware assertion referencing instructions and instructions.dependencies to
ensure dependency.path was normalized using resolveArtifactBaseDir.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/customization.md`:
- Around line 224-227: The docs only list the reserved prefix rule for the
folder field; update the "Validation (enforced by `openspec schema validate`)"
section to enumerate all runtime folder validation rules: the folder value must
not be empty, must not be an absolute path (must not start with '/'), must not
contain parent-traversal components like '..' or any path segments that escape
the repo, and must still reject the reserved 'openspec/' prefix; mention these
checks are enforced by `openspec schema validate` and mirror the runtime
validation logic for clarity.

In `@openspec/changes/add-artifact-target-folder/design.md`:
- Line 3: The design doc references outdated function signatures for
resolveArtifactOutputs — update the mentions on Line 3 and Line 83 so they match
the current implementation that requires an artifact parameter (e.g.,
resolveArtifactOutputs(changeDir, artifact, generates)); ensure the doc text and
any example calls (including the `Write to:` display mention) reflect the new
required `artifact` argument and the exact parameter order/names used by the
implementation.

In `@openspec/changes/add-artifact-target-folder/specs/artifact-graph/spec.md`:
- Around line 28-29: The spec wording is too strict: change the sentence that
states "resolveArtifactOutputs() is the single production call site that uses
this helper" so it reflects reality that resolveArtifactOutputs() is a primary
production call site but not the only one; explicitly allow
resolveArtifactBaseDir to be used directly by instruction-loader.ts for
output/dependency path resolution and note that artifactOutputExists (in
state.ts), instructions.ts, and instruction-loader.ts all ultimately rely on
resolveArtifactBaseDir/resolveArtifactOutputs for folder: support. Update the
language to "primary" or "one of the production call sites" and list
resolveArtifactOutputs(), resolveArtifactBaseDir(), artifactOutputExists, and
instruction-loader.ts so the spec matches the implementation.

In
`@openspec/changes/add-artifact-target-folder/specs/schema-validate-command/spec.md`:
- Line 10: The spec's absolute-path validation currently relies on
path.isAbsolute(folder) which fails to detect Windows-style absolute paths when
running on POSIX: update the rule to check both platform-specific forms instead
of a single isAbsolute call — e.g., use path.posix.isAbsolute(folder) ||
path.win32.isAbsolute(folder) (or equivalent dual-check) where the validation of
the `folder` value occurs so both POSIX `/...` and Windows `C:\...` forms are
rejected.

In `@src/core/artifact-graph/schema.ts`:
- Around line 35-52: The code currently builds formattedPath from e.path and
emits `: <message>` when e.path is empty; update the formattedPath construction
in the same block (the logic that maps e.path and the variable formattedPath) to
detect an empty e.path and return a root fallback label (e.g., "root" or
"<root>") instead of producing an empty string, preserving existing segment
formatting for non-empty paths and leaving the final return `${formattedPath}:
${e.message}` unchanged; reference symbols: formattedPath, e.path,
parsedArtifacts.

---

Nitpick comments:
In `@test/core/artifact-graph/instruction-loader.test.ts`:
- Around line 211-232: The test currently asserts only instructions.outputPath
but must also verify that dependency paths were resolved with the artifact
folder; add an assertion that checks instructions.dependencies[0].path equals
the resolved absolute path for the dependency (use path.join(tempDir, 'ADR',
<dependency filename or glob>) to match how resolveArtifactBaseDir is expected
to resolve paths). Locate the test around the generateInstructions(context,
'adr', tempDir) call and add a folder-aware assertion referencing instructions
and instructions.dependencies to ensure dependency.path was normalized using
resolveArtifactBaseDir.

In `@test/core/artifact-graph/outputs.test.ts`:
- Around line 22-175: The tests are missing cases that pass artifact.folder to
resolveArtifactOutputs and artifactOutputExists; update the test suite to call
artifactWith('proposal.md', '<folder>') (and similar glob cases) and assert
behavior when folder is provided (e.g., a file exists in that folder resolves to
canonical path, directories are not treated as files, and glob patterns resolve
relative to the supplied folder) so that resolveArtifactOutputs and
artifactOutputExists are validated with artifact.folder populated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 06765221-cd97-406f-a39a-e5b83c8702de

📥 Commits

Reviewing files that changed from the base of the PR and between 3c7a05c and 807fb45.

📒 Files selected for processing (22)
  • docs/customization.md
  • openspec/changes/add-artifact-target-folder/.openspec.yaml
  • openspec/changes/add-artifact-target-folder/design.md
  • openspec/changes/add-artifact-target-folder/proposal.md
  • openspec/changes/add-artifact-target-folder/specs/artifact-graph/spec.md
  • openspec/changes/add-artifact-target-folder/specs/cli-artifact-workflow/spec.md
  • openspec/changes/add-artifact-target-folder/specs/schema-validate-command/spec.md
  • openspec/changes/add-artifact-target-folder/tasks.md
  • src/commands/workflow/instructions.ts
  • src/core/artifact-graph/index.ts
  • src/core/artifact-graph/instruction-loader.ts
  • src/core/artifact-graph/outputs.ts
  • src/core/artifact-graph/paths.ts
  • src/core/artifact-graph/schema.ts
  • src/core/artifact-graph/state.ts
  • src/core/artifact-graph/types.ts
  • test/commands/artifact-workflow.test.ts
  • test/core/artifact-graph/instruction-loader.test.ts
  • test/core/artifact-graph/outputs.test.ts
  • test/core/artifact-graph/paths.test.ts
  • test/core/artifact-graph/schema.test.ts
  • test/core/artifact-graph/state.test.ts

Comment thread docs/customization.md
Comment on lines +224 to +227
**Validation** (enforced by `openspec schema validate`):

- Cannot start with the reserved `openspec/` prefix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validation docs are incomplete for folder.
This section currently documents only the reserved prefix rule. It should also list empty/absolute/parent-traversal rejections to match runtime validation.

Suggested doc update
 **Validation** (enforced by `openspec schema validate`):
 
+- Cannot be empty or whitespace-only.
+- Cannot be an absolute path.
+- Cannot traverse outside project root (no parent traversal escapes).
 - Cannot start with the reserved `openspec/` prefix.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Validation** (enforced by `openspec schema validate`):
- Cannot start with the reserved `openspec/` prefix.
**Validation** (enforced by `openspec schema validate`):
- Cannot be empty or whitespace-only.
- Cannot be an absolute path.
- Cannot traverse outside project root (no parent traversal escapes).
- Cannot start with the reserved `openspec/` prefix.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/customization.md` around lines 224 - 227, The docs only list the
reserved prefix rule for the folder field; update the "Validation (enforced by
`openspec schema validate`)" section to enumerate all runtime folder validation
rules: the folder value must not be empty, must not be an absolute path (must
not start with '/'), must not contain parent-traversal components like '..' or
any path segments that escape the repo, and must still reject the reserved
'openspec/' prefix; mention these checks are enforced by `openspec schema
validate` and mirror the runtime validation logic for clarity.

@@ -0,0 +1,128 @@
## Context

Today every artifact in a schema is implicitly written under `openspec/changes/<change>/`, then moved into `openspec/changes/archive/<date>-<change>/` when the change is archived. All path resolution funnels through one function — `resolveArtifactOutputs(changeDir, generates)` at `src/core/artifact-graph/outputs.ts:19` — which `state.ts` (via `artifactOutputExists`) and `instructions.ts:269/277` both call. The display-only `Write to:` line at `instructions.ts:162/174` does its own `path.join(changeDir, …)` for the same value.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update stale function signatures in the design doc.

The signatures in Line 3 and Line 83 no longer match the implementation (artifact is now required). Please update these references so the design stays executable for future contributors.

Suggested doc fix
-All path resolution funnels through one function — `resolveArtifactOutputs(changeDir, generates)` at `src/core/artifact-graph/outputs.ts:19` ...
+All path resolution funnels through one function — `resolveArtifactOutputs(artifact, changeDir)` at `src/core/artifact-graph/outputs.ts:22` ...

-`artifactOutputExists(changeDir, generates)` already calls `resolveArtifactOutputs()` internally...
+`artifactOutputExists(artifact, changeDir)` already calls `resolveArtifactOutputs()` internally...

Also applies to: 83-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/add-artifact-target-folder/design.md` at line 3, The design
doc references outdated function signatures for resolveArtifactOutputs — update
the mentions on Line 3 and Line 83 so they match the current implementation that
requires an artifact parameter (e.g., resolveArtifactOutputs(changeDir,
artifact, generates)); ensure the doc text and any example calls (including the
`Write to:` display mention) reflect the new required `artifact` argument and
the exact parameter order/names used by the implementation.

Comment on lines +28 to +29
The existing `resolveArtifactOutputs()` function SHALL be the single production call site that uses this helper. Because `state.ts` (via `artifactOutputExists`) and `instructions.ts` already funnel through `resolveArtifactOutputs()`, completion detection and instruction rendering inherit `folder:` support transitively without changes of their own.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Spec call-site statement is stricter than current implementation.

Line 28 says resolveArtifactOutputs() is the only production user of resolveArtifactBaseDir, but src/core/artifact-graph/instruction-loader.ts also calls the helper directly for output/dependency paths. Please relax this wording to match reality.

Suggested spec text update
-The existing `resolveArtifactOutputs()` function SHALL be the single production call site that uses this helper.
+`resolveArtifactOutputs()` SHALL use this helper for output existence/matching, and any direct path rendering code paths SHALL use the same helper to keep base-dir resolution consistent.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The existing `resolveArtifactOutputs()` function SHALL be the single production call site that uses this helper. Because `state.ts` (via `artifactOutputExists`) and `instructions.ts` already funnel through `resolveArtifactOutputs()`, completion detection and instruction rendering inherit `folder:` support transitively without changes of their own.
`resolveArtifactOutputs()` SHALL use this helper for output existence/matching, and any direct path rendering code paths SHALL use the same helper to keep base-dir resolution consistent. Because `state.ts` (via `artifactOutputExists`) and `instructions.ts` already funnel through `resolveArtifactOutputs()`, completion detection and instruction rendering inherit `folder:` support transitively without changes of their own.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/add-artifact-target-folder/specs/artifact-graph/spec.md`
around lines 28 - 29, The spec wording is too strict: change the sentence that
states "resolveArtifactOutputs() is the single production call site that uses
this helper" so it reflects reality that resolveArtifactOutputs() is a primary
production call site but not the only one; explicitly allow
resolveArtifactBaseDir to be used directly by instruction-loader.ts for
output/dependency path resolution and note that artifactOutputExists (in
state.ts), instructions.ts, and instruction-loader.ts all ultimately rely on
resolveArtifactBaseDir/resolveArtifactOutputs for folder: support. Update the
language to "primary" or "one of the production call sites" and list
resolveArtifactOutputs(), resolveArtifactBaseDir(), artifactOutputExists, and
instruction-loader.ts so the spec matches the implementation.

The rules SHALL be expressed using only Node's stdlib `path` module — no bespoke cross-platform parsing. The codebase's existing `FileSystemUtils` helpers cover any downstream cross-platform path math.

1. The value MUST NOT be empty (after `trim()`).
2. The value MUST NOT be an absolute path. A single `!path.isAbsolute(folder)` check natively covers POSIX `/...` and Windows `C:\...` forms — no separate Windows clause is required.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Node.js path API docs, does path.isAbsolute("C:\\Windows\\Temp") evaluate as absolute on POSIX hosts, or should path.win32.isAbsolute be used for cross-platform schema validation?

💡 Result:

No, path.isAbsolute("C:\Windows\Temp") does not evaluate as absolute on POSIX hosts. On POSIX systems (like Linux/macOS), path.isAbsolute uses POSIX logic and only returns true for paths starting with '/'. A Windows path like "C:\Windows\Temp" starts with 'C', so it returns false. For cross-platform schema validation of Windows-style absolute paths, use path.win32.isAbsolute, which correctly identifies drive-letter paths like "C:\Windows\Temp" as absolute regardless of the host OS. path.isAbsolute is platform-dependent and delegates to the host's logic (path.posix on POSIX, path.win32 on Windows). The platform-specific variants (path.win32, path.posix) always use fixed logic and are designed for cross-platform path string manipulation.

Citations:


Update the absolute-path validation rule to properly handle cross-platform forms.

Line 10 incorrectly states that a single path.isAbsolute(folder) check covers both POSIX and Windows absolute paths. This is not reliable: on POSIX hosts (Linux, macOS), path.isAbsolute("C:\\Windows\\Temp") returns false because the path does not start with /. For proper cross-platform schema validation, explicitly use both platform-specific checks, as shown in the suggested update below.

Suggested spec text update
-2. The value MUST NOT be an absolute path. A single `!path.isAbsolute(folder)` check natively covers POSIX `/...` and Windows `C:\...` forms — no separate Windows clause is required.
+2. The value MUST NOT be an absolute path. Validation MUST reject both POSIX and Windows absolute forms regardless of host OS (for example, using `path.posix.isAbsolute(...) || path.win32.isAbsolute(...)`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
2. The value MUST NOT be an absolute path. A single `!path.isAbsolute(folder)` check natively covers POSIX `/...` and Windows `C:\...` forms — no separate Windows clause is required.
2. The value MUST NOT be an absolute path. Validation MUST reject both POSIX and Windows absolute forms regardless of host OS (for example, using `path.posix.isAbsolute(...) || path.win32.isAbsolute(...)`).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/add-artifact-target-folder/specs/schema-validate-command/spec.md`
at line 10, The spec's absolute-path validation currently relies on
path.isAbsolute(folder) which fails to detect Windows-style absolute paths when
running on POSIX: update the rule to check both platform-specific forms instead
of a single isAbsolute call — e.g., use path.posix.isAbsolute(folder) ||
path.win32.isAbsolute(folder) (or equivalent dual-check) where the validation of
the `folder` value occurs so both POSIX `/...` and Windows `C:\...` forms are
rejected.

Comment on lines +35 to +52
const formattedPath = e.path
.map((segment, idx) => {
if (
idx === 1 &&
e.path[0] === 'artifacts' &&
typeof segment === 'number' &&
parsedArtifacts[segment] &&
typeof parsedArtifacts[segment].id === 'string' &&
(parsedArtifacts[segment].id as string).length > 0
) {
return `[${parsedArtifacts[segment].id}]`;
}
return typeof segment === 'number' ? `[${segment}]` : `.${String(segment)}`;
})
.join('')
.replace(/^\./, '');
return `${formattedPath}: ${e.message}`;
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle root-level validation errors explicitly.
If e.path is empty, Line [51] currently emits : <message>. Add a root fallback label for clearer errors.

Suggested fix
-        return `${formattedPath}: ${e.message}`;
+        const pathLabel = formattedPath.length > 0 ? formattedPath : '<root>';
+        return `${pathLabel}: ${e.message}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const formattedPath = e.path
.map((segment, idx) => {
if (
idx === 1 &&
e.path[0] === 'artifacts' &&
typeof segment === 'number' &&
parsedArtifacts[segment] &&
typeof parsedArtifacts[segment].id === 'string' &&
(parsedArtifacts[segment].id as string).length > 0
) {
return `[${parsedArtifacts[segment].id}]`;
}
return typeof segment === 'number' ? `[${segment}]` : `.${String(segment)}`;
})
.join('')
.replace(/^\./, '');
return `${formattedPath}: ${e.message}`;
})
const formattedPath = e.path
.map((segment, idx) => {
if (
idx === 1 &&
e.path[0] === 'artifacts' &&
typeof segment === 'number' &&
parsedArtifacts[segment] &&
typeof parsedArtifacts[segment].id === 'string' &&
(parsedArtifacts[segment].id as string).length > 0
) {
return `[${parsedArtifacts[segment].id}]`;
}
return typeof segment === 'number' ? `[${segment}]` : `.${String(segment)}`;
})
.join('')
.replace(/^\./, '');
const pathLabel = formattedPath.length > 0 ? formattedPath : '<root>';
return `${pathLabel}: ${e.message}`;
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/artifact-graph/schema.ts` around lines 35 - 52, The code currently
builds formattedPath from e.path and emits `: <message>` when e.path is empty;
update the formattedPath construction in the same block (the logic that maps
e.path and the variable formattedPath) to detect an empty e.path and return a
root fallback label (e.g., "root" or "<root>") instead of producing an empty
string, preserving existing segment formatting for non-empty paths and leaving
the final return `${formattedPath}: ${e.message}` unchanged; reference symbols:
formattedPath, e.path, parsedArtifacts.

@harikrishnan83 harikrishnan83 changed the title Add optional folder field on schema artifact definitions feat: add optional folder field on schema artifact definitions Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant