Skip to content

Conversation

@CS-Tao
Copy link

@CS-Tao CS-Tao commented Jan 14, 2026

Summary

Adds support for project-level custom schemas stored in <project>/openspec/schemas/. This allows teams to define workflow schemas that are specific to their project without needing to install them globally.

Changes

Modified files:

  • src/core/artifact-graph/resolver.ts - Add getProjectSchemasDir() function; update schema resolution order to prioritize project-local schemas (project → user → package); extend listSchemas() and listSchemasWithInfo() to include project-local schemas; add 'project' as a new source type in SchemaInfo
  • test/core/artifact-graph/resolver.test.ts - Add comprehensive tests for project-local schema resolution, precedence ordering, and listSchemasWithInfo() functionality
  • docs/experimental-workflow.md - Document the new project-level schemas location

Resolution Order

Schema resolution now follows this precedence (highest to lowest):

  1. Project-local: <cwd>/openspec/schemas/<name>/schema.yaml
  2. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
  3. Package built-in: <package>/schemas/<name>/schema.yaml

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for project-local schemas with highest precedence and enhanced source attribution (project/user/package) in schema listings.
  • Bug Fixes / Behavior

    • Schema resolution now deduplicates across sources and skips invalid project schemas silently.
  • Tests

    • Added tests verifying project-local precedence, deduplication, source labeling, metadata, and error-handling.
  • Documentation

    • Updated docs to note project-level schema location and behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@CS-Tao CS-Tao requested a review from TabishB as a code owner January 14, 2026 10:12
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

Project-local schemas at /openspec/schemas are added to the resolver and now take precedence over user and package schemas. The resolver exposes getProjectSchemasDir(), tracks schema origin ('project'|'user'|'package'), and deduplicates schemas across sources when listing them.

Changes

