vigiles 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.claude/settings.json +46 -0
  2. package/.claude/settings.local.json +8 -0
  3. package/.claude-plugin/hooks/post-edit.sh +34 -0
  4. package/.claude-plugin/hooks/pre-edit.sh +40 -0
  5. package/.claude-plugin/hooks/session-start.sh +38 -0
  6. package/.claude-plugin/marketplace.json +14 -0
  7. package/.claude-plugin/plugin.json +47 -0
  8. package/.github/workflows/ci.yml +81 -0
  9. package/.prettierignore +1 -0
  10. package/.vigiles/generated.d.ts +205 -0
  11. package/CLAUDE.md +95 -0
  12. package/CLAUDE.md.spec.ts +142 -0
  13. package/CONTRIBUTING.md +121 -0
  14. package/LICENSE +21 -0
  15. package/README.md +377 -0
  16. package/action.yml +25 -0
  17. package/dist/action.d.ts +7 -0
  18. package/dist/action.d.ts.map +1 -0
  19. package/dist/action.js +180 -0
  20. package/dist/action.js.map +1 -0
  21. package/dist/cli.d.ts +12 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +1267 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/cli.test.d.ts +2 -0
  26. package/dist/cli.test.d.ts.map +1 -0
  27. package/dist/cli.test.js +650 -0
  28. package/dist/cli.test.js.map +1 -0
  29. package/dist/compile.d.ts +101 -0
  30. package/dist/compile.d.ts.map +1 -0
  31. package/dist/compile.js +503 -0
  32. package/dist/compile.js.map +1 -0
  33. package/dist/evolve.d.ts +132 -0
  34. package/dist/evolve.d.ts.map +1 -0
  35. package/dist/evolve.js +599 -0
  36. package/dist/evolve.js.map +1 -0
  37. package/dist/freshness.d.ts +67 -0
  38. package/dist/freshness.d.ts.map +1 -0
  39. package/dist/freshness.js +244 -0
  40. package/dist/freshness.js.map +1 -0
  41. package/dist/freshness.test.d.ts +2 -0
  42. package/dist/freshness.test.d.ts.map +1 -0
  43. package/dist/freshness.test.js +356 -0
  44. package/dist/freshness.test.js.map +1 -0
  45. package/dist/generate-types.d.ts +34 -0
  46. package/dist/generate-types.d.ts.map +1 -0
  47. package/dist/generate-types.js +381 -0
  48. package/dist/generate-types.js.map +1 -0
  49. package/dist/inline.d.ts +58 -0
  50. package/dist/inline.d.ts.map +1 -0
  51. package/dist/inline.js +142 -0
  52. package/dist/inline.js.map +1 -0
  53. package/dist/inline.test.d.ts +5 -0
  54. package/dist/inline.test.d.ts.map +1 -0
  55. package/dist/inline.test.js +152 -0
  56. package/dist/inline.test.js.map +1 -0
  57. package/dist/linters.d.ts +38 -0
  58. package/dist/linters.d.ts.map +1 -0
  59. package/dist/linters.js +588 -0
  60. package/dist/linters.js.map +1 -0
  61. package/dist/proofs.d.ts +272 -0
  62. package/dist/proofs.d.ts.map +1 -0
  63. package/dist/proofs.js +622 -0
  64. package/dist/proofs.js.map +1 -0
  65. package/dist/proofs.test.d.ts +9 -0
  66. package/dist/proofs.test.d.ts.map +1 -0
  67. package/dist/proofs.test.js +952 -0
  68. package/dist/proofs.test.js.map +1 -0
  69. package/dist/spec.d.ts +258 -0
  70. package/dist/spec.d.ts.map +1 -0
  71. package/dist/spec.js +113 -0
  72. package/dist/spec.js.map +1 -0
  73. package/dist/spec.test.d.ts +2 -0
  74. package/dist/spec.test.d.ts.map +1 -0
  75. package/dist/spec.test.js +1222 -0
  76. package/dist/spec.test.js.map +1 -0
  77. package/dist/types.d.ts +101 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +3 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/validate.d.ts +10 -0
  82. package/dist/validate.d.ts.map +1 -0
  83. package/dist/validate.js +286 -0
  84. package/dist/validate.js.map +1 -0
  85. package/dist/validate.test.d.ts +2 -0
  86. package/dist/validate.test.d.ts.map +1 -0
  87. package/dist/validate.test.js +531 -0
  88. package/dist/validate.test.js.map +1 -0
  89. package/docs/agent-setup.md +85 -0
  90. package/docs/agent-workflows.md +103 -0
  91. package/docs/comparison.md +71 -0
  92. package/docs/freshness.md +124 -0
  93. package/docs/inline-mode.md +119 -0
  94. package/docs/linter-support.md +166 -0
  95. package/docs/spec-format.md +194 -0
  96. package/eslint.config.mjs +79 -0
  97. package/examples/CLAUDE.md +54 -0
  98. package/examples/CLAUDE.md.spec.ts +65 -0
  99. package/examples/SKILL.md +50 -0
  100. package/examples/SKILL.md.spec.ts +57 -0
  101. package/fixtures/example-project/CLAUDE.md +11 -0
  102. package/fixtures/example-project/package.json +9 -0
  103. package/fixtures/example-project/src/index.ts +3 -0
  104. package/fixtures/example-project/src/utils.test.ts +2 -0
  105. package/fixtures/example-project/src/utils.ts +3 -0
  106. package/logo.png +0 -0
  107. package/package.json +42 -0
  108. package/research/adoption-strategy.md +111 -0
  109. package/research/agent-integration.md +145 -0
  110. package/research/ai-code-quality.md +197 -0
  111. package/research/code-search-for-agents.md +313 -0
  112. package/research/competitive-landscape.md +163 -0
  113. package/research/doc-freshness.md +516 -0
  114. package/research/executable-specs.md +368 -0
  115. package/research/feature-ideas.md +464 -0
  116. package/research/formal-proofs-for-agents.md +338 -0
  117. package/research/fp-for-agent-harness.md +150 -0
  118. package/research/fp-for-deterministic-ai.md +131 -0
  119. package/research/self-evolving-specs.md +298 -0
  120. package/schemas/claude-md-strict.yml +18 -0
  121. package/schemas/claude-md.yml +6 -0
  122. package/schemas/skill-strict.yml +12 -0
  123. package/schemas/skill.yml +5 -0
  124. package/skills/audit-feedback-loop/SKILL.md +76 -0
  125. package/skills/edit-spec/SKILL.md +131 -0
  126. package/skills/enforce-rules-format/SKILL.md +71 -0
  127. package/skills/generate-logo/SKILL.md +102 -0
  128. package/skills/generate-rule/SKILL.md +90 -0
  129. package/skills/linter-docs/clippy.md +241 -0
  130. package/skills/linter-docs/eslint.md +384 -0
  131. package/skills/linter-docs/pylint.md +288 -0
  132. package/skills/linter-docs/rubocop.md +277 -0
  133. package/skills/linter-docs/ruff.md +187 -0
  134. package/skills/linter-docs/stylelint.md +247 -0
  135. package/skills/migrate-to-spec/SKILL.md +124 -0
  136. package/skills/pr-to-lint-rule/SKILL.md +97 -0
  137. package/skills/strengthen/SKILL.md +168 -0
  138. package/src/action.ts +214 -0
  139. package/src/cli.test.ts +914 -0
  140. package/src/cli.ts +1631 -0
  141. package/src/compile.ts +691 -0
  142. package/src/evolve.ts +781 -0
  143. package/src/freshness.test.ts +449 -0
  144. package/src/freshness.ts +299 -0
  145. package/src/generate-types.ts +448 -0
  146. package/src/inline.test.ts +206 -0
  147. package/src/inline.ts +164 -0
  148. package/src/linters.ts +739 -0
  149. package/src/proofs.test.ts +1314 -0
  150. package/src/proofs.ts +849 -0
  151. package/src/spec.test.ts +1471 -0
  152. package/src/spec.ts +427 -0
  153. package/src/types.ts +117 -0
  154. package/src/validate.test.ts +701 -0
  155. package/src/validate.ts +381 -0
  156. package/tsconfig.json +23 -0
