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/linters.js
CHANGED
|
@@ -4,20 +4,50 @@
|
|
|
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");
|
|
18
20
|
const node_child_process_1 = require("node:child_process");
|
|
19
21
|
const node_module_1 = require("node:module");
|
|
20
22
|
const glob_1 = require("glob");
|
|
23
|
+
/**
|
|
24
|
+
* Prepend version-manager shim directories (rbenv / asdf / rvm) to PATH so
|
|
25
|
+
* gem/pip-installed linters are found in non-login shells and CI, where the
|
|
26
|
+
* shims dir is often missing from PATH even though the tool is installed.
|
|
27
|
+
* Runs once; only adds directories that exist and aren't already present.
|
|
28
|
+
*/
|
|
29
|
+
function augmentToolPath() {
|
|
30
|
+
const home = process.env.HOME ?? "";
|
|
31
|
+
// rbenv/asdf shims don't resolve without a selected version, so add the
|
|
32
|
+
// concrete per-version `bin` dirs (where the gem executables actually live).
|
|
33
|
+
const candidates = [
|
|
34
|
+
...(0, glob_1.globSync)("/opt/rbenv/versions/*/bin", { nodir: false }),
|
|
35
|
+
...(home
|
|
36
|
+
? (0, glob_1.globSync)(`${home}/.rbenv/versions/*/bin`, { nodir: false })
|
|
37
|
+
: []),
|
|
38
|
+
...(home
|
|
39
|
+
? (0, glob_1.globSync)(`${home}/.asdf/installs/*/*/bin`, { nodir: false })
|
|
40
|
+
: []),
|
|
41
|
+
`${home}/.rvm/bin`,
|
|
42
|
+
`${home}/.local/bin`,
|
|
43
|
+
];
|
|
44
|
+
const current = (process.env.PATH ?? "").split(":");
|
|
45
|
+
const additions = candidates.filter((d) => d && (0, node_fs_1.existsSync)(d) && !current.includes(d));
|
|
46
|
+
if (additions.length > 0) {
|
|
47
|
+
process.env.PATH = [...current, ...additions].join(":");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
augmentToolPath();
|
|
21
51
|
// ---------------------------------------------------------------------------
|
|
22
52
|
// Parsing enforcement references
|
|
23
53
|
// ---------------------------------------------------------------------------
|
|
@@ -524,6 +554,114 @@ function getCliRuleSet(linterName, basePath) {
|
|
|
524
554
|
return makeResult(ctx, false, "unknown", `Rule "${ctx.ruleName}" not found in ${ctx.linterName}.${hint}`);
|
|
525
555
|
}
|
|
526
556
|
}
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Vigiles-internal assertion catalog
|
|
559
|
+
//
|
|
560
|
+
// The `vigiles/<id>` namespace lets specs declare mechanical checks that
|
|
561
|
+
// vigiles itself runs (orphan docs, integrity, etc.) without delegating to
|
|
562
|
+
// an external linter. Existence is verified at compile time against this
|
|
563
|
+
// fixed catalog; the actual check runs at audit time.
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
const VIGILES_INTERNAL_RULES = new Set(["orphan-docs"]);
|
|
566
|
+
/** @internal */ function tryVigilesInternal(ctx) {
|
|
567
|
+
if (ctx.linterName !== "vigiles")
|
|
568
|
+
return null;
|
|
569
|
+
if (!VIGILES_INTERNAL_RULES.has(ctx.ruleName)) {
|
|
570
|
+
const suggestions = closestRuleNames(ctx.ruleName, VIGILES_INTERNAL_RULES);
|
|
571
|
+
const hint = suggestions.length > 0
|
|
572
|
+
? ` Did you mean: ${suggestions.map((s) => `"vigiles/${s}"`).join(", ")}?`
|
|
573
|
+
: "";
|
|
574
|
+
return makeResult(ctx, false, "unknown", `Vigiles-internal rule "${ctx.ruleName}" not in known catalog (${[...VIGILES_INTERNAL_RULES].join(", ")}).${hint}`);
|
|
575
|
+
}
|
|
576
|
+
return makeResult(ctx, true, "enabled");
|
|
577
|
+
}
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// Cedar policy resolution (filesystem-based — no Node API, no CLI required)
|
|
580
|
+
//
|
|
581
|
+
// Cedar policies live in .cedar files. A policy is identified by its
|
|
582
|
+
// `@id("name")` annotation when present; otherwise by filename. Presence
|
|
583
|
+
// of a policy in the project counts as "enabled" — Cedar has no separate
|
|
584
|
+
// config layer the way ESLint does, the policy bundle IS the config.
|
|
585
|
+
//
|
|
586
|
+
// Default search dirs: .cedar/ and cedar/ (project root). Override via
|
|
587
|
+
// `options.linters.cedar.rulesDir`.
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
const CEDAR_DEFAULT_DIRS = [".cedar", "cedar"];
|
|
590
|
+
const CEDAR_ID_RE = /@id\("([^"]+)"\)/g;
|
|
591
|
+
const CEDAR_STATEMENT_RE = /\b(?:permit|forbid)\s*\(/;
|
|
592
|
+
const CEDAR_POLICY_CACHE = new Map();
|
|
593
|
+
function cedarCacheKey(basePath, customDirs) {
|
|
594
|
+
const dirs = customDirs
|
|
595
|
+
? Array.isArray(customDirs)
|
|
596
|
+
? customDirs.join("|")
|
|
597
|
+
: customDirs
|
|
598
|
+
: "";
|
|
599
|
+
return `${basePath}::${dirs}`;
|
|
600
|
+
}
|
|
601
|
+
function loadCedarPolicies(basePath, customDirs) {
|
|
602
|
+
const policies = new Set();
|
|
603
|
+
const dirs = customDirs
|
|
604
|
+
? Array.isArray(customDirs)
|
|
605
|
+
? customDirs
|
|
606
|
+
: [customDirs]
|
|
607
|
+
: CEDAR_DEFAULT_DIRS;
|
|
608
|
+
for (const dir of dirs) {
|
|
609
|
+
const fullDir = (0, node_path_1.resolve)(basePath, dir);
|
|
610
|
+
if (!(0, node_fs_1.existsSync)(fullDir))
|
|
611
|
+
continue;
|
|
612
|
+
const files = (0, glob_1.globSync)("**/*.cedar", { cwd: fullDir, nodir: true });
|
|
613
|
+
for (const file of files) {
|
|
614
|
+
let content;
|
|
615
|
+
try {
|
|
616
|
+
content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(fullDir, file), "utf-8");
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const annotated = [...content.matchAll(CEDAR_ID_RE)].map((m) => m[1]);
|
|
622
|
+
if (annotated.length > 0) {
|
|
623
|
+
for (const id of annotated)
|
|
624
|
+
policies.add(id);
|
|
625
|
+
}
|
|
626
|
+
else if (CEDAR_STATEMENT_RE.test(content)) {
|
|
627
|
+
const name = file.replace(/\.cedar$/, "").replace(/\\/g, "/");
|
|
628
|
+
policies.add(name);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return policies;
|
|
633
|
+
}
|
|
634
|
+
function getCedarPolicies(basePath, customDirs) {
|
|
635
|
+
const key = cedarCacheKey(basePath, customDirs);
|
|
636
|
+
const cached = CEDAR_POLICY_CACHE.get(key);
|
|
637
|
+
if (cached)
|
|
638
|
+
return cached;
|
|
639
|
+
const policies = loadCedarPolicies(basePath, customDirs);
|
|
640
|
+
CEDAR_POLICY_CACHE.set(key, policies);
|
|
641
|
+
return policies;
|
|
642
|
+
}
|
|
643
|
+
/** @internal */ function clearCedarCache() {
|
|
644
|
+
CEDAR_POLICY_CACHE.clear();
|
|
645
|
+
}
|
|
646
|
+
/** @internal */ function tryCedarPolicy(ctx) {
|
|
647
|
+
if (ctx.linterName !== "cedar")
|
|
648
|
+
return null;
|
|
649
|
+
const customDirs = ctx.linters?.cedar?.rulesDir;
|
|
650
|
+
const policies = getCedarPolicies(ctx.basePath, customDirs);
|
|
651
|
+
if (policies.size === 0) {
|
|
652
|
+
return makeResult(ctx, false, "unknown", customDirs
|
|
653
|
+
? `No Cedar policies found in configured rulesDir.`
|
|
654
|
+
: `No Cedar policies found. Add .cedar files under .cedar/ or cedar/, or set linters.cedar.rulesDir.`);
|
|
655
|
+
}
|
|
656
|
+
if (!policies.has(ctx.ruleName)) {
|
|
657
|
+
const suggestions = closestRuleNames(ctx.ruleName, policies);
|
|
658
|
+
const hint = suggestions.length > 0
|
|
659
|
+
? ` Did you mean: ${suggestions.map((s) => `"cedar/${s}"`).join(", ")}?`
|
|
660
|
+
: "";
|
|
661
|
+
return makeResult(ctx, false, "unknown", `Cedar policy "${ctx.ruleName}" not found.${hint}`);
|
|
662
|
+
}
|
|
663
|
+
return makeResult(ctx, true, "enabled");
|
|
664
|
+
}
|
|
527
665
|
/** @internal */ function tryCustomRulesDir(ctx) {
|
|
528
666
|
const linterCfg = ctx.linters?.[ctx.linterName];
|
|
529
667
|
if (!linterCfg?.rulesDir)
|
|
@@ -566,9 +704,11 @@ function checkLinterRule(enforcedBy, basePath, options) {
|
|
|
566
704
|
catalogOnly: options?.catalogOnly,
|
|
567
705
|
linters: options?.linters,
|
|
568
706
|
};
|
|
569
|
-
return (
|
|
707
|
+
return (tryVigilesInternal(ctx) ??
|
|
708
|
+
tryNodeResolver(ctx) ??
|
|
570
709
|
tryScopedPlugin(ctx) ??
|
|
571
710
|
tryCliCheck(ctx) ??
|
|
711
|
+
tryCedarPolicy(ctx) ??
|
|
572
712
|
tryCustomRulesDir(ctx) ??
|
|
573
713
|
makeResult(ctx, false, "unknown", `Unknown linter: "${linterName}"`));
|
|
574
714
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** One scripted assistant turn: a final text answer, or a tool call. */
|
|
2
|
+
export interface ModelTurn {
|
|
3
|
+
/** Final text answer (stops the turn). */
|
|
4
|
+
readonly text?: string;
|
|
5
|
+
/** A tool to invoke, e.g. "Bash" | "Write" | "Edit". */
|
|
6
|
+
readonly tool?: string;
|
|
7
|
+
/** The tool input, e.g. `{ file_path, content }` or `{ command }`. */
|
|
8
|
+
readonly input?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** Build a scripted model from an ordered list of turns. */
|
|
11
|
+
export declare function scriptModel(turns: readonly ModelTurn[]): ModelTurn[];
|
|
12
|
+
export interface TurnInfo {
|
|
13
|
+
readonly n: number;
|
|
14
|
+
readonly stream: boolean;
|
|
15
|
+
readonly hasToolResult: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface MockHandle {
|
|
18
|
+
readonly url: string;
|
|
19
|
+
close(): void;
|
|
20
|
+
/** Number of model turns served so far. */
|
|
21
|
+
readonly count: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start the scripted mock on a free port. Each `/v1/messages` POST consumes the
|
|
25
|
+
* next turn (the last turn repeats if the client asks for more). Resolves to a
|
|
26
|
+
* handle with the base `url` and a `close()`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function startMock(script: readonly ModelTurn[], opts?: {
|
|
29
|
+
onTurn?: (info: TurnInfo) => void;
|
|
30
|
+
}): Promise<MockHandle>;
|
|
31
|
+
//# sourceMappingURL=mock-model.d.ts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scriptModel = scriptModel;
|
|
7
|
+
exports.startMock = startMock;
|
|
8
|
+
/**
|
|
9
|
+
* vigiles — a scripted, deterministic Anthropic Messages API mock.
|
|
10
|
+
*
|
|
11
|
+
* Point a Claude Code client at it with `ANTHROPIC_BASE_URL` (and any dummy
|
|
12
|
+
* `ANTHROPIC_API_KEY`) and it serves a fixed *script* of model turns in order —
|
|
13
|
+
* each `POST /v1/messages` returns the next. This is the seam that makes harness
|
|
14
|
+
* testing deterministic: the real `claude` CLI runs your real hooks/settings,
|
|
15
|
+
* but the model's turns are scripted, so the outcome is reproducible and free.
|
|
16
|
+
*
|
|
17
|
+
* scriptModel([
|
|
18
|
+
* { tool: "Write", input: { file_path: "SKILL.md", content: "..." } },
|
|
19
|
+
* { text: "done" },
|
|
20
|
+
* ])
|
|
21
|
+
*
|
|
22
|
+
* Implements the parts a real client needs: SSE streaming (flushed per event),
|
|
23
|
+
* `/v1/messages/count_tokens` (else Claude Code hangs), HEAD/health tolerance,
|
|
24
|
+
* and echoing the requested model.
|
|
25
|
+
*/
|
|
26
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
27
|
+
/** Build a scripted model from an ordered list of turns. */
|
|
28
|
+
function scriptModel(turns) {
|
|
29
|
+
return [...turns];
|
|
30
|
+
}
|
|
31
|
+
function writeEvent(res, event, data) {
|
|
32
|
+
res.write(`event: ${event}\n`);
|
|
33
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
34
|
+
}
|
|
35
|
+
const rid = (p) => `${p}${Math.random().toString(36).slice(2, 10)}`;
|
|
36
|
+
function streamTurn(res, turn, model) {
|
|
37
|
+
res.writeHead(200, {
|
|
38
|
+
"content-type": "text/event-stream",
|
|
39
|
+
"cache-control": "no-cache",
|
|
40
|
+
connection: "keep-alive",
|
|
41
|
+
});
|
|
42
|
+
writeEvent(res, "message_start", {
|
|
43
|
+
type: "message_start",
|
|
44
|
+
message: {
|
|
45
|
+
id: rid("msg_"),
|
|
46
|
+
type: "message",
|
|
47
|
+
role: "assistant",
|
|
48
|
+
model,
|
|
49
|
+
content: [],
|
|
50
|
+
stop_reason: null,
|
|
51
|
+
usage: { input_tokens: 10, output_tokens: 1 },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (turn.tool) {
|
|
55
|
+
writeEvent(res, "content_block_start", {
|
|
56
|
+
type: "content_block_start",
|
|
57
|
+
index: 0,
|
|
58
|
+
content_block: {
|
|
59
|
+
type: "tool_use",
|
|
60
|
+
id: rid("toolu_"),
|
|
61
|
+
name: turn.tool,
|
|
62
|
+
input: {},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
writeEvent(res, "content_block_delta", {
|
|
66
|
+
type: "content_block_delta",
|
|
67
|
+
index: 0,
|
|
68
|
+
delta: {
|
|
69
|
+
type: "input_json_delta",
|
|
70
|
+
partial_json: JSON.stringify(turn.input ?? {}),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
writeEvent(res, "content_block_stop", {
|
|
74
|
+
type: "content_block_stop",
|
|
75
|
+
index: 0,
|
|
76
|
+
});
|
|
77
|
+
writeEvent(res, "message_delta", {
|
|
78
|
+
type: "message_delta",
|
|
79
|
+
delta: { stop_reason: "tool_use", stop_sequence: null },
|
|
80
|
+
usage: { output_tokens: 5 },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
writeEvent(res, "content_block_start", {
|
|
85
|
+
type: "content_block_start",
|
|
86
|
+
index: 0,
|
|
87
|
+
content_block: { type: "text", text: "" },
|
|
88
|
+
});
|
|
89
|
+
writeEvent(res, "content_block_delta", {
|
|
90
|
+
type: "content_block_delta",
|
|
91
|
+
index: 0,
|
|
92
|
+
delta: { type: "text_delta", text: turn.text ?? "" },
|
|
93
|
+
});
|
|
94
|
+
writeEvent(res, "content_block_stop", {
|
|
95
|
+
type: "content_block_stop",
|
|
96
|
+
index: 0,
|
|
97
|
+
});
|
|
98
|
+
writeEvent(res, "message_delta", {
|
|
99
|
+
type: "message_delta",
|
|
100
|
+
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
101
|
+
usage: { output_tokens: 5 },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
writeEvent(res, "message_stop", { type: "message_stop" });
|
|
105
|
+
res.end();
|
|
106
|
+
}
|
|
107
|
+
function jsonTurn(res, turn, model) {
|
|
108
|
+
const content = turn.tool
|
|
109
|
+
? [
|
|
110
|
+
{
|
|
111
|
+
type: "tool_use",
|
|
112
|
+
id: rid("toolu_"),
|
|
113
|
+
name: turn.tool,
|
|
114
|
+
input: turn.input ?? {},
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
: [{ type: "text", text: turn.text ?? "" }];
|
|
118
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
119
|
+
res.end(JSON.stringify({
|
|
120
|
+
id: rid("msg_"),
|
|
121
|
+
type: "message",
|
|
122
|
+
role: "assistant",
|
|
123
|
+
model,
|
|
124
|
+
stop_reason: turn.tool ? "tool_use" : "end_turn",
|
|
125
|
+
stop_sequence: null,
|
|
126
|
+
content,
|
|
127
|
+
usage: { input_tokens: 10, output_tokens: 5 },
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Start the scripted mock on a free port. Each `/v1/messages` POST consumes the
|
|
132
|
+
* next turn (the last turn repeats if the client asks for more). Resolves to a
|
|
133
|
+
* handle with the base `url` and a `close()`.
|
|
134
|
+
*/
|
|
135
|
+
function startMock(script, opts = {}) {
|
|
136
|
+
let i = 0;
|
|
137
|
+
const server = node_http_1.default.createServer((req, res) => {
|
|
138
|
+
let body = "";
|
|
139
|
+
req.on("data", (c) => (body += c));
|
|
140
|
+
req.on("end", () => {
|
|
141
|
+
const url = req.url ?? "";
|
|
142
|
+
const isCount = url.includes("count_tokens");
|
|
143
|
+
const isMessages = url.includes("/v1/messages") && !isCount;
|
|
144
|
+
let reqBody = {};
|
|
145
|
+
try {
|
|
146
|
+
reqBody = JSON.parse(body);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* HEAD / health checks have no JSON body */
|
|
150
|
+
}
|
|
151
|
+
if (req.method === "HEAD" || (!isMessages && !isCount)) {
|
|
152
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
153
|
+
res.end("{}");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (isCount) {
|
|
157
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
158
|
+
res.end(JSON.stringify({ input_tokens: 10 }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const last = JSON.stringify(reqBody.messages?.at(-1)?.content ?? "");
|
|
162
|
+
opts.onTurn?.({
|
|
163
|
+
n: i,
|
|
164
|
+
stream: reqBody.stream === true,
|
|
165
|
+
hasToolResult: last.includes('"tool_result"'),
|
|
166
|
+
});
|
|
167
|
+
const turn = script[Math.min(i, script.length - 1)] ?? { text: "" };
|
|
168
|
+
i++;
|
|
169
|
+
const model = reqBody.model ?? "claude-mock";
|
|
170
|
+
if (reqBody.stream === true)
|
|
171
|
+
streamTurn(res, turn, model);
|
|
172
|
+
else
|
|
173
|
+
jsonTurn(res, turn, model);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
server.listen(0, "127.0.0.1", () => {
|
|
178
|
+
const { port } = server.address();
|
|
179
|
+
resolve({
|
|
180
|
+
url: `http://127.0.0.1:${String(port)}`,
|
|
181
|
+
close: () => server.close(),
|
|
182
|
+
get count() {
|
|
183
|
+
return i;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=mock-model.js.map
|
|
@@ -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);
|