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,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
+ }