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,381 @@
1
+ import { readFileSync, lstatSync, existsSync } from "node:fs";
2
+ import { globSync } from "glob";
3
+ import { resolve, basename as pathBasename } from "node:path";
4
+ import { cosmiconfigSync } from "cosmiconfig";
5
+
6
+ import { hasInlineRules } from "./inline.js";
7
+
8
+ import type {
9
+ ParsedRule,
10
+ ValidationError,
11
+ ValidationResult,
12
+ ReadResult,
13
+ FileResult,
14
+ ValidatePathsResult,
15
+ RulesConfig,
16
+ VigilesConfig,
17
+ MarkerType,
18
+ ParseOptions,
19
+ ValidateOptions,
20
+ ValidatePathsOptions,
21
+ ReadOptions,
22
+ } from "./types.js";
23
+
24
+ // Re-export all types for consumers
25
+ export type {
26
+ ParsedRule,
27
+ ValidationError,
28
+ ValidationResult,
29
+ ReadResult,
30
+ FileResult,
31
+ ValidatePathsResult,
32
+ RulesConfig,
33
+ VigilesConfig,
34
+ MarkerType,
35
+ ParseOptions,
36
+ ValidateOptions,
37
+ ValidatePathsOptions,
38
+ ReadOptions,
39
+ };
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Constants & regex
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const GUIDANCE_RE = /\*\*Guidance only\*\*/;
46
+ const DISABLE_RE = /<!--\s*vigiles-disable\s*-->/;
47
+ const RULE_HEADER_RE = /^###\s+(.+)$/;
48
+ const CHECKBOX_RE = /^- \[([ xX])\]\s+(.+)$/;
49
+
50
+ const VALID_MARKERS: readonly MarkerType[] = ["headings", "checkboxes"];
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Default config
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const DEFAULT_FILES: string[] = ["CLAUDE.md"];
57
+
58
+ const DEFAULT_RULES: Required<RulesConfig> = {
59
+ "require-spec": "warn",
60
+ "require-skill-spec": "warn",
61
+ freshness: "warn",
62
+ };
63
+
64
+ const DEFAULT_CONFIG: VigilesConfig = {
65
+ ruleMarkers: ["headings", "checkboxes"],
66
+ rules: DEFAULT_RULES,
67
+ files: DEFAULT_FILES,
68
+ };
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Instruction file discovery
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export function findInstructionFiles(
75
+ cwd: string = process.cwd(),
76
+ configFiles?: string[],
77
+ ): string[] {
78
+ const candidates = configFiles ?? DEFAULT_FILES;
79
+ return candidates.filter((f) => existsSync(resolve(cwd, f)));
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Config loading
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export function loadConfig(): VigilesConfig {
87
+ try {
88
+ const explorer = cosmiconfigSync("vigiles", {
89
+ searchPlaces: [".vigilesrc.json"],
90
+ mergeSearchPlaces: false,
91
+ });
92
+ const result = explorer.search();
93
+ if (!result?.config) return { ...DEFAULT_CONFIG };
94
+
95
+ const userConfig = result.config as Partial<VigilesConfig> & {
96
+ rules?: Partial<RulesConfig>;
97
+ };
98
+
99
+ const config: VigilesConfig = {
100
+ ...DEFAULT_CONFIG,
101
+ ...userConfig,
102
+ rules: { ...DEFAULT_RULES, ...userConfig.rules },
103
+ files: Array.isArray(userConfig.files) ? userConfig.files : DEFAULT_FILES,
104
+ };
105
+
106
+ if (
107
+ !Array.isArray(config.ruleMarkers) ||
108
+ !config.ruleMarkers.every((m): m is MarkerType =>
109
+ (VALID_MARKERS as readonly string[]).includes(m),
110
+ )
111
+ ) {
112
+ console.warn(
113
+ `Invalid ruleMarkers in config: ${JSON.stringify(config.ruleMarkers)}. Using default.`,
114
+ );
115
+ config.ruleMarkers = [...DEFAULT_CONFIG.ruleMarkers];
116
+ }
117
+
118
+ return config;
119
+ } catch {
120
+ return { ...DEFAULT_CONFIG };
121
+ }
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Parsing
126
+ // ---------------------------------------------------------------------------
127
+
128
+ export function parseRules(
129
+ content: string,
130
+ { ruleMarkers }: ParseOptions = {},
131
+ ): ParsedRule[] {
132
+ const markers = ruleMarkers ?? DEFAULT_CONFIG.ruleMarkers;
133
+ const lines = content.split("\n");
134
+ const rules: ParsedRule[] = [];
135
+
136
+ let currentRule: ParsedRule | null = null;
137
+
138
+ for (let i = 0; i < lines.length; i++) {
139
+ const line = lines[i];
140
+ const headerMatch = markers.includes("headings")
141
+ ? line.match(RULE_HEADER_RE)
142
+ : null;
143
+ const checkboxMatch = markers.includes("checkboxes")
144
+ ? line.match(CHECKBOX_RE)
145
+ : null;
146
+
147
+ if (headerMatch ?? checkboxMatch) {
148
+ if (currentRule) {
149
+ rules.push(currentRule);
150
+ }
151
+ const title = headerMatch
152
+ ? headerMatch[1].trim()
153
+ : (checkboxMatch as RegExpMatchArray)[2].trim();
154
+ currentRule = {
155
+ title,
156
+ line: i + 1,
157
+ enforcement: "missing",
158
+ enforcedBy: null,
159
+ };
160
+ continue;
161
+ }
162
+
163
+ if (!currentRule || currentRule.enforcement !== "missing") continue;
164
+
165
+ const enforcedMatch = line.match(/\*\*Enforced by:\*\*\s*`([^`]+)`/);
166
+ if (enforcedMatch) {
167
+ currentRule.enforcement = "enforced";
168
+ currentRule.enforcedBy = enforcedMatch[1] ?? null;
169
+ continue;
170
+ }
171
+
172
+ if (GUIDANCE_RE.test(line)) {
173
+ currentRule.enforcement = "guidance";
174
+ continue;
175
+ }
176
+
177
+ if (DISABLE_RE.test(line)) {
178
+ currentRule.enforcement = "disabled";
179
+ continue;
180
+ }
181
+ }
182
+
183
+ if (currentRule) {
184
+ rules.push(currentRule);
185
+ }
186
+
187
+ return rules;
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Core validation
192
+ // ---------------------------------------------------------------------------
193
+
194
+ export function validate(
195
+ content: string,
196
+ { ruleMarkers, rules: rulesConfig, filePath }: ValidateOptions = {},
197
+ ): ValidationResult {
198
+ const activeRules = rulesConfig ?? DEFAULT_RULES;
199
+ const parsedRules = parseRules(content, { ruleMarkers });
200
+ const enforced = parsedRules.filter(
201
+ (r) => r.enforcement === "enforced",
202
+ ).length;
203
+ const guidanceOnly = parsedRules.filter(
204
+ (r) => r.enforcement === "guidance",
205
+ ).length;
206
+ const disabled = parsedRules.filter(
207
+ (r) => r.enforcement === "disabled",
208
+ ).length;
209
+ const missingCount = parsedRules.filter(
210
+ (r) => r.enforcement === "missing",
211
+ ).length;
212
+
213
+ const errors: ValidationError[] = [];
214
+ const warnings: ValidationError[] = [];
215
+ const disableComment = /<!--\s*vigiles-disable\s+require-spec\s*-->/;
216
+
217
+ if (filePath) {
218
+ const basename = pathBasename(filePath);
219
+ const isInstruction = basename === "CLAUDE.md" || basename === "AGENTS.md";
220
+ const isSkill = basename === "SKILL.md";
221
+
222
+ // --- require-spec (CLAUDE.md / AGENTS.md) ---
223
+ const specSeverity = activeRules["require-spec"];
224
+ if (specSeverity && isInstruction && !disableComment.test(content)) {
225
+ const specPath = filePath + ".spec.ts";
226
+ // Inline mode counts as a spec — any parseable
227
+ // `<!-- vigiles:enforce ... -->` comment means the file is
228
+ // verified on `vigiles audit` even without a .spec.ts sibling.
229
+ // Delegate to the real parser so a malformed marker can't
230
+ // satisfy require-spec with a rule that audit can't verify.
231
+ const hasInline = hasInlineRules(content);
232
+ if (!existsSync(specPath) && !hasInline) {
233
+ const msg: ValidationError = {
234
+ rule: "require-spec",
235
+ message: `No spec file found for "${filePath}". Expected "${specPath}". Run \`npx vigiles init --target=${filePath}\` to create one, add inline \`<!-- vigiles:enforce ... -->\` comments, or disable with <!-- vigiles-disable require-spec -->.`,
236
+ line: 1,
237
+ };
238
+ if (specSeverity === "error") {
239
+ errors.push(msg);
240
+ } else {
241
+ warnings.push(msg);
242
+ }
243
+ }
244
+ }
245
+
246
+ // --- require-skill-spec (SKILL.md) ---
247
+ const skillSeverity = activeRules["require-skill-spec"];
248
+ if (skillSeverity && isSkill && !disableComment.test(content)) {
249
+ const specPath = filePath + ".spec.ts";
250
+ if (!existsSync(specPath)) {
251
+ const msg: ValidationError = {
252
+ rule: "require-skill-spec",
253
+ message: `No spec file found for "${filePath}". Expected "${specPath}".`,
254
+ line: 1,
255
+ };
256
+ if (skillSeverity === "error") {
257
+ errors.push(msg);
258
+ } else {
259
+ warnings.push(msg);
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ return {
266
+ rules: parsedRules,
267
+ enforced,
268
+ guidanceOnly,
269
+ disabled,
270
+ missing: missingCount,
271
+ total: parsedRules.length,
272
+ errors,
273
+ warnings,
274
+ valid: errors.length === 0,
275
+ };
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // File reading
280
+ // ---------------------------------------------------------------------------
281
+
282
+ export function readInstructionFile(
283
+ filePath: string,
284
+ options: ReadOptions = {},
285
+ ): ReadResult {
286
+ try {
287
+ const stat = lstatSync(filePath);
288
+ if (stat.isSymbolicLink() && !options.followSymlinks) {
289
+ return {
290
+ content: null,
291
+ skipped: true,
292
+ reason: `${filePath} is a symlink (use --follow-symlinks to include)`,
293
+ };
294
+ }
295
+ } catch {
296
+ return {
297
+ content: null,
298
+ skipped: false,
299
+ reason: `File not found: ${filePath}`,
300
+ };
301
+ }
302
+
303
+ try {
304
+ return {
305
+ content: readFileSync(filePath, "utf-8"),
306
+ skipped: false,
307
+ reason: null,
308
+ };
309
+ } catch {
310
+ return {
311
+ content: null,
312
+ skipped: false,
313
+ reason: `Could not read: ${filePath}`,
314
+ };
315
+ }
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Glob expansion
320
+ // ---------------------------------------------------------------------------
321
+
322
+ export function expandGlobs(patterns: string[]): string[] {
323
+ const GLOB_CHARS = /[*?{[]/;
324
+ const paths: string[] = [];
325
+
326
+ for (const pattern of patterns) {
327
+ if (GLOB_CHARS.test(pattern)) {
328
+ const matches = globSync(pattern, { cwd: process.cwd() });
329
+ for (const match of matches.sort()) {
330
+ paths.push(resolve(match));
331
+ }
332
+ } else {
333
+ paths.push(pattern);
334
+ }
335
+ }
336
+
337
+ return paths;
338
+ }
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // Multi-file validation
342
+ // ---------------------------------------------------------------------------
343
+
344
+ export function validatePaths(
345
+ paths: string[],
346
+ {
347
+ followSymlinks = false,
348
+ ruleMarkers,
349
+ rules: rulesConfig,
350
+ }: ValidatePathsOptions = {},
351
+ ): ValidatePathsResult {
352
+ const fileResults: FileResult[] = [];
353
+ let allValid = true;
354
+
355
+ for (const filePath of paths) {
356
+ const { content, skipped, reason } = readInstructionFile(filePath, {
357
+ followSymlinks,
358
+ });
359
+
360
+ if (skipped || content === null) {
361
+ fileResults.push({
362
+ path: filePath,
363
+ skipped,
364
+ reason,
365
+ result: null,
366
+ });
367
+ if (!skipped) allValid = false;
368
+ continue;
369
+ }
370
+
371
+ const result = validate(content, {
372
+ ruleMarkers,
373
+ rules: rulesConfig,
374
+ filePath,
375
+ });
376
+ fileResults.push({ path: filePath, skipped: false, reason: null, result });
377
+ if (!result.valid) allValid = false;
378
+ }
379
+
380
+ return { fileResults, valid: allValid };
381
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "noUncheckedIndexedAccess": false,
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": true,
15
+ "exactOptionalPropertyTypes": false,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "skipLibCheck": true,
18
+ "esModuleInterop": true,
19
+ "resolveJsonModule": true
20
+ },
21
+ "include": ["src/**/*.ts"],
22
+ "exclude": ["node_modules", "dist"]
23
+ }