Cohort / File(s) Summary
Schema resolution & listing
src/core/artifact-graph/resolver.ts
Added getProjectSchemasDir(); updated getSchemaDir() to prefer project-local path (cwd/openspec/schemas/<name>/schema.yaml) before ${XDG_DATA_HOME}/openspec/schemas and package-builtins; extended listSchemas() and listSchemasWithInfo() to include project sources, deduplicate by precedence, label SchemaInfo.source as `'project'
Unit tests
test/core/artifact-graph/resolver.test.ts
Added tests for getProjectSchemasDir(), schema resolution precedence (project > user > package), listSchemas() aggregation/deduplication, and listSchemasWithInfo() source labeling, metadata parsing, ordering, and invalid-schema handling.
Docs
docs/experimental-workflow.md
Documentation updated to mention project-level custom schemas at <project>/openspec/schemas/ alongside user-level path.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Resolver
  participant Project as ProjectDir (cwd/openspec/schemas)
  participant User as UserDir (XDG_DATA_HOME/openspec/schemas)
  participant Package as PackageSchemas

  Client->>Project: check for <name>/schema.yaml
  alt found
    Project-->>Client: return project schema (source: project)
  else not found
    Client->>User: check for <name>/schema.yaml
    alt found
      User-->>Client: return user schema (source: user)
    else not found
      Client->>Package: check for <name>/schema.yaml
      Package-->>Client: return package schema (source: package) or not found
    end
  end
Loading
sequenceDiagram
  participant Lister as listSchemasWithInfo
  participant Project as ProjectDir (cwd/openspec/schemas)
  participant User as UserDir (XDG_DATA_HOME/openspec/schemas)
  participant Package as PackageSchemas

  Lister->>Project: enumerate & parse schemas -> add as 'project'
  Lister->>User: enumerate & parse -> add as 'user' if name not seen
  Lister->>Package: enumerate & parse -> add as 'package' if name not seen
  Lister-->>Client: return deduplicated, ordered list with sources
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰
In the burrow where code seeds sprout,
Project schemas now hop about,
They lead the trail, then user, then pack,
Duplicates skip—no turning back,
I nibble docs and test the stack.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support project-level schemas' accurately and directly describes the main change—adding support for project-local schemas with proper precedence ordering.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b278963 and 2e6dd49.

📒 Files selected for processing (3)
  • docs/experimental-workflow.md
  • src/core/artifact-graph/resolver.ts
  • test/core/artifact-graph/resolver.test.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T22:51:14.330Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2026-01-13T22:51:14.330Z
Learning: Applies to openspec/changes/*/specs/**/spec.md : Use ADDED for new orthogonal capabilities; use MODIFIED for behavior/scope/criteria changes; use RENAMED for name-only changes

Applied to files:

  • docs/experimental-workflow.md
📚 Learning: 2026-01-13T22:51:14.330Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2026-01-13T22:51:14.330Z
Learning: Scaffold `proposal.md`, `tasks.md`, optional `design.md`, and delta specs under `openspec/changes/<id>/` when creating a change proposal

Applied to files:

  • docs/experimental-workflow.md
📚 Learning: 2026-01-13T22:51:14.330Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2026-01-13T22:51:14.330Z
Learning: Applies to openspec/changes/*/{proposal,design,tasks}.md : Use direct file references in format `file.ts:42` and reference specs as `specs/auth/spec.md` for clarity

Applied to files:

  • src/core/artifact-graph/resolver.ts
🧬 Code graph analysis (2)
test/core/artifact-graph/resolver.test.ts (2)
src/core/artifact-graph/resolver.ts (4)
  • getProjectSchemasDir (35-37)
  • getSchemaDir (57-80)
  • listSchemas (143-186)
  • listSchemasWithInfo (202-278)
src/core/artifact-graph/index.ts (3)
  • getSchemaDir (25-25)
  • listSchemas (23-23)
  • listSchemasWithInfo (24-24)
src/core/artifact-graph/resolver.ts (1)
src/core/artifact-graph/schema.ts (1)
  • parseSchema (23-45)
🔇 Additional comments (10)
docs/experimental-workflow.md (1)

476-476: LGTM!

The documentation correctly reflects the new project-level schema path and aligns with the implementation in getProjectSchemasDir(). The parenthetical clarification helps users understand both options.

src/core/artifact-graph/resolver.ts (4)

32-37: LGTM!

The implementation correctly uses process.cwd() to resolve the project-local schemas directory, making it context-aware to the current working directory.


46-79: LGTM!

The precedence implementation (project → user → package) is correct, with early returns for efficiency. The JSDoc accurately documents the resolution order.


139-186: LGTM!

The Set-based deduplication correctly collects all unique schema names regardless of insertion order. The function properly scans all three schema sources.


188-277: LGTM!

The implementation correctly:

  • Extends SchemaInfo.source to include 'project'
  • Processes schemas in correct precedence order (project → user → package)
  • Uses seenNames for deduplication, ensuring only the highest-precedence schema is included
  • Handles invalid schemas gracefully with try-catch
test/core/artifact-graph/resolver.test.ts (5)

1-14: LGTM!

Imports are correctly updated to include vi from vitest for mocking, and the new public APIs (listSchemasWithInfo, getProjectSchemasDir) are imported for testing.


47-62: LGTM!

Good test coverage for getProjectSchemasDir():

  • Verifies correct path construction relative to process.cwd()
  • Tests dynamic behavior when cwd changes via mock
  • Properly restores mocks to prevent test pollution

89-138: LGTM!

Excellent test coverage for the new precedence behavior:

  • Verifies project-local takes precedence over user override
  • Verifies fallback to user override when project-local schema doesn't exist
  • Proper mock management with try/finally blocks

395-431: LGTM!

Tests properly verify:

  • Project-local schemas are included in the listing
  • Deduplication works correctly when same schema name exists across project, user, and package sources

434-576: LGTM!

Comprehensive test suite for listSchemasWithInfo():

  • Validates source field values ('package', 'user', 'project')
  • Confirms correct precedence (project > user > package)
  • Verifies metadata extraction (description, artifacts)
  • Tests sorted output
  • Ensures invalid schemas are silently skipped without throwing

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/artifact-graph/resolver.ts (1)

82-92: JSDoc is inconsistent with implementation.

The documentation still describes only 2 resolution levels (user → package), but since getSchemaDir() now includes project-local schemas, the actual resolution order is project → user → package.

📝 Proposed fix
 /**
  * Resolves a schema name to a SchemaYaml object.
  *
  * Resolution order:
- * 1. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
- * 2. Package built-in: <package>/schemas/<name>/schema.yaml
+ * 1. Project-local: <cwd>/openspec/schemas/<name>/schema.yaml
+ * 2. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
+ * 3. Package built-in: <package>/schemas/<name>/schema.yaml
  *
  * `@param` name - Schema name (e.g., "spec-driven")
  * `@returns` The resolved schema object
  * `@throws` Error if schema is not found in any location
  */
🧹 Nitpick comments (2)
src/core/artifact-graph/resolver.ts (1)

222-224: Consider adding debug logging for skipped schemas.

Silent failures here could make troubleshooting difficult when a schema unexpectedly doesn't appear in listings. A debug-level log would aid diagnostics without affecting user experience.

💡 Example
           } catch {
-            // Skip invalid schemas
+            // Skip invalid schemas - log for debugging
+            // console.debug(`Skipping invalid project schema: ${entry.name}`);
           }
test/core/artifact-graph/resolver.test.ts (1)

140-194: Consider adding a test for resolveSchema with project-local schemas.

The existing resolveSchema tests cover user override and package fallback, but there's no explicit test for project-local schema resolution. While getSchemaDir precedence is tested separately, an integration test for resolveSchema with project-local would improve confidence.

💡 Example test
it('should prefer project-local over user override', () => {
  process.env.XDG_DATA_HOME = tempDir;
  const userSchemaDir = path.join(tempDir, 'openspec', 'schemas', 'spec-driven');
  fs.mkdirSync(userSchemaDir, { recursive: true });
  fs.writeFileSync(path.join(userSchemaDir, 'schema.yaml'), `
