vigiles 2.0.0 → 2.2.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/README.md +182 -134
- package/dist/action-gate.d.ts +28 -0
- package/dist/action-gate.js +73 -0
- package/dist/cli.js +705 -230
- package/dist/community-skills.d.ts +22 -0
- package/dist/community-skills.js +86 -0
- package/dist/compile-generator.d.ts +48 -0
- package/dist/compile-generator.js +322 -0
- package/dist/compile.d.ts +4 -0
- package/dist/compile.js +239 -45
- 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/eval.d.ts +62 -0
- package/dist/eval.js +174 -0
- package/dist/evolve.js +60 -125
- package/dist/frontmatter.d.ts +75 -0
- package/dist/frontmatter.js +263 -0
- package/dist/generate-schema.d.ts +51 -0
- package/dist/generate-schema.js +125 -0
- package/dist/generate-types.js +39 -1
- package/dist/harness-test.d.ts +38 -0
- package/dist/harness-test.js +129 -0
- package/dist/hash.d.ts +8 -0
- package/dist/hash.js +16 -0
- package/dist/inline.d.ts +22 -4
- package/dist/inline.js +60 -13
- package/dist/integrity.d.ts +29 -0
- package/dist/integrity.js +44 -0
- package/dist/linters.d.ts +5 -3
- package/dist/linters.js +144 -4
- package/dist/mock-model.d.ts +31 -0
- package/dist/mock-model.js +189 -0
- package/dist/orphans.d.ts +52 -0
- package/dist/orphans.js +124 -0
- package/dist/proofs.js +22 -16
- package/dist/refs.d.ts +44 -0
- package/dist/refs.js +144 -0
- 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/skill-driver.d.ts +77 -0
- package/dist/skill-driver.js +76 -0
- package/dist/skill-runtime.d.ts +101 -0
- package/dist/skill-runtime.js +289 -0
- package/dist/skill-test.d.ts +47 -0
- package/dist/skill-test.js +77 -0
- package/dist/spec.d.ts +119 -13
- package/dist/spec.js +51 -4
- package/dist/symbols.d.ts +30 -0
- package/dist/symbols.js +142 -0
- 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 +26 -5
- 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,75 @@
|
|
|
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
|
+
/** A `vigiles.files` entry (verified to exist). */
|
|
34
|
+
export interface FrontmatterFileRef {
|
|
35
|
+
/** Project-relative path to verify exists. */
|
|
36
|
+
path: string;
|
|
37
|
+
/** 1-based line number of the entry in the source file (best-effort). */
|
|
38
|
+
line: number;
|
|
39
|
+
}
|
|
40
|
+
/** A `vigiles.commands` entry (npm scripts verified against package.json). */
|
|
41
|
+
export interface FrontmatterCmdRef {
|
|
42
|
+
/** Command to verify. */
|
|
43
|
+
command: string;
|
|
44
|
+
/** 1-based line number of the entry in the source file (best-effort). */
|
|
45
|
+
line: number;
|
|
46
|
+
}
|
|
47
|
+
export interface FrontmatterParseResult {
|
|
48
|
+
rules: FrontmatterRule[];
|
|
49
|
+
files: FrontmatterFileRef[];
|
|
50
|
+
commands: FrontmatterCmdRef[];
|
|
51
|
+
/** Frontmatter that looks like a vigiles block but failed to parse. */
|
|
52
|
+
errors: {
|
|
53
|
+
line: number;
|
|
54
|
+
message: string;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse `vigiles.enforce` rules, `vigiles.files`, and `vigiles.commands` out
|
|
59
|
+
* of a markdown file's YAML frontmatter. Does not touch the filesystem and
|
|
60
|
+
* does not verify references — callers feed rules into `checkLinterRule` and
|
|
61
|
+
* file/command refs into `validateFileRef` / `validateCommandRef`.
|
|
62
|
+
*
|
|
63
|
+
* A file with no frontmatter, or frontmatter with no `vigiles` key, yields
|
|
64
|
+
* empty results with no errors. Malformed YAML or a malformed `vigiles`
|
|
65
|
+
* block yields error findings (never throws).
|
|
66
|
+
*/
|
|
67
|
+
export declare function parseFrontmatterRules(content: string): FrontmatterParseResult;
|
|
68
|
+
/**
|
|
69
|
+
* True if the content has at least one parseable `vigiles` reference in its
|
|
70
|
+
* frontmatter — an `enforce` rule, a `files` entry, or a `commands` entry.
|
|
71
|
+
* Used by `require-spec` validation to treat frontmatter mode as
|
|
72
|
+
* spec-equivalent, mirroring `hasInlineRules`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function hasFrontmatterRules(content: string): boolean;
|
|
75
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -0,0 +1,263 @@
|
|
|
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` mapping. Returns
|
|
60
|
+
* "none" when there's nothing for vigiles to check, "error" when the
|
|
61
|
+
* `vigiles` key is present but not a mapping, or the mapping itself.
|
|
62
|
+
*/
|
|
63
|
+
function getVigiles(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
|
+
return { kind: "map", vigiles: vigiles };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Locate the `vigiles.enforce` list. Returns "none" when absent, "error" when
|
|
85
|
+
* present but not a list, or the list with its source line.
|
|
86
|
+
*/
|
|
87
|
+
function lookupEnforce(vigiles, lines, startLine) {
|
|
88
|
+
const enforce = vigiles.enforce;
|
|
89
|
+
if (enforce === undefined)
|
|
90
|
+
return { kind: "none" };
|
|
91
|
+
const enforceLine = findLine(lines, "enforce:", startLine - 1);
|
|
92
|
+
if (!Array.isArray(enforce)) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "error",
|
|
95
|
+
error: {
|
|
96
|
+
line: enforceLine,
|
|
97
|
+
message: "`vigiles.enforce` must be a list of { rule, why } entries.",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { kind: "list", enforce, enforceLine };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse a `vigiles.<key>` list of plain strings (used for `files` and
|
|
105
|
+
* `commands`). Returns located items plus error findings for the wrong shape
|
|
106
|
+
* or non-string entries; an absent key yields empty results.
|
|
107
|
+
*/
|
|
108
|
+
function parseStringList(vigiles, key, lines, startLine) {
|
|
109
|
+
const raw = vigiles[key];
|
|
110
|
+
if (raw === undefined)
|
|
111
|
+
return { items: [], errors: [] };
|
|
112
|
+
const keyLine = findLine(lines, `${key}:`, startLine - 1);
|
|
113
|
+
if (!Array.isArray(raw)) {
|
|
114
|
+
return {
|
|
115
|
+
items: [],
|
|
116
|
+
errors: [
|
|
117
|
+
{
|
|
118
|
+
line: keyLine,
|
|
119
|
+
message: `\`vigiles.${key}\` must be a list of strings.`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const items = [];
|
|
125
|
+
const errors = [];
|
|
126
|
+
let cursor = keyLine;
|
|
127
|
+
for (let i = 0; i < raw.length; i++) {
|
|
128
|
+
const v = raw[i];
|
|
129
|
+
if (typeof v !== "string" || v.trim() === "") {
|
|
130
|
+
errors.push({
|
|
131
|
+
line: keyLine,
|
|
132
|
+
message: `vigiles.${key}[${String(i)}] must be a non-empty string.`,
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const line = findLine(lines, v, cursor);
|
|
137
|
+
cursor = line;
|
|
138
|
+
items.push({ value: v, line });
|
|
139
|
+
}
|
|
140
|
+
return { items, errors };
|
|
141
|
+
}
|
|
142
|
+
/** Parse one `vigiles.enforce` entry into a rule or an error finding. */
|
|
143
|
+
function parseEntry(entry, index, ctx) {
|
|
144
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
145
|
+
return {
|
|
146
|
+
error: {
|
|
147
|
+
line: ctx.enforceLine,
|
|
148
|
+
message: `vigiles.enforce[${String(index)}] must be a mapping with \`rule\` and \`why\`.`,
|
|
149
|
+
},
|
|
150
|
+
nextCursor: ctx.cursor,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const { rule, why } = entry;
|
|
154
|
+
if (typeof rule !== "string" || rule.trim() === "") {
|
|
155
|
+
return {
|
|
156
|
+
error: {
|
|
157
|
+
line: ctx.enforceLine,
|
|
158
|
+
message: `vigiles.enforce[${String(index)}] is missing a string \`rule\`.`,
|
|
159
|
+
},
|
|
160
|
+
nextCursor: ctx.cursor,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const line = findLine(ctx.lines, rule, ctx.cursor);
|
|
164
|
+
if (typeof why !== "string" || why.trim() === "") {
|
|
165
|
+
return {
|
|
166
|
+
error: {
|
|
167
|
+
line,
|
|
168
|
+
message: `vigiles.enforce[${String(index)}] (rule "${rule}") is missing a string \`why\`.`,
|
|
169
|
+
},
|
|
170
|
+
nextCursor: line,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { rule: { linterRule: rule, why, line }, nextCursor: line };
|
|
174
|
+
}
|
|
175
|
+
/** Fresh empty result — callers may push into the arrays, so never shared. */
|
|
176
|
+
function emptyResult() {
|
|
177
|
+
return { rules: [], files: [], commands: [], errors: [] };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Parse `vigiles.enforce` rules, `vigiles.files`, and `vigiles.commands` out
|
|
181
|
+
* of a markdown file's YAML frontmatter. Does not touch the filesystem and
|
|
182
|
+
* does not verify references — callers feed rules into `checkLinterRule` and
|
|
183
|
+
* file/command refs into `validateFileRef` / `validateCommandRef`.
|
|
184
|
+
*
|
|
185
|
+
* A file with no frontmatter, or frontmatter with no `vigiles` key, yields
|
|
186
|
+
* empty results with no errors. Malformed YAML or a malformed `vigiles`
|
|
187
|
+
* block yields error findings (never throws).
|
|
188
|
+
*/
|
|
189
|
+
function parseFrontmatterRules(content) {
|
|
190
|
+
const fm = extractFrontmatter(content);
|
|
191
|
+
if (!fm)
|
|
192
|
+
return emptyResult();
|
|
193
|
+
const lines = content.split("\n");
|
|
194
|
+
let doc;
|
|
195
|
+
try {
|
|
196
|
+
doc = (0, js_yaml_1.load)(fm.yaml);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
const err = e;
|
|
200
|
+
const line = (err.mark?.line ?? 0) + fm.startLine;
|
|
201
|
+
return {
|
|
202
|
+
rules: [],
|
|
203
|
+
files: [],
|
|
204
|
+
commands: [],
|
|
205
|
+
errors: [
|
|
206
|
+
{
|
|
207
|
+
line,
|
|
208
|
+
message: `Malformed YAML frontmatter: ${err.reason ?? err.message}`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const vig = getVigiles(doc, lines, fm.startLine);
|
|
214
|
+
if (vig.kind === "none")
|
|
215
|
+
return emptyResult();
|
|
216
|
+
if (vig.kind === "error")
|
|
217
|
+
return { rules: [], files: [], commands: [], errors: [vig.error] };
|
|
218
|
+
const rules = [];
|
|
219
|
+
const errors = [];
|
|
220
|
+
const enforceLookup = lookupEnforce(vig.vigiles, lines, fm.startLine);
|
|
221
|
+
if (enforceLookup.kind === "error") {
|
|
222
|
+
errors.push(enforceLookup.error);
|
|
223
|
+
}
|
|
224
|
+
else if (enforceLookup.kind === "list") {
|
|
225
|
+
let cursor = enforceLookup.enforceLine; // search start: line after `enforce:`
|
|
226
|
+
for (let i = 0; i < enforceLookup.enforce.length; i++) {
|
|
227
|
+
const r = parseEntry(enforceLookup.enforce[i], i, {
|
|
228
|
+
lines,
|
|
229
|
+
enforceLine: enforceLookup.enforceLine,
|
|
230
|
+
cursor,
|
|
231
|
+
});
|
|
232
|
+
cursor = r.nextCursor;
|
|
233
|
+
if (r.rule)
|
|
234
|
+
rules.push(r.rule);
|
|
235
|
+
if (r.error)
|
|
236
|
+
errors.push(r.error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const fileList = parseStringList(vig.vigiles, "files", lines, fm.startLine);
|
|
240
|
+
errors.push(...fileList.errors);
|
|
241
|
+
const files = fileList.items.map((it) => ({
|
|
242
|
+
path: it.value,
|
|
243
|
+
line: it.line,
|
|
244
|
+
}));
|
|
245
|
+
const cmdList = parseStringList(vig.vigiles, "commands", lines, fm.startLine);
|
|
246
|
+
errors.push(...cmdList.errors);
|
|
247
|
+
const commands = cmdList.items.map((it) => ({
|
|
248
|
+
command: it.value,
|
|
249
|
+
line: it.line,
|
|
250
|
+
}));
|
|
251
|
+
return { rules, files, commands, errors };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* True if the content has at least one parseable `vigiles` reference in its
|
|
255
|
+
* frontmatter — an `enforce` rule, a `files` entry, or a `commands` entry.
|
|
256
|
+
* Used by `require-spec` validation to treat frontmatter mode as
|
|
257
|
+
* spec-equivalent, mirroring `hasInlineRules`.
|
|
258
|
+
*/
|
|
259
|
+
function hasFrontmatterRules(content) {
|
|
260
|
+
const r = parseFrontmatterRules(content);
|
|
261
|
+
return r.rules.length + r.files.length + r.commands.length > 0;
|
|
262
|
+
}
|
|
263
|
+
//# 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
|