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.
Files changed (184) hide show
  1. package/README.md +182 -134
  2. package/dist/action-gate.d.ts +28 -0
  3. package/dist/action-gate.js +73 -0
  4. package/dist/cli.js +705 -230
  5. package/dist/community-skills.d.ts +22 -0
  6. package/dist/community-skills.js +86 -0
  7. package/dist/compile-generator.d.ts +48 -0
  8. package/dist/compile-generator.js +322 -0
  9. package/dist/compile.d.ts +4 -0
  10. package/dist/compile.js +239 -45
  11. package/dist/coverage.d.ts +56 -0
  12. package/dist/coverage.js +178 -0
  13. package/dist/doc-refs.d.ts +60 -0
  14. package/dist/doc-refs.js +246 -0
  15. package/dist/eval.d.ts +62 -0
  16. package/dist/eval.js +174 -0
  17. package/dist/evolve.js +60 -125
  18. package/dist/frontmatter.d.ts +75 -0
  19. package/dist/frontmatter.js +263 -0
  20. package/dist/generate-schema.d.ts +51 -0
  21. package/dist/generate-schema.js +125 -0
  22. package/dist/generate-types.js +39 -1
  23. package/dist/harness-test.d.ts +38 -0
  24. package/dist/harness-test.js +129 -0
  25. package/dist/hash.d.ts +8 -0
  26. package/dist/hash.js +16 -0
  27. package/dist/inline.d.ts +22 -4
  28. package/dist/inline.js +60 -13
  29. package/dist/integrity.d.ts +29 -0
  30. package/dist/integrity.js +44 -0
  31. package/dist/linters.d.ts +5 -3
  32. package/dist/linters.js +144 -4
  33. package/dist/mock-model.d.ts +31 -0
  34. package/dist/mock-model.js +189 -0
  35. package/dist/orphans.d.ts +52 -0
  36. package/dist/orphans.js +124 -0
  37. package/dist/proofs.js +22 -16
  38. package/dist/refs.d.ts +44 -0
  39. package/dist/refs.js +144 -0
  40. package/dist/session.d.ts +97 -0
  41. package/dist/session.js +306 -0
  42. package/dist/sidecar.d.ts +35 -0
  43. package/dist/sidecar.js +102 -0
  44. package/dist/skill-driver.d.ts +77 -0
  45. package/dist/skill-driver.js +76 -0
  46. package/dist/skill-runtime.d.ts +101 -0
  47. package/dist/skill-runtime.js +289 -0
  48. package/dist/skill-test.d.ts +47 -0
  49. package/dist/skill-test.js +77 -0
  50. package/dist/spec.d.ts +119 -13
  51. package/dist/spec.js +51 -4
  52. package/dist/symbols.d.ts +30 -0
  53. package/dist/symbols.js +142 -0
  54. package/dist/test-utils.d.ts +8 -0
  55. package/dist/test-utils.js +41 -0
  56. package/dist/types.d.ts +34 -8
  57. package/dist/types.js +19 -0
  58. package/dist/validate.js +5 -3
  59. package/package.json +26 -5
  60. package/.claude/settings.json +0 -46
  61. package/.claude/settings.local.json +0 -8
  62. package/.github/workflows/ci.yml +0 -81
  63. package/.prettierignore +0 -1
  64. package/.vigiles/generated.d.ts +0 -205
  65. package/CLAUDE.md +0 -95
  66. package/CLAUDE.md.spec.ts +0 -142
  67. package/CONTRIBUTING.md +0 -121
  68. package/dist/action.d.ts.map +0 -1
  69. package/dist/action.js.map +0 -1
  70. package/dist/cli.d.ts.map +0 -1
  71. package/dist/cli.js.map +0 -1
  72. package/dist/cli.test.d.ts +0 -2
  73. package/dist/cli.test.d.ts.map +0 -1
  74. package/dist/cli.test.js +0 -650
  75. package/dist/cli.test.js.map +0 -1
  76. package/dist/compile.d.ts.map +0 -1
  77. package/dist/compile.js.map +0 -1
  78. package/dist/evolve.d.ts.map +0 -1
  79. package/dist/evolve.js.map +0 -1
  80. package/dist/freshness.d.ts +0 -67
  81. package/dist/freshness.d.ts.map +0 -1
  82. package/dist/freshness.js +0 -244
  83. package/dist/freshness.js.map +0 -1
  84. package/dist/freshness.test.d.ts +0 -2
  85. package/dist/freshness.test.d.ts.map +0 -1
  86. package/dist/freshness.test.js +0 -356
  87. package/dist/freshness.test.js.map +0 -1
  88. package/dist/generate-types.d.ts.map +0 -1
  89. package/dist/generate-types.js.map +0 -1
  90. package/dist/inline.d.ts.map +0 -1
  91. package/dist/inline.js.map +0 -1
  92. package/dist/inline.test.d.ts +0 -5
  93. package/dist/inline.test.d.ts.map +0 -1
  94. package/dist/inline.test.js +0 -152
  95. package/dist/inline.test.js.map +0 -1
  96. package/dist/linters.d.ts.map +0 -1
  97. package/dist/linters.js.map +0 -1
  98. package/dist/proofs.d.ts.map +0 -1
  99. package/dist/proofs.js.map +0 -1
  100. package/dist/proofs.test.d.ts +0 -9
  101. package/dist/proofs.test.d.ts.map +0 -1
  102. package/dist/proofs.test.js +0 -952
  103. package/dist/proofs.test.js.map +0 -1
  104. package/dist/spec.d.ts.map +0 -1
  105. package/dist/spec.js.map +0 -1
  106. package/dist/spec.test.d.ts +0 -2
  107. package/dist/spec.test.d.ts.map +0 -1
  108. package/dist/spec.test.js +0 -1222
  109. package/dist/spec.test.js.map +0 -1
  110. package/dist/types.d.ts.map +0 -1
  111. package/dist/types.js.map +0 -1
  112. package/dist/validate.d.ts.map +0 -1
  113. package/dist/validate.js.map +0 -1
  114. package/dist/validate.test.d.ts +0 -2
  115. package/dist/validate.test.d.ts.map +0 -1
  116. package/dist/validate.test.js +0 -531
  117. package/dist/validate.test.js.map +0 -1
  118. package/docs/agent-setup.md +0 -85
  119. package/docs/agent-workflows.md +0 -103
  120. package/docs/comparison.md +0 -71
  121. package/docs/freshness.md +0 -124
  122. package/docs/inline-mode.md +0 -119
  123. package/docs/linter-support.md +0 -166
  124. package/docs/spec-format.md +0 -194
  125. package/eslint.config.mjs +0 -79
  126. package/examples/CLAUDE.md +0 -54
  127. package/examples/CLAUDE.md.spec.ts +0 -65
  128. package/examples/SKILL.md +0 -50
  129. package/examples/SKILL.md.spec.ts +0 -57
  130. package/fixtures/example-project/CLAUDE.md +0 -11
  131. package/fixtures/example-project/package.json +0 -9
  132. package/fixtures/example-project/src/index.ts +0 -3
  133. package/fixtures/example-project/src/utils.test.ts +0 -2
  134. package/fixtures/example-project/src/utils.ts +0 -3
  135. package/logo.png +0 -0
  136. package/research/adoption-strategy.md +0 -111
  137. package/research/agent-integration.md +0 -145
  138. package/research/ai-code-quality.md +0 -197
  139. package/research/code-search-for-agents.md +0 -313
  140. package/research/competitive-landscape.md +0 -163
  141. package/research/doc-freshness.md +0 -516
  142. package/research/executable-specs.md +0 -368
  143. package/research/feature-ideas.md +0 -464
  144. package/research/formal-proofs-for-agents.md +0 -338
  145. package/research/fp-for-agent-harness.md +0 -150
  146. package/research/fp-for-deterministic-ai.md +0 -131
  147. package/research/self-evolving-specs.md +0 -298
  148. package/schemas/claude-md-strict.yml +0 -18
  149. package/schemas/claude-md.yml +0 -6
  150. package/schemas/skill-strict.yml +0 -12
  151. package/schemas/skill.yml +0 -5
  152. package/skills/audit-feedback-loop/SKILL.md +0 -76
  153. package/skills/edit-spec/SKILL.md +0 -131
  154. package/skills/enforce-rules-format/SKILL.md +0 -71
  155. package/skills/generate-logo/SKILL.md +0 -102
  156. package/skills/generate-rule/SKILL.md +0 -90
  157. package/skills/linter-docs/clippy.md +0 -241
  158. package/skills/linter-docs/eslint.md +0 -384
  159. package/skills/linter-docs/pylint.md +0 -288
  160. package/skills/linter-docs/rubocop.md +0 -277
  161. package/skills/linter-docs/ruff.md +0 -187
  162. package/skills/linter-docs/stylelint.md +0 -247
  163. package/skills/migrate-to-spec/SKILL.md +0 -124
  164. package/skills/pr-to-lint-rule/SKILL.md +0 -97
  165. package/skills/strengthen/SKILL.md +0 -168
  166. package/src/action.ts +0 -214
  167. package/src/cli.test.ts +0 -914
  168. package/src/cli.ts +0 -1631
  169. package/src/compile.ts +0 -691
  170. package/src/evolve.ts +0 -781
  171. package/src/freshness.test.ts +0 -449
  172. package/src/freshness.ts +0 -299
  173. package/src/generate-types.ts +0 -448
  174. package/src/inline.test.ts +0 -206
  175. package/src/inline.ts +0 -164
  176. package/src/linters.ts +0 -739
  177. package/src/proofs.test.ts +0 -1314
  178. package/src/proofs.ts +0 -849
  179. package/src/spec.test.ts +0 -1471
  180. package/src/spec.ts +0 -427
  181. package/src/types.ts +0 -117
  182. package/src/validate.test.ts +0 -701
  183. package/src/validate.ts +0 -381
  184. 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
@@ -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