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/generate-types.js
CHANGED
|
@@ -73,7 +73,7 @@ function discoverEslintRules(basePath) {
|
|
|
73
73
|
const rules = JSON.parse(output.trim() || "[]");
|
|
74
74
|
if (rules.length === 0)
|
|
75
75
|
return null;
|
|
76
|
-
return { linter: "eslint", rules, via: "flat config" };
|
|
76
|
+
return { linter: "eslint", rules, via: "flat config (v9+/v10)" };
|
|
77
77
|
}
|
|
78
78
|
catch {
|
|
79
79
|
return null;
|
|
@@ -199,6 +199,41 @@ function discoverRubocopRules(basePath) {
|
|
|
199
199
|
return null;
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
+
function discoverCedarPolicies(basePath) {
|
|
203
|
+
const ID_RE = /@id\("([^"]+)"\)/g;
|
|
204
|
+
const STATEMENT_RE = /\b(?:permit|forbid)\s*\(/;
|
|
205
|
+
const policies = new Set();
|
|
206
|
+
for (const dir of [".cedar", "cedar"]) {
|
|
207
|
+
const fullDir = (0, node_path_1.resolve)(basePath, dir);
|
|
208
|
+
if (!(0, node_fs_1.existsSync)(fullDir))
|
|
209
|
+
continue;
|
|
210
|
+
const files = (0, glob_1.globSync)("**/*.cedar", { cwd: fullDir, nodir: true });
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
let content;
|
|
213
|
+
try {
|
|
214
|
+
content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(fullDir, file), "utf-8");
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const annotated = [...content.matchAll(ID_RE)].map((m) => m[1]);
|
|
220
|
+
if (annotated.length > 0) {
|
|
221
|
+
for (const id of annotated)
|
|
222
|
+
policies.add(id);
|
|
223
|
+
}
|
|
224
|
+
else if (STATEMENT_RE.test(content)) {
|
|
225
|
+
policies.add(file.replace(/\.cedar$/, "").replace(/\\/g, "/"));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (policies.size === 0)
|
|
230
|
+
return null;
|
|
231
|
+
return {
|
|
232
|
+
linter: "cedar",
|
|
233
|
+
rules: [...policies].sort(),
|
|
234
|
+
via: ".cedar files",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
202
237
|
function discoverClippyRules(basePath) {
|
|
203
238
|
try {
|
|
204
239
|
const cargoPath = (0, node_path_1.resolve)(basePath, "Cargo.toml");
|
|
@@ -300,6 +335,9 @@ function generateTypes(options = {}) {
|
|
|
300
335
|
const clippy = discoverClippyRules(basePath);
|
|
301
336
|
if (clippy)
|
|
302
337
|
linters.push(clippy);
|
|
338
|
+
const cedar = discoverCedarPolicies(basePath);
|
|
339
|
+
if (cedar)
|
|
340
|
+
linters.push(cedar);
|
|
303
341
|
const scripts = discoverNpmScripts(basePath);
|
|
304
342
|
const files = discoverProjectFiles(basePath, fileGlobs);
|
|
305
343
|
// Generate .d.ts
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare const __brand: unique symbol;
|
|
2
|
+
export type SHA256Hash = string & {
|
|
3
|
+
readonly [__brand]: "SHA256Hash";
|
|
4
|
+
};
|
|
5
|
+
export declare function sha256short(data: string | Buffer): SHA256Hash;
|
|
6
|
+
export declare function assertNever(x: never): never;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=hash.d.ts.map
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sha256short = sha256short;
|
|
4
|
+
exports.assertNever = assertNever;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const HASH_LENGTH = 16;
|
|
7
|
+
function sha256short(data) {
|
|
8
|
+
return (0, node_crypto_1.createHash)("sha256")
|
|
9
|
+
.update(data)
|
|
10
|
+
.digest("hex")
|
|
11
|
+
.slice(0, HASH_LENGTH);
|
|
12
|
+
}
|
|
13
|
+
function assertNever(x) {
|
|
14
|
+
throw new Error(`Unexpected value: ${String(x)}`);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=hash.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integrity check: verify a compiled markdown file hasn't been hand-edited.
|
|
3
|
+
*
|
|
4
|
+
* Every compiled file has a SHA-256 hash in its first line:
|
|
5
|
+
* <!-- vigiles:sha256:<hash> compiled from CLAUDE.md.spec.ts -->
|
|
6
|
+
*
|
|
7
|
+
* If the body content's hash no longer matches what's recorded, someone
|
|
8
|
+
* edited the compiled output directly. The check is one-pass and ~free.
|
|
9
|
+
*
|
|
10
|
+
* This is the entire freshness story now: no input fingerprinting, no
|
|
11
|
+
* recompile diffing. Those responsibilities belong elsewhere:
|
|
12
|
+
*
|
|
13
|
+
* - Hand-edit detection → this module
|
|
14
|
+
* - "Did the spec change?" → guard() rules emitting compile hooks
|
|
15
|
+
* - "Are referenced linter rules / files / scripts still valid?"
|
|
16
|
+
* → enforce() / file() / cmd() catch this at compile time
|
|
17
|
+
* - "Are committed compiled files actually fresh?"
|
|
18
|
+
* → CI runs `vigiles compile` then `git diff --exit-code`
|
|
19
|
+
*/
|
|
20
|
+
export interface IntegrityResult {
|
|
21
|
+
intact: boolean;
|
|
22
|
+
reason?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check whether the compiled markdown's SHA-256 hash matches its body.
|
|
26
|
+
* Files without a hash header are treated as hand-written (intact).
|
|
27
|
+
*/
|
|
28
|
+
export declare function checkIntegrity(content: string): IntegrityResult;
|
|
29
|
+
//# sourceMappingURL=integrity.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Integrity check: verify a compiled markdown file hasn't been hand-edited.
|
|
4
|
+
*
|
|
5
|
+
* Every compiled file has a SHA-256 hash in its first line:
|
|
6
|
+
* <!-- vigiles:sha256:<hash> compiled from CLAUDE.md.spec.ts -->
|
|
7
|
+
*
|
|
8
|
+
* If the body content's hash no longer matches what's recorded, someone
|
|
9
|
+
* edited the compiled output directly. The check is one-pass and ~free.
|
|
10
|
+
*
|
|
11
|
+
* This is the entire freshness story now: no input fingerprinting, no
|
|
12
|
+
* recompile diffing. Those responsibilities belong elsewhere:
|
|
13
|
+
*
|
|
14
|
+
* - Hand-edit detection → this module
|
|
15
|
+
* - "Did the spec change?" → guard() rules emitting compile hooks
|
|
16
|
+
* - "Are referenced linter rules / files / scripts still valid?"
|
|
17
|
+
* → enforce() / file() / cmd() catch this at compile time
|
|
18
|
+
* - "Are committed compiled files actually fresh?"
|
|
19
|
+
* → CI runs `vigiles compile` then `git diff --exit-code`
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.checkIntegrity = checkIntegrity;
|
|
23
|
+
const hash_js_1 = require("./hash.js");
|
|
24
|
+
const HASH_LINE_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
|
|
25
|
+
/**
|
|
26
|
+
* Check whether the compiled markdown's SHA-256 hash matches its body.
|
|
27
|
+
* Files without a hash header are treated as hand-written (intact).
|
|
28
|
+
*/
|
|
29
|
+
function checkIntegrity(content) {
|
|
30
|
+
const match = content.match(HASH_LINE_RE);
|
|
31
|
+
if (!match) {
|
|
32
|
+
return { intact: true, reason: "No hash header (hand-written file)" };
|
|
33
|
+
}
|
|
34
|
+
const expectedHash = match[1];
|
|
35
|
+
const body = content.replace(HASH_LINE_RE, "");
|
|
36
|
+
if ((0, hash_js_1.sha256short)(body) !== expectedHash) {
|
|
37
|
+
return {
|
|
38
|
+
intact: false,
|
|
39
|
+
reason: "Compiled file was modified directly — edit the .spec.ts source and recompile",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { intact: true };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=integrity.js.map
|
package/dist/linters.d.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Verifies that linter rule references (e.g., "eslint/no-console") point to
|
|
5
5
|
* real rules that exist and are enabled in project config. Supports:
|
|
6
|
-
* ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI)
|
|
6
|
+
* ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI),
|
|
7
|
+
* Cedar (filesystem policies for AWS Bedrock AgentCore / Vectimus).
|
|
7
8
|
*
|
|
8
|
-
* This is the core moat — no other tool resolves rules
|
|
9
|
-
* and checks config-enabled status.
|
|
9
|
+
* This is the core moat — no other tool resolves rules across 7 catalog APIs
|
|
10
|
+
* (6 linters + Cedar policy language) and checks config-enabled status.
|
|
10
11
|
*/
|
|
11
12
|
export type ConfigEnabledStatus = "enabled" | "disabled" | "unknown";
|
|
12
13
|
export interface LinterCheckResult {
|
|
@@ -23,6 +24,7 @@ export interface DetectedLinter {
|
|
|
23
24
|
}
|
|
24
25
|
/** @internal */ export declare function extractLinterName(enforcedBy: string): string;
|
|
25
26
|
/** @internal */ export declare function extractRuleName(enforcedBy: string): string | null;
|
|
27
|
+
/** @internal */ export declare function clearCedarCache(): void;
|
|
26
28
|
/**
|
|
27
29
|
* Check a single linter rule reference (e.g., "eslint/no-console").
|
|
28
30
|
*
|
package/dist/linters.js
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Verifies that linter rule references (e.g., "eslint/no-console") point to
|
|
6
6
|
* real rules that exist and are enabled in project config. Supports:
|
|
7
|
-
* ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI)
|
|
7
|
+
* ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI),
|
|
8
|
+
* Cedar (filesystem policies for AWS Bedrock AgentCore / Vectimus).
|
|
8
9
|
*
|
|
9
|
-
* This is the core moat — no other tool resolves rules
|
|
10
|
-
* and checks config-enabled status.
|
|
10
|
+
* This is the core moat — no other tool resolves rules across 7 catalog APIs
|
|
11
|
+
* (6 linters + Cedar policy language) and checks config-enabled status.
|
|
11
12
|
*/
|
|
12
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
14
|
exports.extractLinterName = extractLinterName;
|
|
14
15
|
exports.extractRuleName = extractRuleName;
|
|
16
|
+
exports.clearCedarCache = clearCedarCache;
|
|
15
17
|
exports.checkLinterRule = checkLinterRule;
|
|
16
18
|
const node_fs_1 = require("node:fs");
|
|
17
19
|
const node_path_1 = require("node:path");
|
|
@@ -524,6 +526,114 @@ function getCliRuleSet(linterName, basePath) {
|
|
|
524
526
|
return makeResult(ctx, false, "unknown", `Rule "${ctx.ruleName}" not found in ${ctx.linterName}.${hint}`);
|
|
525
527
|
}
|
|
526
528
|
}
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// Vigiles-internal assertion catalog
|
|
531
|
+
//
|
|
532
|
+
// The `vigiles/<id>` namespace lets specs declare mechanical checks that
|
|
533
|
+
// vigiles itself runs (orphan docs, integrity, etc.) without delegating to
|
|
534
|
+
// an external linter. Existence is verified at compile time against this
|
|
535
|
+
// fixed catalog; the actual check runs at audit time.
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
const VIGILES_INTERNAL_RULES = new Set(["orphan-docs"]);
|
|
538
|
+
/** @internal */ function tryVigilesInternal(ctx) {
|
|
539
|
+
if (ctx.linterName !== "vigiles")
|
|
540
|
+
return null;
|
|
541
|
+
if (!VIGILES_INTERNAL_RULES.has(ctx.ruleName)) {
|
|
542
|
+
const suggestions = closestRuleNames(ctx.ruleName, VIGILES_INTERNAL_RULES);
|
|
543
|
+
const hint = suggestions.length > 0
|
|
544
|
+
? ` Did you mean: ${suggestions.map((s) => `"vigiles/${s}"`).join(", ")}?`
|
|
545
|
+
: "";
|
|
546
|
+
return makeResult(ctx, false, "unknown", `Vigiles-internal rule "${ctx.ruleName}" not in known catalog (${[...VIGILES_INTERNAL_RULES].join(", ")}).${hint}`);
|
|
547
|
+
}
|
|
548
|
+
return makeResult(ctx, true, "enabled");
|
|
549
|
+
}
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
// Cedar policy resolution (filesystem-based — no Node API, no CLI required)
|
|
552
|
+
//
|
|
553
|
+
// Cedar policies live in .cedar files. A policy is identified by its
|
|
554
|
+
// `@id("name")` annotation when present; otherwise by filename. Presence
|
|
555
|
+
// of a policy in the project counts as "enabled" — Cedar has no separate
|
|
556
|
+
// config layer the way ESLint does, the policy bundle IS the config.
|
|
557
|
+
//
|
|
558
|
+
// Default search dirs: .cedar/ and cedar/ (project root). Override via
|
|
559
|
+
// `options.linters.cedar.rulesDir`.
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
const CEDAR_DEFAULT_DIRS = [".cedar", "cedar"];
|
|
562
|
+
const CEDAR_ID_RE = /@id\("([^"]+)"\)/g;
|
|
563
|
+
const CEDAR_STATEMENT_RE = /\b(?:permit|forbid)\s*\(/;
|
|
564
|
+
const CEDAR_POLICY_CACHE = new Map();
|
|
565
|
+
function cedarCacheKey(basePath, customDirs) {
|
|
566
|
+
const dirs = customDirs
|
|
567
|
+
? Array.isArray(customDirs)
|
|
568
|
+
? customDirs.join("|")
|
|
569
|
+
: customDirs
|
|
570
|
+
: "";
|
|
571
|
+
return `${basePath}::${dirs}`;
|
|
572
|
+
}
|
|
573
|
+
function loadCedarPolicies(basePath, customDirs) {
|
|
574
|
+
const policies = new Set();
|
|
575
|
+
const dirs = customDirs
|
|
576
|
+
? Array.isArray(customDirs)
|
|
577
|
+
? customDirs
|
|
578
|
+
: [customDirs]
|
|
579
|
+
: CEDAR_DEFAULT_DIRS;
|
|
580
|
+
for (const dir of dirs) {
|
|
581
|
+
const fullDir = (0, node_path_1.resolve)(basePath, dir);
|
|
582
|
+
if (!(0, node_fs_1.existsSync)(fullDir))
|
|
583
|
+
continue;
|
|
584
|
+
const files = (0, glob_1.globSync)("**/*.cedar", { cwd: fullDir, nodir: true });
|
|
585
|
+
for (const file of files) {
|
|
586
|
+
let content;
|
|
587
|
+
try {
|
|
588
|
+
content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(fullDir, file), "utf-8");
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const annotated = [...content.matchAll(CEDAR_ID_RE)].map((m) => m[1]);
|
|
594
|
+
if (annotated.length > 0) {
|
|
595
|
+
for (const id of annotated)
|
|
596
|
+
policies.add(id);
|
|
597
|
+
}
|
|
598
|
+
else if (CEDAR_STATEMENT_RE.test(content)) {
|
|
599
|
+
const name = file.replace(/\.cedar$/, "").replace(/\\/g, "/");
|
|
600
|
+
policies.add(name);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return policies;
|
|
605
|
+
}
|
|
606
|
+
function getCedarPolicies(basePath, customDirs) {
|
|
607
|
+
const key = cedarCacheKey(basePath, customDirs);
|
|
608
|
+
const cached = CEDAR_POLICY_CACHE.get(key);
|
|
609
|
+
if (cached)
|
|
610
|
+
return cached;
|
|
611
|
+
const policies = loadCedarPolicies(basePath, customDirs);
|
|
612
|
+
CEDAR_POLICY_CACHE.set(key, policies);
|
|
613
|
+
return policies;
|
|
614
|
+
}
|
|
615
|
+
/** @internal */ function clearCedarCache() {
|
|
616
|
+
CEDAR_POLICY_CACHE.clear();
|
|
617
|
+
}
|
|
618
|
+
/** @internal */ function tryCedarPolicy(ctx) {
|
|
619
|
+
if (ctx.linterName !== "cedar")
|
|
620
|
+
return null;
|
|
621
|
+
const customDirs = ctx.linters?.cedar?.rulesDir;
|
|
622
|
+
const policies = getCedarPolicies(ctx.basePath, customDirs);
|
|
623
|
+
if (policies.size === 0) {
|
|
624
|
+
return makeResult(ctx, false, "unknown", customDirs
|
|
625
|
+
? `No Cedar policies found in configured rulesDir.`
|
|
626
|
+
: `No Cedar policies found. Add .cedar files under .cedar/ or cedar/, or set linters.cedar.rulesDir.`);
|
|
627
|
+
}
|
|
628
|
+
if (!policies.has(ctx.ruleName)) {
|
|
629
|
+
const suggestions = closestRuleNames(ctx.ruleName, policies);
|
|
630
|
+
const hint = suggestions.length > 0
|
|
631
|
+
? ` Did you mean: ${suggestions.map((s) => `"cedar/${s}"`).join(", ")}?`
|
|
632
|
+
: "";
|
|
633
|
+
return makeResult(ctx, false, "unknown", `Cedar policy "${ctx.ruleName}" not found.${hint}`);
|
|
634
|
+
}
|
|
635
|
+
return makeResult(ctx, true, "enabled");
|
|
636
|
+
}
|
|
527
637
|
/** @internal */ function tryCustomRulesDir(ctx) {
|
|
528
638
|
const linterCfg = ctx.linters?.[ctx.linterName];
|
|
529
639
|
if (!linterCfg?.rulesDir)
|
|
@@ -566,9 +676,11 @@ function checkLinterRule(enforcedBy, basePath, options) {
|
|
|
566
676
|
catalogOnly: options?.catalogOnly,
|
|
567
677
|
linters: options?.linters,
|
|
568
678
|
};
|
|
569
|
-
return (
|
|
679
|
+
return (tryVigilesInternal(ctx) ??
|
|
680
|
+
tryNodeResolver(ctx) ??
|
|
570
681
|
tryScopedPlugin(ctx) ??
|
|
571
682
|
tryCliCheck(ctx) ??
|
|
683
|
+
tryCedarPolicy(ctx) ??
|
|
572
684
|
tryCustomRulesDir(ctx) ??
|
|
573
685
|
makeResult(ctx, false, "unknown", `Unknown linter: "${linterName}"`));
|
|
574
686
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphan-docs detection: the inverse of stale-reference validation.
|
|
3
|
+
*
|
|
4
|
+
* Stale-ref detection catches specs that point at files which no longer
|
|
5
|
+
* exist. Orphan detection catches files which exist but no spec or README
|
|
6
|
+
* points at — docs that quietly rot in `docs/` and `research/` because
|
|
7
|
+
* nothing tells the agent they're still load-bearing.
|
|
8
|
+
*
|
|
9
|
+
* The detector operates purely on the filesystem: it enumerates markdown
|
|
10
|
+
* files under configured doc roots and scans every `.md` in the repo for
|
|
11
|
+
* references (markdown links and backtick paths). Works against source
|
|
12
|
+
* README plus compiled CLAUDE.md — no spec loading required.
|
|
13
|
+
*/
|
|
14
|
+
export interface OrphanReport {
|
|
15
|
+
/** Include globs that were scanned. */
|
|
16
|
+
readonly include: readonly string[];
|
|
17
|
+
/** Total docs discovered under those globs. */
|
|
18
|
+
readonly totalDocs: number;
|
|
19
|
+
/** Docs referenced from at least one other `.md` file. */
|
|
20
|
+
readonly referencedDocs: readonly string[];
|
|
21
|
+
/** Docs that exist but no other `.md` references them. */
|
|
22
|
+
readonly orphans: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface FindOrphansOptions {
|
|
25
|
+
/** Repository root. Defaults to `process.cwd()`. */
|
|
26
|
+
readonly basePath?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Glob patterns of `.md` files to scan. Defaults to
|
|
29
|
+
* `["docs/**\/*.md", "research/**\/*.md"]` — vigiles-repo convention.
|
|
30
|
+
* Set to `[]` to disable scanning entirely. Set to your project's
|
|
31
|
+
* doc directory globs (e.g. `["wiki/**\/*.md"]`) to override.
|
|
32
|
+
*/
|
|
33
|
+
readonly include?: readonly string[];
|
|
34
|
+
/** Glob patterns to exclude within the include scope. */
|
|
35
|
+
readonly exclude?: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Find docs under `include` globs that no other markdown file references.
|
|
39
|
+
*
|
|
40
|
+
* A doc is considered referenced when some OTHER `.md` file in the repo
|
|
41
|
+
* links to it via `[text](path.md)` or mentions it in a backtick span like
|
|
42
|
+
* `` `docs/foo.md` ``. Self-references don't count — an orphan that only
|
|
43
|
+
* links to itself is still an orphan.
|
|
44
|
+
*
|
|
45
|
+
* `include` and `exclude` are tsconfig-style glob arrays. Default include
|
|
46
|
+
* is the vigiles-repo convention `["docs/**\/*.md", "research/**\/*.md"]`;
|
|
47
|
+
* override per-project via `.vigilesrc.json` → `orphans.include`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function findOrphanDocs(options?: FindOrphansOptions): OrphanReport;
|
|
50
|
+
/** Format an orphan report as human-readable text. */
|
|
51
|
+
export declare function formatOrphanReport(report: OrphanReport): string;
|
|
52
|
+
//# sourceMappingURL=orphans.d.ts.map
|
package/dist/orphans.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Orphan-docs detection: the inverse of stale-reference validation.
|
|
4
|
+
*
|
|
5
|
+
* Stale-ref detection catches specs that point at files which no longer
|
|
6
|
+
* exist. Orphan detection catches files which exist but no spec or README
|
|
7
|
+
* points at — docs that quietly rot in `docs/` and `research/` because
|
|
8
|
+
* nothing tells the agent they're still load-bearing.
|
|
9
|
+
*
|
|
10
|
+
* The detector operates purely on the filesystem: it enumerates markdown
|
|
11
|
+
* files under configured doc roots and scans every `.md` in the repo for
|
|
12
|
+
* references (markdown links and backtick paths). Works against source
|
|
13
|
+
* README plus compiled CLAUDE.md — no spec loading required.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.findOrphanDocs = findOrphanDocs;
|
|
17
|
+
exports.formatOrphanReport = formatOrphanReport;
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const node_path_1 = require("node:path");
|
|
20
|
+
const glob_1 = require("glob");
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Internals
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const DEFAULT_INCLUDE = ["docs/**/*.md", "research/**/*.md"];
|
|
25
|
+
const DEFAULT_IGNORE = [
|
|
26
|
+
"node_modules/**",
|
|
27
|
+
"dist/**",
|
|
28
|
+
".vigiles/**",
|
|
29
|
+
".git/**",
|
|
30
|
+
];
|
|
31
|
+
// Match markdown links ](path.md) or ](path.md#anchor)
|
|
32
|
+
const LINK_RE = /\]\(([^)\s]+\.md)(?:#[^)]*)?\)/g;
|
|
33
|
+
// Match backtick code spans wrapping a path ending in .md
|
|
34
|
+
const BACKTICK_RE = /`([^`\s]+\.md)`/g;
|
|
35
|
+
function normalizePath(p) {
|
|
36
|
+
return p.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
37
|
+
}
|
|
38
|
+
function extractRefs(content) {
|
|
39
|
+
const refs = [];
|
|
40
|
+
for (const m of content.matchAll(LINK_RE))
|
|
41
|
+
refs.push(normalizePath(m[1]));
|
|
42
|
+
for (const m of content.matchAll(BACKTICK_RE))
|
|
43
|
+
refs.push(normalizePath(m[1]));
|
|
44
|
+
return refs;
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Public API
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Find docs under `include` globs that no other markdown file references.
|
|
51
|
+
*
|
|
52
|
+
* A doc is considered referenced when some OTHER `.md` file in the repo
|
|
53
|
+
* links to it via `[text](path.md)` or mentions it in a backtick span like
|
|
54
|
+
* `` `docs/foo.md` ``. Self-references don't count — an orphan that only
|
|
55
|
+
* links to itself is still an orphan.
|
|
56
|
+
*
|
|
57
|
+
* `include` and `exclude` are tsconfig-style glob arrays. Default include
|
|
58
|
+
* is the vigiles-repo convention `["docs/**\/*.md", "research/**\/*.md"]`;
|
|
59
|
+
* override per-project via `.vigilesrc.json` → `orphans.include`.
|
|
60
|
+
*/
|
|
61
|
+
function findOrphanDocs(options = {}) {
|
|
62
|
+
const basePath = options.basePath ?? process.cwd();
|
|
63
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
64
|
+
const userExclude = options.exclude ?? [];
|
|
65
|
+
const ignore = [...DEFAULT_IGNORE, ...userExclude];
|
|
66
|
+
const allDocs = new Set();
|
|
67
|
+
for (const pattern of include) {
|
|
68
|
+
const found = (0, glob_1.globSync)(pattern, { cwd: basePath, ignore });
|
|
69
|
+
for (const p of found)
|
|
70
|
+
allDocs.add(normalizePath(p));
|
|
71
|
+
}
|
|
72
|
+
const allMarkdown = (0, glob_1.globSync)("**/*.md", {
|
|
73
|
+
cwd: basePath,
|
|
74
|
+
ignore: [...DEFAULT_IGNORE],
|
|
75
|
+
});
|
|
76
|
+
const referencedBy = new Map();
|
|
77
|
+
for (const mdPath of allMarkdown) {
|
|
78
|
+
const source = normalizePath(mdPath);
|
|
79
|
+
let content;
|
|
80
|
+
try {
|
|
81
|
+
content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(basePath, mdPath), "utf-8");
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const target of extractRefs(content)) {
|
|
87
|
+
if (target === source)
|
|
88
|
+
continue;
|
|
89
|
+
let sources = referencedBy.get(target);
|
|
90
|
+
if (!sources) {
|
|
91
|
+
sources = new Set();
|
|
92
|
+
referencedBy.set(target, sources);
|
|
93
|
+
}
|
|
94
|
+
sources.add(source);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const orphans = [];
|
|
98
|
+
const referencedDocs = [];
|
|
99
|
+
for (const doc of [...allDocs].sort()) {
|
|
100
|
+
if (referencedBy.has(doc))
|
|
101
|
+
referencedDocs.push(doc);
|
|
102
|
+
else
|
|
103
|
+
orphans.push(doc);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
include: [...include],
|
|
107
|
+
totalDocs: allDocs.size,
|
|
108
|
+
referencedDocs,
|
|
109
|
+
orphans,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Format an orphan report as human-readable text. */
|
|
113
|
+
function formatOrphanReport(report) {
|
|
114
|
+
if (report.orphans.length === 0) {
|
|
115
|
+
return `✓ no orphan docs (${String(report.totalDocs)} scanned across ${report.include.join(", ") || "no include patterns"})`;
|
|
116
|
+
}
|
|
117
|
+
const lines = [
|
|
118
|
+
`✗ ${String(report.orphans.length)} orphan doc(s) — referenced by no other .md:`,
|
|
119
|
+
];
|
|
120
|
+
for (const o of report.orphans)
|
|
121
|
+
lines.push(` ${o}`);
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=orphans.js.map
|
package/dist/proofs.js
CHANGED
|
@@ -23,20 +23,22 @@ exports.ruleToBloomFilter = ruleToBloomFilter;
|
|
|
23
23
|
exports.fixedPoint = fixedPoint;
|
|
24
24
|
exports.propertyTest = propertyTest;
|
|
25
25
|
exports.fitness = fitness;
|
|
26
|
-
const node_crypto_1 = require("node:crypto");
|
|
27
26
|
const node_zlib_1 = require("node:zlib");
|
|
27
|
+
const hash_js_1 = require("./hash.js");
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
// 1. Monotonicity Lattice — partial order on rule strength
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
/**
|
|
32
32
|
* Ordinal strength of each rule kind.
|
|
33
33
|
*
|
|
34
|
-
* guidance (0) < enforce (1)
|
|
34
|
+
* guidance (0) < guard (1) = enforce (1)
|
|
35
35
|
*
|
|
36
|
-
*
|
|
36
|
+
* Guard and enforce are both mechanically enforced (different mechanisms,
|
|
37
|
+
* same strength). The lattice ensures specs only get stricter over time.
|
|
37
38
|
*/
|
|
38
39
|
const STRENGTH = {
|
|
39
40
|
guidance: 0,
|
|
41
|
+
guard: 1,
|
|
40
42
|
enforce: 1,
|
|
41
43
|
};
|
|
42
44
|
/**
|
|
@@ -194,11 +196,14 @@ function ruleToText(rule) {
|
|
|
194
196
|
return `${rule.linterRule} ${rule.why}`;
|
|
195
197
|
case "guidance":
|
|
196
198
|
return rule.text;
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
case "guard": {
|
|
200
|
+
const patterns = Array.isArray(rule.watch)
|
|
201
|
+
? rule.watch.join(" ")
|
|
202
|
+
: rule.watch;
|
|
203
|
+
return `${patterns} ${rule.run} ${rule.description}`;
|
|
201
204
|
}
|
|
205
|
+
default:
|
|
206
|
+
return (0, hash_js_1.assertNever)(rule);
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
// ---------------------------------------------------------------------------
|
|
@@ -343,10 +348,10 @@ function fixedPoint(compileFn, initialContent, maxIterations = 10) {
|
|
|
343
348
|
const hashes = [];
|
|
344
349
|
let current = initialContent;
|
|
345
350
|
for (let i = 0; i < maxIterations; i++) {
|
|
346
|
-
const hash = sha256short(current);
|
|
351
|
+
const hash = (0, hash_js_1.sha256short)(current);
|
|
347
352
|
hashes.push(hash);
|
|
348
353
|
const next = compileFn(current);
|
|
349
|
-
const nextHash = sha256short(next);
|
|
354
|
+
const nextHash = (0, hash_js_1.sha256short)(next);
|
|
350
355
|
// Fixed point: output === input
|
|
351
356
|
if (nextHash === hash) {
|
|
352
357
|
return { converged: true, iterations: i + 1, hashes, cycleLength: 0 };
|
|
@@ -370,10 +375,7 @@ function fixedPoint(compileFn, initialContent, maxIterations = 10) {
|
|
|
370
375
|
cycleLength: 0,
|
|
371
376
|
};
|
|
372
377
|
}
|
|
373
|
-
|
|
374
|
-
function sha256short(data) {
|
|
375
|
-
return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex").slice(0, 16);
|
|
376
|
-
}
|
|
378
|
+
// sha256short imported from ./hash.js
|
|
377
379
|
/** Compute the hash of a HistoryNode (excluding the hash field itself). */
|
|
378
380
|
function computeNodeHash(node) {
|
|
379
381
|
const payload = JSON.stringify({
|
|
@@ -383,7 +385,7 @@ function computeNodeHash(node) {
|
|
|
383
385
|
proofs: node.proofs,
|
|
384
386
|
timestamp: node.timestamp,
|
|
385
387
|
});
|
|
386
|
-
return sha256short(payload);
|
|
388
|
+
return (0, hash_js_1.sha256short)(payload);
|
|
387
389
|
}
|
|
388
390
|
class MerkleHistory {
|
|
389
391
|
nodes = [];
|
|
@@ -604,8 +606,12 @@ function fitness(spec, options = {}) {
|
|
|
604
606
|
if (total === 0) {
|
|
605
607
|
return { score: 0, coverage: 0, redundancy: 0, budgetPressure: 0 };
|
|
606
608
|
}
|
|
607
|
-
// Coverage: fraction with teeth (
|
|
608
|
-
|
|
609
|
+
// Coverage: fraction with teeth (mechanically enforced — STRENGTH ≥ 1).
|
|
610
|
+
// Counts both enforce() (linter-verified) and guard() (file-watch reactive).
|
|
611
|
+
// Without guard here, mutations that introduce/keep guard rules look like
|
|
612
|
+
// they reduce coverage and get rejected by runProofSuite even though
|
|
613
|
+
// enforcement strength didn't drop (see proofs.ts STRENGTH lattice).
|
|
614
|
+
const enforced = rules.filter((r) => STRENGTH[r._kind] >= 1).length;
|
|
609
615
|
const coverage = enforced / total;
|
|
610
616
|
// Redundancy: fraction of pairs that are near-duplicates
|
|
611
617
|
const similarPairs = findSimilarRules(spec.rules, ncdThreshold);
|