Skip to content

feat(schema): add Git-backed remote schema sources#1444

Open
Patodo wants to merge 3 commits into
Fission-AI:mainfrom
Patodo:feat/remote-schema-sources
Open

feat(schema): add Git-backed remote schema sources#1444
Patodo wants to merge 3 commits into
Fission-AI:mainfrom
Patodo:feat/remote-schema-sources

Conversation

@Patodo

@Patodo Patodo commented Jul 26, 2026

Copy link
Copy Markdown

Summary

Add explicit Git-backed remote schema sources so teams can share one schema across repositories without copying bundles or allowing ordinary OpenSpec commands to access the network.

Projects keep the existing scalar schema selection and may declare a source separately:

schema: qeda-sdd

schemaSources:
  qeda-sdd:
    git: https://github.com/example/QEDASDD.git
    ref: v1.0.0
    path: schemas/qeda-sdd

openspec schema sync [name] resolves the requested branch, tag, or commit to an immutable commit SHA, validates the selected schema bundle, installs it in a content-addressed local cache, and atomically updates openspec/schemas.lock.yaml. --locked restores or verifies the exact lock state without upgrading it.

Relates to #1131.

Behavior and impact

  • preserves schema: <name> and existing project, user, and package schemas
  • resolves schemas in project-local → locked remote → user → package order
  • keeps normal schema resolution offline and network-free
  • fails closed when a declared remote lock or cache is missing, stale, or corrupt
  • reports remote source, immutable revision, integrity, and shadowing through schema inspection commands
  • supports human and single-document JSON synchronization output
  • registers schema sync in shell completion metadata

Security and reproducibility

  • uses system Git so HTTPS credential helpers and SSH agents/configuration continue to work
  • rejects credential-bearing URLs and unsupported Git remote-helper transports
  • accepts only repository-relative portable bundle paths
  • rejects traversal, absolute/drive/UNC paths, symlinks, submodules, non-UTF-8 paths, case-folded path collisions, and Windows-unsafe names
  • limits bundles to 1,000 regular files and 10 MiB
  • verifies a deterministic SHA-256 digest before every remote-cache resolution
  • validates downloads before atomic cache activation and preserves the previous lock/cache on failure

Documentation and release tracking

  • adds an OpenSpec proposal, design, capability specs, and completed task plan
  • documents public/private repository setup, CI restoration, offline behavior, upgrades, precedence, and security boundaries
  • adds a minor changeset

The configuration and lockfile representation remain experimental and can be adjusted based on maintainer feedback without changing the behavioral model.

Validation

  • pnpm lint
  • pnpm exec tsc --noEmit
  • pnpm run build
  • pnpm test — 120 test files, 2,328 tests passed
  • pnpm exec openspec validate add-remote-schema-sources --strict
  • git diff --check

Integration coverage uses temporary local Git repositories and does not depend on public network access.

Summary by CodeRabbit

  • New Features
    • Added Git-backed “remote schemas” with a dedicated sync command, including deterministic locked mode and --json output.
    • Introduced integrity-verified, cache-backed offline schema resolution and clearer remote schema visibility/precedence.
  • Documentation
    • Expanded CLI and customization docs for remote schemas, locking/sync behavior, and fail-closed troubleshooting.
  • Tests
    • Added end-to-end and unit coverage for sync/lock behavior, offline resolution, validation safety, and credential leak prevention.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Git-backed remote schema sources with explicit synchronization, immutable lockfiles, integrity-verified caching, offline fail-closed resolution, remote-aware discovery, and schema sync CLI support.

Changes

Remote schema sources

