Files
zernie__vigiles/api-surface
zernie 7693c71b96 fix(compile)!: a failed compile has no stamp to write — in all five compilers (#188)
Two constructions, and one live bug that a red-team of #186/#187 turned up.

**THE BUG: #173 was fixed in one compiler out of five.**

`compileClaudeToFile` stopped writing on error. The identical three lines —
write, THEN check errors — sat unchanged in the skill, subagent, railway and
generator compilers, and all four stamped unconditionally. Worse than the
original: `spec-refs` skips non-`claude` specs, so there was no lint-side
backstop either. Reproduced on the built CLI:

    $ vigiles compile skills/demo/SKILL.md.spec.ts
    ✗ [stale-file] File not found: "docs/never-existed.md"
    skills/demo/SKILL.md — 256 bytes, stamped
    $ vigiles lint .
    ✓ skills/demo/SKILL.md — hash valid     # exit 0

So the comment closing #173 was right about `CLAUDE.md` and wrong about scope.

Guarding four more write sites would leave the class writable — the sixth
compiler gets written the same way. `addHash` now mints a branded
`StampedMarkdown`, `seal()` hands it out only when `errors` is empty, and
`writeArtifact` accepts nothing else. `markdown` stays a plain string because
`adoptDiff` legitimately wants the draft to diff against even when a ref is
stale. An erroring compile has no stamp to give the writer.

**Two constructions from the earlier analysis, kept:**

`RepoRelativePath` — an out-of-repo `.gitignore` entry (#176.8) cannot be
constructed, so the write-site guard is deleted rather than duplicated.

`Record<BlockMechanism, …>` — the block mechanisms are a closed table.
`decideHook` was three `||` terms while `HookOutput` declared a fourth nothing
read (#174); a `||` chain has no shape that can be incomplete. `blockedBy`
reports what fired and `haltsTurn` derives from it, so the next mechanism needs
no new boolean.

Also removed a stale `continue` in `rule-meta.test.ts` — `orphan-docs` was
exempted as "built-in, not in RulesConfig", true when written and false since it
moved into `RulesConfig`, and it excused the rule from the severity comparison.

**A third construction was written, measured, and REMOVED.**

A textual check for "every declared rule's severity is read somewhere" — the
#181 class. Five iterations, each fixing what the last missed, and the third is
why it is gone: with every real read of `orphan-docs` deleted, the suite stayed
GREEN, kept alive by an unrelated `new Set(["orphan-docs"])` in `linters.ts`. It
was green-and-dead for the rule that motivated it. The fourth iteration's own
comment-stripper then ate real code, because `cli.ts` holds glob strings whose
slash-star opens a comment that closes somewhere else.

The reasoning is recorded in the file, along with the structural fix that
belongs in its own PR: the ~30 `check*` functions already share one signature and
are a registry nobody wrote down. `Record<RuleName, RuleRunner>` makes a rule
without a runner a tsc error and collapses `lintExitCode`'s hand-maintained
31-line `||` list — which no textual check can see anyway.

BREAKING CHANGE: `addHash` returns `StampedMarkdown` rather than `string`, and
the four compile results gain a required `artifact`. Reading a result is
unaffected; code that constructs one must supply it.

Four mutations, each landing and failing on its own assertion: write-before-check
restored in the skill compiler; `seal` handing out a stamp on errors;
`repoRelative` no longer refusing escapes; a mechanism dropped from the table.

⚠️ The first of those came back green on its first run — it landed on the first
of four identical blocks rather than the skill compiler. Third time this session
that "prove the patch landed" earned its keep.


Claude-Session: https://claude.ai/code/session_01PrS34j2M8XR12P2QHaBer4

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-02 02:02:24 +05:00
..