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.
- package/.claude/settings.json +46 -0
- package/.claude/settings.local.json +8 -0
- package/.claude-plugin/hooks/post-edit.sh +34 -0
- package/.claude-plugin/hooks/pre-edit.sh +40 -0
- package/.claude-plugin/hooks/session-start.sh +38 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +47 -0
- package/.github/workflows/ci.yml +81 -0
- package/.prettierignore +1 -0
- package/.vigiles/generated.d.ts +205 -0
- package/CLAUDE.md +95 -0
- package/CLAUDE.md.spec.ts +142 -0
- package/CONTRIBUTING.md +121 -0
- package/LICENSE +21 -0
- package/README.md +377 -0
- package/action.yml +25 -0
- package/dist/action.d.ts +7 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +180 -0
- package/dist/action.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1267 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +2 -0
- package/dist/cli.test.d.ts.map +1 -0
- package/dist/cli.test.js +650 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/compile.d.ts +101 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +503 -0
- package/dist/compile.js.map +1 -0
- package/dist/evolve.d.ts +132 -0
- package/dist/evolve.d.ts.map +1 -0
- package/dist/evolve.js +599 -0
- package/dist/evolve.js.map +1 -0
- package/dist/freshness.d.ts +67 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +244 -0
- package/dist/freshness.js.map +1 -0
- package/dist/freshness.test.d.ts +2 -0
- package/dist/freshness.test.d.ts.map +1 -0
- package/dist/freshness.test.js +356 -0
- package/dist/freshness.test.js.map +1 -0
- package/dist/generate-types.d.ts +34 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +381 -0
- package/dist/generate-types.js.map +1 -0
- package/dist/inline.d.ts +58 -0
- package/dist/inline.d.ts.map +1 -0
- package/dist/inline.js +142 -0
- package/dist/inline.js.map +1 -0
- package/dist/inline.test.d.ts +5 -0
- package/dist/inline.test.d.ts.map +1 -0
- package/dist/inline.test.js +152 -0
- package/dist/inline.test.js.map +1 -0
- package/dist/linters.d.ts +38 -0
- package/dist/linters.d.ts.map +1 -0
- package/dist/linters.js +588 -0
- package/dist/linters.js.map +1 -0
- package/dist/proofs.d.ts +272 -0
- package/dist/proofs.d.ts.map +1 -0
- package/dist/proofs.js +622 -0
- package/dist/proofs.js.map +1 -0
- package/dist/proofs.test.d.ts +9 -0
- package/dist/proofs.test.d.ts.map +1 -0
- package/dist/proofs.test.js +952 -0
- package/dist/proofs.test.js.map +1 -0
- package/dist/spec.d.ts +258 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +113 -0
- package/dist/spec.js.map +1 -0
- package/dist/spec.test.d.ts +2 -0
- package/dist/spec.test.d.ts.map +1 -0
- package/dist/spec.test.js +1222 -0
- package/dist/spec.test.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +10 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +286 -0
- package/dist/validate.js.map +1 -0
- package/dist/validate.test.d.ts +2 -0
- package/dist/validate.test.d.ts.map +1 -0
- package/dist/validate.test.js +531 -0
- package/dist/validate.test.js.map +1 -0
- package/docs/agent-setup.md +85 -0
- package/docs/agent-workflows.md +103 -0
- package/docs/comparison.md +71 -0
- package/docs/freshness.md +124 -0
- package/docs/inline-mode.md +119 -0
- package/docs/linter-support.md +166 -0
- package/docs/spec-format.md +194 -0
- package/eslint.config.mjs +79 -0
- package/examples/CLAUDE.md +54 -0
- package/examples/CLAUDE.md.spec.ts +65 -0
- package/examples/SKILL.md +50 -0
- package/examples/SKILL.md.spec.ts +57 -0
- package/fixtures/example-project/CLAUDE.md +11 -0
- package/fixtures/example-project/package.json +9 -0
- package/fixtures/example-project/src/index.ts +3 -0
- package/fixtures/example-project/src/utils.test.ts +2 -0
- package/fixtures/example-project/src/utils.ts +3 -0
- package/logo.png +0 -0
- package/package.json +42 -0
- package/research/adoption-strategy.md +111 -0
- package/research/agent-integration.md +145 -0
- package/research/ai-code-quality.md +197 -0
- package/research/code-search-for-agents.md +313 -0
- package/research/competitive-landscape.md +163 -0
- package/research/doc-freshness.md +516 -0
- package/research/executable-specs.md +368 -0
- package/research/feature-ideas.md +464 -0
- package/research/formal-proofs-for-agents.md +338 -0
- package/research/fp-for-agent-harness.md +150 -0
- package/research/fp-for-deterministic-ai.md +131 -0
- package/research/self-evolving-specs.md +298 -0
- package/schemas/claude-md-strict.yml +18 -0
- package/schemas/claude-md.yml +6 -0
- package/schemas/skill-strict.yml +12 -0
- package/schemas/skill.yml +5 -0
- package/skills/audit-feedback-loop/SKILL.md +76 -0
- package/skills/edit-spec/SKILL.md +131 -0
- package/skills/enforce-rules-format/SKILL.md +71 -0
- package/skills/generate-logo/SKILL.md +102 -0
- package/skills/generate-rule/SKILL.md +90 -0
- package/skills/linter-docs/clippy.md +241 -0
- package/skills/linter-docs/eslint.md +384 -0
- package/skills/linter-docs/pylint.md +288 -0
- package/skills/linter-docs/rubocop.md +277 -0
- package/skills/linter-docs/ruff.md +187 -0
- package/skills/linter-docs/stylelint.md +247 -0
- package/skills/migrate-to-spec/SKILL.md +124 -0
- package/skills/pr-to-lint-rule/SKILL.md +97 -0
- package/skills/strengthen/SKILL.md +168 -0
- package/src/action.ts +214 -0
- package/src/cli.test.ts +914 -0
- package/src/cli.ts +1631 -0
- package/src/compile.ts +691 -0
- package/src/evolve.ts +781 -0
- package/src/freshness.test.ts +449 -0
- package/src/freshness.ts +299 -0
- package/src/generate-types.ts +448 -0
- package/src/inline.test.ts +206 -0
- package/src/inline.ts +164 -0
- package/src/linters.ts +739 -0
- package/src/proofs.test.ts +1314 -0
- package/src/proofs.ts +849 -0
- package/src/spec.test.ts +1471 -0
- package/src/spec.ts +427 -0
- package/src/types.ts +117 -0
- package/src/validate.test.ts +701 -0
- package/src/validate.ts +381 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { describe, it, before, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
detectLockFiles,
|
|
11
|
+
detectLinterConfigs,
|
|
12
|
+
discoverInputs,
|
|
13
|
+
computeInputHash,
|
|
14
|
+
addInputHash,
|
|
15
|
+
extractInputHash,
|
|
16
|
+
checkOutputHashFreshness,
|
|
17
|
+
checkInputHashFreshness,
|
|
18
|
+
} from "./freshness.js";
|
|
19
|
+
import type { ClaudeSpec } from "./spec.js";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function makeTmpDir(): string {
|
|
26
|
+
return mkdtempSync(join(tmpdir(), "vigiles-freshness-"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeSpec(overrides?: Partial<ClaudeSpec>): ClaudeSpec {
|
|
30
|
+
return {
|
|
31
|
+
_specType: "claude",
|
|
32
|
+
rules: {},
|
|
33
|
+
...overrides,
|
|
34
|
+
} as ClaudeSpec;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// detectLockFiles
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe("detectLockFiles", () => {
|
|
42
|
+
let tmpDir: string;
|
|
43
|
+
|
|
44
|
+
before(() => {
|
|
45
|
+
tmpDir = makeTmpDir();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
after(() => {
|
|
49
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns empty array when no lock files exist", () => {
|
|
53
|
+
const result = detectLockFiles(tmpDir);
|
|
54
|
+
assert.deepEqual(result, []);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("detects package-lock.json", () => {
|
|
58
|
+
writeFileSync(join(tmpDir, "package-lock.json"), "{}");
|
|
59
|
+
const result = detectLockFiles(tmpDir);
|
|
60
|
+
assert.ok(result.includes("package-lock.json"));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("detects yarn.lock", () => {
|
|
64
|
+
writeFileSync(join(tmpDir, "yarn.lock"), "");
|
|
65
|
+
const result = detectLockFiles(tmpDir);
|
|
66
|
+
assert.ok(result.includes("yarn.lock"));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("detects pnpm-lock.yaml", () => {
|
|
70
|
+
writeFileSync(join(tmpDir, "pnpm-lock.yaml"), "");
|
|
71
|
+
const result = detectLockFiles(tmpDir);
|
|
72
|
+
assert.ok(result.includes("pnpm-lock.yaml"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("detects bun.lockb", () => {
|
|
76
|
+
writeFileSync(join(tmpDir, "bun.lockb"), "");
|
|
77
|
+
const result = detectLockFiles(tmpDir);
|
|
78
|
+
assert.ok(result.includes("bun.lockb"));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("detects Gemfile.lock", () => {
|
|
82
|
+
writeFileSync(join(tmpDir, "Gemfile.lock"), "");
|
|
83
|
+
const result = detectLockFiles(tmpDir);
|
|
84
|
+
assert.ok(result.includes("Gemfile.lock"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("detects poetry.lock", () => {
|
|
88
|
+
writeFileSync(join(tmpDir, "poetry.lock"), "");
|
|
89
|
+
const result = detectLockFiles(tmpDir);
|
|
90
|
+
assert.ok(result.includes("poetry.lock"));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("detects uv.lock", () => {
|
|
94
|
+
writeFileSync(join(tmpDir, "uv.lock"), "");
|
|
95
|
+
const result = detectLockFiles(tmpDir);
|
|
96
|
+
assert.ok(result.includes("uv.lock"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("detects pdm.lock", () => {
|
|
100
|
+
writeFileSync(join(tmpDir, "pdm.lock"), "");
|
|
101
|
+
const result = detectLockFiles(tmpDir);
|
|
102
|
+
assert.ok(result.includes("pdm.lock"));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("detects Cargo.lock", () => {
|
|
106
|
+
writeFileSync(join(tmpDir, "Cargo.lock"), "");
|
|
107
|
+
const result = detectLockFiles(tmpDir);
|
|
108
|
+
assert.ok(result.includes("Cargo.lock"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("detects go.sum", () => {
|
|
112
|
+
writeFileSync(join(tmpDir, "go.sum"), "");
|
|
113
|
+
const result = detectLockFiles(tmpDir);
|
|
114
|
+
assert.ok(result.includes("go.sum"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("detects composer.lock", () => {
|
|
118
|
+
writeFileSync(join(tmpDir, "composer.lock"), "{}");
|
|
119
|
+
const result = detectLockFiles(tmpDir);
|
|
120
|
+
assert.ok(result.includes("composer.lock"));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("detects packages.lock.json (.NET)", () => {
|
|
124
|
+
writeFileSync(join(tmpDir, "packages.lock.json"), "{}");
|
|
125
|
+
const result = detectLockFiles(tmpDir);
|
|
126
|
+
assert.ok(result.includes("packages.lock.json"));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("detects Package.resolved (Swift)", () => {
|
|
130
|
+
writeFileSync(join(tmpDir, "Package.resolved"), "{}");
|
|
131
|
+
const result = detectLockFiles(tmpDir);
|
|
132
|
+
assert.ok(result.includes("Package.resolved"));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("detects mix.lock (Elixir)", () => {
|
|
136
|
+
writeFileSync(join(tmpDir, "mix.lock"), "");
|
|
137
|
+
const result = detectLockFiles(tmpDir);
|
|
138
|
+
assert.ok(result.includes("mix.lock"));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("detects requirements.txt", () => {
|
|
142
|
+
writeFileSync(join(tmpDir, "requirements.txt"), "flask==2.0");
|
|
143
|
+
const result = detectLockFiles(tmpDir);
|
|
144
|
+
assert.ok(result.includes("requirements.txt"));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("returns multiple lock files for polyglot projects", () => {
|
|
148
|
+
// tmpDir already has all the above files
|
|
149
|
+
const result = detectLockFiles(tmpDir);
|
|
150
|
+
assert.ok(
|
|
151
|
+
result.length > 5,
|
|
152
|
+
`Expected many lock files, got ${result.length}`,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// detectLinterConfigs
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
describe("detectLinterConfigs", () => {
|
|
162
|
+
let tmpDir: string;
|
|
163
|
+
|
|
164
|
+
before(() => {
|
|
165
|
+
tmpDir = makeTmpDir();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
after(() => {
|
|
169
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns empty array when no configs exist", () => {
|
|
173
|
+
const result = detectLinterConfigs(tmpDir);
|
|
174
|
+
assert.deepEqual(result, []);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("detects eslint.config.mjs", () => {
|
|
178
|
+
writeFileSync(join(tmpDir, "eslint.config.mjs"), "export default [];");
|
|
179
|
+
const result = detectLinterConfigs(tmpDir);
|
|
180
|
+
assert.ok(result.includes("eslint.config.mjs"));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("detects .rubocop.yml", () => {
|
|
184
|
+
writeFileSync(join(tmpDir, ".rubocop.yml"), "---");
|
|
185
|
+
const result = detectLinterConfigs(tmpDir);
|
|
186
|
+
assert.ok(result.includes(".rubocop.yml"));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("detects pyproject.toml", () => {
|
|
190
|
+
writeFileSync(join(tmpDir, "pyproject.toml"), "[tool.ruff]");
|
|
191
|
+
const result = detectLinterConfigs(tmpDir);
|
|
192
|
+
assert.ok(result.includes("pyproject.toml"));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("detects .pylintrc", () => {
|
|
196
|
+
writeFileSync(join(tmpDir, ".pylintrc"), "[MAIN]");
|
|
197
|
+
const result = detectLinterConfigs(tmpDir);
|
|
198
|
+
assert.ok(result.includes(".pylintrc"));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("detects Cargo.toml", () => {
|
|
202
|
+
writeFileSync(join(tmpDir, "Cargo.toml"), "[package]");
|
|
203
|
+
const result = detectLinterConfigs(tmpDir);
|
|
204
|
+
assert.ok(result.includes("Cargo.toml"));
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// discoverInputs
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
describe("discoverInputs", () => {
|
|
213
|
+
let tmpDir: string;
|
|
214
|
+
|
|
215
|
+
before(() => {
|
|
216
|
+
tmpDir = makeTmpDir();
|
|
217
|
+
writeFileSync(join(tmpDir, "CLAUDE.md.spec.ts"), "export default {};");
|
|
218
|
+
writeFileSync(
|
|
219
|
+
join(tmpDir, "package.json"),
|
|
220
|
+
JSON.stringify({ name: "test", scripts: {} }),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
after(() => {
|
|
225
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("always includes the spec file", () => {
|
|
229
|
+
const spec = makeSpec();
|
|
230
|
+
const result = discoverInputs("CLAUDE.md.spec.ts", spec, tmpDir);
|
|
231
|
+
assert.ok(result.files.includes("CLAUDE.md.spec.ts"));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("includes package.json when present", () => {
|
|
235
|
+
const spec = makeSpec();
|
|
236
|
+
const result = discoverInputs("CLAUDE.md.spec.ts", spec, tmpDir);
|
|
237
|
+
assert.ok(result.files.includes("package.json"));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("includes keyFiles from spec", () => {
|
|
241
|
+
const spec = makeSpec({
|
|
242
|
+
keyFiles: { "src/index.ts": "Entry point" },
|
|
243
|
+
});
|
|
244
|
+
const result = discoverInputs("CLAUDE.md.spec.ts", spec, tmpDir);
|
|
245
|
+
assert.ok(result.files.includes("src/index.ts"));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("includes extra configured inputs", () => {
|
|
249
|
+
const spec = makeSpec();
|
|
250
|
+
const result = discoverInputs("CLAUDE.md.spec.ts", spec, tmpDir, [
|
|
251
|
+
"../../yarn.lock",
|
|
252
|
+
]);
|
|
253
|
+
assert.ok(result.files.includes("../../yarn.lock"));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("includes detected linter configs", () => {
|
|
257
|
+
writeFileSync(join(tmpDir, "eslint.config.mjs"), "export default [];");
|
|
258
|
+
const spec = makeSpec();
|
|
259
|
+
const result = discoverInputs("CLAUDE.md.spec.ts", spec, tmpDir);
|
|
260
|
+
assert.ok(result.files.includes("eslint.config.mjs"));
|
|
261
|
+
assert.ok(result.linterConfigs.includes("eslint.config.mjs"));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("includes detected lock files", () => {
|
|
265
|
+
writeFileSync(join(tmpDir, "package-lock.json"), "{}");
|
|
266
|
+
const spec = makeSpec();
|
|
267
|
+
const result = discoverInputs("CLAUDE.md.spec.ts", spec, tmpDir);
|
|
268
|
+
assert.ok(result.files.includes("package-lock.json"));
|
|
269
|
+
assert.ok(result.lockFiles.includes("package-lock.json"));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("includes .vigiles/generated.d.ts when present", () => {
|
|
273
|
+
mkdirSync(join(tmpDir, ".vigiles"), { recursive: true });
|
|
274
|
+
writeFileSync(join(tmpDir, ".vigiles/generated.d.ts"), "// types");
|
|
275
|
+
const spec = makeSpec();
|
|
276
|
+
const result = discoverInputs("CLAUDE.md.spec.ts", spec, tmpDir);
|
|
277
|
+
assert.ok(result.files.includes(".vigiles/generated.d.ts"));
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("returns sorted file list", () => {
|
|
281
|
+
const spec = makeSpec();
|
|
282
|
+
const result = discoverInputs("CLAUDE.md.spec.ts", spec, tmpDir);
|
|
283
|
+
const sorted = [...result.files].sort();
|
|
284
|
+
assert.deepEqual(result.files, sorted);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// computeInputHash
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
describe("computeInputHash", () => {
|
|
293
|
+
let tmpDir: string;
|
|
294
|
+
|
|
295
|
+
before(() => {
|
|
296
|
+
tmpDir = makeTmpDir();
|
|
297
|
+
writeFileSync(join(tmpDir, "a.txt"), "hello");
|
|
298
|
+
writeFileSync(join(tmpDir, "b.txt"), "world");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
after(() => {
|
|
302
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("returns a 16-char hex string", () => {
|
|
306
|
+
const hash = computeInputHash(["a.txt", "b.txt"], tmpDir);
|
|
307
|
+
assert.match(hash, /^[a-f0-9]{16}$/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("returns same hash for same inputs", () => {
|
|
311
|
+
const h1 = computeInputHash(["a.txt", "b.txt"], tmpDir);
|
|
312
|
+
const h2 = computeInputHash(["a.txt", "b.txt"], tmpDir);
|
|
313
|
+
assert.equal(h1, h2);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("changes when file content changes", () => {
|
|
317
|
+
const h1 = computeInputHash(["a.txt"], tmpDir);
|
|
318
|
+
writeFileSync(join(tmpDir, "a.txt"), "changed");
|
|
319
|
+
const h2 = computeInputHash(["a.txt"], tmpDir);
|
|
320
|
+
assert.notEqual(h1, h2);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("changes when a file is deleted", () => {
|
|
324
|
+
writeFileSync(join(tmpDir, "c.txt"), "temp");
|
|
325
|
+
const h1 = computeInputHash(["c.txt"], tmpDir);
|
|
326
|
+
rmSync(join(tmpDir, "c.txt"));
|
|
327
|
+
const h2 = computeInputHash(["c.txt"], tmpDir);
|
|
328
|
+
assert.notEqual(h1, h2);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// addInputHash / extractInputHash
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
describe("addInputHash / extractInputHash", () => {
|
|
337
|
+
it("embeds hash after sha256 comment", () => {
|
|
338
|
+
const md =
|
|
339
|
+
"<!-- vigiles:sha256:abc123 compiled from X.spec.ts -->\n\n# Doc\n";
|
|
340
|
+
const result = addInputHash(md, "deadbeef12345678");
|
|
341
|
+
assert.ok(result.includes("<!-- vigiles:inputs:deadbeef12345678 -->"));
|
|
342
|
+
// sha256 line should still be first
|
|
343
|
+
assert.ok(result.startsWith("<!-- vigiles:sha256:"));
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("extracts embedded input hash", () => {
|
|
347
|
+
const md =
|
|
348
|
+
"<!-- vigiles:sha256:abc123 compiled from X.spec.ts -->\n<!-- vigiles:inputs:deadbeef12345678 -->\n\n# Doc\n";
|
|
349
|
+
const hash = extractInputHash(md);
|
|
350
|
+
assert.equal(hash, "deadbeef12345678");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("returns null when no input hash present", () => {
|
|
354
|
+
const md =
|
|
355
|
+
"<!-- vigiles:sha256:abc123 compiled from X.spec.ts -->\n\n# Doc\n";
|
|
356
|
+
const hash = extractInputHash(md);
|
|
357
|
+
assert.equal(hash, null);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// checkOutputHashFreshness
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
describe("checkOutputHashFreshness", () => {
|
|
366
|
+
it("returns fresh for hand-written file (no hash)", () => {
|
|
367
|
+
const result = checkOutputHashFreshness("# CLAUDE.md\n\nSome content.\n");
|
|
368
|
+
assert.equal(result.fresh, true);
|
|
369
|
+
assert.equal(result.mode, "output-hash");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("returns fresh when hash matches", () => {
|
|
373
|
+
// Build a valid hashed file
|
|
374
|
+
const body = "# CLAUDE.md\n\nContent.\n";
|
|
375
|
+
const hash = createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
376
|
+
const content = `<!-- vigiles:sha256:${hash} compiled from CLAUDE.md.spec.ts -->\n\n${body}`;
|
|
377
|
+
const result = checkOutputHashFreshness(content);
|
|
378
|
+
assert.equal(result.fresh, true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("returns stale when hash mismatches", () => {
|
|
382
|
+
const content =
|
|
383
|
+
"<!-- vigiles:sha256:0000000000000000 compiled from CLAUDE.md.spec.ts -->\n\n# CLAUDE.md\n\nEdited.\n";
|
|
384
|
+
const result = checkOutputHashFreshness(content);
|
|
385
|
+
assert.equal(result.fresh, false);
|
|
386
|
+
assert.equal(result.mode, "output-hash");
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// checkInputHashFreshness
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
describe("checkInputHashFreshness", () => {
|
|
395
|
+
let tmpDir: string;
|
|
396
|
+
|
|
397
|
+
before(() => {
|
|
398
|
+
tmpDir = makeTmpDir();
|
|
399
|
+
writeFileSync(join(tmpDir, "spec.ts"), "export default {};");
|
|
400
|
+
writeFileSync(join(tmpDir, "config.js"), "module.exports = {};");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
after(() => {
|
|
404
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("returns stale when no input hash is stored", () => {
|
|
408
|
+
const content =
|
|
409
|
+
"<!-- vigiles:sha256:abc compiled from spec.ts -->\n\n# Doc\n";
|
|
410
|
+
const result = checkInputHashFreshness(content, ["spec.ts"], tmpDir);
|
|
411
|
+
assert.equal(result.fresh, false);
|
|
412
|
+
assert.equal(result.mode, "input-hash");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("returns fresh when input hash matches", () => {
|
|
416
|
+
const files = ["spec.ts", "config.js"];
|
|
417
|
+
const hash = computeInputHash(files, tmpDir);
|
|
418
|
+
const content = `<!-- vigiles:sha256:abc compiled from spec.ts -->\n<!-- vigiles:inputs:${hash} -->\n\n# Doc\n`;
|
|
419
|
+
const result = checkInputHashFreshness(content, files, tmpDir);
|
|
420
|
+
assert.equal(result.fresh, true);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("returns stale when input file changes", () => {
|
|
424
|
+
const files = ["spec.ts", "config.js"];
|
|
425
|
+
const hash = computeInputHash(files, tmpDir);
|
|
426
|
+
const content = `<!-- vigiles:sha256:abc compiled from spec.ts -->\n<!-- vigiles:inputs:${hash} -->\n\n# Doc\n`;
|
|
427
|
+
|
|
428
|
+
// Change an input file
|
|
429
|
+
writeFileSync(
|
|
430
|
+
join(tmpDir, "config.js"),
|
|
431
|
+
"module.exports = { changed: true };",
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const result = checkInputHashFreshness(content, files, tmpDir);
|
|
435
|
+
assert.equal(result.fresh, false);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("reports deleted files", () => {
|
|
439
|
+
writeFileSync(join(tmpDir, "temp.txt"), "exists");
|
|
440
|
+
const files = ["spec.ts", "temp.txt"];
|
|
441
|
+
const hash = computeInputHash(files, tmpDir);
|
|
442
|
+
const content = `<!-- vigiles:sha256:abc compiled from spec.ts -->\n<!-- vigiles:inputs:${hash} -->\n\n# Doc\n`;
|
|
443
|
+
|
|
444
|
+
rmSync(join(tmpDir, "temp.txt"));
|
|
445
|
+
const result = checkInputHashFreshness(content, files, tmpDir);
|
|
446
|
+
assert.equal(result.fresh, false);
|
|
447
|
+
assert.ok(result.changedFiles?.some((f) => f.includes("temp.txt")));
|
|
448
|
+
});
|
|
449
|
+
});
|