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
package/src/freshness.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness detection for compiled instruction files.
|
|
3
|
+
*
|
|
4
|
+
* Three modes:
|
|
5
|
+
* - "strict": recompile in memory, diff against existing output (zero false positives)
|
|
6
|
+
* - "input-hash": hash tracked input files, compare to stored fingerprint (fast)
|
|
7
|
+
* - "output-hash": existing behavior — only detects hand-edits to compiled .md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
import type { FreshnessMode } from "./types.js";
|
|
15
|
+
import type { ClaudeSpec } from "./spec.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Lock file detection
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Known lock files, ordered by ecosystem then preference. */
|
|
22
|
+
const KNOWN_LOCK_FILES: readonly string[] = [
|
|
23
|
+
// Node.js
|
|
24
|
+
"package-lock.json",
|
|
25
|
+
"yarn.lock",
|
|
26
|
+
"pnpm-lock.yaml",
|
|
27
|
+
"bun.lockb",
|
|
28
|
+
// Ruby
|
|
29
|
+
"Gemfile.lock",
|
|
30
|
+
// Python
|
|
31
|
+
"poetry.lock",
|
|
32
|
+
"uv.lock",
|
|
33
|
+
"pdm.lock",
|
|
34
|
+
"requirements.txt",
|
|
35
|
+
// Rust
|
|
36
|
+
"Cargo.lock",
|
|
37
|
+
// Go
|
|
38
|
+
"go.sum",
|
|
39
|
+
// PHP
|
|
40
|
+
"composer.lock",
|
|
41
|
+
// .NET
|
|
42
|
+
"packages.lock.json",
|
|
43
|
+
// Swift
|
|
44
|
+
"Package.resolved",
|
|
45
|
+
// Elixir
|
|
46
|
+
"mix.lock",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Known linter configuration files. */
|
|
50
|
+
const KNOWN_LINTER_CONFIGS: readonly string[] = [
|
|
51
|
+
// ESLint
|
|
52
|
+
"eslint.config.mjs",
|
|
53
|
+
"eslint.config.js",
|
|
54
|
+
"eslint.config.ts",
|
|
55
|
+
"eslint.config.cjs",
|
|
56
|
+
".eslintrc.json",
|
|
57
|
+
".eslintrc.js",
|
|
58
|
+
".eslintrc.yml",
|
|
59
|
+
".eslintrc.yaml",
|
|
60
|
+
".eslintrc.cjs",
|
|
61
|
+
// Stylelint
|
|
62
|
+
".stylelintrc.json",
|
|
63
|
+
".stylelintrc.js",
|
|
64
|
+
".stylelintrc.yml",
|
|
65
|
+
".stylelintrc.yaml",
|
|
66
|
+
"stylelint.config.js",
|
|
67
|
+
"stylelint.config.cjs",
|
|
68
|
+
"stylelint.config.mjs",
|
|
69
|
+
// Python
|
|
70
|
+
"pyproject.toml",
|
|
71
|
+
"ruff.toml",
|
|
72
|
+
".pylintrc",
|
|
73
|
+
"setup.cfg",
|
|
74
|
+
// Rust
|
|
75
|
+
"Cargo.toml",
|
|
76
|
+
"clippy.toml",
|
|
77
|
+
".clippy.toml",
|
|
78
|
+
// Ruby
|
|
79
|
+
".rubocop.yml",
|
|
80
|
+
".rubocop.yaml",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect lock files present at `basePath`.
|
|
85
|
+
* Returns all found (a project may have multiple ecosystems).
|
|
86
|
+
*/
|
|
87
|
+
export function detectLockFiles(basePath: string): string[] {
|
|
88
|
+
return KNOWN_LOCK_FILES.filter((f) => existsSync(resolve(basePath, f)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect linter config files present at `basePath`.
|
|
93
|
+
*/
|
|
94
|
+
export function detectLinterConfigs(basePath: string): string[] {
|
|
95
|
+
return KNOWN_LINTER_CONFIGS.filter((f) => existsSync(resolve(basePath, f)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Input discovery
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export interface DiscoveredInputs {
|
|
103
|
+
/** All input file paths (relative to basePath), sorted. */
|
|
104
|
+
files: string[];
|
|
105
|
+
/** Which lock files were detected. */
|
|
106
|
+
lockFiles: string[];
|
|
107
|
+
/** Which linter configs were detected. */
|
|
108
|
+
linterConfigs: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Discover all input files that affect a compiled spec's output.
|
|
113
|
+
*
|
|
114
|
+
* Categories:
|
|
115
|
+
* 1. Spec source file
|
|
116
|
+
* 2. Linter configuration files
|
|
117
|
+
* 3. Package manifest (package.json)
|
|
118
|
+
* 4. Lock files (per-ecosystem)
|
|
119
|
+
* 5. Referenced files from keyFiles
|
|
120
|
+
* 6. Generated types (.vigiles/generated.d.ts)
|
|
121
|
+
* 7. Extra files from freshnessInputs config
|
|
122
|
+
*/
|
|
123
|
+
export function discoverInputs(
|
|
124
|
+
specFile: string,
|
|
125
|
+
spec: ClaudeSpec,
|
|
126
|
+
basePath: string,
|
|
127
|
+
extraInputs?: string[],
|
|
128
|
+
): DiscoveredInputs {
|
|
129
|
+
const files = new Set<string>();
|
|
130
|
+
|
|
131
|
+
// 1. Spec source
|
|
132
|
+
files.add(specFile);
|
|
133
|
+
|
|
134
|
+
// 2. Linter configs
|
|
135
|
+
const linterConfigs = detectLinterConfigs(basePath);
|
|
136
|
+
for (const cfg of linterConfigs) files.add(cfg);
|
|
137
|
+
|
|
138
|
+
// 3. Package manifest
|
|
139
|
+
if (existsSync(resolve(basePath, "package.json"))) {
|
|
140
|
+
files.add("package.json");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 4. Lock files
|
|
144
|
+
const lockFiles = detectLockFiles(basePath);
|
|
145
|
+
for (const lf of lockFiles) files.add(lf);
|
|
146
|
+
|
|
147
|
+
// 5. Referenced files from keyFiles
|
|
148
|
+
if (spec.keyFiles) {
|
|
149
|
+
for (const filePath of Object.keys(spec.keyFiles)) {
|
|
150
|
+
files.add(filePath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 6. Generated types
|
|
155
|
+
if (existsSync(resolve(basePath, ".vigiles/generated.d.ts"))) {
|
|
156
|
+
files.add(".vigiles/generated.d.ts");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 7. Extra configured inputs
|
|
160
|
+
if (extraInputs) {
|
|
161
|
+
for (const f of extraInputs) files.add(f);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const sorted = [...files].sort();
|
|
165
|
+
return { files: sorted, lockFiles, linterConfigs };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Input hash computation
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
const INPUT_HASH_RE = /^<!-- vigiles:inputs:([a-f0-9]+) -->\r?\n?/m;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Compute a combined SHA-256 fingerprint of all input files.
|
|
176
|
+
* Missing files hash to "MISSING:<path>" so deletion changes the hash.
|
|
177
|
+
*/
|
|
178
|
+
export function computeInputHash(
|
|
179
|
+
inputFiles: string[],
|
|
180
|
+
basePath: string,
|
|
181
|
+
): string {
|
|
182
|
+
const fileHashes = inputFiles.map((f) => {
|
|
183
|
+
const fullPath = resolve(basePath, f);
|
|
184
|
+
if (!existsSync(fullPath)) return `MISSING:${f}`;
|
|
185
|
+
const content = readFileSync(fullPath);
|
|
186
|
+
return createHash("sha256").update(content).digest("hex");
|
|
187
|
+
});
|
|
188
|
+
return createHash("sha256")
|
|
189
|
+
.update(fileHashes.join("\n"))
|
|
190
|
+
.digest("hex")
|
|
191
|
+
.slice(0, 16);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Embed input hash as an HTML comment in compiled markdown. */
|
|
195
|
+
export function addInputHash(markdown: string, inputHash: string): string {
|
|
196
|
+
// Insert after the existing vigiles:sha256 comment (first line)
|
|
197
|
+
const lines = markdown.split("\n");
|
|
198
|
+
if (lines[0].startsWith("<!-- vigiles:sha256:")) {
|
|
199
|
+
lines.splice(1, 0, `<!-- vigiles:inputs:${inputHash} -->`);
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
// Fallback: prepend
|
|
203
|
+
return `<!-- vigiles:inputs:${inputHash} -->\n${markdown}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Extract stored input hash from compiled markdown. */
|
|
207
|
+
export function extractInputHash(content: string): string | null {
|
|
208
|
+
const match = content.match(INPUT_HASH_RE);
|
|
209
|
+
return match?.[1] ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Freshness check result
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export interface FreshnessResult {
|
|
217
|
+
fresh: boolean;
|
|
218
|
+
mode: FreshnessMode;
|
|
219
|
+
reason?: string;
|
|
220
|
+
/** Files that changed (input-hash mode only). */
|
|
221
|
+
changedFiles?: string[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check freshness of a compiled file using output-hash mode.
|
|
226
|
+
* Only detects hand-edits to the compiled markdown.
|
|
227
|
+
*/
|
|
228
|
+
export function checkOutputHashFreshness(content: string): FreshnessResult {
|
|
229
|
+
// Re-use existing hash verification
|
|
230
|
+
const hashLine = content.match(
|
|
231
|
+
/^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->/,
|
|
232
|
+
);
|
|
233
|
+
if (!hashLine) {
|
|
234
|
+
return {
|
|
235
|
+
fresh: true,
|
|
236
|
+
mode: "output-hash",
|
|
237
|
+
reason: "No hash found (hand-written file)",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const expectedHash = hashLine[1];
|
|
241
|
+
const body = content
|
|
242
|
+
.replace(
|
|
243
|
+
/^<!-- vigiles:sha256:[a-f0-9]+ compiled from .+ -->\r?\n\r?\n?/,
|
|
244
|
+
"",
|
|
245
|
+
)
|
|
246
|
+
.replace(INPUT_HASH_RE, "");
|
|
247
|
+
const actualHash = createHash("sha256")
|
|
248
|
+
.update(body)
|
|
249
|
+
.digest("hex")
|
|
250
|
+
.slice(0, 16);
|
|
251
|
+
if (actualHash !== expectedHash) {
|
|
252
|
+
return {
|
|
253
|
+
fresh: false,
|
|
254
|
+
mode: "output-hash",
|
|
255
|
+
reason: "Compiled file was manually edited (hash mismatch)",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { fresh: true, mode: "output-hash" };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check freshness using input-hash mode.
|
|
263
|
+
* Compares stored input fingerprint against current file state.
|
|
264
|
+
*/
|
|
265
|
+
export function checkInputHashFreshness(
|
|
266
|
+
content: string,
|
|
267
|
+
inputFiles: string[],
|
|
268
|
+
basePath: string,
|
|
269
|
+
): FreshnessResult {
|
|
270
|
+
const storedHash = extractInputHash(content);
|
|
271
|
+
if (!storedHash) {
|
|
272
|
+
return {
|
|
273
|
+
fresh: false,
|
|
274
|
+
mode: "input-hash",
|
|
275
|
+
reason: "No input hash found — run `vigiles compile` to generate one",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const currentHash = computeInputHash(inputFiles, basePath);
|
|
280
|
+
if (storedHash !== currentHash) {
|
|
281
|
+
// Report missing files (we can't identify other changes without
|
|
282
|
+
// storing per-file hashes, but missing files are obvious)
|
|
283
|
+
const changedFiles: string[] = [];
|
|
284
|
+
for (const f of inputFiles) {
|
|
285
|
+
if (!existsSync(resolve(basePath, f))) {
|
|
286
|
+
changedFiles.push(`${f} (deleted)`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
fresh: false,
|
|
292
|
+
mode: "input-hash",
|
|
293
|
+
reason: "Inputs changed since last compile — run `vigiles compile`",
|
|
294
|
+
changedFiles: changedFiles.length > 0 ? changedFiles : undefined,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { fresh: true, mode: "input-hash" };
|
|
299
|
+
}
|