vigiles 2.0.0 → 2.1.1
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/README.md +108 -131
- package/dist/cli.js +303 -161
- package/dist/compile.d.ts +1 -0
- package/dist/compile.js +22 -19
- package/dist/coverage.d.ts +56 -0
- package/dist/coverage.js +178 -0
- package/dist/doc-refs.d.ts +60 -0
- package/dist/doc-refs.js +246 -0
- package/dist/evolve.js +60 -125
- package/dist/frontmatter.d.ts +57 -0
- package/dist/frontmatter.js +190 -0
- package/dist/generate-schema.d.ts +51 -0
- package/dist/generate-schema.js +115 -0
- package/dist/generate-types.js +39 -1
- package/dist/hash.d.ts +8 -0
- package/dist/hash.js +16 -0
- package/dist/integrity.d.ts +29 -0
- package/dist/integrity.js +44 -0
- package/dist/linters.d.ts +5 -3
- package/dist/linters.js +116 -4
- package/dist/orphans.d.ts +52 -0
- package/dist/orphans.js +124 -0
- package/dist/proofs.js +22 -16
- package/dist/session.d.ts +97 -0
- package/dist/session.js +306 -0
- package/dist/sidecar.d.ts +35 -0
- package/dist/sidecar.js +102 -0
- package/dist/spec.d.ts +29 -9
- package/dist/spec.js +22 -4
- package/dist/test-utils.d.ts +8 -0
- package/dist/test-utils.js +41 -0
- package/dist/types.d.ts +34 -8
- package/dist/types.js +19 -0
- package/dist/validate.js +5 -3
- package/package.json +14 -2
- package/.claude/settings.json +0 -46
- package/.claude/settings.local.json +0 -8
- package/.github/workflows/ci.yml +0 -81
- package/.prettierignore +0 -1
- package/.vigiles/generated.d.ts +0 -205
- package/CLAUDE.md +0 -95
- package/CLAUDE.md.spec.ts +0 -142
- package/CONTRIBUTING.md +0 -121
- package/dist/action.d.ts.map +0 -1
- package/dist/action.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/cli.test.d.ts +0 -2
- package/dist/cli.test.d.ts.map +0 -1
- package/dist/cli.test.js +0 -650
- package/dist/cli.test.js.map +0 -1
- package/dist/compile.d.ts.map +0 -1
- package/dist/compile.js.map +0 -1
- package/dist/evolve.d.ts.map +0 -1
- package/dist/evolve.js.map +0 -1
- package/dist/freshness.d.ts +0 -67
- package/dist/freshness.d.ts.map +0 -1
- package/dist/freshness.js +0 -244
- package/dist/freshness.js.map +0 -1
- package/dist/freshness.test.d.ts +0 -2
- package/dist/freshness.test.d.ts.map +0 -1
- package/dist/freshness.test.js +0 -356
- package/dist/freshness.test.js.map +0 -1
- package/dist/generate-types.d.ts.map +0 -1
- package/dist/generate-types.js.map +0 -1
- package/dist/inline.d.ts.map +0 -1
- package/dist/inline.js.map +0 -1
- package/dist/inline.test.d.ts +0 -5
- package/dist/inline.test.d.ts.map +0 -1
- package/dist/inline.test.js +0 -152
- package/dist/inline.test.js.map +0 -1
- package/dist/linters.d.ts.map +0 -1
- package/dist/linters.js.map +0 -1
- package/dist/proofs.d.ts.map +0 -1
- package/dist/proofs.js.map +0 -1
- package/dist/proofs.test.d.ts +0 -9
- package/dist/proofs.test.d.ts.map +0 -1
- package/dist/proofs.test.js +0 -952
- package/dist/proofs.test.js.map +0 -1
- package/dist/spec.d.ts.map +0 -1
- package/dist/spec.js.map +0 -1
- package/dist/spec.test.d.ts +0 -2
- package/dist/spec.test.d.ts.map +0 -1
- package/dist/spec.test.js +0 -1222
- package/dist/spec.test.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/validate.test.d.ts +0 -2
- package/dist/validate.test.d.ts.map +0 -1
- package/dist/validate.test.js +0 -531
- package/dist/validate.test.js.map +0 -1
- package/docs/agent-setup.md +0 -85
- package/docs/agent-workflows.md +0 -103
- package/docs/comparison.md +0 -71
- package/docs/freshness.md +0 -124
- package/docs/inline-mode.md +0 -119
- package/docs/linter-support.md +0 -166
- package/docs/spec-format.md +0 -194
- package/eslint.config.mjs +0 -79
- package/examples/CLAUDE.md +0 -54
- package/examples/CLAUDE.md.spec.ts +0 -65
- package/examples/SKILL.md +0 -50
- package/examples/SKILL.md.spec.ts +0 -57
- package/fixtures/example-project/CLAUDE.md +0 -11
- package/fixtures/example-project/package.json +0 -9
- package/fixtures/example-project/src/index.ts +0 -3
- package/fixtures/example-project/src/utils.test.ts +0 -2
- package/fixtures/example-project/src/utils.ts +0 -3
- package/logo.png +0 -0
- package/research/adoption-strategy.md +0 -111
- package/research/agent-integration.md +0 -145
- package/research/ai-code-quality.md +0 -197
- package/research/code-search-for-agents.md +0 -313
- package/research/competitive-landscape.md +0 -163
- package/research/doc-freshness.md +0 -516
- package/research/executable-specs.md +0 -368
- package/research/feature-ideas.md +0 -464
- package/research/formal-proofs-for-agents.md +0 -338
- package/research/fp-for-agent-harness.md +0 -150
- package/research/fp-for-deterministic-ai.md +0 -131
- package/research/self-evolving-specs.md +0 -298
- package/schemas/claude-md-strict.yml +0 -18
- package/schemas/claude-md.yml +0 -6
- package/schemas/skill-strict.yml +0 -12
- package/schemas/skill.yml +0 -5
- package/skills/audit-feedback-loop/SKILL.md +0 -76
- package/skills/edit-spec/SKILL.md +0 -131
- package/skills/enforce-rules-format/SKILL.md +0 -71
- package/skills/generate-logo/SKILL.md +0 -102
- package/skills/generate-rule/SKILL.md +0 -90
- package/skills/linter-docs/clippy.md +0 -241
- package/skills/linter-docs/eslint.md +0 -384
- package/skills/linter-docs/pylint.md +0 -288
- package/skills/linter-docs/rubocop.md +0 -277
- package/skills/linter-docs/ruff.md +0 -187
- package/skills/linter-docs/stylelint.md +0 -247
- package/skills/migrate-to-spec/SKILL.md +0 -124
- package/skills/pr-to-lint-rule/SKILL.md +0 -97
- package/skills/strengthen/SKILL.md +0 -168
- package/src/action.ts +0 -214
- package/src/cli.test.ts +0 -914
- package/src/cli.ts +0 -1631
- package/src/compile.ts +0 -691
- package/src/evolve.ts +0 -781
- package/src/freshness.test.ts +0 -449
- package/src/freshness.ts +0 -299
- package/src/generate-types.ts +0 -448
- package/src/inline.test.ts +0 -206
- package/src/inline.ts +0 -164
- package/src/linters.ts +0 -739
- package/src/proofs.test.ts +0 -1314
- package/src/proofs.ts +0 -849
- package/src/spec.test.ts +0 -1471
- package/src/spec.ts +0 -427
- package/src/types.ts +0 -117
- package/src/validate.test.ts +0 -701
- package/src/validate.ts +0 -381
- package/tsconfig.json +0 -23
package/dist/evolve.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.applyMutation = applyMutation;
|
|
|
13
13
|
exports.runProofSuite = runProofSuite;
|
|
14
14
|
const proofs_js_1 = require("./proofs.js");
|
|
15
15
|
const compile_js_1 = require("./compile.js");
|
|
16
|
+
const hash_js_1 = require("./hash.js");
|
|
16
17
|
/**
|
|
17
18
|
* Shallow-clone a Rule. Rules are simple value types (primitive fields only),
|
|
18
19
|
* so a spread is sufficient to decouple engine state from the caller's
|
|
@@ -119,7 +120,7 @@ function applyMutation(rules, mutation) {
|
|
|
119
120
|
rules,
|
|
120
121
|
error: {
|
|
121
122
|
mutation,
|
|
122
|
-
reason: `Rule "${mutation.ruleId}" is already at maximum strength (
|
|
123
|
+
reason: `Rule "${mutation.ruleId}" is already at maximum strength (${rule._kind})`,
|
|
123
124
|
},
|
|
124
125
|
};
|
|
125
126
|
}
|
|
@@ -142,6 +143,12 @@ function applyMutation(rules, mutation) {
|
|
|
142
143
|
text: rule.why,
|
|
143
144
|
};
|
|
144
145
|
}
|
|
146
|
+
else if (rule._kind === "guard") {
|
|
147
|
+
next[mutation.ruleId] = {
|
|
148
|
+
_kind: "guidance",
|
|
149
|
+
text: rule.description,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
145
152
|
else {
|
|
146
153
|
return {
|
|
147
154
|
rules,
|
|
@@ -230,8 +237,13 @@ function applyMutation(rules, mutation) {
|
|
|
230
237
|
else if (rule._kind === "enforce") {
|
|
231
238
|
next[mutation.ruleId] = { ...rule, why: mutation.newText };
|
|
232
239
|
}
|
|
240
|
+
else if (rule._kind === "guard") {
|
|
241
|
+
next[mutation.ruleId] = { ...rule, description: mutation.newText };
|
|
242
|
+
}
|
|
233
243
|
return { rules: next };
|
|
234
244
|
}
|
|
245
|
+
default:
|
|
246
|
+
return (0, hash_js_1.assertNever)(mutation);
|
|
235
247
|
}
|
|
236
248
|
}
|
|
237
249
|
/**
|
|
@@ -258,107 +270,61 @@ function runProofSuite(before, after, options = {}) {
|
|
|
258
270
|
});
|
|
259
271
|
// 2. NCD deduplication. Only fail on pairs NEWLY INTRODUCED by the
|
|
260
272
|
// candidate change — a repo with historical duplication must not
|
|
261
|
-
// block every unrelated mutation.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
name: "ncd-dedup",
|
|
273
|
-
passed: ncdPassed,
|
|
274
|
-
detail: ncdPassed
|
|
275
|
-
? "No new near-duplicate rules"
|
|
276
|
-
: `${similar.length} new near-duplicate pairs: ${similar.map((p) => `${p.idA}↔${p.idB} (${p.distance.toFixed(3)})`).join(", ")}`,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
catch (e) {
|
|
280
|
-
receipts.push({
|
|
281
|
-
name: "ncd-dedup",
|
|
282
|
-
passed: false,
|
|
283
|
-
detail: `Similarity check failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
273
|
+
// block every unrelated mutation.
|
|
274
|
+
const beforePairs = new Set((0, proofs_js_1.findSimilarRules)(before, ncdThreshold).map((p) => [p.idA, p.idB].sort().join("|")));
|
|
275
|
+
const similar = (0, proofs_js_1.findSimilarRules)(after, ncdThreshold).filter((p) => !beforePairs.has([p.idA, p.idB].sort().join("|")));
|
|
276
|
+
const ncdPassed = similar.length === 0;
|
|
277
|
+
receipts.push({
|
|
278
|
+
name: "ncd-dedup",
|
|
279
|
+
passed: ncdPassed,
|
|
280
|
+
detail: ncdPassed
|
|
281
|
+
? "No new near-duplicate rules"
|
|
282
|
+
: `${similar.length} new near-duplicate pairs: ${similar.map((p) => `${p.idA}↔${p.idB} (${p.distance.toFixed(3)})`).join(", ")}`,
|
|
283
|
+
});
|
|
286
284
|
// 3. Bloom filter cross-check (fast sanity check for token overlap).
|
|
287
285
|
// Baseline is built from rules that still exist in `after` — any
|
|
288
286
|
// rule removed by the candidate mutation (e.g. the two sources of
|
|
289
287
|
// a merge) must be excluded, otherwise the newly introduced merge
|
|
290
288
|
// rule would collide against its own sources and the merge would
|
|
291
289
|
// be rejected for the very similarity it was meant to deduplicate.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const similarity = proofs_js_1.BloomFilter.jaccardSimilarity(newFilter, existingFilter);
|
|
312
|
-
if (similarity > 0.7) {
|
|
313
|
-
bloomOverlaps.push(`${newId}↔${existingId} (jaccard=${similarity.toFixed(3)})`);
|
|
314
|
-
bloomPassed = false;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
// Different filter sizes — skip comparison
|
|
290
|
+
let bloomPassed = true;
|
|
291
|
+
const newRuleIds = Object.keys(after).filter((id) => !(id in before));
|
|
292
|
+
const existingFilters = new Map();
|
|
293
|
+
for (const [id, rule] of Object.entries(before)) {
|
|
294
|
+
if (!(id in after))
|
|
295
|
+
continue;
|
|
296
|
+
existingFilters.set(id, (0, proofs_js_1.ruleToBloomFilter)(rule));
|
|
297
|
+
}
|
|
298
|
+
const bloomOverlaps = [];
|
|
299
|
+
for (const newId of newRuleIds) {
|
|
300
|
+
const newFilter = (0, proofs_js_1.ruleToBloomFilter)(after[newId]);
|
|
301
|
+
for (const [existingId, existingFilter] of existingFilters) {
|
|
302
|
+
if (existingId === newId)
|
|
303
|
+
continue;
|
|
304
|
+
try {
|
|
305
|
+
const similarity = proofs_js_1.BloomFilter.jaccardSimilarity(newFilter, existingFilter);
|
|
306
|
+
if (similarity > 0.7) {
|
|
307
|
+
bloomOverlaps.push(`${newId}↔${existingId} (jaccard=${similarity.toFixed(3)})`);
|
|
308
|
+
bloomPassed = false;
|
|
319
309
|
}
|
|
320
310
|
}
|
|
311
|
+
catch {
|
|
312
|
+
// Different filter sizes — skip comparison
|
|
313
|
+
}
|
|
321
314
|
}
|
|
322
|
-
receipts.push({
|
|
323
|
-
name: "bloom-overlap",
|
|
324
|
-
passed: bloomPassed,
|
|
325
|
-
detail: bloomPassed
|
|
326
|
-
? "No suspicious token overlap"
|
|
327
|
-
: `High overlap: ${bloomOverlaps.join(", ")}`,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
catch (e) {
|
|
331
|
-
receipts.push({
|
|
332
|
-
name: "bloom-overlap",
|
|
333
|
-
passed: false,
|
|
334
|
-
detail: `Bloom filter check failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
335
|
-
});
|
|
336
315
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
316
|
+
receipts.push({
|
|
317
|
+
name: "bloom-overlap",
|
|
318
|
+
passed: bloomPassed,
|
|
319
|
+
detail: bloomPassed
|
|
320
|
+
? "No suspicious token overlap"
|
|
321
|
+
: `High overlap: ${bloomOverlaps.join(", ")}`,
|
|
322
|
+
});
|
|
341
323
|
const specForFitness = { rules: after };
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
ncdThreshold,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
catch (e) {
|
|
350
|
-
receipts.push({
|
|
351
|
-
name: "fitness",
|
|
352
|
-
passed: false,
|
|
353
|
-
detail: `Fitness computation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
354
|
-
});
|
|
355
|
-
fitnessResult = {
|
|
356
|
-
score: 0,
|
|
357
|
-
coverage: 0,
|
|
358
|
-
redundancy: 0,
|
|
359
|
-
budgetPressure: 0,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
324
|
+
const fitnessResult = (0, proofs_js_1.fitness)(specForFitness, {
|
|
325
|
+
maxTokens: options.maxTokens,
|
|
326
|
+
ncdThreshold,
|
|
327
|
+
});
|
|
362
328
|
const allPassed = receipts.every((r) => r.passed);
|
|
363
329
|
return { passed: allPassed, receipts, fitness: fitnessResult };
|
|
364
330
|
}
|
|
@@ -454,40 +420,7 @@ class EvolutionEngine {
|
|
|
454
420
|
* Returns a detailed result including proof receipts and fitness comparison.
|
|
455
421
|
*/
|
|
456
422
|
propose(mutation) {
|
|
457
|
-
|
|
458
|
-
// (legacy _kind, JS caller, cast bypass), fitness() reaches ruleToText
|
|
459
|
-
// and throws BEFORE we enter runProofSuite's own try/catch. Return a
|
|
460
|
-
// clean rejection with a neutral fitness instead of crashing the flow.
|
|
461
|
-
let beforeFitness;
|
|
462
|
-
try {
|
|
463
|
-
beforeFitness = this.getFitness();
|
|
464
|
-
}
|
|
465
|
-
catch (e) {
|
|
466
|
-
const neutral = {
|
|
467
|
-
score: 0,
|
|
468
|
-
coverage: 0,
|
|
469
|
-
redundancy: 0,
|
|
470
|
-
budgetPressure: 0,
|
|
471
|
-
};
|
|
472
|
-
return {
|
|
473
|
-
accepted: false,
|
|
474
|
-
mutation,
|
|
475
|
-
proofs: {
|
|
476
|
-
passed: false,
|
|
477
|
-
receipts: [
|
|
478
|
-
{
|
|
479
|
-
name: "baseline-fitness",
|
|
480
|
-
passed: false,
|
|
481
|
-
detail: `Baseline fitness failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
482
|
-
},
|
|
483
|
-
],
|
|
484
|
-
fitness: neutral,
|
|
485
|
-
},
|
|
486
|
-
beforeFitness: neutral,
|
|
487
|
-
afterFitness: neutral,
|
|
488
|
-
error: `Baseline fitness failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
489
|
-
};
|
|
490
|
-
}
|
|
423
|
+
const beforeFitness = this.getFitness();
|
|
491
424
|
// Apply the mutation
|
|
492
425
|
const { rules: candidateRules, error } = applyMutation(this.rules, mutation);
|
|
493
426
|
if (error) {
|
|
@@ -594,6 +527,8 @@ function describeMutation(mutation) {
|
|
|
594
527
|
return `Merge "${mutation.sourceIds[0]}" + "${mutation.sourceIds[1]}" → "${mutation.mergedId}"`;
|
|
595
528
|
case "reword":
|
|
596
529
|
return `Reword rule "${mutation.ruleId}"`;
|
|
530
|
+
default:
|
|
531
|
+
return (0, hash_js_1.assertNever)(mutation);
|
|
597
532
|
}
|
|
598
533
|
}
|
|
599
534
|
//# sourceMappingURL=evolve.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — YAML frontmatter rule mode (Level 1 adoption).
|
|
3
|
+
*
|
|
4
|
+
* Parses a `vigiles.enforce` block out of a markdown file's YAML
|
|
5
|
+
* frontmatter, so a project can declare enforce rules in structured YAML
|
|
6
|
+
* instead of `<!-- vigiles:enforce ... -->` HTML comments (Level 0) or a
|
|
7
|
+
* typed `.spec.ts` (Level 2). Every frontmatter rule goes through the same
|
|
8
|
+
* `checkLinterRule` verification as inline and spec rules.
|
|
9
|
+
*
|
|
10
|
+
* Shape (verbose — chosen so a JSON Schema can give `rule` an enum that
|
|
11
|
+
* YAML LSP autocompletes and squiggles on typo):
|
|
12
|
+
*
|
|
13
|
+
* ---
|
|
14
|
+
* # yaml-language-server: $schema=./.vigiles/schema.json
|
|
15
|
+
* vigiles:
|
|
16
|
+
* enforce:
|
|
17
|
+
* - rule: "@typescript-eslint/no-explicit-any"
|
|
18
|
+
* why: "Use unknown"
|
|
19
|
+
* ---
|
|
20
|
+
*
|
|
21
|
+
* Like inline mode, only `enforce` is supported — the prose body of the
|
|
22
|
+
* file is the guidance. Malformed YAML is reported, never thrown: a broken
|
|
23
|
+
* frontmatter block produces an error finding, not a crash.
|
|
24
|
+
*/
|
|
25
|
+
export interface FrontmatterRule {
|
|
26
|
+
/** Linter rule reference, e.g. "eslint/no-console". */
|
|
27
|
+
linterRule: string;
|
|
28
|
+
/** Why this rule is enforced (human-readable, shown in agent context). */
|
|
29
|
+
why: string;
|
|
30
|
+
/** 1-based line number of the rule in the source file (best-effort). */
|
|
31
|
+
line: number;
|
|
32
|
+
}
|
|
33
|
+
export interface FrontmatterParseResult {
|
|
34
|
+
rules: FrontmatterRule[];
|
|
35
|
+
/** Frontmatter that looks like a vigiles block but failed to parse. */
|
|
36
|
+
errors: {
|
|
37
|
+
line: number;
|
|
38
|
+
message: string;
|
|
39
|
+
}[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse `vigiles.enforce` rules out of a markdown file's YAML frontmatter.
|
|
43
|
+
* Does not touch the filesystem and does not verify rules against any
|
|
44
|
+
* linter — callers feed the returned rules into `checkLinterRule`.
|
|
45
|
+
*
|
|
46
|
+
* A file with no frontmatter, or frontmatter with no `vigiles` key, yields
|
|
47
|
+
* empty results with no errors. Malformed YAML or a malformed `vigiles`
|
|
48
|
+
* block yields error findings (never throws).
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseFrontmatterRules(content: string): FrontmatterParseResult;
|
|
51
|
+
/**
|
|
52
|
+
* True if the content has at least one parseable `vigiles.enforce` rule in
|
|
53
|
+
* its frontmatter. Used by `require-spec` validation to treat frontmatter
|
|
54
|
+
* mode as spec-equivalent, mirroring `hasInlineRules`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function hasFrontmatterRules(content: string): boolean;
|
|
57
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles — YAML frontmatter rule mode (Level 1 adoption).
|
|
4
|
+
*
|
|
5
|
+
* Parses a `vigiles.enforce` block out of a markdown file's YAML
|
|
6
|
+
* frontmatter, so a project can declare enforce rules in structured YAML
|
|
7
|
+
* instead of `<!-- vigiles:enforce ... -->` HTML comments (Level 0) or a
|
|
8
|
+
* typed `.spec.ts` (Level 2). Every frontmatter rule goes through the same
|
|
9
|
+
* `checkLinterRule` verification as inline and spec rules.
|
|
10
|
+
*
|
|
11
|
+
* Shape (verbose — chosen so a JSON Schema can give `rule` an enum that
|
|
12
|
+
* YAML LSP autocompletes and squiggles on typo):
|
|
13
|
+
*
|
|
14
|
+
* ---
|
|
15
|
+
* # yaml-language-server: $schema=./.vigiles/schema.json
|
|
16
|
+
* vigiles:
|
|
17
|
+
* enforce:
|
|
18
|
+
* - rule: "@typescript-eslint/no-explicit-any"
|
|
19
|
+
* why: "Use unknown"
|
|
20
|
+
* ---
|
|
21
|
+
*
|
|
22
|
+
* Like inline mode, only `enforce` is supported — the prose body of the
|
|
23
|
+
* file is the guidance. Malformed YAML is reported, never thrown: a broken
|
|
24
|
+
* frontmatter block produces an error finding, not a crash.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.parseFrontmatterRules = parseFrontmatterRules;
|
|
28
|
+
exports.hasFrontmatterRules = hasFrontmatterRules;
|
|
29
|
+
const js_yaml_1 = require("js-yaml");
|
|
30
|
+
const BOM = "";
|
|
31
|
+
const OPEN_RE = /^---[ \t]*\r?\n/;
|
|
32
|
+
const CLOSE_RE = /\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/;
|
|
33
|
+
/**
|
|
34
|
+
* Extract the YAML frontmatter block. Frontmatter must be the very first
|
|
35
|
+
* thing in the file (after an optional BOM): a `---` line, the YAML body,
|
|
36
|
+
* then a closing `---` or `...` line. Returns null when no block is present.
|
|
37
|
+
*/
|
|
38
|
+
function extractFrontmatter(content) {
|
|
39
|
+
const text = content.startsWith(BOM) ? content.slice(BOM.length) : content;
|
|
40
|
+
const open = OPEN_RE.exec(text);
|
|
41
|
+
if (!open)
|
|
42
|
+
return null;
|
|
43
|
+
const rest = text.slice(open[0].length);
|
|
44
|
+
const close = CLOSE_RE.exec(rest);
|
|
45
|
+
if (!close)
|
|
46
|
+
return null;
|
|
47
|
+
// Line 1 is the opening `---`; YAML content starts on line 2.
|
|
48
|
+
return { yaml: rest.slice(0, close.index), startLine: 2 };
|
|
49
|
+
}
|
|
50
|
+
/** First 1-based line at or after `fromIndex` whose text contains `needle`. */
|
|
51
|
+
function findLine(lines, needle, fromIndex) {
|
|
52
|
+
for (let i = fromIndex; i < lines.length; i++) {
|
|
53
|
+
if (lines[i].includes(needle))
|
|
54
|
+
return i + 1;
|
|
55
|
+
}
|
|
56
|
+
return fromIndex + 1;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Navigate a parsed frontmatter document to its `vigiles.enforce` list.
|
|
60
|
+
* Returns "none" when there's nothing for vigiles to check, "error" when a
|
|
61
|
+
* `vigiles`/`enforce` key is present but the wrong shape, or the list.
|
|
62
|
+
*/
|
|
63
|
+
function lookupEnforce(doc, lines, startLine) {
|
|
64
|
+
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
|
|
65
|
+
return { kind: "none" };
|
|
66
|
+
}
|
|
67
|
+
const vigiles = doc.vigiles;
|
|
68
|
+
if (vigiles === undefined)
|
|
69
|
+
return { kind: "none" };
|
|
70
|
+
if (vigiles === null ||
|
|
71
|
+
typeof vigiles !== "object" ||
|
|
72
|
+
Array.isArray(vigiles)) {
|
|
73
|
+
return {
|
|
74
|
+
kind: "error",
|
|
75
|
+
error: {
|
|
76
|
+
line: findLine(lines, "vigiles:", startLine - 1),
|
|
77
|
+
message: "`vigiles` frontmatter key must be a mapping.",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const enforce = vigiles.enforce;
|
|
82
|
+
if (enforce === undefined)
|
|
83
|
+
return { kind: "none" };
|
|
84
|
+
const enforceLine = findLine(lines, "enforce:", startLine - 1);
|
|
85
|
+
if (!Array.isArray(enforce)) {
|
|
86
|
+
return {
|
|
87
|
+
kind: "error",
|
|
88
|
+
error: {
|
|
89
|
+
line: enforceLine,
|
|
90
|
+
message: "`vigiles.enforce` must be a list of { rule, why } entries.",
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { kind: "list", enforce, enforceLine };
|
|
95
|
+
}
|
|
96
|
+
/** Parse one `vigiles.enforce` entry into a rule or an error finding. */
|
|
97
|
+
function parseEntry(entry, index, ctx) {
|
|
98
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
99
|
+
return {
|
|
100
|
+
error: {
|
|
101
|
+
line: ctx.enforceLine,
|
|
102
|
+
message: `vigiles.enforce[${String(index)}] must be a mapping with \`rule\` and \`why\`.`,
|
|
103
|
+
},
|
|
104
|
+
nextCursor: ctx.cursor,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const { rule, why } = entry;
|
|
108
|
+
if (typeof rule !== "string" || rule.trim() === "") {
|
|
109
|
+
return {
|
|
110
|
+
error: {
|
|
111
|
+
line: ctx.enforceLine,
|
|
112
|
+
message: `vigiles.enforce[${String(index)}] is missing a string \`rule\`.`,
|
|
113
|
+
},
|
|
114
|
+
nextCursor: ctx.cursor,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const line = findLine(ctx.lines, rule, ctx.cursor);
|
|
118
|
+
if (typeof why !== "string" || why.trim() === "") {
|
|
119
|
+
return {
|
|
120
|
+
error: {
|
|
121
|
+
line,
|
|
122
|
+
message: `vigiles.enforce[${String(index)}] (rule "${rule}") is missing a string \`why\`.`,
|
|
123
|
+
},
|
|
124
|
+
nextCursor: line,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { rule: { linterRule: rule, why, line }, nextCursor: line };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Parse `vigiles.enforce` rules out of a markdown file's YAML frontmatter.
|
|
131
|
+
* Does not touch the filesystem and does not verify rules against any
|
|
132
|
+
* linter — callers feed the returned rules into `checkLinterRule`.
|
|
133
|
+
*
|
|
134
|
+
* A file with no frontmatter, or frontmatter with no `vigiles` key, yields
|
|
135
|
+
* empty results with no errors. Malformed YAML or a malformed `vigiles`
|
|
136
|
+
* block yields error findings (never throws).
|
|
137
|
+
*/
|
|
138
|
+
function parseFrontmatterRules(content) {
|
|
139
|
+
const fm = extractFrontmatter(content);
|
|
140
|
+
if (!fm)
|
|
141
|
+
return { rules: [], errors: [] };
|
|
142
|
+
const lines = content.split("\n");
|
|
143
|
+
let doc;
|
|
144
|
+
try {
|
|
145
|
+
doc = (0, js_yaml_1.load)(fm.yaml);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
const err = e;
|
|
149
|
+
const line = (err.mark?.line ?? 0) + fm.startLine;
|
|
150
|
+
return {
|
|
151
|
+
rules: [],
|
|
152
|
+
errors: [
|
|
153
|
+
{
|
|
154
|
+
line,
|
|
155
|
+
message: `Malformed YAML frontmatter: ${err.reason ?? err.message}`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const lookup = lookupEnforce(doc, lines, fm.startLine);
|
|
161
|
+
if (lookup.kind === "none")
|
|
162
|
+
return { rules: [], errors: [] };
|
|
163
|
+
if (lookup.kind === "error")
|
|
164
|
+
return { rules: [], errors: [lookup.error] };
|
|
165
|
+
const rules = [];
|
|
166
|
+
const errors = [];
|
|
167
|
+
let cursor = lookup.enforceLine; // search start: line after `enforce:`
|
|
168
|
+
for (let i = 0; i < lookup.enforce.length; i++) {
|
|
169
|
+
const r = parseEntry(lookup.enforce[i], i, {
|
|
170
|
+
lines,
|
|
171
|
+
enforceLine: lookup.enforceLine,
|
|
172
|
+
cursor,
|
|
173
|
+
});
|
|
174
|
+
cursor = r.nextCursor;
|
|
175
|
+
if (r.rule)
|
|
176
|
+
rules.push(r.rule);
|
|
177
|
+
if (r.error)
|
|
178
|
+
errors.push(r.error);
|
|
179
|
+
}
|
|
180
|
+
return { rules, errors };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* True if the content has at least one parseable `vigiles.enforce` rule in
|
|
184
|
+
* its frontmatter. Used by `require-spec` validation to treat frontmatter
|
|
185
|
+
* mode as spec-equivalent, mirroring `hasInlineRules`.
|
|
186
|
+
*/
|
|
187
|
+
function hasFrontmatterRules(content) {
|
|
188
|
+
return parseFrontmatterRules(content).rules.length > 0;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=frontmatter.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles generate-schema — emit a JSON Schema for `vigiles:` frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* Sibling of `generate-types` (which emits a `.d.ts` for the spec.ts path).
|
|
5
|
+
* This emits `.vigiles/schema.json` so a YAML LSP can autocomplete rule
|
|
6
|
+
* names and squiggle typos in markdown frontmatter:
|
|
7
|
+
*
|
|
8
|
+
* ---
|
|
9
|
+
* # yaml-language-server: $schema=./.vigiles/schema.json
|
|
10
|
+
* vigiles:
|
|
11
|
+
* enforce:
|
|
12
|
+
* - rule: eslint/no-consolee # red squiggle in VS Code
|
|
13
|
+
* ---
|
|
14
|
+
*
|
|
15
|
+
* The `rule` enum is populated from the project's ACTUAL enabled linter
|
|
16
|
+
* rules — the same discovery `generate-types` runs — so the schema reflects
|
|
17
|
+
* what is really enforceable, not a static catalog.
|
|
18
|
+
*/
|
|
19
|
+
export interface GenerateSchemaOptions {
|
|
20
|
+
basePath?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Custom linters from `.vigilesrc.json` (`rulesDir`-backed). These aren't
|
|
23
|
+
* auto-discovered by `generate-types`, but `vigiles audit` resolves their
|
|
24
|
+
* rules via `checkLinterRule`, so the schema enum must include them too —
|
|
25
|
+
* otherwise the YAML LSP false-flags a rule that CI accepts.
|
|
26
|
+
*/
|
|
27
|
+
linters?: Record<string, {
|
|
28
|
+
rulesDir?: string | string[];
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export interface GenerateSchemaResult {
|
|
32
|
+
/** The JSON Schema document. */
|
|
33
|
+
schema: Record<string, unknown>;
|
|
34
|
+
/** Pretty-printed JSON, newline-terminated. */
|
|
35
|
+
json: string;
|
|
36
|
+
/** Rule names included in the enum (sorted, deduped). */
|
|
37
|
+
ruleNames: string[];
|
|
38
|
+
/** Linters discovered, with rule counts. */
|
|
39
|
+
linters: {
|
|
40
|
+
linter: string;
|
|
41
|
+
count: number;
|
|
42
|
+
}[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a JSON Schema for the `vigiles:` frontmatter block from the
|
|
46
|
+
* project's discovered linter rules. When no rules are discoverable the
|
|
47
|
+
* `rule` field falls back to a freeform string so the schema never
|
|
48
|
+
* false-flags a valid reference.
|
|
49
|
+
*/
|
|
50
|
+
export declare function generateSchema(options?: GenerateSchemaOptions): GenerateSchemaResult;
|
|
51
|
+
//# sourceMappingURL=generate-schema.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles generate-schema — emit a JSON Schema for `vigiles:` frontmatter.
|
|
4
|
+
*
|
|
5
|
+
* Sibling of `generate-types` (which emits a `.d.ts` for the spec.ts path).
|
|
6
|
+
* This emits `.vigiles/schema.json` so a YAML LSP can autocomplete rule
|
|
7
|
+
* names and squiggle typos in markdown frontmatter:
|
|
8
|
+
*
|
|
9
|
+
* ---
|
|
10
|
+
* # yaml-language-server: $schema=./.vigiles/schema.json
|
|
11
|
+
* vigiles:
|
|
12
|
+
* enforce:
|
|
13
|
+
* - rule: eslint/no-consolee # red squiggle in VS Code
|
|
14
|
+
* ---
|
|
15
|
+
*
|
|
16
|
+
* The `rule` enum is populated from the project's ACTUAL enabled linter
|
|
17
|
+
* rules — the same discovery `generate-types` runs — so the schema reflects
|
|
18
|
+
* what is really enforceable, not a static catalog.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.generateSchema = generateSchema;
|
|
22
|
+
const node_fs_1 = require("node:fs");
|
|
23
|
+
const node_path_1 = require("node:path");
|
|
24
|
+
const glob_1 = require("glob");
|
|
25
|
+
const generate_types_js_1 = require("./generate-types.js");
|
|
26
|
+
/**
|
|
27
|
+
* Rule references for config-declared custom linters. Mirrors
|
|
28
|
+
* `checkLinterRule`'s rulesDir lookup (any file `<rule>.*` is a rule) so the
|
|
29
|
+
* enum matches what `vigiles audit` accepts for these linters.
|
|
30
|
+
*/
|
|
31
|
+
function customRuleRefs(basePath, linters) {
|
|
32
|
+
const refs = [];
|
|
33
|
+
for (const [name, cfg] of Object.entries(linters ?? {})) {
|
|
34
|
+
const dirs = Array.isArray(cfg.rulesDir) ? cfg.rulesDir : [cfg.rulesDir];
|
|
35
|
+
for (const dir of dirs) {
|
|
36
|
+
if (!dir)
|
|
37
|
+
continue;
|
|
38
|
+
const full = (0, node_path_1.resolve)(basePath, dir);
|
|
39
|
+
if (!(0, node_fs_1.existsSync)(full))
|
|
40
|
+
continue;
|
|
41
|
+
for (const f of (0, glob_1.globSync)("*.*", { cwd: full, nodir: true })) {
|
|
42
|
+
refs.push(`${name}/${f.replace(/\.[^.]+$/, "")}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return refs;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a JSON Schema for the `vigiles:` frontmatter block from the
|
|
50
|
+
* project's discovered linter rules. When no rules are discoverable the
|
|
51
|
+
* `rule` field falls back to a freeform string so the schema never
|
|
52
|
+
* false-flags a valid reference.
|
|
53
|
+
*/
|
|
54
|
+
function generateSchema(options = {}) {
|
|
55
|
+
const basePath = options.basePath ?? process.cwd();
|
|
56
|
+
const { linters } = (0, generate_types_js_1.generateTypes)({ basePath });
|
|
57
|
+
const ruleNames = new Set();
|
|
58
|
+
for (const { linter, rules } of linters) {
|
|
59
|
+
for (const rule of rules) {
|
|
60
|
+
ruleNames.add(`${linter}/${rule}`);
|
|
61
|
+
// ESLint also accepts scoped-plugin rules in bare form (the scope is
|
|
62
|
+
// treated as the linter, e.g. "@typescript-eslint/no-explicit-any").
|
|
63
|
+
// Emit both so neither form produces a false squiggle.
|
|
64
|
+
if (linter === "eslint" && rule.includes("/")) {
|
|
65
|
+
ruleNames.add(rule);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const ref of customRuleRefs(basePath, options.linters)) {
|
|
70
|
+
ruleNames.add(ref);
|
|
71
|
+
}
|
|
72
|
+
const sorted = [...ruleNames].sort();
|
|
73
|
+
const ruleSchema = sorted.length > 0 ? { enum: sorted } : { type: "string" };
|
|
74
|
+
const schema = {
|
|
75
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
76
|
+
$id: "https://vigiles.dev/frontmatter.schema.json",
|
|
77
|
+
title: "vigiles frontmatter",
|
|
78
|
+
description: "vigiles enforce rules declared in markdown YAML frontmatter (Level 1).",
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
vigiles: {
|
|
82
|
+
type: "object",
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
properties: {
|
|
85
|
+
enforce: {
|
|
86
|
+
type: "array",
|
|
87
|
+
description: "Linter rules to enforce, verified by `vigiles audit`.",
|
|
88
|
+
items: {
|
|
89
|
+
type: "object",
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
required: ["rule", "why"],
|
|
92
|
+
properties: {
|
|
93
|
+
rule: {
|
|
94
|
+
description: "Linter rule reference, e.g. eslint/no-console. Verified to exist AND be enabled.",
|
|
95
|
+
...ruleSchema,
|
|
96
|
+
},
|
|
97
|
+
why: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Why this rule is enforced — shown to the agent as context.",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
schema,
|
|
110
|
+
json: JSON.stringify(schema, null, 2) + "\n",
|
|
111
|
+
ruleNames: sorted,
|
|
112
|
+
linters: linters.map((l) => ({ linter: l.linter, count: l.rules.length })),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=generate-schema.js.map
|