name: user-override
version: 50
artifacts:
  - id: user-artifact
    generates: user.md
    description: User artifact
    template: user.md
`);

  const projectSchemaDir = path.join(tempDir, 'project', 'openspec', 'schemas', 'spec-driven');
  fs.mkdirSync(projectSchemaDir, { recursive: true });
  fs.writeFileSync(path.join(projectSchemaDir, 'schema.yaml'), `
name: project-local
version: 100
artifacts:
  - id: project-artifact
    generates: project.md
    description: Project artifact
    template: project.md
`);

  const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(path.join(tempDir, 'project'));
  try {
    const schema = resolveSchema('spec-driven');
    expect(schema.name).toBe('project-local');
    expect(schema.version).toBe(100);
  } finally {
    cwdSpy.mockRestore();
  }
});
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf4bc24 and b278963.

📒 Files selected for processing (2)
  • src/core/artifact-graph/resolver.ts
  • test/core/artifact-graph/resolver.test.ts
🧰 Additional context used
🧬 Code graph analysis (2)
test/core/artifact-graph/resolver.test.ts (1)
src/core/artifact-graph/resolver.ts (3)
  • getProjectSchemasDir (35-37)
  • listSchemas (143-186)
  • listSchemasWithInfo (202-278)
src/core/artifact-graph/resolver.ts (2)
src/core/artifact-graph/index.ts (3)
  • getSchemaDir (25-25)
  • getUserSchemasDir (27-27)
  • parseSchema (12-12)
src/core/artifact-graph/schema.ts (1)
  • parseSchema (23-45)
🔇 Additional comments (10)
src/core/artifact-graph/resolver.ts (5)

32-37: LGTM!

The getProjectSchemasDir() function correctly uses process.cwd() to resolve the project-local schemas directory path relative to the current working directory.


57-80: LGTM!

The resolution order correctly implements project-local → user → package precedence, matching the updated JSDoc.


143-186: LGTM!

The Set correctly deduplicates schema names across all three sources. The processing order doesn't affect the output since this function only collects unique names without tracking source.


191-196: LGTM!

The SchemaInfo interface correctly includes 'project' in the source union type.


206-228: LGTM!

The project-local schemas are correctly processed first with highest precedence. The seenNames tracking ensures proper deduplication.

test/core/artifact-graph/resolver.test.ts (5)

1-14: LGTM!

Imports correctly include the new listSchemasWithInfo and getProjectSchemasDir exports.


47-62: LGTM!

Good test coverage for getProjectSchemasDir() including mock-based verification that the function respects process.cwd(). The try/finally pattern ensures proper mock cleanup.


89-139: LGTM!

Excellent coverage for project-local precedence in getSchemaDir:

  • Verifies project-local wins over user override
  • Verifies fallback to user when project-local doesn't have the schema

The test setup is clean and mock cleanup is properly handled.


395-431: LGTM!

Good test coverage for listSchemas() with project-local schemas:

  • Verifies project-local schemas are included in listings
  • Verifies deduplication works correctly across all sources

434-576: LGTM!

Comprehensive test suite for listSchemasWithInfo() covering:

  • Source field verification for all three sources (package, user, project)
  • Precedence handling (project > user > package)
  • Sorting behavior
  • Error tolerance for invalid schemas

The tests are well-structured with proper mock cleanup.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/artifact-graph/resolver.ts (1)

82-92: Stale JSDoc: resolution order is outdated.

The JSDoc for resolveSchema() still documents the old resolution order (user → package) but should reflect the new order (project → user → package) since it calls getSchemaDir() internally.

📝 Suggested fix
 /**
  * Resolves a schema name to a SchemaYaml object.
  *
  * Resolution order:
- * 1. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
- * 2. Package built-in: <package>/schemas/<name>/schema.yaml
+ * 1. Project-local: <cwd>/openspec/schemas/<name>/schema.yaml
+ * 2. User override: ${XDG_DATA_HOME}/openspec/schemas/<name>/schema.yaml
+ * 3. Package built-in: <package>/schemas/<name>/schema.yaml
  *
  * `@param` name - Schema name (e.g., "spec-driven")
  * `@returns` The resolved schema object
  * `@throws` Error if schema is not found in any location
  */
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b278963 and 2e6dd49.

📒 Files selected for processing (3)
  • docs/experimental-workflow.md
  • src/core/artifact-graph/resolver.ts
  • test/core/artifact-graph/resolver.test.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T22:51:14.330Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2026-01-13T22:51:14.330Z
Learning: Applies to openspec/changes/*/specs/**/spec.md : Use ADDED for new orthogonal capabilities; use MODIFIED for behavior/scope/criteria changes; use RENAMED for name-only changes

Applied to files:

  • docs/experimental-workflow.md
📚 Learning: 2026-01-13T22:51:14.330Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2026-01-13T22:51:14.330Z
Learning: Scaffold `proposal.md`, `tasks.md`, optional `design.md`, and delta specs under `openspec/changes/<id>/` when creating a change proposal

Applied to files:

  • docs/experimental-workflow.md
📚 Learning: 2026-01-13T22:51:14.330Z
Learnt from: CR
Repo: Fission-AI/OpenSpec PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2026-01-13T22:51:14.330Z
Learning: Applies to openspec/changes/*/{proposal,design,tasks}.md : Use direct file references in format `file.ts:42` and reference specs as `specs/auth/spec.md` for clarity

Applied to files:

  • src/core/artifact-graph/resolver.ts
🧬 Code graph analysis (2)
test/core/artifact-graph/resolver.test.ts (2)
src/core/artifact-graph/resolver.ts (4)
  • getProjectSchemasDir (35-37)
  • getSchemaDir (57-80)
  • listSchemas (143-186)
  • listSchemasWithInfo (202-278)
src/core/artifact-graph/index.ts (3)
  • getSchemaDir (25-25)
  • listSchemas (23-23)
  • listSchemasWithInfo (24-24)
src/core/artifact-graph/resolver.ts (1)
src/core/artifact-graph/schema.ts (1)
  • parseSchema (23-45)
🔇 Additional comments (10)
docs/experimental-workflow.md (1)

476-476: LGTM!

The documentation correctly reflects the new project-level schema path and aligns with the implementation in getProjectSchemasDir(). The parenthetical clarification helps users understand both options.

src/core/artifact-graph/resolver.ts (4)

32-37: LGTM!

The implementation correctly uses process.cwd() to resolve the project-local schemas directory, making it context-aware to the current working directory.


46-79: LGTM!

The precedence implementation (project → user → package) is correct, with early returns for efficiency. The JSDoc accurately documents the resolution order.


139-186: LGTM!

The Set-based deduplication correctly collects all unique schema names regardless of insertion order. The function properly scans all three schema sources.


188-277: LGTM!

The implementation correctly:

  • Extends SchemaInfo.source to include 'project'
  • Processes schemas in correct precedence order (project → user → package)
  • Uses seenNames for deduplication, ensuring only the highest-precedence schema is included
  • Handles invalid schemas gracefully with try-catch
test/core/artifact-graph/resolver.test.ts (5)

1-14: LGTM!

Imports are correctly updated to include vi from vitest for mocking, and the new public APIs (listSchemasWithInfo, getProjectSchemasDir) are imported for testing.


47-62: LGTM!

Good test coverage for getProjectSchemasDir():

  • Verifies correct path construction relative to process.cwd()
  • Tests dynamic behavior when cwd changes via mock
  • Properly restores mocks to prevent test pollution

89-138: LGTM!

Excellent test coverage for the new precedence behavior:

  • Verifies project-local takes precedence over user override
  • Verifies fallback to user override when project-local schema doesn't exist
  • Proper mock management with try/finally blocks

395-431: LGTM!

Tests properly verify:

  • Project-local schemas are included in the listing
  • Deduplication works correctly when same schema name exists across project, user, and package sources

434-576: LGTM!

Comprehensive test suite for listSchemasWithInfo():

  • Validates source field values ('package', 'user', 'project')
  • Confirms correct precedence (project > user > package)
  • Verifies metadata extraction (description, artifacts)
  • Tests sorted output
  • Ensures invalid schemas are silently skipped without throwing

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@TabishB
Copy link
Contributor

TabishB commented Jan 14, 2026

@CS-Tao Haha I'm working on the same thing here: https://github.com/Fission-AI/OpenSpec/pull/499/changes#diff-3f104fb5dfabd94e8105edd5ebab1d621c5ce037dd2e77a6ab7b44bed6c4cb3a

I'll take a look at your implementation of it to see if it matches the spec I had in mind

@vibe-kanban-cloud
Copy link

Review Complete

Your review story is ready!

View Story

Comment !reviewfast on this PR to re-generate the story.

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.

2 participants