Skip to content

fix(spec-graph): hold the write-path invariants - #331

Open
Ivan Logutov (VanishJr) wants to merge 1 commit into
JetBrains:mainfrom
VanishJr:fix/spec-graph-write-path
Open

fix(spec-graph): hold the write-path invariants#331
Ivan Logutov (VanishJr) wants to merge 1 commit into
JetBrains:mainfrom
VanishJr:fix/spec-graph-write-path

Conversation

@VanishJr

@VanishJr Ivan Logutov (VanishJr) commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

fix(spec-graph): hold the write-path invariants

The bug

spec_create passed its path parameter straight to join(root, path), so it wrote outside the project, through symlinks, into ignored directories and under non .md names, reporting Created … for files that no other spec tool can read back.

It also reported the caller's spelling rather than the path the index produces.

Before

01-before-card

After

02-after-card

The fix

core gains resolveSpecPath(root, path), the single answer to whether the index could ever see a path, returning the canonical relative path the tool then reports. It requires root relative, inside the root, .md, outside the ignored directories, an existing root, and no symlink at any component beneath the root, checked per component with lstat because the walk never descends one. spec_create also parses the bytes it is about to write, refuses them unless they read back as a spec (an empty id serialized to a file that was not one), and writes with flag: "wx".

Three more defects in the same module, each with a regression test:

  • spec_update picked the file's line ending from the whole text, so one CRLF anywhere in the body rewrote every line ending in the file. It now rewrites the frontmatter block alone and splices the body back byte for byte, BOM included.
  • SpecIndex consumed readdir order, so which file won a duplicate id depended on the filesystem. The walk now sorts on NFC normalized names.
  • spec_grep matched lines still carrying a trailing \r or a leading BOM, so no anchored pattern matched a CRLF spec, and a non positive limit reported No matches. while matches existed.

Scope

A realpath containment check was tried and withdrawn: it accepts a symlink that points back inside the root, and the walk skips that file anyway. Rejecting every symlinked component is both stricter and simpler.

Further defects in this module are left out and want their own issues: an indented --- inside a multi line YAML scalar relocates the closing fence, list edits drop comments on the field they edit, a model supplied regex can stall the in process host, spec_update can overwrite an external edit landing inside the index's revalidation window, and spec_validate reports none of the schema rules the skill documents.

Verifying

Before: spec_create with path: "../evil.md" answers Created ../evil.md and the file exists outside the project. With path: "notes/spec.txt" it answers Created and spec_get then answers No spec with id.

After: both are refused with the reason, and nothing is written.

Checklist

  • Fast gates pass: bun run lint, bun run typecheck, bun run test
  • E2E suite passes for app-affecting changes (bun run e2e, or bun run e2e:full when touching agent behavior)
  • Relevant SPEC.md / top-level specs updated to reflect any boundary, contract, or behavior change
  • I have read the Contributing guide and agree to the Code of Conduct

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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