Skip to content

Fix apply instructions for glob artifact outputs#967

Merged
TabishB merged 3 commits intomainfrom
TabishB/validate-issue-942
Apr 12, 2026
Merged

Fix apply instructions for glob artifact outputs#967
TabishB merged 3 commits intomainfrom
TabishB/validate-issue-942

Conversation

@TabishB
Copy link
Copy Markdown
Contributor

@TabishB TabishB commented Apr 12, 2026

Summary

Fix openspec instructions apply so glob-generated artifacts resolve consistently with openspec status.

Closes #942.

Problem

instructions apply used its own hand-written artifact matcher while status used the shared artifact-graph completion logic backed by fast-glob.

That caused two user-visible issues:

  • status could report a glob artifact as complete while instructions apply still blocked on it
  • contextFiles could contain raw glob patterns like specs/**/*.md instead of real file paths

The mismatch was especially visible for single-star nested patterns like specs/*/spec.md.

Changes

  • Added a shared artifact output resolver in src/core/artifact-graph/outputs.ts
  • Updated artifact completion detection to reuse the shared resolver
  • Updated instructions apply to resolve concrete artifact outputs instead of using a custom matcher
  • Changed contextFiles to return Record<string, string[]> so glob artifacts can expose all concrete matched files
  • Updated apply/verify workflow templates and the CLI artifact workflow spec to match the concrete-path contract
  • Added regression tests for:
    • single-star nested glob resolution (specs/*/spec.md)
    • broader glob support (?, [], basename-sensitive patterns)
    • concrete contextFiles output for built-in spec-driven schemas

Verification

  • pnpm run build
  • node "$(node -p "require.resolve('vitest/vitest.mjs')")" run

Summary by CodeRabbit

  • New Features

    • Apply instructions now map each artifact ID to an array of resolved file paths (multiple paths per artifact).
    • Glob patterns in artifact outputs are supported and resolve to matching files.
  • Bug Fixes

    • Improved detection of artifact outputs so existing files and globs are recognized reliably; JSON/text instruction outputs use the new shape.
  • Documentation

    • Workflow instructions updated to reflect the artifact→array contextFiles shape.
  • Tests

    • Added and updated tests verifying glob resolution and new apply-instructions JSON shape.

@1code-async
Copy link
Copy Markdown
Contributor

1code-async bot commented Apr 12, 2026

Task completed.

I'll review this pull request by first examining the changes in detail.


View full conversation

Powered by 1Code

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Refactors artifact output resolution by centralizing glob detection/resolution (resolveArtifactOutputs, artifactOutputExists, isGlobPattern) and changes apply instructions’ contextFiles from single-path-per-artifact to artifact-ID → array-of-paths.

Changes

Cohort / File(s) Summary
Specification Schema
openspec/specs/cli-artifact-workflow/spec.md
Updated contextFiles output contract to map artifact IDs to arrays of concrete file paths.
CLI Instructions
src/commands/workflow/instructions.ts, src/commands/workflow/shared.ts
contextFiles type changed to Record<string, string[]>; generateApplyInstructions now uses resolveArtifactOutputs and populates arrays; printing adjusted to list multiple paths.
Artifact Graph Utilities
src/core/artifact-graph/outputs.ts, src/core/artifact-graph/index.ts
Added isGlobPattern, resolveArtifactOutputs, and artifactOutputExists; re-exported from index.
Artifact State
src/core/artifact-graph/state.ts
Replaced inline glob/path detection with calls to artifactOutputExists.
Workflow Templates
src/core/templates/workflows/apply-change.ts, src/core/templates/workflows/verify-change.ts
Instructional text updated to treat contextFiles as artifact-ID→path-array and to iterate/read all listed paths.
Tests
test/commands/artifact-workflow.test.ts, test/core/artifact-graph/outputs.test.ts, test/core/templates/skill-templates-parity.test.ts
Added tests for glob resolution and multi-path outputs; updated apply-instructions assertions to expect arrays of resolved absolute paths; refreshed parity hashes for modified templates.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI (instructions apply)
  participant Instr as Instructions Generator
  participant AG as Artifact Graph (resolveArtifactOutputs)
  participant FS as Filesystem

  CLI->>Instr: request apply instructions for change
  Instr->>AG: resolveArtifactOutputs(changeDir, artifact.generates)
  AG->>FS: glob/stat checks
  FS-->>AG: matched paths[]
  AG-->>Instr: returns paths[] per artifact
  Instr-->>CLI: respond with contextFiles {artifactId: [paths...] }
  CLI->>CLI: print / JSON-emit each artifact path
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • alfred-openspec

Poem

🐰 I hopped through globs and files today,

Paths no longer lost astray,
Artifact IDs now hold arrays,
Every spec in tidy arrays,
Hooray, code carrots all the way!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing the apply instructions to properly handle glob artifact outputs.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch TabishB/validate-issue-942

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.

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: 1

