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/refs.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** An inline code span with its 1-based source line. */
|
|
2
|
+
export interface Span {
|
|
3
|
+
readonly text: string;
|
|
4
|
+
readonly line: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract inline code spans, skipping fenced code blocks (R1). Returns each
|
|
8
|
+
* span's trimmed text and 1-based line.
|
|
9
|
+
*/
|
|
10
|
+
export declare function inlineSpans(markdown: string): Span[];
|
|
11
|
+
/** A parsed file-qualified reference. */
|
|
12
|
+
export interface SymbolRef {
|
|
13
|
+
readonly file: string;
|
|
14
|
+
readonly symbol: string;
|
|
15
|
+
readonly line: number;
|
|
16
|
+
}
|
|
17
|
+
/** A reference that failed verification. */
|
|
18
|
+
export interface SymbolRefError extends SymbolRef {
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
}
|
|
21
|
+
/** Extract the `vigiles:symbol` references from a markdown file. */
|
|
22
|
+
export declare function symbolRefs(markdown: string): SymbolRef[];
|
|
23
|
+
/**
|
|
24
|
+
* Verify the file-qualified symbol references in a markdown file: the named
|
|
25
|
+
* file must exist and define the named symbol. `basePath` is the directory the
|
|
26
|
+
* paths resolve against (the instruction file's own directory).
|
|
27
|
+
*/
|
|
28
|
+
export declare function verifySymbolRefs(markdown: string, basePath: string): SymbolRefError[];
|
|
29
|
+
/**
|
|
30
|
+
* Whether a span looks like a *code reference* that ought to carry a
|
|
31
|
+
* file-qualified mark — a scoped name, or an identifier that isn't a bare
|
|
32
|
+
* lowercase prose word. A function-call form `` `foo(args)` `` is treated as a
|
|
33
|
+
* reference to its callee `foo`. Paths/filenames are excluded (they are `file`
|
|
34
|
+
* refs).
|
|
35
|
+
*/
|
|
36
|
+
export declare function isCodeShaped(text: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Code-shaped inline references that are NOT yet marked — the spans the
|
|
39
|
+
* enforcement hook makes the agent mark as `` `vigiles:symbol path.ext#symbol` ``
|
|
40
|
+
* or opt out of with `<!-- vigiles:ignore -->` (or `<!-- vigiles:ignore-file -->`
|
|
41
|
+
* for the whole file).
|
|
42
|
+
*/
|
|
43
|
+
export declare function unmarkedCodeRefs(markdown: string): Span[];
|
|
44
|
+
//# sourceMappingURL=refs.d.ts.map
|
package/dist/refs.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inlineSpans = inlineSpans;
|
|
4
|
+
exports.symbolRefs = symbolRefs;
|
|
5
|
+
exports.verifySymbolRefs = verifySymbolRefs;
|
|
6
|
+
exports.isCodeShaped = isCodeShaped;
|
|
7
|
+
exports.unmarkedCodeRefs = unmarkedCodeRefs;
|
|
8
|
+
/**
|
|
9
|
+
* vigiles — file-qualified symbol reference verification (variant A).
|
|
10
|
+
*
|
|
11
|
+
* A reference names both the file and the symbol, as an inline code span:
|
|
12
|
+
*
|
|
13
|
+
* See `src/config.ts#parseConfig` for the loader.
|
|
14
|
+
* Render with `app/models/user.rb#full_name`.
|
|
15
|
+
*
|
|
16
|
+
* We parse *that one named file* and check it defines the symbol. No
|
|
17
|
+
* project-wide index, no cross-file resolution, no autoloader chasing, no
|
|
18
|
+
* ambiguity (the file disambiguates). Because the `path.ext#symbol` shape is
|
|
19
|
+
* unmistakable and deliberately written, it is a *declared* reference — like
|
|
20
|
+
* `vigiles:file` / `vigiles:cmd` — so a broken one is an **error**, not an
|
|
21
|
+
* inferred-prose warning. The author names the file; vigiles proves the symbol.
|
|
22
|
+
*
|
|
23
|
+
* R1 (cross-compat): only inline code spans are read. Fenced code blocks are
|
|
24
|
+
* never touched, so rustdoc doctests / typescript-docs-verifier keep working.
|
|
25
|
+
*/
|
|
26
|
+
const node_fs_1 = require("node:fs");
|
|
27
|
+
const node_path_1 = require("node:path");
|
|
28
|
+
const symbols_js_1 = require("./symbols.js");
|
|
29
|
+
const FENCE = /^\s*```/;
|
|
30
|
+
const SPAN = /`([^`\n]+)`/g;
|
|
31
|
+
/**
|
|
32
|
+
* Extract inline code spans, skipping fenced code blocks (R1). Returns each
|
|
33
|
+
* span's trimmed text and 1-based line.
|
|
34
|
+
*/
|
|
35
|
+
function inlineSpans(markdown) {
|
|
36
|
+
const spans = [];
|
|
37
|
+
let inFence = false;
|
|
38
|
+
const lines = markdown.split("\n");
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
if (FENCE.test(lines[i])) {
|
|
41
|
+
inFence = !inFence;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inFence)
|
|
45
|
+
continue;
|
|
46
|
+
for (const m of lines[i].matchAll(SPAN)) {
|
|
47
|
+
spans.push({ text: m[1].trim(), line: i + 1 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return spans;
|
|
51
|
+
}
|
|
52
|
+
// A file-qualified symbol reference: `<path>.<ext>` then `#`/`::` then a symbol.
|
|
53
|
+
// Requires a real file extension before the separator, so a bare scoped symbol
|
|
54
|
+
// An explicit `vigiles:symbol <path>.<ext>#<symbol>` directive inside a code
|
|
55
|
+
// span. The literal `vigiles:symbol` prefix means zero detection heuristic — a
|
|
56
|
+
// span either carries it or it does not — consistent with the rest of vigiles'
|
|
57
|
+
// markers. The mark is self-contained (file + symbol in one inline token), so
|
|
58
|
+
// it binds unambiguously even in a long line with several references.
|
|
59
|
+
const SYMBOL_MARK = /^vigiles:symbol\s+([\w@./-]+\.[A-Za-z0-9]+)(?:#|::)([A-Za-z_]\w*[?!]?)$/;
|
|
60
|
+
/** Extract the `vigiles:symbol` references from a markdown file. */
|
|
61
|
+
function symbolRefs(markdown) {
|
|
62
|
+
const refs = [];
|
|
63
|
+
for (const span of inlineSpans(markdown)) {
|
|
64
|
+
const m = SYMBOL_MARK.exec(span.text);
|
|
65
|
+
if (m)
|
|
66
|
+
refs.push({ file: m[1], symbol: m[2], line: span.line });
|
|
67
|
+
}
|
|
68
|
+
return refs;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verify the file-qualified symbol references in a markdown file: the named
|
|
72
|
+
* file must exist and define the named symbol. `basePath` is the directory the
|
|
73
|
+
* paths resolve against (the instruction file's own directory).
|
|
74
|
+
*/
|
|
75
|
+
function verifySymbolRefs(markdown, basePath) {
|
|
76
|
+
const errors = [];
|
|
77
|
+
for (const ref of symbolRefs(markdown)) {
|
|
78
|
+
const full = (0, node_path_1.resolve)(basePath, ref.file);
|
|
79
|
+
if (!(0, node_fs_1.existsSync)(full)) {
|
|
80
|
+
errors.push({ ...ref, reason: `File not found: "${ref.file}"` });
|
|
81
|
+
}
|
|
82
|
+
else if ((0, symbols_js_1.langForFile)(ref.file) === null) {
|
|
83
|
+
errors.push({
|
|
84
|
+
...ref,
|
|
85
|
+
reason: `Unsupported language for symbol check: "${ref.file}"`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else if (!(0, symbols_js_1.fileDefinesSymbol)(full, ref.symbol)) {
|
|
89
|
+
errors.push({
|
|
90
|
+
...ref,
|
|
91
|
+
reason: `"${ref.symbol}" is not defined in ${ref.file}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return errors;
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Enforcement: force code references to carry the file-qualified mark
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
const PATH_LIKE = /[/\\]|\.[A-Za-z0-9]+$/; // a path or a bare filename
|
|
101
|
+
const PLAIN_ID = /^[A-Za-z_]\w*$/;
|
|
102
|
+
const SCOPED = /^[A-Za-z_]\w*(?:#|::)[\w?!]+$/;
|
|
103
|
+
const IGNORE_FILE = /<!--\s*vigiles:ignore-file\s*-->/;
|
|
104
|
+
const IGNORE_LINE = /<!--\s*vigiles:ignore\s*-->/;
|
|
105
|
+
/**
|
|
106
|
+
* Whether a span looks like a *code reference* that ought to carry a
|
|
107
|
+
* file-qualified mark — a scoped name, or an identifier that isn't a bare
|
|
108
|
+
* lowercase prose word. A function-call form `` `foo(args)` `` is treated as a
|
|
109
|
+
* reference to its callee `foo`. Paths/filenames are excluded (they are `file`
|
|
110
|
+
* refs).
|
|
111
|
+
*/
|
|
112
|
+
function isCodeShaped(text) {
|
|
113
|
+
const callee = text.replace(/\s*\([^)]*\)\s*$/, ""); // `foo(args)` → `foo`
|
|
114
|
+
if (SCOPED.test(callee))
|
|
115
|
+
return true;
|
|
116
|
+
if (!PLAIN_ID.test(callee))
|
|
117
|
+
return false;
|
|
118
|
+
const hasUnderscore = callee.includes("_");
|
|
119
|
+
const hasCamel = /[a-z][A-Z]/.test(callee);
|
|
120
|
+
const isPascal = /^[A-Z][a-z]/.test(callee);
|
|
121
|
+
const isScreaming = /^[A-Z][A-Z0-9_]+$/.test(callee);
|
|
122
|
+
return hasUnderscore || hasCamel || isPascal || isScreaming;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Code-shaped inline references that are NOT yet marked — the spans the
|
|
126
|
+
* enforcement hook makes the agent mark as `` `vigiles:symbol path.ext#symbol` ``
|
|
127
|
+
* or opt out of with `<!-- vigiles:ignore -->` (or `<!-- vigiles:ignore-file -->`
|
|
128
|
+
* for the whole file).
|
|
129
|
+
*/
|
|
130
|
+
function unmarkedCodeRefs(markdown) {
|
|
131
|
+
if (IGNORE_FILE.test(markdown))
|
|
132
|
+
return [];
|
|
133
|
+
const lines = markdown.split("\n");
|
|
134
|
+
return inlineSpans(markdown).filter((span) => {
|
|
135
|
+
if (IGNORE_LINE.test(lines[span.line - 1] ?? ""))
|
|
136
|
+
return false;
|
|
137
|
+
if (SYMBOL_MARK.test(span.text))
|
|
138
|
+
return false; // already a vigiles:symbol mark
|
|
139
|
+
if (PATH_LIKE.test(span.text))
|
|
140
|
+
return false; // a path/filename → file ref
|
|
141
|
+
return isCodeShaped(span.text);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=refs.js.map
|
|
@@ -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
|