@@ -0,0 +1,516 @@
1
+ # Doc Freshness: Input Fingerprinting for Stale Spec Detection
2
+
3
+ ## The Gap
4
+
5
+ vigiles hashes the **compiled output** (the `.md` file). This catches manual edits to the markdown. It does NOT catch when the **inputs** that produced that markdown have changed.
6
+
7
+ After `vigiles compile` runs successfully:
8
+
9
+ | What changes | Detected today? | Consequence |
10
+ | ------------------------------------ | ----------------------- | --------------------------------------------- |
11
+ | Someone edits CLAUDE.md by hand | Yes (hash mismatch) | `vigiles audit` catches it |
12
+ | Linter rule gets disabled in config | No | Spec claims `enforce()`, rule is actually off |
13
+ | Referenced file deleted | No (until next compile) | Markdown contains stale `file()` path |
14
+ | npm script removed from package.json | No (until next compile) | Markdown references dead command |
15
+ | Spec file edited, nobody recompiles | No | Markdown and spec diverge |
16
+ | ESLint plugin uninstalled | No (until next compile) | Markdown references non-existent rules |
17
+ | `.vigiles/generated.d.ts` stale | Yes (`--check` mode) | Types don't match reality |
18
+
19
+ The hash answers: "Was the output tampered with?" It doesn't answer: "Is the output still valid given what's true about the project right now?"
20
+
21
+ ## Prior Art
22
+
23
+ ### Make / CMake — mtime comparison
24
+
25
+ Track file modification times. If source is newer than target, rebuild.
26
+
27
+ - Pro: simple, zero overhead
28
+ - Con: mtime is unreliable (git clone resets it, clock skew in CI, `touch` defeats it)
29
+ - Con: doesn't detect content changes on same-second edits
30
+ - **Verdict:** Not suitable. vigiles needs content-based detection, not time-based.
31
+
32
+ ### Bazel / Buck — input fingerprinting
33
+
34
+ Hash **all inputs** to a build action into a single key. Cache the output by that key. If any input changes, the key changes, the cache misses, and the action re-runs.
35
+
36
+ - Pro: precise — catches any input variation
37
+ - Pro: deterministic — same inputs always produce same output
38
+ - Pro: cacheable — skip redundant work
39
+ - Con: requires strict input isolation (must enumerate every file that affects the output)
40
+ - **Verdict:** The right model. vigiles can enumerate its inputs: spec file, linter configs, package.json, referenced files.
41
+
42
+ ### Nix — derivation input hashing
43
+
44
+ Every build step declares its inputs. The derivation hash changes if any input hash changes. The output path is derived from the input hash, so stale outputs are impossible — they live at a different path.
45
+
46
+ - Pro: hermetic — output is always fresh by construction
47
+ - Con: overkill for vigiles (we're not building packages, just compiling markdown)
48
+ - **Verdict:** The principle is right (hash inputs, not outputs). The mechanism is too heavy.
49
+
50
+ ### Terraform — drift detection
51
+
52
+ `terraform plan` compares desired state (config) against actual state (cloud resources). Any delta is flagged as drift.
53
+
54
+ - Pro: detects both directions of drift (config changed, resource changed)
55
+ - Pro: shows a diff, not just "stale"
56
+ - **Verdict:** Good analogy. vigiles should compare "what the spec claims" against "what the project actually has."
57
+
58
+ ### Git tree hashes
59
+
60
+ Git hashes directory trees content-addressably. Changing one file changes the tree hash all the way up. `git diff --stat` shows exactly what changed.
61
+
62
+ - Pro: always available, zero setup
63
+ - Pro: can pin a specific commit as "known good"
64
+ - Con: requires git repo
65
+ - Con: only tracks committed state (uncommitted changes invisible)
66
+ - **Verdict:** Useful as one signal. Can record "compiled at git tree hash X" and check if the relevant files changed since.
67
+
68
+ ### TypeScript `tsBuildInfo` — incremental compilation
69
+
70
+ Stores per-file hashes and dependency graph. On next compile, skips files whose hash (and all transitive dependency hashes) haven't changed.
71
+
72
+ - Pro: fast incremental builds
73
+ - Con: complex dependency graph maintenance
74
+ - **Verdict:** vigiles has a simpler dependency model (spec → {linter configs, files, package.json}). Full DAG is unnecessary.
75
+
76
+ ## Design
77
+
78
+ ### Input manifest
79
+
80
+ At compile time, record every input that affected the output. Store alongside the output hash.
81
+
82
+ ```
83
+ <!-- vigiles:sha256:a1b2c3d4e5f6g7h8 compiled from CLAUDE.md.spec.ts -->
84
+ <!-- vigiles:inputs:sha256:f9e8d7c6b5a49382 -->
85
+ ```
86
+
87
+ The input hash is a SHA-256 of the **sorted, concatenated hashes** of all input files:
88
+
89
+ ```
90
+ inputs = sort([
91
+ sha256(readFile("CLAUDE.md.spec.ts")), # the spec source
92
+ sha256(readFile("eslint.config.mjs")), # linter config
93
+ sha256(readFile("package.json")), # scripts + deps
94
+ sha256(readFile("tsconfig.json")), # if referenced
95
+ sha256(readFile("src/compile.ts")), # each file() reference
96
+ sha256(readFile("src/linters.ts")), # each file() reference
97
+ ... # all keyFiles entries
98
+ ])
99
+ inputHash = sha256(inputs.join("\n"))
100
+ ```
101
+
102
+ ### What counts as an input
103
+
104
+ | Input category | Files | Why |
105
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
106
+ | Spec source | `CLAUDE.md.spec.ts` | Any change to the spec should force recompile |
107
+ | Linter configs | `eslint.config.*`, `.eslintrc.*`, `pyproject.toml` `[tool.ruff]`, `Cargo.toml` `[lints.clippy]`, `.pylintrc`, `.rubocop.yml`, `.stylelintrc.*` | Disabling a rule makes `enforce()` claims stale |
108
+ | Package manifest | `package.json` | Scripts (`cmd()` references) and deps (linter plugins) |
109
+ | Referenced files | Every `file()` call in the spec | Deletion or rename makes the reference stale |
110
+ | Generated types | `.vigiles/generated.d.ts` | If types are stale, rule references may be invalid |
111
+
112
+ ### What does NOT count as an input
113
+
114
+ - Source code files that aren't in `keyFiles` — vigiles doesn't lint code, it lints specs
115
+ - `node_modules/` — too large, too volatile; linter config is the proxy
116
+ - `.git/` — internal state, not a build input
117
+ - Other specs — each spec is compiled independently
118
+
119
+ ### Audit behavior
120
+
121
+ `vigiles audit` gains a new check:
122
+
123
+ 1. Read the compiled `.md` file
124
+ 2. Extract the output hash (existing) and input hash (new)
125
+ 3. Recompute the input hash from current file state
126
+ 4. If input hash mismatches: **"Inputs changed since last compile — run `vigiles compile`"**
127
+
128
+ This is separate from the output hash check:
129
+
130
+ | Output hash | Input hash | Meaning |
131
+ | ----------- | ---------- | -------------------------------------------------- |
132
+ | Valid | Valid | Everything fresh |
133
+ | Invalid | Valid | Someone hand-edited the markdown |
134
+ | Valid | Invalid | Inputs changed, output is stale — recompile needed |
135
+ | Invalid | Invalid | Both changed — recompile needed |
136
+
137
+ ### Discovery: which linter configs exist
138
+
139
+ vigiles already detects linters in `src/linters.ts` and `src/generate-types.ts`. The input manifest reuses this:
140
+
141
+ ```typescript
142
+ function discoverInputFiles(spec: ClaudeSpec, basePath: string): string[] {
143
+ const inputs: string[] = [];
144
+
145
+ // 1. Spec source
146
+ inputs.push(spec._sourceFile);
147
+
148
+ // 2. Linter configs — check existence of known config files
149
+ const linterConfigs = [
150
+ "eslint.config.mjs",
151
+ "eslint.config.js",
152
+ "eslint.config.ts",
153
+ ".eslintrc.json",
154
+ ".eslintrc.js",
155
+ ".eslintrc.yml",
156
+ "pyproject.toml",
157
+ "ruff.toml",
158
+ "Cargo.toml",
159
+ ".pylintrc",
160
+ ".rubocop.yml",
161
+ ".stylelintrc.json",
162
+ ".stylelintrc.js",
163
+ ".stylelintrc.yml",
164
+ ];
165
+ for (const cfg of linterConfigs) {
166
+ if (existsSync(resolve(basePath, cfg))) inputs.push(cfg);
167
+ }
168
+
169
+ // 3. Package manifest
170
+ inputs.push("package.json");
171
+
172
+ // 4. Referenced files from keyFiles
173
+ for (const filePath of Object.keys(spec.keyFiles ?? {})) {
174
+ inputs.push(filePath);
175
+ }
176
+
177
+ // 5. Generated types
178
+ if (existsSync(resolve(basePath, ".vigiles/generated.d.ts"))) {
179
+ inputs.push(".vigiles/generated.d.ts");
180
+ }
181
+
182
+ return inputs.sort();
183
+ }
184
+ ```
185
+
186
+ ### Hash computation
187
+
188
+ ```typescript
189
+ function computeInputHash(inputs: string[], basePath: string): string {
190
+ const fileHashes = inputs.map((f) => {
191
+ const fullPath = resolve(basePath, f);
192
+ if (!existsSync(fullPath)) return `MISSING:${f}`;
193
+ const content = readFileSync(fullPath, "utf-8");
194
+ return createHash("sha256").update(content).digest("hex");
195
+ });
196
+ return createHash("sha256")
197
+ .update(fileHashes.join("\n"))
198
+ .digest("hex")
199
+ .slice(0, 16);
200
+ }
201
+ ```
202
+
203
+ Missing files hash to `MISSING:<path>` so that file deletion changes the input hash (correctly signaling staleness).
204
+
205
+ ### Storage format
206
+
207
+ Two options:
208
+
209
+ **Option A: Second HTML comment** (minimal change)
210
+
211
+ ```html
212
+ <!-- vigiles:sha256:a1b2c3d4e5f6g7h8 compiled from CLAUDE.md.spec.ts -->
213
+ <!-- vigiles:inputs:f9e8d7c6b5a4 -->
214
+ ```
215
+
216
+ Pro: backward-compatible (old vigiles ignores the new comment). Con: two comments at the top.
217
+
218
+ **Option B: Extend existing comment**
219
+
220
+ ```html
221
+ <!-- vigiles:sha256:a1b2c3d4e5f6g7h8 inputs:f9e8d7c6b5a4 compiled from CLAUDE.md.spec.ts -->
222
+ ```
223
+
224
+ Pro: single comment. Con: breaks existing regex; requires migration.
225
+
226
+ **Recommendation:** Option A. Backward-compatible, no migration needed.
227
+
228
+ ### Git-based enhancement (optional)
229
+
230
+ In addition to content hashing, record the git tree hash of tracked input files at compile time:
231
+
232
+ ```html
233
+ <!-- vigiles:git:abc1234 -->
234
+ ```
235
+
236
+ On audit, compare:
237
+
238
+ ```bash
239
+ git diff --name-only abc1234 -- eslint.config.mjs package.json src/compile.ts ...
240
+ ```
241
+
242
+ If any listed file changed in git since that commit, flag as stale.
243
+
244
+ Pros:
245
+
246
+ - Catches committed changes (CI scenario)
247
+ - Can show exactly which files changed
248
+ - Very fast (git does the diffing)
249
+
250
+ Cons:
251
+
252
+ - Doesn't catch uncommitted changes
253
+ - Requires git repo
254
+ - Commit hash may not exist on shallow clones
255
+
256
+ **Recommendation:** Content hash is the primary mechanism. Git hash is an optional fast-path optimization for CI.
257
+
258
+ ## Edge Cases
259
+
260
+ ### File deleted after compile
261
+
262
+ `MISSING:src/utils.ts` in the input hash computation ensures the hash changes when a referenced file is deleted. Audit correctly flags staleness.
263
+
264
+ ### Linter config changes but rules stay the same
265
+
266
+ Example: reformatting `eslint.config.mjs` (whitespace change, no semantic change). The input hash changes, audit says "recompile needed", but `vigiles compile` produces identical output. This is a false positive.
267
+
268
+ **Mitigation:** Could hash the "effective config" (linter rule set) instead of the raw config file. But this requires running `calculateConfigForFile` on every audit, which is slow. The false positive is cheap (just re-run compile), so raw file hashing is acceptable.
269
+
270
+ ### Monorepo with shared configs
271
+
272
+ A root `.eslintrc.json` is inherited by all packages. Changing the root config should invalidate all specs that use ESLint. The input discovery already handles this — it checks for config files at `basePath`, which is the spec's directory.
273
+
274
+ **Gap:** If the root config is at `../../.eslintrc.json` via ESLint's config cascade, we won't discover it. Fix: resolve the actual ESLint config file location using ESLint's API, not just checking known filenames.
275
+
276
+ ### Large `package.json`
277
+
278
+ In monorepos, `package.json` can be large. Hashing the full file means any dependency change (even unrelated) triggers a stale signal.
279
+
280
+ **Mitigation:** Hash only the `scripts` and `devDependencies` sections (the parts vigiles cares about). This is a minor optimization; the false positive cost is low.
281
+
282
+ ## Implementation Plan
283
+
284
+ ### The simpler alternative: `compile --check`
285
+
286
+ Before building input fingerprinting, consider: if compilation is cheap (2-5 seconds), the simplest freshness check is to just recompile in memory and diff:
287
+
288
+ ```bash
289
+ vigiles compile --check # recompile in memory, compare to existing output
290
+ ```
291
+
292
+ No manifest, no input tracking, no false positives from whitespace reformatting. If the output would differ, it's stale. If identical, it's fresh. This is exactly what `generate-types --check` already does.
293
+
294
+ Input fingerprinting only wins when compilation is expensive enough that you want to **avoid** running it. For vigiles today, it isn't.
295
+
296
+ ### Recommended: `freshness` validation rule
297
+
298
+ Add `freshness` to `RulesConfig` alongside `require-spec` and `require-skill-spec`. Global rule in `.vigilesrc.json`, optional per-spec override in the spec itself.
299
+
300
+ ```json
301
+ {
302
+ "rules": {
303
+ "require-spec": "warn",
304
+ "freshness": "error"
305
+ },
306
+ "freshnessMode": "strict"
307
+ }
308
+ ```
309
+
310
+ Severity controls what happens when staleness is detected (`"error"` = CI fails, `"warn"` = prints warning, `false` = skip). Mode controls **how** staleness is detected:
311
+
312
+ | Mode | What `vigiles audit` does | Cost | False positives | False negatives |
313
+ | -------------------- | ------------------------------------------------------------------------------------------ | ------------------------ | -------------------------------------------------- | ------------------------------- |
314
+ | `"strict"` (default) | Recompiles in memory, diffs output. Fails if compiled markdown would change. | 2-5s (runs full compile) | Zero — it checks the actual output | Zero |
315
+ | `"input-hash"` | Checks input fingerprint only. Fails if any tracked input file changed since last compile. | <100ms (hash comparison) | Possible — whitespace changes in config trigger it | Possible — transitive deps |
316
+ | `"output-hash"` | Current behavior. Only checks if the `.md` was hand-edited. | <1ms (single hash) | Zero — but misses input drift entirely | Many — misses all input changes |
317
+
318
+ **Strict mode is correct by default.** Zero false positives AND zero false negatives. The cost is re-running compilation, which takes the same time as `vigiles compile`.
319
+
320
+ **Input-hash mode is the fast-path optimization.** For projects where compilation is slow (many specs, large linter configs, slow ESLint plugin loading). Accepts occasional false positives in exchange for faster CI.
321
+
322
+ **Output-hash mode is the minimal fallback.** "Don't hand-edit the markdown" enforcement only.
323
+
324
+ ### Per-spec override
325
+
326
+ A spec can override the global freshness mode:
327
+
328
+ ```typescript
329
+ export default claude({
330
+ freshness: "input-hash", // override global "strict" for this slow spec
331
+ rules: { ... },
332
+ });
333
+ ```
334
+
335
+ ### Lock files as inputs
336
+
337
+ For input-hash mode, the lock file is a better signal than `package.json` for dependency changes. vigiles auto-detects lock files by language:
338
+
339
+ | Lock file | Language | What it catches |
340
+ | ------------------- | -------- | -------------------------------------------- |
341
+ | `package-lock.json` | Node.js | ESLint/Stylelint plugin version changes |
342
+ | `yarn.lock` | Node.js | Same as above (Yarn) |
343
+ | `pnpm-lock.yaml` | Node.js | Same as above (pnpm) |
344
+ | `bun.lockb` | Node.js | Same as above (Bun) |
345
+ | `Gemfile.lock` | Ruby | RuboCop gem version changes |
346
+ | `poetry.lock` | Python | Pylint plugin version changes |
347
+ | `uv.lock` | Python | Same as above (uv) |
348
+ | `Cargo.lock` | Rust | Clippy version changes (via rustc version) |
349
+ | `requirements.txt` | Python | Fallback if no lock file (pip freeze output) |
350
+
351
+ Detection is simple: check `existsSync` for each. First match wins (projects rarely have competing lock files for the same language). The lock file goes into the input hash alongside linter configs and `package.json`.
352
+
353
+ Why the lock file and not just `package.json`? Because `package.json` can stay identical while the resolved dependency tree changes (version ranges). A Stylelint plugin upgrade from 15.0.0 to 16.0.0 might add/remove rules — `package.json` says `"^15.0.0"` in both cases, but the lock file changes.
354
+
355
+ For strict mode this doesn't matter (it recompiles from scratch). For input-hash mode it prevents a class of false negatives where dependencies change but `package.json` doesn't.
356
+
357
+ If the auto-detection is wrong (e.g., monorepo with lock file at a non-standard location), it can be configured explicitly:
358
+
359
+ ```json
360
+ {
361
+ "freshnessInputs": ["../../yarn.lock"]
362
+ }
363
+ ```
364
+
365
+ ### Type changes
366
+
367
+ ```typescript
368
+ // src/types.ts
369
+
370
+ export type FreshnessMode = "strict" | "input-hash" | "output-hash";
371
+
372
+ export interface RulesConfig {
373
+ "require-spec"?: RuleSeverity;
374
+ "require-skill-spec"?: RuleSeverity;
375
+ freshness?: RuleSeverity; // NEW
376
+ }
377
+
378
+ export interface VigilesConfig {
379
+ ruleMarkers: MarkerType[];
380
+ rules: Required<RulesConfig>;
381
+ files: string[];
382
+ freshnessMode?: FreshnessMode; // NEW — default "strict"
383
+ freshnessInputs?: string[]; // NEW — extra files to include in input hash
384
+ }
385
+ ```
386
+
387
+ ### Default behavior change
388
+
389
+ Today: `vigiles audit` only checks output hashes (hand-edit detection).
390
+ After: `vigiles audit` also recompiles in memory and diffs (freshness: "error", strict mode by default).
391
+
392
+ This is a **breaking change** for projects where the compiled markdown has drifted from the spec. But that's the point — those projects have stale instructions. The `"warn"` severity softens the migration, and `false` opts out entirely.
393
+
394
+ ### Phase 1: `compile --check` (strict mode)
395
+
396
+ 1. `compile.ts` — add `--check` / `dryRun` flag that compiles in memory, compares to existing file
397
+ 2. `cli.ts` (`audit`) — when `freshness` rule is enabled and mode is `"strict"`, run compile in check mode
398
+ 3. Error message: `"CLAUDE.md is stale — run vigiles compile"`
399
+
400
+ ### Phase 2: Input fingerprinting (input-hash mode)
401
+
402
+ 1. `compile.ts` — compute input hash after compilation, embed as second HTML comment
403
+ 2. Auto-detect lock files + linter configs as inputs
404
+ 3. `cli.ts` (`audit`) — when mode is `"input-hash"`, extract and verify input hash
405
+ 4. Error message: `"Inputs changed since last compile (eslint.config.mjs, yarn.lock) — run vigiles compile"`
406
+ 5. Show which files changed (diff input list against current state)
407
+
408
+ ### Phase 3: Granular reporting
409
+
410
+ 1. Store the individual file paths + hashes in a sidecar file (`.vigiles/CLAUDE.md.inputs.json`)
411
+ 2. On audit, report exactly which inputs changed
412
+ 3. Optionally show the delta: "eslint.config.mjs: rule `no-console` was disabled"
413
+
414
+ ### Phase 4: Git integration (optional)
415
+
416
+ 1. Record git commit hash at compile time
417
+ 2. On audit, use `git diff` for fast change detection before falling back to content hashing
418
+ 3. Support shallow clones by falling back to content hash when commit is missing
419
+
420
+ ## TOC Manifests: Recursive Directory Fingerprinting
421
+
422
+ A complementary approach: require a `TOC.md` (or `INDEX.md`) in each documented directory that lists all files with descriptions. This turns "directory contents" into a tracked, verifiable artifact.
423
+
424
+ ### How it works
425
+
426
+ ```markdown
427
+ <!-- docs/TOC.md -->
428
+
429
+ # docs
430
+
431
+ - `linter-support.md` — Linter cross-referencing engine (6 linters + generate-types)
432
+ - `spec-format.md` — Spec format reference (target, sections, rules)
433
+ - `agent-workflows.md` — Agent workflows (Claude Code, Codex, multi-agent, Cursor)
434
+ - `agent-setup.md` — Non-interactive agent setup guide
435
+ - `inline-mode.md` — Inline mode for gradual adoption
436
+ - `eslint.md` — ESLint reference (shared by strengthen + pr-to-lint-rule)
437
+ ```
438
+
439
+ ### Recursive nesting
440
+
441
+ If a subdirectory has its own `TOC.md`, the parent TOC references the directory, not its individual files:
442
+
443
+ ```markdown
444
+ <!-- skills/TOC.md -->
445
+
446
+ # skills
447
+
448
+ - `strengthen/` — Upgrade guidance() → enforce() using linter reference docs
449
+ - `pr-to-lint-rule/` — Convert PR review comments into automated lint rules
450
+ - `edit-spec/` — Edit a spec file with guided workflow
451
+ - `linter-docs/` — Per-linter reference docs (ESLint, RuboCop, Pylint)
452
+ - See `linter-docs/TOC.md` for contents
453
+ ```
454
+
455
+ ### What this enables
456
+
457
+ 1. **Staleness detection** — `vigiles audit` can diff the TOC against `fs.readdirSync`. File added without TOC entry? File deleted but still in TOC? Both are errors.
458
+ 2. **Agent discoverability** — Agents reading CLAUDE.md can follow TOC.md to find relevant docs without scanning the filesystem. It's a curated index, not `ls`.
459
+ 3. **Compilation** — vigiles can compile TOC.md from a TOC.md.spec.ts, getting the same hash-based freshness guarantees as CLAUDE.md.
460
+ 4. **Nested verification** — audit walks the TOC tree recursively. Each level is self-contained.
461
+
462
+ ### Prior art
463
+
464
+ | Tool | Mechanism | Notes |
465
+ | ------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
466
+ | Rust `mod.rs` / `lib.rs` | Module declarations | Every `.rs` file must be declared in its parent module. Undeclared files are dead code. Same principle — directory manifest enforced by compiler. |
467
+ | Python `__init__.py` | Package marker | Historically required for Python packages. Declares what's importable. Not a full manifest but marks directory as "this is intentional." |
468
+ | Cargo `[lib]` / `[[bin]]` | Explicit targets | Cargo.toml must list all crate entry points. Adding a file without updating Cargo.toml = nothing happens. |
469
+ | Go module / package | Implicit (convention) | Go uses directory = package, file = source. No manifest — but the convention IS the manifest. |
470
+ | mdBook `SUMMARY.md` | Book structure | mdBook requires `SUMMARY.md` listing all chapters in order. Unlisted files are excluded from the build. Closest prior art to TOC.md. |
471
+ | Docusaurus `sidebars.js` | Sidebar structure | Lists all docs in navigation order. `autogenerated` mode scans filesystem but explicit mode is a manifest. |
472
+ | Sphinx `toctree` | Document tree | RST directive listing sub-documents. Missing entries = build warning. Closest to nested TOC approach. |
473
+ | CODEOWNERS | File ownership | GitHub's `CODEOWNERS` lists file patterns → owners. Not a manifest but a structured file-level declaration. |
474
+
475
+ **mdBook's `SUMMARY.md` is the closest match.** It's a markdown file that lists all chapters. Unlisted files are excluded. It's the source of truth for book structure.
476
+
477
+ ### Integration with input fingerprinting
478
+
479
+ The TOC becomes an input to the fingerprint:
480
+
481
+ ```
482
+ inputs = sort([
483
+ sha256(readFile("CLAUDE.md.spec.ts")),
484
+ sha256(readFile("docs/TOC.md")), # NEW: directory manifest
485
+ sha256(readFile("skills/TOC.md")), # NEW: nested manifest
486
+ sha256(readFile("eslint.config.mjs")),
487
+ ...
488
+ ])
489
+ ```
490
+
491
+ If someone adds a file to `docs/` without updating `docs/TOC.md`:
492
+
493
+ 1. The TOC doesn't change → input hash unchanged → no recompile triggered
494
+ 2. But `vigiles audit` independently checks TOC completeness → flags the unlisted file
495
+ 3. Two independent signals: "TOC is incomplete" + "inputs haven't changed since last compile"
496
+
497
+ ### vigiles-specific design
498
+
499
+ For vigiles, the TOC.md could be:
500
+
501
+ 1. **Hand-written** — just a markdown file that humans/agents maintain. Audit verifies it matches the directory.
502
+ 2. **Compiled from a spec** — `docs/TOC.md.spec.ts` that uses `file()` references. Then the compiler verifies every listed file exists, and audit verifies no unlisted files snuck in.
503
+ 3. **Auto-generated** — `vigiles generate-toc docs/` scans the directory and emits a TOC. Then it's a build artifact like CLAUDE.md, with the same hash-based freshness.
504
+
505
+ Option 3 (auto-generated) is cleanest: the filesystem is the source of truth, the TOC is a build artifact, and audit verifies freshness. No manual maintenance.
506
+
507
+ But option 2 (compiled from spec) is more valuable: descriptions can't be auto-generated (they require understanding), so the spec is the right place for human-written descriptions. The compiler just verifies nothing was missed.
508
+
509
+ ## Open Questions
510
+
511
+ 1. **Should `vigiles compile` auto-verify inputs before compiling?** (i.e., if inputs haven't changed, skip compilation entirely — Bazel-style memoization)
512
+ 2. **Should the input manifest include the vigiles version?** (A vigiles upgrade might change compilation output even with identical inputs)
513
+ 3. **Should `vigiles audit --fix` automatically recompile when inputs are stale?** (Convenient but hides drift)
514
+ 4. **How to handle inline mode?** Inline rules have no spec file — the "spec source" input doesn't exist. Input hash would cover just the markdown file + linter configs.
515
+ 5. **Should TOC.md be a new compilation target?** (`vigiles init --target=TOC.md` scaffolds a TOC spec for a directory.) Or should it be a separate `vigiles toc` command?
516
+ 6. **Recursive TOC depth:** Should nested TOCs be mandatory, or should a root TOC be allowed to list all files flat? Mandatory nesting scales better but adds friction for small projects.