🧹 Nitpick comments (2)
src/commands/workflow/instructions.ts (1)

266-280: Resolve artifact outputs once and reuse them.

resolveArtifactOutputs is now doing the same filesystem walk twice per artifact: once for missingArtifacts and again for contextFiles. Caching the resolved outputs in one pass is cheaper and keeps both structures derived from the same snapshot.

♻️ Suggested refactor
-  const missingArtifacts: string[] = [];
-  for (const artifactId of requiredArtifactIds) {
-    const artifact = schema.artifacts.find((a) => a.id === artifactId);
-    if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
-      missingArtifacts.push(artifactId);
-    }
-  }
-
-  // Build context files from all existing artifacts in schema
-  const contextFiles: Record<string, string[]> = {};
-  for (const artifact of schema.artifacts) {
-    const outputs = resolveArtifactOutputs(changeDir, artifact.generates);
-    if (outputs.length > 0) {
-      contextFiles[artifact.id] = outputs;
-    }
-  }
+  const outputsByArtifact = new Map(
+    schema.artifacts.map((artifact) => [
+      artifact.id,
+      resolveArtifactOutputs(changeDir, artifact.generates),
+    ])
+  );
+
+  const missingArtifacts = requiredArtifactIds.filter((artifactId) => {
+    const outputs = outputsByArtifact.get(artifactId);
+    return outputs !== undefined && outputs.length === 0;
+  });
+
+  // Build context files from all existing artifacts in schema
+  const contextFiles: Record<string, string[]> = {};
+  for (const [artifactId, outputs] of outputsByArtifact) {
+    if (outputs.length > 0) {
+      contextFiles[artifactId] = outputs;
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/workflow/instructions.ts` around lines 266 - 280, The code calls
resolveArtifactOutputs twice per artifact; change the loop so you compute
outputs once and reuse them: iterate schema.artifacts, for each artifact call
resolveArtifactOutputs(changeDir, artifact.generates) once, store the result in
a local map (e.g. outputsByArtifactId), then use that cached outputs to populate
missingArtifacts (push artifact.id if requiredArtifactIds contains it and
outputs.length === 0) and to build contextFiles (set contextFiles[artifact.id] =
outputs if outputs.length > 0); reference symbols: resolveArtifactOutputs,
schema.artifacts, artifact.generates, missingArtifacts, contextFiles.
test/commands/artifact-workflow.test.ts (1)

450-480: Exercise multiple glob matches end-to-end here.

This test still produces only one specs/*/spec.md match, so a regression that accidentally truncates contextFiles.specs to the first resolved file would still pass. Adding a second matching file would better cover the new array contract.

🧪 Suggested test hardening
-      const specPath = path.join(changeDir, 'specs', 'single-star-glob', 'spec.md');
-      await fs.mkdir(path.dirname(specPath), { recursive: true });
+      const specPathA = path.join(changeDir, 'specs', 'single-star-glob', 'spec.md');
+      const specPathB = path.join(changeDir, 'specs', 'another-match', 'spec.md');
+      await fs.mkdir(path.dirname(specPathA), { recursive: true });
+      await fs.mkdir(path.dirname(specPathB), { recursive: true });
       await fs.writeFile(path.join(changeDir, '.openspec.yaml'), 'schema: glob-test\n');
-      await fs.writeFile(specPath, '# Nested spec\n');
+      await fs.writeFile(specPathA, '# Nested spec A\n');
+      await fs.writeFile(specPathB, '# Nested spec B\n');
...
-      const resolvedSpecPath = await fs.realpath(specPath);
+      const resolvedSpecPaths = await Promise.all([
+        fs.realpath(specPathA),
+        fs.realpath(specPathB),
+      ]);
       expect(applyJson.state).toBe('ready');
       expect(applyJson.missingArtifacts).toBeUndefined();
       expect(applyJson.contextFiles).toEqual({
-        specs: [resolvedSpecPath],
+        specs: expect.arrayContaining(resolvedSpecPaths),
       });
+      expect(applyJson.contextFiles.specs).toHaveLength(2);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/commands/artifact-workflow.test.ts` around lines 450 - 480, The test
only creates one matching specs/*/spec.md so it can't catch regressions that
trim contextFiles.specs to a single entry; update the test around
changeDir/specs to create a second matching spec (e.g., a sibling directory with
its own spec.md), write both files and compute both realpaths, then assert that
statusJson.artifacts still contains the glob and that
applyJson.contextFiles.specs is an array containing both resolved spec paths
(order-insensitive or sorted) instead of a single path; use the existing
variables (changeDir, specPath, runCLI, statusResult, applyResult) and compare
against the two resolved paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/artifact-graph/outputs.ts`:
- Around line 20-22: The non-glob branch currently treats any existing path as a
valid output; change it to enforce file-only semantics like the glob branch. In
the block that checks isGlobPattern(generates) and returns
fs.existsSync(fullPattern) ? [fullPattern] : [], replace the existence check
with a file-only check (e.g., verify fs.existsSync(fullPattern) &&
fs.lstatSync(fullPattern).isFile()) so directories are not treated as completed
outputs; keep the same return shape ([fullPattern] or []). Ensure you reference
the variables/methods isGlobPattern, generates, and fullPattern when making the
change.

---

Nitpick comments:
In `@src/commands/workflow/instructions.ts`:
- Around line 266-280: The code calls resolveArtifactOutputs twice per artifact;
change the loop so you compute outputs once and reuse them: iterate
schema.artifacts, for each artifact call resolveArtifactOutputs(changeDir,
artifact.generates) once, store the result in a local map (e.g.
outputsByArtifactId), then use that cached outputs to populate missingArtifacts
(push artifact.id if requiredArtifactIds contains it and outputs.length === 0)
and to build contextFiles (set contextFiles[artifact.id] = outputs if
outputs.length > 0); reference symbols: resolveArtifactOutputs,
schema.artifacts, artifact.generates, missingArtifacts, contextFiles.

In `@test/commands/artifact-workflow.test.ts`:
- Around line 450-480: The test only creates one matching specs/*/spec.md so it
can't catch regressions that trim contextFiles.specs to a single entry; update
the test around changeDir/specs to create a second matching spec (e.g., a
sibling directory with its own spec.md), write both files and compute both
realpaths, then assert that statusJson.artifacts still contains the glob and
that applyJson.contextFiles.specs is an array containing both resolved spec
paths (order-insensitive or sorted) instead of a single path; use the existing
variables (changeDir, specPath, runCLI, statusResult, applyResult) and compare
against the two resolved paths.
🪄 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: 18306353-214a-456d-a847-94c7424f7c06

📥 Commits

Reviewing files that changed from the base of the PR and between cd5e493 and 8882814.

📒 Files selected for processing (11)
  • openspec/specs/cli-artifact-workflow/spec.md
  • src/commands/workflow/instructions.ts
  • src/commands/workflow/shared.ts
  • src/core/artifact-graph/index.ts
  • src/core/artifact-graph/outputs.ts
  • src/core/artifact-graph/state.ts
  • src/core/templates/workflows/apply-change.ts
  • src/core/templates/workflows/verify-change.ts
  • test/commands/artifact-workflow.test.ts
  • test/core/artifact-graph/outputs.test.ts
  • test/core/templates/skill-templates-parity.test.ts

Comment thread src/core/artifact-graph/outputs.ts
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.

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

9-11: Replace isGlobPattern with fg.isDynamicPattern.

The manual check using simple includes() can miss edge cases like escaped characters (\*), brace expansions, and extglobs that fast-glob itself recognizes. Using fg.isDynamicPattern() directly ensures pattern detection aligns with actual glob behavior on line 29.

export function isGlobPattern(pattern: string): boolean {
  return fg.isDynamicPattern(pattern);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/artifact-graph/outputs.ts` around lines 9 - 11, Replace the manual
pattern check in isGlobPattern with fast-glob's canonical detector: change
isGlobPattern to call fg.isDynamicPattern(pattern) so it correctly handles
escaped characters, brace expansions and extglobs; also ensure the module
importing fast-glob (fg) is present or add "import fg from 'fast-glob'" at the
top if missing so isGlobPattern uses fg.isDynamicPattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/core/artifact-graph/outputs.ts`:
- Around line 9-11: Replace the manual pattern check in isGlobPattern with
fast-glob's canonical detector: change isGlobPattern to call
fg.isDynamicPattern(pattern) so it correctly handles escaped characters, brace
expansions and extglobs; also ensure the module importing fast-glob (fg) is
present or add "import fg from 'fast-glob'" at the top if missing so
isGlobPattern uses fg.isDynamicPattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ac9787ca-e36d-4a95-a9eb-8a6cee81bf8c

📥 Commits

Reviewing files that changed from the base of the PR and between 8882814 and f45b933.

📒 Files selected for processing (2)
  • src/core/artifact-graph/outputs.ts
  • test/core/artifact-graph/outputs.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/core/artifact-graph/outputs.test.ts

Copy link
Copy Markdown
Collaborator

@alfred-openspec alfred-openspec left a comment

Choose a reason for hiding this comment

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

Looks good. Centralizing artifact output resolution fixes the status/apply mismatch cleanly, and the regression coverage for nested globs plus concrete contextFiles makes me comfortable merging this.

@TabishB TabishB added this pull request to the merge queue Apr 12, 2026
Merged via the queue into main with commit 7fe45ca Apr 12, 2026
9 checks passed
@TabishB TabishB deleted the TabishB/validate-issue-942 branch April 12, 2026 14:36
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.

instructions apply fails to detect glob-pattern artifacts (e.g. specs/*/spec.md)

2 participants