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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-session audit: analyze what an agent changed vs. what specs expected.
|
|
3
|
+
*
|
|
4
|
+
* Compares the git diff (files changed since a base ref) against the spec
|
|
5
|
+
* surface area (keyFiles, commands, tracked inputs). Reports:
|
|
6
|
+
* - Files modified that aren't in any spec's keyFiles
|
|
7
|
+
* - Specs whose inputs changed (may need recompile)
|
|
8
|
+
*
|
|
9
|
+
* Read-only analysis. No commands executed, no files modified.
|
|
10
|
+
*/
|
|
11
|
+
import type { SidecarManifest } from "./sidecar.js";
|
|
12
|
+
/**
|
|
13
|
+
* Get files changed since a base ref (commit, branch, or HEAD~N).
|
|
14
|
+
* Returns paths relative to the repo root.
|
|
15
|
+
*
|
|
16
|
+
* Uses execFileSync with an argv array — `baseRef` is passed as a separate
|
|
17
|
+
* argument so shell metacharacters can't escape into command execution.
|
|
18
|
+
* `--no-renames` keeps both source and destination paths visible (without it,
|
|
19
|
+
* `git mv old.ts new.ts` would only report `new.ts`).
|
|
20
|
+
*
|
|
21
|
+
* Throws on git failure (e.g., invalid baseRef) so a typo doesn't silently
|
|
22
|
+
* masquerade as "no changes" and suppress real findings downstream.
|
|
23
|
+
*/
|
|
24
|
+
export declare function gitChangedFiles(basePath: string, baseRef: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Get the most recent commit hash. Returns null if not a git repo.
|
|
27
|
+
*/
|
|
28
|
+
export declare function gitHead(basePath: string): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Find a reasonable base ref for session analysis.
|
|
31
|
+
*
|
|
32
|
+
* Strategy:
|
|
33
|
+
* 1. If `--base` is provided, use it directly.
|
|
34
|
+
* 2. If sidecar manifests exist, use the most recent compiledAt commit.
|
|
35
|
+
* 3. Fall back to HEAD (shows only uncommitted changes).
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveBaseRef(explicitBase?: string): string;
|
|
38
|
+
/** Aggregated surface area from all specs via sidecar manifests. */
|
|
39
|
+
export interface SpecSurface {
|
|
40
|
+
/** All files declared in any spec's keyFiles. */
|
|
41
|
+
keyFiles: Set<string>;
|
|
42
|
+
/** All files tracked as inputs to any spec. */
|
|
43
|
+
trackedInputs: Set<string>;
|
|
44
|
+
/** All spec source files. */
|
|
45
|
+
specFiles: Set<string>;
|
|
46
|
+
/** All compiled targets. */
|
|
47
|
+
targets: Set<string>;
|
|
48
|
+
/** Map: file → which spec targets track it. */
|
|
49
|
+
fileToSpecs: Map<string, string[]>;
|
|
50
|
+
/** Loaded manifests. */
|
|
51
|
+
manifests: SidecarManifest[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Load all sidecar manifests and build the aggregate spec surface area.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadSpecSurface(basePath: string): SpecSurface;
|
|
57
|
+
/**
|
|
58
|
+
* Load keyFiles from compiled markdown files. Reads each target's
|
|
59
|
+
* "## Key Files" section and extracts the file paths.
|
|
60
|
+
*/
|
|
61
|
+
export declare function loadKeyFilesFromSpecs(basePath: string): Set<string>;
|
|
62
|
+
/** A finding about a file not tracked by any spec. */
|
|
63
|
+
interface UntrackedFinding {
|
|
64
|
+
type: "untracked-file";
|
|
65
|
+
file: string;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
/** A finding about a file tracked by one or more specs. */
|
|
69
|
+
interface TrackedFinding {
|
|
70
|
+
type: "stale-spec" | "spec-modified" | "target-modified";
|
|
71
|
+
file: string;
|
|
72
|
+
message: string;
|
|
73
|
+
specs: string[];
|
|
74
|
+
}
|
|
75
|
+
/** A single finding from session analysis. */
|
|
76
|
+
export type SessionFinding = UntrackedFinding | TrackedFinding;
|
|
77
|
+
/** Full session analysis result. */
|
|
78
|
+
export interface SessionReport {
|
|
79
|
+
/** Git base ref used for comparison. */
|
|
80
|
+
baseRef: string;
|
|
81
|
+
/** All files changed since baseRef. */
|
|
82
|
+
changedFiles: string[];
|
|
83
|
+
/** Files changed that are tracked by at least one spec. */
|
|
84
|
+
trackedChanges: string[];
|
|
85
|
+
/** Files changed that are NOT tracked by any spec. */
|
|
86
|
+
untrackedChanges: string[];
|
|
87
|
+
/** Individual findings. */
|
|
88
|
+
findings: SessionFinding[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Analyze a session: compare changed files against spec surface area.
|
|
92
|
+
*/
|
|
93
|
+
export declare function analyzeSession(basePath: string, baseRef: string): SessionReport;
|
|
94
|
+
/** Format a session report as human-readable text. */
|
|
95
|
+
export declare function formatSessionReport(report: SessionReport): string;
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=session.d.ts.map
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Post-session audit: analyze what an agent changed vs. what specs expected.
|
|
4
|
+
*
|
|
5
|
+
* Compares the git diff (files changed since a base ref) against the spec
|
|
6
|
+
* surface area (keyFiles, commands, tracked inputs). Reports:
|
|
7
|
+
* - Files modified that aren't in any spec's keyFiles
|
|
8
|
+
* - Specs whose inputs changed (may need recompile)
|
|
9
|
+
*
|
|
10
|
+
* Read-only analysis. No commands executed, no files modified.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.gitChangedFiles = gitChangedFiles;
|
|
14
|
+
exports.gitHead = gitHead;
|
|
15
|
+
exports.resolveBaseRef = resolveBaseRef;
|
|
16
|
+
exports.loadSpecSurface = loadSpecSurface;
|
|
17
|
+
exports.loadKeyFilesFromSpecs = loadKeyFilesFromSpecs;
|
|
18
|
+
exports.analyzeSession = analyzeSession;
|
|
19
|
+
exports.formatSessionReport = formatSessionReport;
|
|
20
|
+
const node_child_process_1 = require("node:child_process");
|
|
21
|
+
const node_fs_1 = require("node:fs");
|
|
22
|
+
const node_path_1 = require("node:path");
|
|
23
|
+
const sidecar_js_1 = require("./sidecar.js");
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Git helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Get files changed since a base ref (commit, branch, or HEAD~N).
|
|
29
|
+
* Returns paths relative to the repo root.
|
|
30
|
+
*
|
|
31
|
+
* Uses execFileSync with an argv array — `baseRef` is passed as a separate
|
|
32
|
+
* argument so shell metacharacters can't escape into command execution.
|
|
33
|
+
* `--no-renames` keeps both source and destination paths visible (without it,
|
|
34
|
+
* `git mv old.ts new.ts` would only report `new.ts`).
|
|
35
|
+
*
|
|
36
|
+
* Throws on git failure (e.g., invalid baseRef) so a typo doesn't silently
|
|
37
|
+
* masquerade as "no changes" and suppress real findings downstream.
|
|
38
|
+
*/
|
|
39
|
+
function gitChangedFiles(basePath, baseRef) {
|
|
40
|
+
const exec = (args) => (0, node_child_process_1.execFileSync)("git", args, {
|
|
41
|
+
cwd: basePath,
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
44
|
+
}).trim();
|
|
45
|
+
let diffOutput;
|
|
46
|
+
try {
|
|
47
|
+
diffOutput = exec([
|
|
48
|
+
"diff",
|
|
49
|
+
"--name-only",
|
|
50
|
+
"--no-renames",
|
|
51
|
+
baseRef,
|
|
52
|
+
"--",
|
|
53
|
+
".",
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
58
|
+
throw new Error(`git diff failed for baseRef "${baseRef}": ${detail}`, {
|
|
59
|
+
cause: e,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
let untrackedOutput = "";
|
|
63
|
+
try {
|
|
64
|
+
untrackedOutput = exec([
|
|
65
|
+
"ls-files",
|
|
66
|
+
"--others",
|
|
67
|
+
"--exclude-standard",
|
|
68
|
+
"--",
|
|
69
|
+
".",
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ls-files failure is non-fatal — diff already succeeded
|
|
74
|
+
}
|
|
75
|
+
const files = new Set();
|
|
76
|
+
if (diffOutput) {
|
|
77
|
+
for (const f of diffOutput.split("\n"))
|
|
78
|
+
files.add(f);
|
|
79
|
+
}
|
|
80
|
+
if (untrackedOutput) {
|
|
81
|
+
for (const f of untrackedOutput.split("\n"))
|
|
82
|
+
files.add(f);
|
|
83
|
+
}
|
|
84
|
+
return [...files].sort();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the most recent commit hash. Returns null if not a git repo.
|
|
88
|
+
*/
|
|
89
|
+
function gitHead(basePath) {
|
|
90
|
+
try {
|
|
91
|
+
return (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
92
|
+
cwd: basePath,
|
|
93
|
+
encoding: "utf-8",
|
|
94
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
95
|
+
}).trim();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Find a reasonable base ref for session analysis.
|
|
103
|
+
*
|
|
104
|
+
* Strategy:
|
|
105
|
+
* 1. If `--base` is provided, use it directly.
|
|
106
|
+
* 2. If sidecar manifests exist, use the most recent compiledAt commit.
|
|
107
|
+
* 3. Fall back to HEAD (shows only uncommitted changes).
|
|
108
|
+
*/
|
|
109
|
+
function resolveBaseRef(explicitBase) {
|
|
110
|
+
if (explicitBase)
|
|
111
|
+
return explicitBase;
|
|
112
|
+
return "HEAD";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Load all sidecar manifests and build the aggregate spec surface area.
|
|
116
|
+
*/
|
|
117
|
+
function loadSpecSurface(basePath) {
|
|
118
|
+
const surface = {
|
|
119
|
+
keyFiles: new Set(),
|
|
120
|
+
trackedInputs: new Set(),
|
|
121
|
+
specFiles: new Set(),
|
|
122
|
+
targets: new Set(),
|
|
123
|
+
fileToSpecs: new Map(),
|
|
124
|
+
manifests: [],
|
|
125
|
+
};
|
|
126
|
+
(0, sidecar_js_1.iterateSidecars)(basePath, (_target, manifest) => {
|
|
127
|
+
surface.manifests.push(manifest);
|
|
128
|
+
surface.targets.add(manifest.target);
|
|
129
|
+
surface.specFiles.add(manifest.specFile);
|
|
130
|
+
for (const file of Object.keys(manifest.files)) {
|
|
131
|
+
surface.trackedInputs.add(file);
|
|
132
|
+
const specs = surface.fileToSpecs.get(file);
|
|
133
|
+
if (specs) {
|
|
134
|
+
specs.push(manifest.target);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
surface.fileToSpecs.set(file, [manifest.target]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return surface;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Load keyFiles from compiled markdown files. Reads each target's
|
|
145
|
+
* "## Key Files" section and extracts the file paths.
|
|
146
|
+
*/
|
|
147
|
+
function loadKeyFilesFromSpecs(basePath) {
|
|
148
|
+
const keyFiles = new Set();
|
|
149
|
+
(0, sidecar_js_1.iterateSidecars)(basePath, (_target, manifest) => {
|
|
150
|
+
const targetPath = (0, node_path_1.resolve)(basePath, manifest.target);
|
|
151
|
+
if (!(0, node_fs_1.existsSync)(targetPath))
|
|
152
|
+
return;
|
|
153
|
+
try {
|
|
154
|
+
// Normalize CRLF → LF so the regex works on Windows checkouts.
|
|
155
|
+
const content = (0, node_fs_1.readFileSync)(targetPath, "utf-8").replace(/\r\n/g, "\n");
|
|
156
|
+
const keyFilesMatch = content.match(/## Key Files\n\n((?:- `[^`]+` — .+\n?)+)/);
|
|
157
|
+
if (keyFilesMatch) {
|
|
158
|
+
const lines = keyFilesMatch[1].split("\n");
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
const pathMatch = line.match(/^- `([^`]+)`/);
|
|
161
|
+
if (pathMatch)
|
|
162
|
+
keyFiles.add(pathMatch[1]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Skip unreadable files
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return keyFiles;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Analyze a session: compare changed files against spec surface area.
|
|
174
|
+
*/
|
|
175
|
+
function analyzeSession(basePath, baseRef) {
|
|
176
|
+
const changedFiles = gitChangedFiles(basePath, baseRef);
|
|
177
|
+
const surface = loadSpecSurface(basePath);
|
|
178
|
+
const keyFiles = loadKeyFilesFromSpecs(basePath);
|
|
179
|
+
const trackedChanges = [];
|
|
180
|
+
const untrackedChanges = [];
|
|
181
|
+
const findings = [];
|
|
182
|
+
for (const file of changedFiles) {
|
|
183
|
+
const isTracked = surface.trackedInputs.has(file) || keyFiles.has(file);
|
|
184
|
+
// Treat any *.md.spec.ts as a spec even when sidecars haven't been
|
|
185
|
+
// written yet — without this, the first compile after `vigiles init`
|
|
186
|
+
// would silently drop the spec edit from the session report.
|
|
187
|
+
const isSpec = surface.specFiles.has(file) || file.endsWith(".md.spec.ts");
|
|
188
|
+
const isTarget = surface.targets.has(file);
|
|
189
|
+
const affectedSpecs = surface.fileToSpecs.get(file);
|
|
190
|
+
if (isTarget) {
|
|
191
|
+
findings.push({
|
|
192
|
+
type: "target-modified",
|
|
193
|
+
file,
|
|
194
|
+
message: `Compiled output modified directly — edit the .spec.ts source instead`,
|
|
195
|
+
specs: affectedSpecs ?? [],
|
|
196
|
+
});
|
|
197
|
+
trackedChanges.push(file);
|
|
198
|
+
}
|
|
199
|
+
else if (isSpec) {
|
|
200
|
+
findings.push({
|
|
201
|
+
type: "spec-modified",
|
|
202
|
+
file,
|
|
203
|
+
message: "Spec modified — run `vigiles compile` to update output",
|
|
204
|
+
specs: affectedSpecs ?? [],
|
|
205
|
+
});
|
|
206
|
+
trackedChanges.push(file);
|
|
207
|
+
}
|
|
208
|
+
else if (isTracked) {
|
|
209
|
+
findings.push({
|
|
210
|
+
type: "stale-spec",
|
|
211
|
+
file,
|
|
212
|
+
message: `Tracked input changed — affected specs may need recompile`,
|
|
213
|
+
specs: affectedSpecs ?? [],
|
|
214
|
+
});
|
|
215
|
+
trackedChanges.push(file);
|
|
216
|
+
}
|
|
217
|
+
else if (!file.startsWith(".vigiles/") && !isIgnoredFile(file)) {
|
|
218
|
+
// Agent modified a file not in any spec
|
|
219
|
+
untrackedChanges.push(file);
|
|
220
|
+
findings.push({
|
|
221
|
+
type: "untracked-file",
|
|
222
|
+
file,
|
|
223
|
+
message: "Modified file not tracked by any spec",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
baseRef,
|
|
229
|
+
changedFiles,
|
|
230
|
+
trackedChanges,
|
|
231
|
+
untrackedChanges,
|
|
232
|
+
findings,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** Files that are always expected to change and shouldn't be flagged. */
|
|
236
|
+
function isIgnoredFile(file) {
|
|
237
|
+
return (file === "package-lock.json" ||
|
|
238
|
+
file === "yarn.lock" ||
|
|
239
|
+
file === "pnpm-lock.yaml" ||
|
|
240
|
+
file === "bun.lockb" ||
|
|
241
|
+
file.startsWith("node_modules/") ||
|
|
242
|
+
file.startsWith("dist/") ||
|
|
243
|
+
file.startsWith(".git/"));
|
|
244
|
+
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Formatting
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
/** Format a session report as human-readable text. */
|
|
249
|
+
function formatSessionReport(report) {
|
|
250
|
+
if (report.changedFiles.length === 0) {
|
|
251
|
+
return "No changes detected since " + report.baseRef;
|
|
252
|
+
}
|
|
253
|
+
const lines = [];
|
|
254
|
+
lines.push(`Session analysis (${String(report.changedFiles.length)} files changed since ${report.baseRef}):`);
|
|
255
|
+
lines.push("");
|
|
256
|
+
const stale = report.findings.filter((f) => f.type === "stale-spec");
|
|
257
|
+
const untracked = report.findings.filter((f) => f.type === "untracked-file");
|
|
258
|
+
const targetMods = report.findings.filter((f) => f.type === "target-modified");
|
|
259
|
+
const specMods = report.findings.filter((f) => f.type === "spec-modified");
|
|
260
|
+
if (targetMods.length > 0) {
|
|
261
|
+
lines.push(" Compiled outputs modified directly:");
|
|
262
|
+
for (const f of targetMods) {
|
|
263
|
+
lines.push(` ${f.file} — edit the .spec.ts source instead`);
|
|
264
|
+
}
|
|
265
|
+
lines.push("");
|
|
266
|
+
}
|
|
267
|
+
if (specMods.length > 0) {
|
|
268
|
+
lines.push(" Specs modified:");
|
|
269
|
+
for (const f of specMods) {
|
|
270
|
+
lines.push(` ${f.file} — run vigiles compile`);
|
|
271
|
+
}
|
|
272
|
+
lines.push("");
|
|
273
|
+
}
|
|
274
|
+
if (stale.length > 0) {
|
|
275
|
+
lines.push(" Tracked inputs changed (specs may need recompile):");
|
|
276
|
+
for (const f of stale) {
|
|
277
|
+
const specList = f.specs.join(", ") || "unknown";
|
|
278
|
+
lines.push(` ${f.file} (affects ${specList})`);
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
}
|
|
282
|
+
if (untracked.length > 0) {
|
|
283
|
+
lines.push(" Files not tracked by any spec:");
|
|
284
|
+
for (const f of untracked) {
|
|
285
|
+
lines.push(` ${f.file}`);
|
|
286
|
+
}
|
|
287
|
+
lines.push("");
|
|
288
|
+
}
|
|
289
|
+
// Summary
|
|
290
|
+
const issues = targetMods.length + stale.length;
|
|
291
|
+
if (issues === 0 && untracked.length === 0) {
|
|
292
|
+
lines.push(" All changes are within spec surface area.");
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const parts = [];
|
|
296
|
+
if (targetMods.length > 0)
|
|
297
|
+
parts.push(`${String(targetMods.length)} compiled output(s) edited directly`);
|
|
298
|
+
if (stale.length > 0)
|
|
299
|
+
parts.push(`${String(stale.length)} input(s) changed`);
|
|
300
|
+
if (untracked.length > 0)
|
|
301
|
+
parts.push(`${String(untracked.length)} untracked file(s)`);
|
|
302
|
+
lines.push(" Summary: " + parts.join(", "));
|
|
303
|
+
}
|
|
304
|
+
return lines.join("\n");
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-spec sidecar manifests stored at `.vigiles/<target>.inputs.json`.
|
|
3
|
+
*
|
|
4
|
+
* Used by the post-session audit to know which targets exist and which
|
|
5
|
+
* spec source / inputs each one tracks. The compile pipeline writes
|
|
6
|
+
* these whenever a spec is built; readers (currently only session.ts)
|
|
7
|
+
* consume them at audit time.
|
|
8
|
+
*
|
|
9
|
+
* This module is the ONLY place sidecars live now — the freshness rule
|
|
10
|
+
* doesn't depend on them anymore.
|
|
11
|
+
*/
|
|
12
|
+
export interface SidecarManifest {
|
|
13
|
+
/** The spec source file (relative to basePath). */
|
|
14
|
+
specFile: string;
|
|
15
|
+
/** The compilation target (e.g., "CLAUDE.md"). */
|
|
16
|
+
target: string;
|
|
17
|
+
/** ISO 8601 timestamp of last compilation. */
|
|
18
|
+
compiledAt: string;
|
|
19
|
+
/** Per-file SHA-256 hashes (truncated, 16 hex chars). */
|
|
20
|
+
files: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
export declare function sidecarPath(basePath: string, target: string): string;
|
|
23
|
+
export declare function writeSidecarManifest(basePath: string, manifest: SidecarManifest): void;
|
|
24
|
+
export declare function readSidecarManifest(basePath: string, target: string): SidecarManifest | null;
|
|
25
|
+
export declare function computePerFileHashes(inputFiles: string[], basePath: string): Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Iterate all sidecar manifests under `.vigiles/`, including nested
|
|
28
|
+
* directories. Handles directory-not-found and read errors gracefully.
|
|
29
|
+
*
|
|
30
|
+
* Recurses so that nested targets (e.g. `.github/copilot-instructions.md`,
|
|
31
|
+
* which lives at `.vigiles/.github/copilot-instructions.md.inputs.json`)
|
|
32
|
+
* are not silently skipped.
|
|
33
|
+
*/
|
|
34
|
+
export declare function iterateSidecars(basePath: string, fn: (target: string, manifest: SidecarManifest) => void): void;
|
|
35
|
+
//# sourceMappingURL=sidecar.d.ts.map
|
package/dist/sidecar.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-spec sidecar manifests stored at `.vigiles/<target>.inputs.json`.
|
|
4
|
+
*
|
|
5
|
+
* Used by the post-session audit to know which targets exist and which
|
|
6
|
+
* spec source / inputs each one tracks. The compile pipeline writes
|
|
7
|
+
* these whenever a spec is built; readers (currently only session.ts)
|
|
8
|
+
* consume them at audit time.
|
|
9
|
+
*
|
|
10
|
+
* This module is the ONLY place sidecars live now — the freshness rule
|
|
11
|
+
* doesn't depend on them anymore.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.sidecarPath = sidecarPath;
|
|
15
|
+
exports.writeSidecarManifest = writeSidecarManifest;
|
|
16
|
+
exports.readSidecarManifest = readSidecarManifest;
|
|
17
|
+
exports.computePerFileHashes = computePerFileHashes;
|
|
18
|
+
exports.iterateSidecars = iterateSidecars;
|
|
19
|
+
const node_fs_1 = require("node:fs");
|
|
20
|
+
const node_path_1 = require("node:path");
|
|
21
|
+
const hash_js_1 = require("./hash.js");
|
|
22
|
+
const SIDECAR_ROOT = ".vigiles";
|
|
23
|
+
const MANIFEST_SUFFIX = ".inputs.json";
|
|
24
|
+
function sidecarPath(basePath, target) {
|
|
25
|
+
return (0, node_path_1.resolve)(basePath, SIDECAR_ROOT, `${target}${MANIFEST_SUFFIX}`);
|
|
26
|
+
}
|
|
27
|
+
function writeSidecarManifest(basePath, manifest) {
|
|
28
|
+
// Targets can be nested (e.g. ".github/copilot-instructions.md"), so
|
|
29
|
+
// ensure every intermediate directory under .vigiles/ exists before
|
|
30
|
+
// writing — otherwise writeFileSync throws ENOENT.
|
|
31
|
+
const filePath = sidecarPath(basePath, manifest.target);
|
|
32
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(filePath), { recursive: true });
|
|
33
|
+
(0, node_fs_1.writeFileSync)(filePath, JSON.stringify(manifest, null, 2) + "\n");
|
|
34
|
+
}
|
|
35
|
+
function readSidecarManifest(basePath, target) {
|
|
36
|
+
const filePath = sidecarPath(basePath, target);
|
|
37
|
+
if (!(0, node_fs_1.existsSync)(filePath))
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf-8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function computePerFileHashes(inputFiles, basePath) {
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const f of inputFiles) {
|
|
49
|
+
const fullPath = (0, node_path_1.resolve)(basePath, f);
|
|
50
|
+
result[f] = (0, node_fs_1.existsSync)(fullPath)
|
|
51
|
+
? (0, hash_js_1.sha256short)((0, node_fs_1.readFileSync)(fullPath))
|
|
52
|
+
: "MISSING";
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Iterate all sidecar manifests under `.vigiles/`, including nested
|
|
58
|
+
* directories. Handles directory-not-found and read errors gracefully.
|
|
59
|
+
*
|
|
60
|
+
* Recurses so that nested targets (e.g. `.github/copilot-instructions.md`,
|
|
61
|
+
* which lives at `.vigiles/.github/copilot-instructions.md.inputs.json`)
|
|
62
|
+
* are not silently skipped.
|
|
63
|
+
*/
|
|
64
|
+
function iterateSidecars(basePath, fn) {
|
|
65
|
+
const root = (0, node_path_1.resolve)(basePath, SIDECAR_ROOT);
|
|
66
|
+
if (!(0, node_fs_1.existsSync)(root))
|
|
67
|
+
return;
|
|
68
|
+
walk(root, root, basePath, fn);
|
|
69
|
+
}
|
|
70
|
+
function walk(dir, root, basePath, fn) {
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
entries = (0, node_fs_1.readdirSync)(dir);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const fullPath = (0, node_path_1.resolve)(dir, entry);
|
|
80
|
+
let isDir;
|
|
81
|
+
try {
|
|
82
|
+
isDir = (0, node_fs_1.statSync)(fullPath).isDirectory();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isDir) {
|
|
88
|
+
walk(fullPath, root, basePath, fn);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!entry.endsWith(MANIFEST_SUFFIX))
|
|
92
|
+
continue;
|
|
93
|
+
// Reconstruct the target name from the path relative to .vigiles/
|
|
94
|
+
const rel = (0, node_path_1.relative)(root, fullPath);
|
|
95
|
+
const target = rel.slice(0, -MANIFEST_SUFFIX.length);
|
|
96
|
+
const manifest = readSidecarManifest(basePath, target);
|
|
97
|
+
if (!manifest)
|
|
98
|
+
continue;
|
|
99
|
+
fn(target, manifest);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=sidecar.js.map
|
package/dist/spec.d.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* enforce() — delegated to an external linter (ESLint, Ruff, Clippy, etc.)
|
|
9
9
|
* guidance() — prose only, no mechanical enforcement
|
|
10
10
|
*/
|
|
11
|
-
/** Linters vigiles can cross-reference. */
|
|
12
|
-
type BuiltinLinter = "eslint" | "stylelint" | "ruff" | "clippy" | "pylint" | "rubocop";
|
|
11
|
+
/** Linters and policy catalogs vigiles can cross-reference. */
|
|
12
|
+
type BuiltinLinter = "eslint" | "stylelint" | "ruff" | "clippy" | "pylint" | "rubocop" | "cedar";
|
|
13
13
|
/** Scoped ESLint plugin prefix (e.g., @typescript-eslint). */
|
|
14
14
|
type ScopedPlugin = `@${string}/${string}`;
|
|
15
15
|
/** A linter/rule reference: "eslint/no-console", "ruff/T201", "@typescript-eslint/no-explicit-any". */
|
|
@@ -52,12 +52,12 @@ export type StrictFile = [keyof KnownProjectFiles] extends [never] ? string : Kn
|
|
|
52
52
|
export type StrictCmd = [keyof KnownNpmScripts] extends [never] ? string : `npm run ${KnownNpmScripts[keyof KnownNpmScripts] & string}` | `npm ${KnownNpmScripts[keyof KnownNpmScripts] & string}` | (string & {});
|
|
53
53
|
export type ClaudeTool = "Read" | "Write" | "Edit" | "Bash" | "Grep" | "Glob" | "Agent" | "TodoWrite" | "WebSearch" | "WebFetch" | "NotebookEdit";
|
|
54
54
|
export type HookEvent = "PreToolUse" | "PostToolUse" | "PreSession" | "PostSession" | "Notification";
|
|
55
|
-
/** A rule delegated to an external tool (linter, ast-grep, dependency-cruiser, etc.). */
|
|
55
|
+
/** A rule delegated to an external tool (linter, ast-grep, dependency-cruiser, etc.) or to a vigiles-internal check. */
|
|
56
56
|
export interface EnforceRule {
|
|
57
57
|
readonly _kind: "enforce";
|
|
58
|
-
readonly linterRule: LinterRule;
|
|
58
|
+
readonly linterRule: LinterRule | VigilesRef;
|
|
59
59
|
readonly why: string;
|
|
60
|
-
/** Skip
|
|
60
|
+
/** Skip verification for this rule. Default: true (verify). */
|
|
61
61
|
readonly verify: boolean;
|
|
62
62
|
}
|
|
63
63
|
/** A guidance-only rule (prose, no enforcement). */
|
|
@@ -65,18 +65,28 @@ export interface GuidanceRule {
|
|
|
65
65
|
readonly _kind: "guidance";
|
|
66
66
|
readonly text: string;
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
/** A reactive rule: runs a command when watched files change. */
|
|
69
|
+
export interface GuardRule {
|
|
70
|
+
readonly _kind: "guard";
|
|
71
|
+
readonly watch: string | readonly string[];
|
|
72
|
+
readonly run: string;
|
|
73
|
+
readonly description: string;
|
|
74
|
+
}
|
|
75
|
+
export type Rule = EnforceRule | GuidanceRule | GuardRule;
|
|
69
76
|
/**
|
|
70
|
-
* Declare a rule enforced by an external tool.
|
|
77
|
+
* Declare a rule enforced by an external tool or vigiles itself.
|
|
71
78
|
*
|
|
72
79
|
* When generated types are present, the `linterRule` argument is narrowed
|
|
73
|
-
* to only accept rules that exist in your linter configs.
|
|
80
|
+
* to only accept rules that exist in your linter configs. Vigiles-internal
|
|
81
|
+
* checks use the `vigiles/<assertion-id>` namespace and are dispatched to
|
|
82
|
+
* built-in mechanical validators (e.g. `vigiles/orphan-docs`).
|
|
74
83
|
*
|
|
75
84
|
* enforce("eslint/no-console", "Use structured logger.")
|
|
76
85
|
* enforce("@typescript-eslint/no-floating-promises", "Always await.")
|
|
77
86
|
* enforce("ruff/T201", "Use logging module.")
|
|
87
|
+
* enforce("vigiles/orphan-docs", "No docs without spec references.")
|
|
78
88
|
*/
|
|
79
|
-
export declare function enforce(
|
|
89
|
+
export declare function enforce(ref: NoInfer<StrictLinterRule> | VigilesRef, why: string, options?: {
|
|
80
90
|
verify?: boolean;
|
|
81
91
|
}): EnforceRule;
|
|
82
92
|
/**
|
|
@@ -85,6 +95,16 @@ export declare function enforce(linterRule: NoInfer<StrictLinterRule>, why: stri
|
|
|
85
95
|
* guidance("Google unfamiliar APIs before implementing.")
|
|
86
96
|
*/
|
|
87
97
|
export declare function guidance(text: string): GuidanceRule;
|
|
98
|
+
/**
|
|
99
|
+
* Declare a reactive guard: runs a command when watched files change.
|
|
100
|
+
*
|
|
101
|
+
* guard({ watch: "*.spec.ts", run: "npx vigiles compile" }, "Recompile on spec change")
|
|
102
|
+
* guard({ watch: ["eslint.config.*", "package.json"], run: "npx vigiles generate-types" }, "Regen types")
|
|
103
|
+
*/
|
|
104
|
+
export declare function guard(options: {
|
|
105
|
+
watch: string | readonly string[];
|
|
106
|
+
run: string;
|
|
107
|
+
}, description: string): GuardRule;
|
|
88
108
|
/**
|
|
89
109
|
* Branded string types — these prove a reference has gone through
|
|
90
110
|
* vigiles's verification. The compiler only accepts branded refs,
|
package/dist/spec.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.enforce = enforce;
|
|
14
14
|
exports.guidance = guidance;
|
|
15
|
+
exports.guard = guard;
|
|
15
16
|
exports.file = file;
|
|
16
17
|
exports.cmd = cmd;
|
|
17
18
|
exports.ref = ref;
|
|
@@ -23,20 +24,23 @@ exports.defineConfig = defineConfig;
|
|
|
23
24
|
// Builder functions
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
/**
|
|
26
|
-
* Declare a rule enforced by an external tool.
|
|
27
|
+
* Declare a rule enforced by an external tool or vigiles itself.
|
|
27
28
|
*
|
|
28
29
|
* When generated types are present, the `linterRule` argument is narrowed
|
|
29
|
-
* to only accept rules that exist in your linter configs.
|
|
30
|
+
* to only accept rules that exist in your linter configs. Vigiles-internal
|
|
31
|
+
* checks use the `vigiles/<assertion-id>` namespace and are dispatched to
|
|
32
|
+
* built-in mechanical validators (e.g. `vigiles/orphan-docs`).
|
|
30
33
|
*
|
|
31
34
|
* enforce("eslint/no-console", "Use structured logger.")
|
|
32
35
|
* enforce("@typescript-eslint/no-floating-promises", "Always await.")
|
|
33
36
|
* enforce("ruff/T201", "Use logging module.")
|
|
37
|
+
* enforce("vigiles/orphan-docs", "No docs without spec references.")
|
|
34
38
|
*/
|
|
35
|
-
function enforce(
|
|
39
|
+
function enforce(ref, why, options) {
|
|
36
40
|
return {
|
|
37
41
|
_kind: "enforce",
|
|
38
42
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
39
|
-
linterRule:
|
|
43
|
+
linterRule: ref,
|
|
40
44
|
why,
|
|
41
45
|
verify: options?.verify ?? true,
|
|
42
46
|
};
|
|
@@ -49,6 +53,20 @@ function enforce(linterRule, why, options) {
|
|
|
49
53
|
function guidance(text) {
|
|
50
54
|
return { _kind: "guidance", text };
|
|
51
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Declare a reactive guard: runs a command when watched files change.
|
|
58
|
+
*
|
|
59
|
+
* guard({ watch: "*.spec.ts", run: "npx vigiles compile" }, "Recompile on spec change")
|
|
60
|
+
* guard({ watch: ["eslint.config.*", "package.json"], run: "npx vigiles generate-types" }, "Regen types")
|
|
61
|
+
*/
|
|
62
|
+
function guard(options, description) {
|
|
63
|
+
return {
|
|
64
|
+
_kind: "guard",
|
|
65
|
+
watch: options.watch,
|
|
66
|
+
run: options.run,
|
|
67
|
+
description,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
52
70
|
/**
|
|
53
71
|
* Reference a file path — verified to exist at compile time.
|
|
54
72
|
* When generated types are present, narrowed to known project files.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ClaudeSpec } from "./spec.js";
|
|
2
|
+
export declare function makeTmpDir(suffix?: string): string;
|
|
3
|
+
export declare function cleanupTmpDir(dir: string): void;
|
|
4
|
+
export declare function makeSpec(overrides?: Partial<ClaudeSpec>): ClaudeSpec;
|
|
5
|
+
declare function git(cwd: string, cmd: string): string;
|
|
6
|
+
export declare function initGitRepo(dir: string): void;
|
|
7
|
+
export { git };
|
|
8
|
+
//# sourceMappingURL=test-utils.d.ts.map
|