Layer / File(s) Summary
Configuration, lock, and bundle contracts
src/core/remote-schema/*, src/core/project-config.ts, src/core/artifact-graph/schema-directory.ts, src/core/artifact-graph/schema.ts, test/core/remote-schema/*, test/core/project-config.test.ts
Adds validated schemaSources, strict lockfile parsing and atomic writes, portable bundle validation, deterministic SHA-256 integrity, and diagnostic schema inspection.
Git synchronization and cache installation
src/core/remote-schema/git.ts, src/core/remote-schema/sync.ts, src/core/remote-schema/cache.ts, test/core/remote-schema/*
Fetches Git refs, validates and extracts bundles, supports update and locked restoration modes, and preserves prior lock/cache state on failures.
Remote schema resolution and discovery
src/core/artifact-graph/resolver.ts, src/core/artifact-graph/index.ts, test/core/artifact-graph/*
Adds remote precedence between project-local and user schemas, verifies locked caches without network access, and reports unavailable or shadowed remote schemas.
Schema CLI and command reporting
src/commands/schema.ts, src/commands/workflow/schemas.ts, src/core/completions/command-registry.ts, test/commands/schema-sync.test.ts
Adds schema sync [name] with --locked and --json, and extends schema which and schema listings with remote metadata and structured errors.
Specification, documentation, and release tracking
openspec/changes/add-remote-schema-sources/*, docs/cli.md, docs/customization.md, .changeset/*
Documents remote source configuration, synchronization, precedence, offline behavior, validation rules, and the minor release change.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Sync
  participant Git
  participant Cache
  participant Lockfile
  CLI->>Sync: Run schema sync
  Sync->>Git: Fetch and validate bundle
  Git-->>Sync: Commit and integrity
  Sync->>Cache: Install verified bundle
  Sync->>Lockfile: Write resolved metadata
  Sync-->>CLI: Return sync result
Loading

Possibly related PRs

Suggested reviewers: tabishb

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% 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.
Title check ✅ Passed The title clearly matches the main change: adding Git-backed remote schema sources.
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.
✨ 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.

@Patodo
Patodo marked this pull request as ready for review July 26, 2026 05:15
@Patodo
Patodo requested a review from TabishB as a code owner July 26, 2026 05:15

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
src/commands/schema.ts (1)

82-153: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid index-based shadow computation in getSchemaResolution.

getRemoteSchemaDir can throw before returning a path, so checkAllLocations can report a non-existent remote location while getSchemaDir never reaches it. The fallback active has no matching entry, activeIndex is -1, and JS slices from 0 up, reporting every existing location after project as shadows. Also, the fallback drops requestedRef, resolvedCommit, bundlePath, and integrity, which the remote schema JSON spec requires. Use precedence-rank filtering for shadows and carry remote lock metadata through, or merge shadow computation with getSchemaDir so the active path cannot diverge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/schema.ts` around lines 82 - 153, Update getSchemaResolution to
avoid relying on activeIndex when the active path is absent from
checkAllLocations; compute shadows by filtering existing locations to only
lower-precedence entries than the resolved active source/path. Preserve the
remote lock metadata (requestedRef, resolvedCommit, bundlePath, and integrity)
in the fallback active result by carrying through the remote entry’s metadata,
while keeping getSchemaDir resolution behavior unchanged.
🧹 Nitpick comments (11)
src/core/remote-schema/lockfile.ts (1)

16-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

.strict() is deprecated in Zod 4.

Object schemas in Zod 4 still strip unknown keys by default, but .passthrough(), .strict(), and .strip() are all deprecated in favor of z.strictObject()/z.looseObject(). Functionally equivalent today, but worth migrating to avoid future removal.

♻️ Optional migration to `z.strictObject()`
-const LockEntrySchema = z
-  .object({
+const LockEntrySchema = z
+  .strictObject({
     git: z...
-  })
-  .strict();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/remote-schema/lockfile.ts` around lines 16 - 47, Replace the
deprecated .strict() calls in LockEntrySchema and LockSchema with
z.strictObject() while preserving all existing fields, validators, and strict
unknown-key behavior.
src/core/remote-schema/git.ts (1)

179-205: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use bulk blob extraction instead of one git cat-file blob per file.

The 1,000-file bundle limit makes this loop spawn up to 1,000 sequential Git subprocesses, each with its own process-spawn cost and timeout budget. Switch to git cat-file --batch/--batch-command to read all blobs through a single long-lived subprocess instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/remote-schema/git.ts` around lines 179 - 205, Replace the per-entry
runGit cat-file calls in the blob extraction loop with a single git cat-file
--batch or --batch-command subprocess, feeding all entry.objectId values and
parsing each returned header/content record. Preserve totalBytes enforcement,
blob-to-entry association, timeout handling, and subsequent destination writes
while ensuring only one Git subprocess is spawned.
src/core/artifact-graph/schema-directory.ts (1)

59-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate candidate expression.

Both branches recompute path.join(templatesDir, ...normalizedTemplate.split('/')). Hoisting the split and the templates candidate reads better.

♻️ Proposed fix
-    const candidates = options.requireTemplatesDirectory
-      ? [path.join(templatesDir, ...normalizedTemplate.split('/'))]
-      : [
-          path.join(templatesDir, ...normalizedTemplate.split('/')),
-          path.join(schemaDir, ...normalizedTemplate.split('/')),
-        ];
+    const segments = normalizedTemplate.split('/');
+    const candidates = [path.join(templatesDir, ...segments)];
+    if (!options.requireTemplatesDirectory) {
+      candidates.push(path.join(schemaDir, ...segments));
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/artifact-graph/schema-directory.ts` around lines 59 - 64, Refactor
the candidate construction to compute the normalized template path segments and
the templates-directory candidate once before the requireTemplatesDirectory
conditional. Reuse that candidate in both branches, while retaining the
schema-directory candidate only when templates are not required.
test/core/remote-schema/cache.test.ts (1)

51-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ino is not a reliable identity signal on Windows.

fs.Stats.ino is 0/unstable for directories on Windows, so this assertion degrades to 0 === 0 there and stops detecting a replaced cache dir. Writing a marker file into installed before the second install and asserting it survives is portable.

💚 Proposed fix
     const installed = installRemoteSchemaCache(source, integrity, dataDir);
-    const before = fs.statSync(installed).ino;
+    const marker = path.join(installed, '.marker');
+    fs.writeFileSync(marker, 'kept');

     expect(installRemoteSchemaCache(source, integrity, dataDir)).toBe(installed);
-    expect(fs.statSync(installed).ino).toBe(before);
+    expect(fs.existsSync(marker)).toBe(true);

Note this also changes the directory contents, so it must be written after the integrity check — which is the case here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/remote-schema/cache.test.ts` around lines 51 - 61, Replace the
platform-dependent fs.statSync(installed).ino identity check in the “does not
replace an existing valid cache entry” test with a portable persistence check:
write a marker file inside installed after the initial install and assert it
still exists after the second install. Keep the existing integrity computation
and install calls unchanged.
src/core/remote-schema/bundle.ts (2)

128-150: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Length prefix can desynchronize from hashed content.

contentLength is written from the statSync size captured during traversal, while the hashed bytes come from a later readFileSync. If the file changes in between, the digest embeds a length that doesn't match the content it prefixes. Hashing content.length from the buffer you actually read keeps the framing self-consistent.

🐛 Proposed fix
-    const contentLength = Buffer.allocUnsafe(8);
-    contentLength.writeBigUInt64BE(BigInt(file.size));
     hash.update(pathLength);
     hash.update(pathBytes);
+    const content = fs.readFileSync(file.absolutePath);
+    const contentLength = Buffer.allocUnsafe(8);
+    contentLength.writeBigUInt64BE(BigInt(content.length));
     hash.update(contentLength);
-    hash.update(fs.readFileSync(file.absolutePath));
+    hash.update(content);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/remote-schema/bundle.ts` around lines 128 - 150, Update the hashing
loop in the bundle integrity calculation to read each file into a buffer before
constructing its length prefix, and write contentLength from that buffer’s
actual byte length instead of file.size. Continue hashing the path, length
prefix, and same content buffer in the existing order.

116-126: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Enforce file-count/byte limits during traversal, not after.

visit() fully enumerates the tree before the limits are checked, so a hostile or accidentally huge bundle is walked (and its statSync calls performed) in its entirety before rejection. Contents are still read only after the check, so this is bounded-memory, but the walk itself is unbounded. Checking incrementally inside visit() fails fast.

♻️ Sketch
       const size = fs.statSync(absolutePath).size;
       files.push({ relativePath, absolutePath, size });
+      if (files.length > limits.maxFiles) {
+        throw new Error(`Remote schema bundle contains more than ${limits.maxFiles} files`);
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/remote-schema/bundle.ts` around lines 116 - 126, Move the maxFiles
and maxBytes enforcement into the recursive visit() traversal so each discovered
entry is counted and its size accumulated immediately, throwing as soon as
either limit is exceeded. Preserve assertPortableBundleEntries and the existing
post-traversal flow, but remove reliance on checking limits only after files has
been fully enumerated.
src/core/remote-schema/sync.ts (1)

33-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

status: [] is a dead field typed as an empty tuple.

The type literal [] permits only the empty array, and the implementation always returns [], so consumers (including the JSON sync output) can never receive anything. Either drop it or give it a real element type now — widening a published field later is a breaking change for the JSON contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/remote-schema/sync.ts` around lines 33 - 39, Update the
SyncRemoteSchemasResult interface by removing the dead status field or replacing
its empty-tuple type with the intended concrete status element type, and align
all returned values and JSON sync output with that decision. Preserve the
existing result fields and ensure the published contract can represent actual
status values if the field is retained.
test/core/remote-schema/bundle.test.ts (1)

100-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test leaks a file outside the temp bundle dir and passes silently when symlinks are unavailable.

outside is created in os.tmpdir() (the parent of bundleDir) and never cleaned up, since afterEach only removes bundleDir. Also, the bare return in the catch makes the test green on platforms where symlinkSync fails; ctx.skip() makes that visible.

♻️ Proposed fix
-  it('rejects symlinks without reading their targets', () => {
-    const outside = path.join(path.dirname(bundleDir), 'outside-schema-secret.txt');
-    fs.writeFileSync(outside, 'do-not-read');
-    const link = path.join(bundleDir, 'templates', 'linked.md');
-    try {
-      fs.symlinkSync(outside, link, 'file');
-    } catch {
-      return;
-    }
-
-    expect(() => computeBundleIntegrity(bundleDir)).toThrow(/symbolic link/);
-  });
+  it('rejects symlinks without reading their targets', (ctx) => {
+    const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-schema-outside-'));
+    const outside = path.join(outsideDir, 'outside-schema-secret.txt');
+    try {
+      fs.writeFileSync(outside, 'do-not-read');
+      const link = path.join(bundleDir, 'templates', 'linked.md');
+      try {
+        fs.symlinkSync(outside, link, 'file');
+      } catch {
+        ctx.skip();
+        return;
+      }
+
+      expect(() => computeBundleIntegrity(bundleDir)).toThrow(/symbolic link/);
+    } finally {
+      fs.rmSync(outsideDir, { recursive: true, force: true });
+    }
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/remote-schema/bundle.test.ts` around lines 100 - 111, Update the
symlink test around computeBundleIntegrity to create the outside target within
the test’s managed cleanup scope, or explicitly remove it during cleanup, so no
file remains in the temporary parent directory. Replace the bare return in the
symlinkSync catch with the test framework’s visible skip mechanism, such as
ctx.skip(), so unsupported platforms report the test as skipped rather than
passing silently.
test/core/artifact-graph/schema-directory.test.ts (1)

43-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add nested/backslash template coverage and symlink rejection cases.

Two gaps:

  1. Every case uses a flat proposal.md, so the normalizedTemplate.split('/')path.join(...) conversion in schema-directory.ts Lines 59-64 — the part that is actually separator-sensitive — is never exercised. A nested/proposal.md positive case (and a templates\proposal.md rejection case) would cover it.
  2. The symlink guards at schema-directory.ts Lines 26 and 68 have no coverage.
💚 Suggested additions
+  it('resolves a nested template path across platform separators', () => {
+    fs.mkdirSync(path.join(schemaDir, 'templates', 'nested'), { recursive: true });
+    fs.writeFileSync(
+      path.join(schemaDir, 'schema.yaml'),
+      schemaYaml('qeda-sdd', 'nested/proposal.md')
+    );
+    fs.writeFileSync(
+      path.join(schemaDir, 'templates', 'nested', 'proposal.md'),
+      '# Proposal\n'
+    );
+
+    const result = validateSchemaDirectory(schemaDir, {
+      expectedName: 'qeda-sdd',
+      requireTemplatesDirectory: true,
+    });
+    expect(result.templatePaths.proposal).toBe(
+      path.join(schemaDir, 'templates', 'nested', 'proposal.md')
+    );
+  });

And add to the it.each table:

+    ['backslash template separator', true, true, String.raw`nested\proposal.md`, /unsafe template path/],

As per path instructions: "When touching path behavior, add coverage that would fail on Windows path separators".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/artifact-graph/schema-directory.test.ts` around lines 43 - 62, Add
nested template coverage and symlink rejection cases to the schema-directory
tests. Extend the existing valid-case coverage with a nested template such as
nested/proposal.md, add a rejection case using a backslash-separated templates
path such as templates\proposal.md, and create symlink cases targeting both the
schema file and template file so validateSchemaDirectory rejects each through
its symlink guards.

Source: Path instructions

test/core/project-config.test.ts (1)

486-519: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pollution assertion is vacuous.

polluted is never a key in the fixture, so line 509 passes regardless. Assert against a property the fixture could actually have injected via __proto__.

💚 Proposed fix
-        expect(({} as Record<string, unknown>).polluted).toBeUndefined();
+        const probe = {} as Record<string, unknown>;
+        expect(probe.git).toBeUndefined();
+        expect(probe.ref).toBeUndefined();
+        expect(probe.path).toBeUndefined();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/project-config.test.ts` around lines 486 - 519, Update the
prototype-pollution assertion in the test “rejects prototype keys explicitly
without mutating object prototypes” to check a property that the __proto__
fixture could actually inject, such as the configured git/ref/path data, rather
than the absent polluted key. Keep the existing config and warning assertions
unchanged.
test/commands/schema-sync.test.ts (1)

38-46: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Verify XDG_DATA_HOME reliably isolates the global schema cache across platforms.

Based on learnings, this repo has an established, adapter-agnostic pattern for redirecting global writes during tests: "OPENSPEC_GLOBAL_ROOT should unconditionally redirect global writes to the specified directory for testing/CI, regardless of adapter support." This test instead sets XDG_DATA_HOME. If getUserSchemasDir()/the global schema-cache directory doesn't honor XDG_DATA_HOME consistently on every CI platform (Windows/macOS), these tests could write into the real global cache instead of tempDir, risking flaky or polluted CI runs — especially relevant given tasks.md calls for Windows CI coverage of this feature.

Can you confirm the schema-cache/global-data-dir resolution used here honors XDG_DATA_HOME on all supported platforms, or should this test also set OPENSPEC_GLOBAL_ROOT for reliable isolation?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/commands/schema-sync.test.ts` around lines 38 - 46, Update the test
setup in beforeEach to set OPENSPEC_GLOBAL_ROOT to the temporary directory using
the repository’s established global-write isolation mechanism. Ensure
schema-cache/global-data-dir resolution uses this override across platforms,
while preserving the existing temporary working directory and environment setup.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/cli.md`:
- Around line 903-919: Update the “Example JSON success” output in docs/cli.md
to include the fields emitted by the schema sync action: add git and
requestedRef to each schemas entry and add the top-level lockfile field,
matching the structure shown in design.md Decision `#8`.

In `@src/commands/schema.ts`:
- Around line 181-204: Update validateSchema and the validateSchemaDirectory
flow to preserve and return every schema validation failure as a separate
ValidationIssue instead of converting only the first thrown error into a
schema.yaml issue. Ensure failures from duplicate artifact IDs, invalid
dependencies, cycles, and broken templates are all surfaced, retaining accurate
paths or context when available.

In `@src/core/remote-schema/cache.ts`:
- Around line 49-114: Add a Windows-compatible test covering
installRemoteSchemaCache replacing an existing stale cache directory: create a
cache entry for a different integrity, invoke installRemoteSchemaCache with the
intended source and integrity, then verify the new cache is valid and no
displaced directory remains. Place the test alongside the existing remote-schema
cache tests and ensure resources or file handles are closed before asserting
replacement behavior.

In `@src/core/remote-schema/git.ts`:
- Around line 38-72: Update the execFile invocation in runGit to force
non-interactive Git authentication by providing an environment that includes
GIT_TERMINAL_PROMPT=0, while preserving the existing process environment and
other execution options.
- Around line 147-166: Update the fetch flow around fetchTarget and
resolvedCommit so options.lockedCommit is not passed directly to git fetch as a
raw SHA. Resolve the locked commit through an advertised ref or another
reachable-history/clone path, then verify the resulting commit matches the
requested lockedCommit; preserve the existing requestedRef fetch behavior and
commit-SHA validation for normal refs.

---

Outside diff comments:
In `@src/commands/schema.ts`:
- Around line 82-153: Update getSchemaResolution to avoid relying on activeIndex
when the active path is absent from checkAllLocations; compute shadows by
filtering existing locations to only lower-precedence entries than the resolved
active source/path. Preserve the remote lock metadata (requestedRef,
resolvedCommit, bundlePath, and integrity) in the fallback active result by
carrying through the remote entry’s metadata, while keeping getSchemaDir
resolution behavior unchanged.

---

Nitpick comments:
In `@src/core/artifact-graph/schema-directory.ts`:
- Around line 59-64: Refactor the candidate construction to compute the
normalized template path segments and the templates-directory candidate once
before the requireTemplatesDirectory conditional. Reuse that candidate in both
branches, while retaining the schema-directory candidate only when templates are
not required.

In `@src/core/remote-schema/bundle.ts`:
- Around line 128-150: Update the hashing loop in the bundle integrity
calculation to read each file into a buffer before constructing its length
prefix, and write contentLength from that buffer’s actual byte length instead of
file.size. Continue hashing the path, length prefix, and same content buffer in
the existing order.
- Around line 116-126: Move the maxFiles and maxBytes enforcement into the
recursive visit() traversal so each discovered entry is counted and its size
accumulated immediately, throwing as soon as either limit is exceeded. Preserve
assertPortableBundleEntries and the existing post-traversal flow, but remove
reliance on checking limits only after files has been fully enumerated.

In `@src/core/remote-schema/git.ts`:
- Around line 179-205: Replace the per-entry runGit cat-file calls in the blob
extraction loop with a single git cat-file --batch or --batch-command
subprocess, feeding all entry.objectId values and parsing each returned
header/content record. Preserve totalBytes enforcement, blob-to-entry
association, timeout handling, and subsequent destination writes while ensuring
only one Git subprocess is spawned.

In `@src/core/remote-schema/lockfile.ts`:
- Around line 16-47: Replace the deprecated .strict() calls in LockEntrySchema
and LockSchema with z.strictObject() while preserving all existing fields,
validators, and strict unknown-key behavior.

In `@src/core/remote-schema/sync.ts`:
- Around line 33-39: Update the SyncRemoteSchemasResult interface by removing
the dead status field or replacing its empty-tuple type with the intended
concrete status element type, and align all returned values and JSON sync output
with that decision. Preserve the existing result fields and ensure the published
contract can represent actual status values if the field is retained.

In `@test/commands/schema-sync.test.ts`:
- Around line 38-46: Update the test setup in beforeEach to set
OPENSPEC_GLOBAL_ROOT to the temporary directory using the repository’s
established global-write isolation mechanism. Ensure
schema-cache/global-data-dir resolution uses this override across platforms,
while preserving the existing temporary working directory and environment setup.

In `@test/core/artifact-graph/schema-directory.test.ts`:
- Around line 43-62: Add nested template coverage and symlink rejection cases to
the schema-directory tests. Extend the existing valid-case coverage with a
nested template such as nested/proposal.md, add a rejection case using a
backslash-separated templates path such as templates\proposal.md, and create
symlink cases targeting both the schema file and template file so
validateSchemaDirectory rejects each through its symlink guards.

In `@test/core/project-config.test.ts`:
- Around line 486-519: Update the prototype-pollution assertion in the test
“rejects prototype keys explicitly without mutating object prototypes” to check
a property that the __proto__ fixture could actually inject, such as the
configured git/ref/path data, rather than the absent polluted key. Keep the
existing config and warning assertions unchanged.

In `@test/core/remote-schema/bundle.test.ts`:
- Around line 100-111: Update the symlink test around computeBundleIntegrity to
create the outside target within the test’s managed cleanup scope, or explicitly
remove it during cleanup, so no file remains in the temporary parent directory.
Replace the bare return in the symlinkSync catch with the test framework’s
visible skip mechanism, such as ctx.skip(), so unsupported platforms report the
test as skipped rather than passing silently.

In `@test/core/remote-schema/cache.test.ts`:
- Around line 51-61: Replace the platform-dependent fs.statSync(installed).ino
identity check in the “does not replace an existing valid cache entry” test with
a portable persistence check: write a marker file inside installed after the
initial install and assert it still exists after the second install. Keep the
existing integrity computation and install calls unchanged.
🪄 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 Plus

Run ID: 3239137f-b2dd-47e2-b797-300e37ab65f4

📥 Commits

Reviewing files that changed from the base of the PR and between 19d4171 and c73df61.

📒 Files selected for processing (34)
  • .changeset/remote-schema-sources.md
  • docs/cli.md
  • docs/customization.md
  • openspec/changes/add-remote-schema-sources/.openspec.yaml
  • openspec/changes/add-remote-schema-sources/design.md
  • openspec/changes/add-remote-schema-sources/proposal.md
  • openspec/changes/add-remote-schema-sources/specs/config-loading/spec.md
  • openspec/changes/add-remote-schema-sources/specs/remote-schema-sources/spec.md
  • openspec/changes/add-remote-schema-sources/specs/schema-resolution/spec.md
  • openspec/changes/add-remote-schema-sources/specs/schema-which-command/spec.md
  • openspec/changes/add-remote-schema-sources/tasks.md
  • src/commands/schema.ts
  • src/commands/workflow/schemas.ts
  • src/core/artifact-graph/index.ts
  • src/core/artifact-graph/resolver.ts
  • src/core/artifact-graph/schema-directory.ts
  • src/core/completions/command-registry.ts
  • src/core/project-config.ts
  • src/core/remote-schema/bundle.ts
  • src/core/remote-schema/cache.ts
  • src/core/remote-schema/config.ts
  • src/core/remote-schema/git.ts
  • src/core/remote-schema/lockfile.ts
  • src/core/remote-schema/sync.ts
  • src/core/remote-schema/types.ts
  • test/commands/schema-sync.test.ts
  • test/core/artifact-graph/remote-resolver.test.ts
  • test/core/artifact-graph/schema-directory.test.ts
  • test/core/project-config.test.ts
  • test/core/remote-schema/bundle.test.ts
  • test/core/remote-schema/cache.test.ts
  • test/core/remote-schema/git.test.ts
  • test/core/remote-schema/lockfile.test.ts
  • test/core/remote-schema/sync.test.ts

Comment thread docs/cli.md
Comment thread src/commands/schema.ts
Comment thread src/core/remote-schema/cache.ts
Comment thread src/core/remote-schema/git.ts
Comment thread src/core/remote-schema/git.ts Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
src/core/remote-schema/git.ts (1)

46-59: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make SSH remote-schema fetches fail non-interactively

GIT_TERMINAL_PROMPT: '0' only affects Git’s own prompts. SSH fetches can still require host-key verification or a key passphrase, which will hang until timeoutMs and report as a generic Git fetch failure. Preserve any existing Git SSH command but add BatchMode=yes, then add the host-key policy you want for automation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/remote-schema/git.ts` around lines 46 - 59, Update the git execFile
invocation to preserve any existing GIT_SSH_COMMAND while adding SSH
BatchMode=yes and an explicit non-interactive host-key policy suitable for
automation. Ensure remote-schema fetches cannot prompt for host-key verification
or key passphrases, while keeping the existing timeout and Git environment
behavior unchanged.
🧹 Nitpick comments (5)
src/core/artifact-graph/schema-directory.ts (1)

33-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Surface all issues, not just the first.

inspectSchemaDirectory now collects multiple diagnostics, but the throwing wrapper discards all but issues[0]. Sync callers (src/core/remote-schema/sync.ts) would benefit from the full list in the error message.

♻️ Proposed change
   if (!inspection.schema || inspection.issues.length > 0) {
-    throw new Error(inspection.issues[0]?.message ?? 'Schema validation failed');
+    throw new Error(
+      inspection.issues.map((issue) => issue.message).join('; ') || 'Schema validation failed'
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/artifact-graph/schema-directory.ts` around lines 33 - 35, Update the
throwing validation wrapper in inspectSchemaDirectory to include every
diagnostic from inspection.issues in the thrown error message, while preserving
the existing fallback message when no issues are available and the current
invalid-schema condition.
src/core/remote-schema/bundle.ts (1)

113-121: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Check size before reading the whole file into memory.

fs.readFileSync completes before the maxBytes check, so a single oversized file in a locally-supplied bundle dir is fully buffered before rejection. The Git path pre-checks sizes, but direct callers (e.g. cache verification) don't. A statSync pre-check keeps the guard cheap.

♻️ Proposed guard
-      const content = fs.readFileSync(absolutePath);
-      files.push({ relativePath, content });
-      if (files.length > limits.maxFiles) {
+      if (files.length + 1 > limits.maxFiles) {
         throw new Error(`Remote schema bundle contains more than ${limits.maxFiles} files`);
       }
-      totalBytes += content.length;
-      if (totalBytes > limits.maxBytes) {
+      const size = fs.statSync(absolutePath).size;
+      if (totalBytes + size > limits.maxBytes) {
         throw new Error(`Remote schema bundle contains more than ${limits.maxBytes} bytes`);
       }
+      const content = fs.readFileSync(absolutePath);
+      files.push({ relativePath, content });
+      totalBytes += content.length;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/remote-schema/bundle.ts` around lines 113 - 121, Update the bundle
file-processing flow around fs.readFileSync to stat each file and validate its
size against limits.maxBytes before reading it into memory. Account for the
file’s size in totalBytes during this pre-check, while preserving the existing
maxFiles validation, file collection, and oversized-bundle error behavior.
test/core/remote-schema/git.test.ts (1)

122-142: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test may not actually exercise the merge-base --is-ancestor rejection path.

git fetch origin main only transfers objects reachable from main; otherCommit (created on the unfetched other branch) likely never lands in the temp repo. That means the earlier rev-parse ${lockedCommit}^{commit} call probably fails with "unknown revision" before merge-base --is-ancestor ever runs — the assertion on /locked commit verification/i passes either way, so this test doesn't clearly prove the ancestor check itself rejects a fetched-but-non-ancestor commit.

Consider also covering the case where the commit is present locally (e.g., fetch other too, or merge other into main then reset main back) but is not an ancestor of the resolved ref, to directly exercise merge-base --is-ancestor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/remote-schema/git.test.ts` around lines 122 - 142, Update the test
case around fetchSchemaBundleFromGit so otherCommit is present in the temporary
repository while remaining unreachable from requestedRef 'main'; fetch the other
branch or otherwise retain its commit object without advancing main. Keep the
lockedCommit value set to otherCommit and assert the rejection from the locked
commit verification path, ensuring rev-parse succeeds and merge-base
--is-ancestor performs the rejection.
test/core/artifact-graph/schema-directory.test.ts (1)

84-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Symlink-skip test setup duplicates a pattern also used in bundle.test.ts.

The try/create-symlink → catch/cleanup+ctx.skip() scaffolding here is functionally identical to the one in test/core/remote-schema/bundle.test.ts (lines 114-125). Worth extracting into a shared test helper (e.g. trySymlinkOrSkip(ctx, target, link)) to avoid drift between the two copies. See consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/artifact-graph/schema-directory.test.ts` around lines 84 - 120,
Extract the duplicated symlink setup and skip handling from this test and the
corresponding bundle test into a shared helper such as trySymlinkOrSkip(ctx,
target, link). Update both tests to use the helper, preserving cleanup and
ctx.skip() behavior when symlink creation is unavailable.
test/core/remote-schema/bundle.test.ts (1)

100-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid coverage of traversal byte-limit and symlink rejection. Same symlink-skip scaffolding duplication noted in schema-directory.test.ts; see consolidated comment for the DRY suggestion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/remote-schema/bundle.test.ts` around lines 100 - 132, Deduplicate
the repeated symlink setup and cleanup used by “enforces byte limits while
traversing the bundle” and “rejects symlinks without reading their targets.”
Extract the shared scaffolding into an existing or local test helper, then reuse
it here and in schema-directory.test.ts while preserving each test’s distinct
assertions and skip behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/core/remote-schema/git.ts`:
- Around line 46-59: Update the git execFile invocation to preserve any existing
GIT_SSH_COMMAND while adding SSH BatchMode=yes and an explicit non-interactive
host-key policy suitable for automation. Ensure remote-schema fetches cannot
prompt for host-key verification or key passphrases, while keeping the existing
timeout and Git environment behavior unchanged.

---

Nitpick comments:
In `@src/core/artifact-graph/schema-directory.ts`:
- Around line 33-35: Update the throwing validation wrapper in
inspectSchemaDirectory to include every diagnostic from inspection.issues in the
thrown error message, while preserving the existing fallback message when no
issues are available and the current invalid-schema condition.

In `@src/core/remote-schema/bundle.ts`:
- Around line 113-121: Update the bundle file-processing flow around
fs.readFileSync to stat each file and validate its size against limits.maxBytes
before reading it into memory. Account for the file’s size in totalBytes during
this pre-check, while preserving the existing maxFiles validation, file
collection, and oversized-bundle error behavior.

In `@test/core/artifact-graph/schema-directory.test.ts`:
- Around line 84-120: Extract the duplicated symlink setup and skip handling
from this test and the corresponding bundle test into a shared helper such as
trySymlinkOrSkip(ctx, target, link). Update both tests to use the helper,
preserving cleanup and ctx.skip() behavior when symlink creation is unavailable.

In `@test/core/remote-schema/bundle.test.ts`:
- Around line 100-132: Deduplicate the repeated symlink setup and cleanup used
by “enforces byte limits while traversing the bundle” and “rejects symlinks
without reading their targets.” Extract the shared scaffolding into an existing
or local test helper, then reuse it here and in schema-directory.test.ts while
preserving each test’s distinct assertions and skip behavior.

In `@test/core/remote-schema/git.test.ts`:
- Around line 122-142: Update the test case around fetchSchemaBundleFromGit so
otherCommit is present in the temporary repository while remaining unreachable
from requestedRef 'main'; fetch the other branch or otherwise retain its commit
object without advancing main. Keep the lockedCommit value set to otherCommit
and assert the rejection from the locked commit verification path, ensuring
rev-parse succeeds and merge-base --is-ancestor performs the rejection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e060cc2-af5b-40a4-b0e6-14e01071a418

📥 Commits

Reviewing files that changed from the base of the PR and between c73df61 and 2cbbe92.

📒 Files selected for processing (14)
  • docs/cli.md
  • src/commands/schema.ts
  • src/core/artifact-graph/schema-directory.ts
  • src/core/artifact-graph/schema.ts
  • src/core/remote-schema/bundle.ts
  • src/core/remote-schema/git.ts
  • src/core/remote-schema/lockfile.ts
  • src/core/remote-schema/sync.ts
  • test/commands/schema-sync.test.ts
  • test/core/artifact-graph/schema-directory.test.ts
  • test/core/project-config.test.ts
  • test/core/remote-schema/bundle.test.ts
  • test/core/remote-schema/cache.test.ts
  • test/core/remote-schema/git.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/core/project-config.test.ts
  • src/core/remote-schema/sync.ts
  • docs/cli.md
  • src/core/remote-schema/lockfile.ts

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