scriptonia 0.9.0-rc.1 → 0.9.0-rc.2

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -11
  3. package/assets/grammar-manifest.json +32 -0
  4. package/assets/grammars/tree-sitter-bash.wasm +0 -0
  5. package/assets/grammars/tree-sitter-c.wasm +0 -0
  6. package/assets/grammars/tree-sitter-cpp.wasm +0 -0
  7. package/assets/grammars/tree-sitter-dart.wasm +0 -0
  8. package/assets/grammars/tree-sitter-go.wasm +0 -0
  9. package/assets/grammars/tree-sitter-java.wasm +0 -0
  10. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  11. package/assets/grammars/tree-sitter-kotlin.wasm +0 -0
  12. package/assets/grammars/tree-sitter-objc.wasm +0 -0
  13. package/assets/grammars/tree-sitter-python.wasm +0 -0
  14. package/assets/grammars/tree-sitter-rust.wasm +0 -0
  15. package/assets/grammars/tree-sitter-swift.wasm +0 -0
  16. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  17. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  18. package/assets/licenses/tree-sitter-wasms-LICENSE +24 -0
  19. package/assets/licenses/web-tree-sitter-LICENSE +21 -0
  20. package/assets/queries/manifest.json +12 -0
  21. package/assets/queries/v1/dart.scm +7 -0
  22. package/assets/queries/v1/go.scm +5 -0
  23. package/assets/queries/v1/javascript.scm +15 -0
  24. package/assets/queries/v1/python.scm +5 -0
  25. package/assets/queries/v1/typescript.scm +17 -0
  26. package/assets/tree-sitter.wasm +0 -0
  27. package/bin/scriptonia.mjs +243 -76
  28. package/bin/verify.mjs +40 -6
  29. package/dist/brain-ast-worker.js +62 -0
  30. package/dist/brain-ast-worker.js.map +1 -0
  31. package/dist/chunk-6TLQP3LV.js +4048 -0
  32. package/dist/chunk-6TLQP3LV.js.map +1 -0
  33. package/dist/chunk-ZLKAW77A.js +16613 -0
  34. package/dist/chunk-ZLKAW77A.js.map +1 -0
  35. package/dist/index.js +1407 -662
  36. package/dist/index.js.map +1 -1
  37. package/dist/legacy-brain-bridge.js +156 -0
  38. package/dist/legacy-brain-bridge.js.map +1 -0
  39. package/package.json +32 -3
package/dist/index.js CHANGED
@@ -1,76 +1,54 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ BrainStore,
4
+ MAX_DEEP_COVERAGE_DIAGNOSTIC_ITEMS,
5
+ brainExtractResponseV1Schema,
6
+ buildBrain,
7
+ buildContextPack,
8
+ buildSnapshot,
9
+ configSchema,
10
+ createBrainImpactProvider,
11
+ createContextGraphExpansionAdapter,
12
+ decisionSchema,
13
+ evalCaseSchema,
14
+ evidenceRequirementSchema,
15
+ findRepositoryRoot,
16
+ initializeTelemetry,
17
+ listFilesAtRef,
18
+ loadCurrentBrainImpactProvider,
19
+ parseDiffHunks,
20
+ readFileAtRef,
21
+ runEventSchema,
22
+ withSpan
23
+ } from "./chunk-ZLKAW77A.js";
24
+ import {
25
+ aggregateGate,
26
+ assertInside,
27
+ createId,
28
+ exitCodeForGate,
29
+ hashObject,
30
+ normalizeRepoPath,
31
+ nowIso,
32
+ sha256,
33
+ walkRepo
34
+ } from "./chunk-6TLQP3LV.js";
2
35
 
3
36
  // src/index.ts
4
37
  import fs9 from "fs";
5
- import os from "os";
6
- import path10 from "path";
38
+ import os2 from "os";
39
+ import path9 from "path";
7
40
  import { Command } from "commander";
8
- import pc from "picocolors";
41
+ import pc2 from "picocolors";
9
42
  import YAML3 from "yaml";
10
43
 
11
- // ../packages/core/src/index.ts
12
- import { createHash, randomBytes } from "crypto";
13
- import path from "path";
14
- function canonicalize(value) {
15
- return JSON.stringify(normalize(value));
16
- }
17
- function normalize(value) {
18
- if (value === null || typeof value === "string" || typeof value === "boolean") return typeof value === "string" ? value.replace(/\r\n?/g, "\n") : value;
19
- if (typeof value === "number") {
20
- if (!Number.isFinite(value)) throw new TypeError("canonical JSON rejects non-finite numbers");
21
- return value;
22
- }
23
- if (Array.isArray(value)) return value.map(normalize);
24
- if (typeof value === "object") {
25
- return Object.fromEntries(Object.entries(value).filter(([, item2]) => item2 !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, item2]) => [key, normalize(item2)]));
26
- }
27
- throw new TypeError(`canonical JSON rejects ${typeof value}`);
28
- }
29
- function sha256(value) {
30
- return createHash("sha256").update(value).digest("hex");
31
- }
32
- function hashObject(value) {
33
- return sha256(canonicalize(value));
34
- }
35
- function createId(prefix, at = Date.now()) {
36
- return `${prefix}_${at.toString(36).padStart(9, "0")}${randomBytes(8).toString("hex")}`;
37
- }
38
- function normalizeRepoPath(input) {
39
- if (input.includes("\0")) throw new Error("repository path contains a null byte");
40
- const normalized = input.replaceAll("\\", "/").replace(/^\.\//, "");
41
- if (path.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
42
- throw new Error(`path escapes repository: ${input}`);
43
- }
44
- return path.posix.normalize(normalized);
45
- }
46
- function assertInside(root, candidate) {
47
- const resolvedRoot = path.resolve(root);
48
- const resolved = path.resolve(candidate);
49
- if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path.sep}`)) throw new Error(`path escapes repository: ${candidate}`);
50
- return resolved;
51
- }
52
- function aggregateGate(checks, incompleteBlocks = true) {
53
- if (checks.some((item2) => item2.state === "FAIL" && item2.enforcement === "block")) return "BLOCKED";
54
- if (checks.some((item2) => item2.state === "FAIL" && item2.enforcement === "action_required")) return "ACTION_REQUIRED";
55
- if (checks.some((item2) => item2.state === "ERROR" || incompleteBlocks && item2.state === "INCONCLUSIVE" && item2.enforcement === "block")) return "INCOMPLETE";
56
- if (checks.some((item2) => item2.state === "FAIL" || item2.state === "INCONCLUSIVE")) return "PASS_WITH_WARNINGS";
57
- return "PASS";
58
- }
59
- function exitCodeForGate(state) {
60
- return state === "PASS" || state === "PASS_WITH_WARNINGS" ? 0 : state === "ACTION_REQUIRED" ? 2 : state === "BLOCKED" ? 3 : 4;
61
- }
62
- function nowIso() {
63
- return (/* @__PURE__ */ new Date()).toISOString();
64
- }
65
-
66
44
  // ../packages/eval-engine/src/index.ts
67
- import fs7 from "fs";
68
- import path8 from "path";
45
+ import fs5 from "fs";
46
+ import path5 from "path";
69
47
  import YAML2 from "yaml";
70
48
 
71
49
  // ../packages/evidence/src/index.ts
72
50
  import fs from "fs";
73
- import path2 from "path";
51
+ import path from "path";
74
52
  import fg from "fast-glob";
75
53
  import { XMLParser } from "fast-xml-parser";
76
54
  function item(kind, collector, source, state, payload) {
@@ -103,20 +81,20 @@ function collectJUnit(root, patterns) {
103
81
  const skipped = Boolean(test.skipped);
104
82
  const suite = String(test.classname ?? test.suite ?? "");
105
83
  const name = String(test.name ?? "unnamed");
106
- items.push(item("junit_test", "junit-xml", path2.relative(root, filename), skipped ? "missing" : failed ? "failed" : "available", { suite, test: name, failed, skipped }));
84
+ items.push(item("junit_test", "junit-xml", path.relative(root, filename), skipped ? "missing" : failed ? "failed" : "available", { suite, test: name, failed, skipped }));
107
85
  }
108
86
  } catch (error) {
109
- items.push(item("junit_test", "junit-xml", path2.relative(root, filename), "error", { error: error instanceof Error ? error.message : String(error) }));
87
+ items.push(item("junit_test", "junit-xml", path.relative(root, filename), "error", { error: error instanceof Error ? error.message : String(error) }));
110
88
  }
111
89
  }
112
90
  return items;
113
91
  }
114
92
  function flattenTests(value, suite = "") {
115
93
  if (!value || typeof value !== "object") return [];
116
- const record = value;
117
- const currentSuite = String(record["@_name"] ?? suite);
118
- const tests = asArray(record.testcase).map((entry) => ({ ...entry, suite: currentSuite, classname: entry["@_classname"], name: entry["@_name"], failure: entry.failure, error: entry.error, skipped: entry.skipped }));
119
- for (const key of ["testsuite", "testsuites"]) for (const child of asArray(record[key])) tests.push(...flattenTests(child, currentSuite));
94
+ const record3 = value;
95
+ const currentSuite = String(record3["@_name"] ?? suite);
96
+ const tests = asArray(record3.testcase).map((entry) => ({ ...entry, suite: currentSuite, classname: entry["@_classname"], name: entry["@_name"], failure: entry.failure, error: entry.error, skipped: entry.skipped }));
97
+ for (const key of ["testsuite", "testsuites"]) for (const child of asArray(record3[key])) tests.push(...flattenTests(child, currentSuite));
120
98
  return tests;
121
99
  }
122
100
  function asArray(value) {
@@ -124,7 +102,7 @@ function asArray(value) {
124
102
  }
125
103
  function collectRepositoryFacts(root, changedFiles) {
126
104
  return changedFiles.map((file) => {
127
- const full = path2.join(root, file);
105
+ const full = path.join(root, file);
128
106
  let exists = false;
129
107
  let size = 0;
130
108
  try {
@@ -154,124 +132,9 @@ function requirementMatches(requirement, candidate) {
154
132
  return true;
155
133
  }
156
134
 
157
- // ../packages/git-snapshot/src/index.ts
158
- import fs2 from "fs";
159
- import path3 from "path";
160
- import { spawnSync } from "child_process";
161
- function git(root, args, allowFailure = false) {
162
- const result = spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 4e7, shell: false });
163
- if (result.error) throw result.error;
164
- if (result.status !== 0 && !allowFailure) throw new Error(`git ${args[0] ?? "command"} failed: ${(result.stderr || "").trim()}`);
165
- return result.status === 0 ? (result.stdout || "").trimEnd() : "";
166
- }
167
- function findRepositoryRoot(cwd = process.cwd()) {
168
- const root = git(cwd, ["rev-parse", "--show-toplevel"]);
169
- if (!root) throw new Error("not inside a Git repository");
170
- return path3.resolve(root);
171
- }
172
- function resolveRef(root, ref) {
173
- const sha = git(root, ["rev-parse", "--verify", `${ref}^{commit}`]);
174
- if (!/^[0-9a-f]{40}$/i.test(sha)) throw new Error(`invalid Git ref: ${ref}`);
175
- return sha.toLowerCase();
176
- }
177
- function readFileAtRef(root, ref, filename) {
178
- const sha = resolveRef(root, ref);
179
- const safe = normalizeRepoPath(filename);
180
- const result = spawnSync("git", ["show", `${sha}:${safe}`], { cwd: root, encoding: "utf8", maxBuffer: 1e7, shell: false });
181
- return result.status === 0 ? result.stdout : void 0;
182
- }
183
- function listFilesAtRef(root, ref, prefixes) {
184
- const sha = resolveRef(root, ref);
185
- const safe = prefixes.map(normalizeRepoPath);
186
- const output = git(root, ["ls-tree", "-r", "--name-only", sha, "--", ...safe], true);
187
- return output.split("\n").filter(Boolean).map(normalizeRepoPath).sort();
188
- }
189
- function parseNameStatus(text) {
190
- const out = [];
191
- for (const line of text.split("\n")) {
192
- if (!line) continue;
193
- const [rawStatus = "", first = "", second] = line.split(" ");
194
- const code = rawStatus[0];
195
- const status = code === "A" ? "added" : code === "M" ? "modified" : code === "D" ? "deleted" : code === "R" ? "renamed" : code === "C" ? "copied" : "unknown";
196
- const target = status === "renamed" || status === "copied" ? second : first;
197
- if (!target) continue;
198
- const item2 = { path: normalizeRepoPath(target), status, binary: false };
199
- if ((status === "renamed" || status === "copied") && first) item2.oldPath = normalizeRepoPath(first);
200
- out.push(item2);
201
- }
202
- return out;
203
- }
204
- function untrackedFiles(root) {
205
- return git(root, ["ls-files", "--others", "--exclude-standard"], true).split("\n").filter(Boolean).map(normalizeRepoPath).filter((file) => !isRuntimeFile(file)).map((file) => ({ path: file, status: "added", binary: isBinary(path3.join(root, file)) }));
206
- }
207
- function isRuntimeFile(file) {
208
- return /^\.scriptonia\/(?:brain\.db(?:-shm|-wal)?|artifacts\/|result(?:\.|-)|self-verification\.)/.test(file);
209
- }
210
- function isBinary(filename) {
211
- try {
212
- const fd = fs2.openSync(filename, "r");
213
- const buffer = Buffer.alloc(8192);
214
- const read = fs2.readSync(fd, buffer, 0, buffer.length, 0);
215
- fs2.closeSync(fd);
216
- return buffer.subarray(0, read).includes(0);
217
- } catch {
218
- return false;
219
- }
220
- }
221
- function untrackedPatch(root, files) {
222
- const chunks = [];
223
- for (const file of files) {
224
- if (file.binary) {
225
- chunks.push(`diff --git a/${file.path} b/${file.path}
226
- new file mode 100644
227
- Binary files /dev/null and b/${file.path} differ`);
228
- continue;
229
- }
230
- const full = path3.join(root, file.path);
231
- try {
232
- const stat = fs2.statSync(full);
233
- if (!stat.isFile() || stat.size > 1e6) continue;
234
- const body = fs2.readFileSync(full, "utf8").replace(/\r\n?/g, "\n");
235
- const lines = body.split("\n");
236
- chunks.push(`diff --git a/${file.path} b/${file.path}
237
- new file mode 100644
238
- --- /dev/null
239
- +++ b/${file.path}
240
- @@ -0,0 +1,${lines.length} @@
241
- ${lines.map((line) => `+${line}`).join("\n")}`);
242
- } catch {
243
- }
244
- }
245
- return chunks.join("\n");
246
- }
247
- function buildSnapshot(options = {}) {
248
- const root = findRepositoryRoot(options.cwd);
249
- const baseRef = options.base ?? "HEAD";
250
- const baseSha = resolveRef(root, baseRef);
251
- const headSha = resolveRef(root, "HEAD");
252
- const includeWorkingTree = options.includeWorkingTree ?? true;
253
- const range = baseSha === headSha ? "HEAD" : `${baseSha}...${headSha}`;
254
- let diff = git(root, ["diff", "--no-ext-diff", "--binary", "--unified=20", range]);
255
- let files = parseNameStatus(git(root, ["diff", "--name-status", "--find-renames", range]));
256
- let dirty = false;
257
- if (includeWorkingTree) {
258
- const working = git(root, ["diff", "--no-ext-diff", "--binary", "--unified=20", "HEAD"]);
259
- const workingFiles = parseNameStatus(git(root, ["diff", "--name-status", "--find-renames", "HEAD"]));
260
- const untracked = untrackedFiles(root);
261
- dirty = workingFiles.length > 0 || untracked.length > 0;
262
- diff = [diff, working, untrackedPatch(root, untracked)].filter(Boolean).join("\n");
263
- const map = new Map([...files, ...workingFiles, ...untracked].map((file) => [file.path, file]));
264
- files = [...map.values()].sort((a, b) => a.path.localeCompare(b.path));
265
- }
266
- for (const file of files) if (!file.binary && file.status !== "deleted") file.binary = isBinary(path3.join(root, file.path));
267
- const diffHash = sha256(diff.replace(/\r\n?/g, "\n"));
268
- const identity = { schemaVersion: "1", baseSha, headSha, dirty, diffHash, files: files.map(({ path: name, status, oldPath, binary }) => ({ path: name, status, oldPath, binary })) };
269
- return { schemaVersion: "1", repositoryRoot: root, baseRef, baseSha, headSha, dirty, diff, diffHash, files, snapshotId: hashObject(identity) };
270
- }
271
-
272
135
  // ../packages/impact/src/index.ts
273
- import fs3 from "fs";
274
- import path4 from "path";
136
+ import fs2 from "fs";
137
+ import path2 from "path";
275
138
  import fg2 from "fast-glob";
276
139
  var SOURCE = ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}", "!**/node_modules/**", "!**/dist/**", "!**/.next/**", "!**/coverage/**"];
277
140
  var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
@@ -282,7 +145,7 @@ function buildFallbackImpactIndex(root) {
282
145
  for (const file of files) {
283
146
  let text;
284
147
  try {
285
- text = fs3.readFileSync(path4.join(root, file), "utf8");
148
+ text = fs2.readFileSync(path2.join(root, file), "utf8");
286
149
  } catch {
287
150
  continue;
288
151
  }
@@ -293,7 +156,7 @@ function buildFallbackImpactIndex(root) {
293
156
  for (const match of imports) {
294
157
  const specifier = match[1];
295
158
  if (!specifier?.startsWith(".")) continue;
296
- const base = normalizeRepoPath(path4.posix.join(path4.posix.dirname(file), specifier));
159
+ const base = normalizeRepoPath(path2.posix.join(path2.posix.dirname(file), specifier));
297
160
  const target = EXTENSIONS.map((extension) => `${base}${extension}`).find((candidate) => known.has(candidate));
298
161
  if (target) edges.push({ from: file, to: target, kind: "import", confidence: "high", provenance: `static-import:${file}` });
299
162
  }
@@ -324,16 +187,39 @@ function relatedTests(index, changedFiles) {
324
187
  return impacted.filter((file) => /(?:^|\/)(?:__tests__|tests?|spec)(?:\/|$)|\.(?:test|spec)\.[^.]+$/i.test(file));
325
188
  }
326
189
  function loadCodeGraphIndex(root) {
327
- const filename = path4.join(root, ".scriptonia", "codegraph.json");
328
- if (!fs3.existsSync(filename)) return void 0;
190
+ const filename = path2.join(root, ".scriptonia", "codegraph.json");
191
+ if (!fs2.existsSync(filename)) return void 0;
329
192
  try {
330
- const value = JSON.parse(fs3.readFileSync(filename, "utf8"));
193
+ const value = JSON.parse(fs2.readFileSync(filename, "utf8"));
331
194
  const edges = (value.edges ?? []).filter((edge) => edge && typeof edge.from === "string" && typeof edge.to === "string" && ["high", "medium", "low"].includes(edge.confidence));
332
195
  return { provider: "codegraph-file-adapter", version: "1", id: hashObject({ sourceVersion: value.version, edges }), edges };
333
196
  } catch {
334
197
  return void 0;
335
198
  }
336
199
  }
200
+ function createIndexImpactProvider(index) {
201
+ return {
202
+ index,
203
+ generation: index.id,
204
+ coverageValid: true,
205
+ changedSymbols: () => [],
206
+ dependents: (change, depth = 2) => impactedFiles(index, change.files, depth),
207
+ relatedTests: (change) => relatedTests(index, change.files)
208
+ };
209
+ }
210
+ function selectImpactProvider(options) {
211
+ if (options.brain?.coverageValid) return options.brain;
212
+ return createIndexImpactProvider(options.codeGraph ?? options.fallback());
213
+ }
214
+ function impactProviderIdentity(provider) {
215
+ return {
216
+ provider: provider.index.provider,
217
+ version: provider.index.version,
218
+ id: provider.index.id,
219
+ generation: provider.generation,
220
+ coverageValid: provider.coverageValid
221
+ };
222
+ }
337
223
 
338
224
  // ../packages/model-gateway/src/index.ts
339
225
  import { z } from "zod";
@@ -343,19 +229,34 @@ var judgeResultSchema = z.object({
343
229
  evidenceRefs: z.array(z.string()).default([]),
344
230
  reason: z.string()
345
231
  });
232
+ function secureBaseUrl(value, label) {
233
+ let url;
234
+ try {
235
+ url = new URL(value);
236
+ } catch {
237
+ return { error: `${label} must be a valid absolute URL` };
238
+ }
239
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
240
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
241
+ return { error: `${label} must use HTTPS except for localhost, 127.0.0.1, or [::1]` };
242
+ }
243
+ return { url: url.toString().replace(/\/$/, "") };
244
+ }
346
245
  async function semanticJudge(input) {
347
246
  const model = input.model ?? process.env.OPENAI_OUTPUT_MODEL ?? "gpt-5.6-terra";
348
247
  const rubric = "Assess only whether the supplied evidence supports or contradicts the criterion. Return uncertain when evidence is insufficient. Never infer behavior from code style or plausibility.";
349
248
  const serialized = JSON.stringify(input.evidenceBundle);
350
249
  const maxBytes = input.maxBytes ?? 12e4;
351
250
  if (Buffer.byteLength(serialized) > maxBytes) return { available: false, error: `evidence bundle exceeds ${maxBytes} byte limit` };
352
- const gatewayUrl = process.env.SCRIPTONIA_MODEL_GATEWAY_URL?.replace(/\/$/, "");
251
+ const configuredGatewayUrl = process.env.SCRIPTONIA_MODEL_GATEWAY_URL;
353
252
  const gatewayToken = process.env.SCRIPTONIA_API_TOKEN;
354
- if (gatewayUrl && gatewayToken) {
253
+ if (configuredGatewayUrl && gatewayToken) {
254
+ const gateway = secureBaseUrl(configuredGatewayUrl, "Scriptonia model gateway URL");
255
+ if (!gateway.url) return { available: false, error: gateway.error };
355
256
  const controller2 = new AbortController();
356
257
  const timeout2 = setTimeout(() => controller2.abort(), input.timeoutMs ?? 2e4);
357
258
  try {
358
- const response = await (input.fetchImpl ?? fetch)(`${gatewayUrl}/api/v2/judge`, { method: "POST", headers: { authorization: `Bearer ${gatewayToken}`, "content-type": "application/json" }, signal: controller2.signal, body: JSON.stringify({ criterion: input.criterion, evidence: input.evidenceBundle }) });
259
+ const response = await (input.fetchImpl ?? fetch)(`${gateway.url}/api/v2/judge`, { method: "POST", headers: { authorization: `Bearer ${gatewayToken}`, "content-type": "application/json" }, signal: controller2.signal, body: JSON.stringify({ criterion: input.criterion, evidence: input.evidenceBundle }) });
359
260
  const body = await response.json();
360
261
  if (!response.ok) return { available: false, error: `Scriptonia model gateway returned ${response.status}` };
361
262
  const parsed = judgeResultSchema.safeParse(body);
@@ -370,10 +271,12 @@ async function semanticJudge(input) {
370
271
  const endpoint = input.endpoint ?? process.env.LITELLM_BASE_URL;
371
272
  const apiKey = input.apiKey ?? process.env.LITELLM_MASTER_KEY;
372
273
  if (!endpoint || !apiKey) return { available: false, error: "Scriptonia model gateway is not configured" };
274
+ const liteLLM = secureBaseUrl(endpoint, "LiteLLM base URL");
275
+ if (!liteLLM.url) return { available: false, error: liteLLM.error };
373
276
  const directModel = input.model ?? process.env.LITELLM_MODEL ?? "semantic-judge";
374
277
  const controller = new AbortController();
375
278
  const timeout = setTimeout(() => controller.abort(), input.timeoutMs ?? 2e4);
376
- const url = `${endpoint.replace(/\/$/, "")}/v1/chat/completions`;
279
+ const url = `${liteLLM.url}/v1/chat/completions`;
377
280
  try {
378
281
  const response = await (input.fetchImpl ?? fetch)(url, {
379
282
  method: "POST",
@@ -404,168 +307,28 @@ Return JSON with result, confidence, evidenceRefs, reason.` },
404
307
  }
405
308
  }
406
309
 
407
- // ../packages/observability/src/index.ts
408
- import { SpanStatusCode, trace } from "@opentelemetry/api";
409
- var started = false;
410
- async function initializeTelemetry() {
411
- if (started || process.env.SCRIPTONIA_TELEMETRY !== "1" || !process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
412
- const [{ NodeSDK }, { OTLPTraceExporter }] = await Promise.all([import("@opentelemetry/sdk-node"), import("@opentelemetry/exporter-trace-otlp-http")]);
413
- const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter({ url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, "")}/v1/traces` }) });
414
- await sdk.start();
415
- started = true;
416
- }
417
- async function withSpan(name, attributes, operation) {
418
- const tracer = trace.getTracer("scriptonia", "0.9.0-rc.1");
419
- return tracer.startActiveSpan(name, { attributes }, async (span) => {
420
- const startedAt = performance.now();
421
- try {
422
- const result = await operation();
423
- span.setAttribute("duration_bucket_ms", bucket(performance.now() - startedAt));
424
- span.setStatus({ code: SpanStatusCode.OK });
425
- return result;
426
- } catch (error) {
427
- span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) });
428
- throw error;
429
- } finally {
430
- span.end();
431
- }
432
- });
433
- }
434
- function bucket(duration) {
435
- const buckets = [10, 25, 50, 100, 250, 500, 1e3, 2e3, 5e3, 1e4, 3e4];
436
- return buckets.find((value) => duration <= value) ?? 6e4;
437
- }
438
-
439
310
  // ../packages/policy-engine/src/index.ts
440
- import fs5 from "fs";
441
- import path6 from "path";
311
+ import fs4 from "fs";
312
+ import path4 from "path";
442
313
  import picomatch2 from "picomatch";
443
314
  import YAML from "yaml";
444
315
 
445
- // ../packages/schemas/src/index.ts
446
- import { z as z2 } from "zod";
447
- var checkStateSchema = z2.enum(["PASS", "FAIL", "INCONCLUSIVE", "SKIPPED", "ERROR"]);
448
- var gateStateSchema = z2.enum(["PASS", "PASS_WITH_WARNINGS", "ACTION_REQUIRED", "BLOCKED", "INCOMPLETE"]);
449
- var enforcementSchema = z2.enum(["observe", "warn", "action_required", "block"]);
450
- var lifecycleSchema = z2.enum(["draft", "active", "superseded", "expired", "stale", "quarantined", "archived"]);
451
- var defaultScope = {
452
- repositories: [],
453
- packages: [],
454
- services: [],
455
- paths: { include: ["**"], exclude: [] },
456
- symbols: [],
457
- dependencies: [],
458
- owners: [],
459
- tags: []
460
- };
461
- var scopeSchema = z2.object({
462
- repositories: z2.array(z2.string()).default([]),
463
- packages: z2.array(z2.string()).default([]),
464
- services: z2.array(z2.string()).default([]),
465
- paths: z2.object({ include: z2.array(z2.string()).default(["**"]), exclude: z2.array(z2.string()).default([]) }).default({ include: ["**"], exclude: [] }),
466
- symbols: z2.array(z2.string()).default([]),
467
- dependencies: z2.array(z2.string()).default([]),
468
- owners: z2.array(z2.string()).default([]),
469
- tags: z2.array(z2.string()).default([])
470
- }).default(defaultScope);
471
- var predicateKinds = [
472
- "path_forbidden",
473
- "path_requires_owner",
474
- "dependency_forbidden",
475
- "dependency_required",
476
- "symbol_forbidden",
477
- "symbol_requires_test",
478
- "file_pattern_required",
479
- "permission_required",
480
- "check_required"
481
- ];
482
- var predicateSchema = z2.object({
483
- kind: z2.enum(predicateKinds),
484
- names: z2.array(z2.string()).default([]),
485
- patterns: z2.array(z2.string()).default([]),
486
- owner: z2.string().optional(),
487
- permission: z2.string().optional(),
488
- check: z2.string().optional()
489
- });
490
- var decisionSchema = z2.object({
491
- schema_version: z2.literal("1").default("1"),
492
- id: z2.string().min(1),
493
- version: z2.number().int().positive().default(1),
494
- title: z2.string().min(1),
495
- state: lifecycleSchema.default("draft"),
496
- owner: z2.string().optional(),
497
- source_refs: z2.array(z2.string()).default([]),
498
- scope: scopeSchema,
499
- predicate: predicateSchema,
500
- enforcement: enforcementSchema.default("observe"),
501
- created_at: z2.string().datetime().optional(),
502
- confirmed_by: z2.string().optional(),
503
- supersedes: z2.string().optional()
504
- });
505
- var evidenceRequirementSchema = z2.object({
506
- kind: z2.enum(["github_check", "junit_test", "repository_fact", "decision_predicate", "state_validator", "human_confirmation", "semantic_judge"]),
507
- name: z2.string().optional(),
508
- suite: z2.string().optional(),
509
- test: z2.string().optional(),
510
- path: z2.string().optional(),
511
- value: z2.string().optional()
512
- });
513
- var evalCaseSchema = z2.object({
514
- schema_version: z2.literal("1").default("1"),
515
- id: z2.string().min(1),
516
- version: z2.number().int().positive().default(1),
517
- title: z2.string().min(1),
518
- state: lifecycleSchema.default("draft"),
519
- owner: z2.string().optional(),
520
- origin: z2.object({ kind: z2.string(), run_ref: z2.string().optional(), finding_ref: z2.string().optional() }),
521
- decision_refs: z2.array(z2.string()).default([]),
522
- signal_refs: z2.array(z2.string()).default([]),
523
- scope: scopeSchema,
524
- assertion: z2.object({ kind: z2.string(), description: z2.string().optional() }),
525
- evidence: z2.object({ all: z2.array(evidenceRequirementSchema).default([]), any: z2.array(evidenceRequirementSchema).default([]) }).default({ all: [], any: [] }),
526
- enforcement: enforcementSchema.default("observe"),
527
- fingerprint: z2.string().optional(),
528
- last_confirmed_at: z2.string().datetime().optional(),
529
- review_after: z2.string().datetime().optional()
530
- });
531
- var runEventSchema = z2.object({
532
- schemaVersion: z2.literal("1"),
533
- eventId: z2.string(),
534
- runId: z2.string(),
535
- sequence: z2.number().int().nonnegative(),
536
- occurredAt: z2.string().datetime(),
537
- source: z2.object({ adapter: z2.string(), adapterVersion: z2.string(), agent: z2.string().optional(), agentVersion: z2.string().optional() }),
538
- type: z2.enum(["run.started", "run.finished", "tool.requested", "tool.completed", "file.changed", "command.requested", "human.corrected", "error"]),
539
- payload: z2.unknown(),
540
- redaction: z2.object({ applied: z2.boolean(), policyVersion: z2.string() })
541
- });
542
- var configSchema = z2.object({
543
- schema_version: z2.literal("1").default("1"),
544
- base_ref: z2.string().default("HEAD"),
545
- required_plan: z2.boolean().default(false),
546
- incomplete_blocks: z2.boolean().default(true),
547
- trusted_config_ref: z2.string().default("HEAD"),
548
- evidence: z2.object({ junit: z2.array(z2.string()).default([]), github_needs_env: z2.string().default("SCRIPTONIA_NEEDS_JSON") }).default({ junit: [], github_needs_env: "SCRIPTONIA_NEEDS_JSON" }),
549
- semantic: z2.object({ enabled: z2.boolean().default(false), endpoint: z2.string().url().optional(), model: z2.string().optional(), max_bytes: z2.number().int().positive().default(12e4), timeout_ms: z2.number().int().positive().default(2e4) }).default({ enabled: false, max_bytes: 12e4, timeout_ms: 2e4 }),
550
- redaction: z2.object({ env_names: z2.array(z2.string()).default([]), patterns: z2.array(z2.string()).default([]), max_payload_bytes: z2.number().int().positive().default(128e3) }).default({ env_names: [], patterns: [], max_payload_bytes: 128e3 })
551
- });
552
-
553
316
  // ../packages/scope-engine/src/index.ts
554
- import fs4 from "fs";
555
- import path5 from "path";
317
+ import fs3 from "fs";
318
+ import path3 from "path";
556
319
  import picomatch from "picomatch";
557
320
  function discoverPackages(root) {
558
321
  const result = /* @__PURE__ */ new Map();
559
322
  const visit = (relative, depth) => {
560
323
  if (depth > 4) return;
561
- const absolute = path5.join(root, relative);
324
+ const absolute = path3.join(root, relative);
562
325
  for (const entry of safeReadDir(absolute)) {
563
326
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
564
- const next = normalizeRepoPath(path5.posix.join(relative.replaceAll("\\", "/"), entry.name));
565
- const pkgFile = path5.join(root, next, "package.json");
566
- if (fs4.existsSync(pkgFile)) {
327
+ const next = normalizeRepoPath(path3.posix.join(relative.replaceAll("\\", "/"), entry.name));
328
+ const pkgFile = path3.join(root, next, "package.json");
329
+ if (fs3.existsSync(pkgFile)) {
567
330
  try {
568
- result.set(next, String(JSON.parse(fs4.readFileSync(pkgFile, "utf8")).name || next));
331
+ result.set(next, String(JSON.parse(fs3.readFileSync(pkgFile, "utf8")).name || next));
569
332
  } catch {
570
333
  result.set(next, next);
571
334
  }
@@ -573,9 +336,9 @@ function discoverPackages(root) {
573
336
  visit(next, depth + 1);
574
337
  }
575
338
  };
576
- if (fs4.existsSync(path5.join(root, "package.json"))) {
339
+ if (fs3.existsSync(path3.join(root, "package.json"))) {
577
340
  try {
578
- result.set(".", String(JSON.parse(fs4.readFileSync(path5.join(root, "package.json"), "utf8")).name || "."));
341
+ result.set(".", String(JSON.parse(fs3.readFileSync(path3.join(root, "package.json"), "utf8")).name || "."));
579
342
  } catch {
580
343
  result.set(".", ".");
581
344
  }
@@ -585,17 +348,17 @@ function discoverPackages(root) {
585
348
  }
586
349
  function safeReadDir(dir) {
587
350
  try {
588
- return fs4.readdirSync(dir, { withFileTypes: true });
351
+ return fs3.readdirSync(dir, { withFileTypes: true });
589
352
  } catch {
590
353
  return [];
591
354
  }
592
355
  }
593
356
  function parseCodeowners(root) {
594
357
  const candidates = ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS"];
595
- const file = candidates.map((name) => path5.join(root, name)).find(fs4.existsSync);
358
+ const file = candidates.map((name) => path3.join(root, name)).find(fs3.existsSync);
596
359
  const owners = /* @__PURE__ */ new Map();
597
360
  if (!file) return owners;
598
- for (const raw of fs4.readFileSync(file, "utf8").split("\n")) {
361
+ for (const raw of fs3.readFileSync(file, "utf8").split("\n")) {
599
362
  const line = raw.trim();
600
363
  if (!line || line.startsWith("#")) continue;
601
364
  const [pattern, ...names] = line.split(/\s+/);
@@ -645,7 +408,7 @@ function discoverDependencies(root) {
645
408
  const names = /* @__PURE__ */ new Set();
646
409
  for (const [directory] of discoverPackages(root)) {
647
410
  try {
648
- const pkg = JSON.parse(fs4.readFileSync(path5.join(root, directory, "package.json"), "utf8"));
411
+ const pkg = JSON.parse(fs3.readFileSync(path3.join(root, directory, "package.json"), "utf8"));
649
412
  for (const section of [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies]) for (const name of Object.keys(section || {})) names.add(name);
650
413
  } catch {
651
414
  }
@@ -658,7 +421,7 @@ function extractSymbols(root, files) {
658
421
  for (const file of files) {
659
422
  if (!/\.[cm]?[jt]sx?$/.test(file)) continue;
660
423
  try {
661
- const text = fs4.readFileSync(path5.join(root, file), "utf8");
424
+ const text = fs3.readFileSync(path3.join(root, file), "utf8");
662
425
  for (const match of text.matchAll(pattern)) if (match[1]) symbols.add(match[1]);
663
426
  } catch {
664
427
  }
@@ -668,13 +431,13 @@ function extractSymbols(root, files) {
668
431
 
669
432
  // ../packages/policy-engine/src/index.ts
670
433
  function loadDecisions(root) {
671
- const directory = path6.join(root, ".scriptonia", "decisions");
672
- if (!fs5.existsSync(directory)) return { decisions: [], errors: [] };
434
+ const directory = path4.join(root, ".scriptonia", "decisions");
435
+ if (!fs4.existsSync(directory)) return { decisions: [], errors: [] };
673
436
  const decisions = [];
674
437
  const errors = [];
675
- for (const name of fs5.readdirSync(directory).filter((file) => /\.ya?ml$/i.test(file)).sort()) {
438
+ for (const name of fs4.readdirSync(directory).filter((file) => /\.ya?ml$/i.test(file)).sort()) {
676
439
  try {
677
- decisions.push(decisionSchema.parse(YAML.parse(fs5.readFileSync(path6.join(directory, name), "utf8"))));
440
+ decisions.push(decisionSchema.parse(YAML.parse(fs4.readFileSync(path4.join(directory, name), "utf8"))));
678
441
  } catch (error) {
679
442
  errors.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
680
443
  }
@@ -753,247 +516,13 @@ function evaluateDecision(decision, context) {
753
516
  }
754
517
  }
755
518
 
756
- // ../packages/storage-sqlite/src/index.ts
757
- import fs6 from "fs";
758
- import path7 from "path";
759
- import Database from "better-sqlite3";
760
- var MIGRATION_1 = `
761
- CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL);
762
- CREATE TABLE IF NOT EXISTS agent_runs (
763
- id TEXT PRIMARY KEY, adapter TEXT NOT NULL, command_json TEXT NOT NULL, started_at TEXT NOT NULL,
764
- finished_at TEXT, base_sha TEXT, head_sha TEXT, diff_hash TEXT, exit_code INTEGER, status TEXT NOT NULL
765
- );
766
- CREATE TABLE IF NOT EXISTS run_events (
767
- event_id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES agent_runs(id), sequence INTEGER NOT NULL,
768
- occurred_at TEXT NOT NULL, type TEXT NOT NULL, payload_json TEXT NOT NULL, redacted INTEGER NOT NULL,
769
- UNIQUE(run_id, sequence)
770
- );
771
- CREATE TABLE IF NOT EXISTS git_snapshots (
772
- id TEXT PRIMARY KEY, base_sha TEXT NOT NULL, head_sha TEXT NOT NULL, diff_hash TEXT NOT NULL,
773
- payload_json TEXT NOT NULL, created_at TEXT NOT NULL
774
- );
775
- CREATE TABLE IF NOT EXISTS verification_runs (
776
- id TEXT PRIMARY KEY, snapshot_id TEXT NOT NULL, gate_state TEXT NOT NULL, result_json TEXT NOT NULL,
777
- created_at TEXT NOT NULL, refreshed_from TEXT
778
- );
779
- CREATE TABLE IF NOT EXISTS verdict_cache (
780
- snapshot_id TEXT PRIMARY KEY, verification_id TEXT NOT NULL REFERENCES verification_runs(id), result_json TEXT NOT NULL,
781
- created_at TEXT NOT NULL
782
- );
783
- CREATE TABLE IF NOT EXISTS findings (
784
- id TEXT PRIMARY KEY, snapshot_id TEXT NOT NULL, fingerprint TEXT NOT NULL, evaluator TEXT NOT NULL,
785
- state TEXT NOT NULL, enforcement TEXT NOT NULL, owner TEXT, payload_json TEXT NOT NULL,
786
- first_seen TEXT NOT NULL, last_seen TEXT NOT NULL, UNIQUE(fingerprint, snapshot_id)
787
- );
788
- CREATE TABLE IF NOT EXISTS acknowledgements (
789
- id TEXT PRIMARY KEY, finding_id TEXT NOT NULL REFERENCES findings(id), reason TEXT NOT NULL,
790
- comment TEXT, actor TEXT NOT NULL, snapshot_id TEXT NOT NULL, created_at TEXT NOT NULL
791
- );
792
- CREATE TABLE IF NOT EXISTS eval_cases (
793
- id TEXT NOT NULL, version INTEGER NOT NULL, state TEXT NOT NULL, fingerprint TEXT,
794
- payload_json TEXT NOT NULL, indexed_at TEXT NOT NULL, PRIMARY KEY(id, version)
795
- );
796
- CREATE TABLE IF NOT EXISTS decision_versions (
797
- id TEXT NOT NULL, version INTEGER NOT NULL, state TEXT NOT NULL, payload_json TEXT NOT NULL,
798
- indexed_at TEXT NOT NULL, PRIMARY KEY(id, version)
799
- );
800
- CREATE TABLE IF NOT EXISTS evidence_items (
801
- id TEXT PRIMARY KEY, content_hash TEXT NOT NULL, kind TEXT NOT NULL, state TEXT NOT NULL,
802
- payload_json TEXT NOT NULL, collected_at TEXT NOT NULL
803
- );
804
- CREATE INDEX IF NOT EXISTS idx_findings_fingerprint ON findings(fingerprint);
805
- CREATE INDEX IF NOT EXISTS idx_runs_started ON agent_runs(started_at DESC);
806
- CREATE INDEX IF NOT EXISTS idx_verifications_created ON verification_runs(created_at DESC);
807
- `;
808
- var MIGRATION_2 = `
809
- CREATE TABLE IF NOT EXISTS signals (
810
- id TEXT PRIMARY KEY, source TEXT NOT NULL, body TEXT NOT NULL, content_hash TEXT NOT NULL,
811
- metadata_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL
812
- );
813
- CREATE TABLE IF NOT EXISTS plans (
814
- id TEXT NOT NULL, version INTEGER NOT NULL, state TEXT NOT NULL, body TEXT NOT NULL,
815
- content_hash TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY(id, version)
816
- );
817
- CREATE TABLE IF NOT EXISTS decisions (
818
- id TEXT PRIMARY KEY, current_version INTEGER NOT NULL, state TEXT NOT NULL, title TEXT NOT NULL, owner TEXT
819
- );
820
- CREATE TABLE IF NOT EXISTS decision_links (
821
- decision_id TEXT NOT NULL, ref_type TEXT NOT NULL, ref_id TEXT NOT NULL,
822
- PRIMARY KEY(decision_id, ref_type, ref_id)
823
- );
824
- CREATE TABLE IF NOT EXISTS changed_files (
825
- snapshot_id TEXT NOT NULL, path TEXT NOT NULL, status TEXT NOT NULL, old_path TEXT, binary INTEGER NOT NULL,
826
- PRIMARY KEY(snapshot_id, path)
827
- );
828
- CREATE TABLE IF NOT EXISTS changed_symbols (
829
- snapshot_id TEXT NOT NULL, symbol TEXT NOT NULL, path TEXT NOT NULL, language TEXT,
830
- PRIMARY KEY(snapshot_id, symbol, path)
831
- );
832
- CREATE TABLE IF NOT EXISTS impact_indexes (
833
- id TEXT PRIMARY KEY, provider TEXT NOT NULL, version TEXT NOT NULL, snapshot_id TEXT NOT NULL,
834
- payload_json TEXT NOT NULL, created_at TEXT NOT NULL
835
- );
836
- CREATE TABLE IF NOT EXISTS impact_edges (
837
- impact_index_id TEXT NOT NULL, from_ref TEXT NOT NULL, to_ref TEXT NOT NULL, kind TEXT NOT NULL,
838
- confidence TEXT NOT NULL, provenance TEXT NOT NULL,
839
- PRIMARY KEY(impact_index_id, from_ref, to_ref, kind)
840
- );
841
- CREATE TABLE IF NOT EXISTS eval_versions (
842
- id TEXT NOT NULL, version INTEGER NOT NULL, state TEXT NOT NULL, payload_json TEXT NOT NULL,
843
- indexed_at TEXT NOT NULL, PRIMARY KEY(id, version)
844
- );
845
- CREATE TABLE IF NOT EXISTS eval_links (
846
- eval_id TEXT NOT NULL, ref_type TEXT NOT NULL, ref_id TEXT NOT NULL,
847
- PRIMARY KEY(eval_id, ref_type, ref_id)
848
- );
849
- CREATE TABLE IF NOT EXISTS verification_checks (
850
- verification_id TEXT NOT NULL, check_id TEXT NOT NULL, state TEXT NOT NULL, enforcement TEXT NOT NULL,
851
- fingerprint TEXT NOT NULL, payload_json TEXT NOT NULL, PRIMARY KEY(verification_id, check_id)
852
- );
853
- CREATE TABLE IF NOT EXISTS evaluator_calibration (
854
- evaluator TEXT NOT NULL, version TEXT NOT NULL, sample_id TEXT NOT NULL, judge_result TEXT NOT NULL,
855
- human_result TEXT NOT NULL, agreed INTEGER NOT NULL, created_at TEXT NOT NULL,
856
- PRIMARY KEY(evaluator, version, sample_id)
857
- );
858
- CREATE VIRTUAL TABLE IF NOT EXISTS fts_memory USING fts5(kind, ref_id UNINDEXED, title, body, tokenize='unicode61');
859
- `;
860
- var BrainStore = class {
861
- db;
862
- filename;
863
- constructor(repoRoot, filename = path7.join(repoRoot, ".scriptonia", "brain.db")) {
864
- fs6.mkdirSync(path7.dirname(filename), { recursive: true });
865
- this.filename = filename;
866
- this.db = new Database(filename);
867
- this.db.pragma("journal_mode = WAL");
868
- this.db.pragma("foreign_keys = ON");
869
- this.db.pragma("busy_timeout = 5000");
870
- this.db.pragma("synchronous = NORMAL");
871
- this.migrate();
872
- }
873
- migrate() {
874
- const current = Number(this.db.pragma("user_version", { simple: true }));
875
- if (current < 1) this.db.transaction(() => {
876
- this.db.exec(MIGRATION_1);
877
- this.db.prepare("INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)").run(1, nowIso());
878
- this.db.pragma("user_version = 1");
879
- })();
880
- if (current < 2) this.db.transaction(() => {
881
- this.db.exec(MIGRATION_2);
882
- this.db.prepare("INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)").run(2, nowIso());
883
- this.db.pragma("user_version = 2");
884
- })();
885
- }
886
- close() {
887
- this.db.close();
888
- }
889
- createRun(run) {
890
- this.db.prepare(`INSERT INTO agent_runs(id,adapter,command_json,started_at,base_sha,status) VALUES (?,?,?,?,?,?)`).run(run.id, run.adapter, canonicalize(run.command), run.startedAt, run.baseSha ?? null, "running");
891
- }
892
- appendEvent(event) {
893
- this.db.prepare(`INSERT INTO run_events(event_id,run_id,sequence,occurred_at,type,payload_json,redacted) VALUES (?,?,?,?,?,?,?)`).run(event.eventId, event.runId, event.sequence, event.occurredAt, event.type, canonicalize(event.payload), event.redacted ? 1 : 0);
894
- }
895
- finishRun(run) {
896
- this.db.prepare(`UPDATE agent_runs SET finished_at=?, head_sha=?, diff_hash=?, exit_code=?, status=? WHERE id=?`).run(run.finishedAt, run.headSha ?? null, run.diffHash ?? null, run.exitCode, run.status, run.id);
897
- }
898
- listRuns(limit = 20) {
899
- return this.db.prepare(`SELECT id,adapter,command_json AS command,started_at,finished_at,base_sha,head_sha,diff_hash,exit_code,status FROM agent_runs ORDER BY started_at DESC LIMIT ?`).all(limit).map((row) => ({ ...row, command: JSON.parse(String(row.command)) }));
900
- }
901
- getRun(id) {
902
- const run = this.db.prepare(`SELECT * FROM agent_runs WHERE id=?`).get(id);
903
- if (!run) return void 0;
904
- const events = this.db.prepare(`SELECT event_id,sequence,occurred_at,type,payload_json,redacted FROM run_events WHERE run_id=? ORDER BY sequence`).all(id).map((row) => {
905
- const { payload_json, ...rest2 } = row;
906
- return { ...rest2, payload: JSON.parse(String(payload_json)) };
907
- });
908
- const { command_json, ...rest } = run;
909
- return { ...rest, command: JSON.parse(String(command_json)), events };
910
- }
911
- getCached(snapshotId) {
912
- const row = this.db.prepare(`SELECT result_json FROM verdict_cache WHERE snapshot_id=?`).get(snapshotId);
913
- return row ? JSON.parse(row.result_json) : void 0;
914
- }
915
- indexDecision(decision) {
916
- const json = canonicalize(decision);
917
- this.db.transaction(() => {
918
- this.db.prepare(`INSERT INTO decisions(id,current_version,state,title,owner) VALUES (?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET current_version=excluded.current_version,state=excluded.state,title=excluded.title,owner=excluded.owner`).run(decision.id, decision.version, decision.state, decision.title, decision.owner ?? null);
919
- this.db.prepare(`INSERT INTO decision_versions(id,version,state,payload_json,indexed_at) VALUES (?,?,?,?,?) ON CONFLICT(id,version) DO UPDATE SET state=excluded.state,payload_json=excluded.payload_json,indexed_at=excluded.indexed_at`).run(decision.id, decision.version, decision.state, json, nowIso());
920
- this.db.prepare(`DELETE FROM fts_memory WHERE kind='decision' AND ref_id=?`).run(decision.id);
921
- this.db.prepare(`INSERT INTO fts_memory(kind,ref_id,title,body) VALUES ('decision',?,?,?)`).run(decision.id, decision.title, json);
922
- for (const ref of decision.source_refs ?? []) this.db.prepare(`INSERT OR IGNORE INTO decision_links(decision_id,ref_type,ref_id) VALUES (?,?,?)`).run(decision.id, ref.split("_")[0] || "ref", ref);
923
- })();
924
- }
925
- indexEval(evaluation) {
926
- const json = canonicalize(evaluation);
927
- this.db.transaction(() => {
928
- this.db.prepare(`INSERT INTO eval_cases(id,version,state,fingerprint,payload_json,indexed_at) VALUES (?,?,?,?,?,?) ON CONFLICT(id,version) DO UPDATE SET state=excluded.state,fingerprint=excluded.fingerprint,payload_json=excluded.payload_json,indexed_at=excluded.indexed_at`).run(evaluation.id, evaluation.version, evaluation.state, String(evaluation.fingerprint ?? ""), json, nowIso());
929
- this.db.prepare(`INSERT INTO eval_versions(id,version,state,payload_json,indexed_at) VALUES (?,?,?,?,?) ON CONFLICT(id,version) DO UPDATE SET state=excluded.state,payload_json=excluded.payload_json,indexed_at=excluded.indexed_at`).run(evaluation.id, evaluation.version, evaluation.state, json, nowIso());
930
- this.db.prepare(`DELETE FROM fts_memory WHERE kind='eval' AND ref_id=?`).run(evaluation.id);
931
- this.db.prepare(`INSERT INTO fts_memory(kind,ref_id,title,body) VALUES ('eval',?,?,?)`).run(evaluation.id, evaluation.title, json);
932
- for (const ref of evaluation.decision_refs ?? []) this.db.prepare(`INSERT OR IGNORE INTO eval_links(eval_id,ref_type,ref_id) VALUES (?,?,?)`).run(evaluation.id, "decision", ref);
933
- for (const ref of evaluation.signal_refs ?? []) this.db.prepare(`INSERT OR IGNORE INTO eval_links(eval_id,ref_type,ref_id) VALUES (?,?,?)`).run(evaluation.id, "signal", ref);
934
- })();
935
- }
936
- saveSnapshot(snapshot) {
937
- this.db.transaction(() => {
938
- this.db.prepare(`INSERT OR IGNORE INTO git_snapshots(id,base_sha,head_sha,diff_hash,payload_json,created_at) VALUES (?,?,?,?,?,?)`).run(snapshot.id, snapshot.baseSha, snapshot.headSha, snapshot.diffHash, canonicalize(snapshot.payload), nowIso());
939
- const statement = this.db.prepare(`INSERT OR REPLACE INTO changed_files(snapshot_id,path,status,old_path,binary) VALUES (?,?,?,?,?)`);
940
- for (const file of snapshot.files) statement.run(snapshot.id, file.path, file.status, file.oldPath ?? null, file.binary ? 1 : 0);
941
- })();
942
- }
943
- saveImpact(input) {
944
- this.db.transaction(() => {
945
- this.db.prepare(`INSERT OR IGNORE INTO impact_indexes(id,provider,version,snapshot_id,payload_json,created_at) VALUES (?,?,?,?,?,?)`).run(input.id, input.provider, input.version, input.snapshotId, canonicalize(input), nowIso());
946
- const statement = this.db.prepare(`INSERT OR IGNORE INTO impact_edges(impact_index_id,from_ref,to_ref,kind,confidence,provenance) VALUES (?,?,?,?,?,?)`);
947
- for (const edge of input.edges) statement.run(input.id, edge.from, edge.to, edge.kind, edge.confidence, edge.provenance);
948
- })();
949
- }
950
- saveEvidence(items) {
951
- const statement = this.db.prepare(`INSERT OR REPLACE INTO evidence_items(id,content_hash,kind,state,payload_json,collected_at) VALUES (?,?,?,?,?,?)`);
952
- this.db.transaction(() => {
953
- for (const evidence of items) statement.run(evidence.id, evidence.contentHash, evidence.kind, evidence.state, canonicalize(evidence.payload), evidence.collectedAt);
954
- })();
955
- }
956
- saveChecks(verificationId, checks) {
957
- const statement = this.db.prepare(`INSERT OR REPLACE INTO verification_checks(verification_id,check_id,state,enforcement,fingerprint,payload_json) VALUES (?,?,?,?,?,?)`);
958
- this.db.transaction(() => {
959
- for (const check of checks) statement.run(verificationId, check.id, check.state, check.enforcement, check.fingerprint, canonicalize(check));
960
- })();
961
- }
962
- saveVerification(input) {
963
- const json = canonicalize(input.result);
964
- this.db.transaction(() => {
965
- this.db.prepare(`INSERT INTO verification_runs(id,snapshot_id,gate_state,result_json,created_at) VALUES (?,?,?,?,?)`).run(input.id, input.snapshotId, input.gateState, json, nowIso());
966
- this.db.prepare(`INSERT INTO verdict_cache(snapshot_id,verification_id,result_json,created_at) VALUES (?,?,?,?) ON CONFLICT(snapshot_id) DO UPDATE SET verification_id=excluded.verification_id,result_json=excluded.result_json,created_at=excluded.created_at`).run(input.snapshotId, input.id, json, nowIso());
967
- })();
968
- }
969
- saveFinding(finding) {
970
- const now = nowIso();
971
- const existing = this.db.prepare(`SELECT id FROM findings WHERE fingerprint=? ORDER BY first_seen LIMIT 1`).get(finding.fingerprint);
972
- if (existing) {
973
- this.db.prepare(`UPDATE findings SET snapshot_id=?,state=?,enforcement=?,owner=?,payload_json=?,last_seen=? WHERE id=?`).run(finding.snapshotId, finding.state, finding.enforcement, finding.owner ?? null, canonicalize(finding.payload), now, existing.id);
974
- return;
975
- }
976
- this.db.prepare(`INSERT INTO findings(id,snapshot_id,fingerprint,evaluator,state,enforcement,owner,payload_json,first_seen,last_seen) VALUES (?,?,?,?,?,?,?,?,?,?)`).run(finding.id, finding.snapshotId, finding.fingerprint, finding.evaluator, finding.state, finding.enforcement, finding.owner ?? null, canonicalize(finding.payload), now, now);
977
- }
978
- findFindingId(fingerprint) {
979
- return this.db.prepare(`SELECT id FROM findings WHERE fingerprint=? ORDER BY first_seen LIMIT 1`).get(fingerprint)?.id;
980
- }
981
- getFinding(id) {
982
- const row = this.db.prepare(`SELECT * FROM findings WHERE id=?`).get(id);
983
- return row ? { ...row, payload: JSON.parse(String(row.payload_json)) } : void 0;
984
- }
985
- acknowledge(input) {
986
- this.db.prepare(`INSERT INTO acknowledgements(id,finding_id,reason,comment,actor,snapshot_id,created_at) VALUES (?,?,?,?,?,?,?)`).run(input.id, input.findingId, input.reason, input.comment ?? null, input.actor, input.snapshotId, nowIso());
987
- }
988
- };
989
-
990
519
  // ../packages/eval-engine/src/index.ts
991
520
  var DEFAULT_CONFIG = configSchema.parse({});
992
521
  function loadConfig(root) {
993
- const filename = path8.join(root, ".scriptonia", "config.yaml");
994
- if (!fs7.existsSync(filename)) return { config: DEFAULT_CONFIG, errors: [] };
522
+ const filename = path5.join(root, ".scriptonia", "config.yaml");
523
+ if (!fs5.existsSync(filename)) return { config: DEFAULT_CONFIG, errors: [] };
995
524
  try {
996
- return { config: configSchema.parse(YAML2.parse(fs7.readFileSync(filename, "utf8"))), errors: [] };
525
+ return { config: configSchema.parse(YAML2.parse(fs5.readFileSync(filename, "utf8"))), errors: [] };
997
526
  } catch (error) {
998
527
  return { config: DEFAULT_CONFIG, errors: [`config.yaml: ${error instanceof Error ? error.message : String(error)}`] };
999
528
  }
@@ -1007,21 +536,21 @@ function parseConfigDocument(text, name = "config.yaml") {
1007
536
  }
1008
537
  }
1009
538
  function ensureConfig(root) {
1010
- const directory = path8.join(root, ".scriptonia");
1011
- const filename = path8.join(directory, "config.yaml");
1012
- fs7.mkdirSync(path8.join(directory, "decisions"), { recursive: true });
1013
- fs7.mkdirSync(path8.join(directory, "evals"), { recursive: true });
1014
- if (!fs7.existsSync(filename)) fs7.writeFileSync(filename, YAML2.stringify(DEFAULT_CONFIG));
539
+ const directory = path5.join(root, ".scriptonia");
540
+ const filename = path5.join(directory, "config.yaml");
541
+ fs5.mkdirSync(path5.join(directory, "decisions"), { recursive: true });
542
+ fs5.mkdirSync(path5.join(directory, "evals"), { recursive: true });
543
+ if (!fs5.existsSync(filename)) fs5.writeFileSync(filename, YAML2.stringify(DEFAULT_CONFIG));
1015
544
  return filename;
1016
545
  }
1017
546
  function loadEvals(root) {
1018
- const directory = path8.join(root, ".scriptonia", "evals");
1019
- if (!fs7.existsSync(directory)) return { evals: [], errors: [] };
547
+ const directory = path5.join(root, ".scriptonia", "evals");
548
+ if (!fs5.existsSync(directory)) return { evals: [], errors: [] };
1020
549
  const evals = [];
1021
550
  const errors = [];
1022
- for (const filename of fs7.readdirSync(directory).filter((file) => /\.ya?ml$/i.test(file)).sort()) {
551
+ for (const filename of fs5.readdirSync(directory).filter((file) => /\.ya?ml$/i.test(file)).sort()) {
1023
552
  try {
1024
- evals.push(evalCaseSchema.parse(YAML2.parse(fs7.readFileSync(path8.join(directory, filename), "utf8"))));
553
+ evals.push(evalCaseSchema.parse(YAML2.parse(fs5.readFileSync(path5.join(directory, filename), "utf8"))));
1025
554
  } catch (error) {
1026
555
  errors.push(`${filename}: ${error instanceof Error ? error.message : String(error)}`);
1027
556
  }
@@ -1046,8 +575,8 @@ async function verifyRepository(options) {
1046
575
  const loadedConfig = trustedRef ? parseConfigDocument(readFileAtRef(options.root, trustedRef, ".scriptonia/config.yaml"), `${trustedRef}:.scriptonia/config.yaml`) : loadConfig(options.root);
1047
576
  const config = loadedConfig.config;
1048
577
  const snapshot = await withSpan("snapshot.build", { base_configured: Boolean(options.base ?? config.base_ref) }, () => buildSnapshot({ cwd: options.root, base: options.base ?? config.base_ref }));
1049
- const planPath = path8.resolve(options.root, options.plan ?? "PLAN.md");
1050
- const plan = fs7.existsSync(planPath) ? fs7.readFileSync(planPath, "utf8") : void 0;
578
+ const planPath = path5.resolve(options.root, options.plan ?? "PLAN.md");
579
+ const plan = fs5.existsSync(planPath) ? fs5.readFileSync(planPath, "utf8") : void 0;
1051
580
  const loadedDecisions = trustedRef ? parseDecisionDocuments(listFilesAtRef(options.root, trustedRef, [".scriptonia/decisions"]).filter((name) => /\.ya?ml$/i.test(name)).map((name) => ({ name: `${trustedRef}:${name}`, text: readFileAtRef(options.root, trustedRef, name) ?? "" }))) : loadDecisions(options.root);
1052
581
  const loadedEvals = trustedRef ? parseEvalDocuments(listFilesAtRef(options.root, trustedRef, [".scriptonia/evals"]).filter((name) => /\.ya?ml$/i.test(name)).map((name) => ({ name: `${trustedRef}:${name}`, text: readFileAtRef(options.root, trustedRef, name) ?? "" }))) : loadEvals(options.root);
1053
582
  const evals = options.evalFilter ? loadedEvals.evals.filter((entry) => entry.id === options.evalFilter || entry.title.toLowerCase().includes(options.evalFilter.toLowerCase())) : loadedEvals.evals;
@@ -1059,8 +588,17 @@ async function verifyRepository(options) {
1059
588
  });
1060
589
  const { github, evidence } = collected;
1061
590
  const passedChecks = github.filter((item2) => item2.state === "available").map((item2) => String(item2.payload.name));
1062
- const impactIndex = await withSpan("impact.index", { changed_file_count: snapshot.files.length }, () => loadCodeGraphIndex(options.root) ?? buildFallbackImpactIndex(options.root));
1063
- const impact = impactedFiles(impactIndex, snapshot.files.map((file) => file.path));
591
+ const impactChange = {
592
+ files: snapshot.files.map((file) => file.path),
593
+ lines: parseDiffHunks(snapshot.diff).map((hunk) => ({ path: hunk.path, startLine: Math.max(1, hunk.newRange.startLine), endLineExclusive: Math.max(1, hunk.newRange.endLineExclusive) }))
594
+ };
595
+ const impactProvider = await withSpan("impact.index", { changed_file_count: snapshot.files.length }, () => selectImpactProvider({
596
+ brain: loadCurrentBrainImpactProvider(options.root),
597
+ codeGraph: loadCodeGraphIndex(options.root),
598
+ fallback: () => buildFallbackImpactIndex(options.root)
599
+ }));
600
+ const impactIndex = impactProvider.index;
601
+ const impact = impactProvider.dependents(impactChange, 2);
1064
602
  const scope = await withSpan("scope.resolve", { impacted_file_count: impact.files.length }, () => buildScopeContext(options.root, impact.files, { checks: passedChecks }));
1065
603
  const policy = await withSpan("policy.evaluate", { decision_count: loadedDecisions.decisions.length }, () => evaluateDecisions(loadedDecisions.decisions, scope));
1066
604
  const identity = {
@@ -1073,8 +611,8 @@ async function verifyRepository(options) {
1073
611
  decisions: loadedDecisions.decisions.map((entry) => ({ id: entry.id, version: entry.version, hash: hashObject(entry) })),
1074
612
  evals: evals.map((entry) => ({ id: entry.id, version: entry.version, hash: hashObject(entry) })),
1075
613
  evidence: evidence.map((entry) => ({ hash: entry.contentHash, state: entry.state })),
1076
- impact: { provider: impactIndex.provider, version: impactIndex.version, id: impactIndex.id, edges: impact.edges },
1077
- evaluators: { policy: "1", eval: "1", plan: "1", gate: "1" },
614
+ impact: { ...impactProviderIdentity(impactProvider), edges: impact.edges },
615
+ evaluators: { policy: "1", eval: "1", plan: "1", gate: "1", testCoverage: "2" },
1078
616
  semantic: config.semantic.enabled || options.semantic ? {
1079
617
  model: config.semantic.model ?? process.env.OPENAI_OUTPUT_MODEL ?? process.env.LITELLM_MODEL ?? "gpt-5.6-terra",
1080
618
  endpoint: config.semantic.endpoint ?? process.env.SCRIPTONIA_MODEL_GATEWAY_URL ?? process.env.LITELLM_BASE_URL ?? "unconfigured"
@@ -1098,7 +636,12 @@ async function verifyRepository(options) {
1098
636
  checks.push(...policy.map(policyToCheck));
1099
637
  checks.push(...evaluateEvals(evals, scope, evidence, policy));
1100
638
  checks.push(...evaluatePlan(plan, config.required_plan, evidence));
1101
- checks.push(testCoverageCheck(snapshot, relatedTests(impactIndex, snapshot.files.map((file) => file.path))));
639
+ const coverageSourcePaths = new Set(snapshot.files.filter((file) => isProductSourceForCoverage(file.path, coverageSample(snapshot, file.path, file.binary), file.binary)).map((file) => file.path.replaceAll("\\", "/")));
640
+ const coverageChange = {
641
+ files: impactChange.files.filter((file) => coverageSourcePaths.has(file.replaceAll("\\", "/"))),
642
+ lines: impactChange.lines.filter((range) => coverageSourcePaths.has(range.path.replaceAll("\\", "/")))
643
+ };
644
+ checks.push(testCoverageCheck(snapshot, impactProvider.relatedTests(coverageChange), impactProvider.changedSymbols(coverageChange.lines)));
1102
645
  checks.push(...github.map(githubCheck));
1103
646
  let semanticCoverage = config.semantic.enabled || options.semantic ? "unavailable" : "disabled";
1104
647
  if ((config.semantic.enabled || options.semantic) && plan) {
@@ -1203,12 +746,178 @@ function parseInlineEvidence(text) {
1203
746
  return parsed.success ? [parsed.data] : [];
1204
747
  });
1205
748
  }
1206
- function testCoverageCheck(snapshot, impactedTests) {
1207
- const source = snapshot.files.filter((file) => /\.(?:[cm]?[jt]sx?|py|go|rs|java|rb|php|swift|kt)$/i.test(file.path) && !/(?:^|\/)(?:scripts?|migrations?|drizzle)(?:\/|$)/i.test(file.path));
1208
- const tests = snapshot.files.filter((file) => /(?:^|\/)(?:__tests__|tests?|spec)(?:\/|$)|\.(?:test|spec)\.[^.]+$/i.test(file.path));
1209
- const state = source.length && !tests.length ? impactedTests.length ? "INCONCLUSIVE" : "FAIL" : "PASS";
1210
- const evidence = tests.length ? tests.map((file) => ({ kind: "path", value: file.path })) : impactedTests.length ? impactedTests.map((file) => ({ kind: "related_test", value: file })) : source.map((file) => ({ kind: "path", value: file.path }));
1211
- return makeCheck({ evaluator: "test-coverage", title: "Changed behavior has test evidence", state, enforcement: "warn", reason: state === "FAIL" ? "Product code changed without a changed or statically related test file." : state === "INCONCLUSIVE" ? "Related tests exist, but no result artifact proves they ran." : "A changed test is present or no product code changed.", evidence, refs: [] });
749
+ var COVERAGE_TEST_EXTENSION = "(?:c|cc|cp|cpp|cxx|h|hh|hpp|hxx|inc|m|mm|cs|dart|ex|exs|go|java|js|jsx|mjs|cjs|kt|kts|lua|pl|pm|php|proto|py|pyi|rb|rs|scala|sh|bash|zsh|fish|bzl|bazel|star|swift|ts|tsx|mts|cts|vue)";
750
+ var COVERAGE_TEST_BASENAME = new RegExp(`(?:^|/)(?:test_[^/]+|[^/]+_(?:test|spec)|[^/]+\\.(?:test|spec))\\.${COVERAGE_TEST_EXTENSION}$`, "i");
751
+ var COVERAGE_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
752
+ ".c",
753
+ ".h",
754
+ ".cc",
755
+ ".cp",
756
+ ".cpp",
757
+ ".cxx",
758
+ ".hh",
759
+ ".hpp",
760
+ ".hxx",
761
+ ".inc",
762
+ ".m",
763
+ ".mm",
764
+ ".cs",
765
+ ".css",
766
+ ".scss",
767
+ ".sass",
768
+ ".less",
769
+ ".dart",
770
+ ".ex",
771
+ ".exs",
772
+ ".go",
773
+ ".graphql",
774
+ ".gql",
775
+ ".html",
776
+ ".htm",
777
+ ".java",
778
+ ".js",
779
+ ".jsx",
780
+ ".mjs",
781
+ ".cjs",
782
+ ".json",
783
+ ".jsonc",
784
+ ".kt",
785
+ ".kts",
786
+ ".lua",
787
+ ".pl",
788
+ ".pm",
789
+ ".php",
790
+ ".proto",
791
+ ".py",
792
+ ".pyi",
793
+ ".rb",
794
+ ".rs",
795
+ ".scala",
796
+ ".sh",
797
+ ".bash",
798
+ ".zsh",
799
+ ".fish",
800
+ ".sql",
801
+ ".bzl",
802
+ ".bazel",
803
+ ".star",
804
+ ".swift",
805
+ ".toml",
806
+ ".ts",
807
+ ".tsx",
808
+ ".mts",
809
+ ".cts",
810
+ ".vue",
811
+ ".xml",
812
+ ".plist",
813
+ ".yaml",
814
+ ".yml"
815
+ ]);
816
+ var COVERAGE_STARLARK_BASENAMES = /* @__PURE__ */ new Set(["BUILD", "BUILD.bazel", "WORKSPACE", "WORKSPACE.bzlmod", "MODULE.bazel"]);
817
+ var COVERAGE_CONFIG_BASENAMES = /* @__PURE__ */ new Set([
818
+ ".bazelignore",
819
+ ".bazelrc",
820
+ ".bazelversion",
821
+ ".clang-format",
822
+ ".clang-tidy",
823
+ ".dockerignore",
824
+ ".editorconfig",
825
+ ".env.example",
826
+ ".flake8",
827
+ ".gitignore",
828
+ ".npmrc",
829
+ ".nvmrc",
830
+ ".prettierrc",
831
+ ".yamllint",
832
+ "CMakeLists.txt",
833
+ "CODEOWNERS",
834
+ "Cargo.toml",
835
+ "Dockerfile",
836
+ "Gemfile",
837
+ "Gemfile.lock",
838
+ "Makefile",
839
+ "Podfile",
840
+ "analysis_options.yaml",
841
+ "build.gradle",
842
+ "build.gradle.kts",
843
+ "composer.json",
844
+ "composer.lock",
845
+ "deps.yaml",
846
+ "extensions_metadata.yaml",
847
+ "go.mod",
848
+ "go.sum",
849
+ "gradle.properties",
850
+ "MODULE.bazel.lock",
851
+ "package-lock.json",
852
+ "package.json",
853
+ "pnpm-lock.yaml",
854
+ "pnpm-workspace.yaml",
855
+ "pom.xml",
856
+ "pubspec.lock",
857
+ "pubspec.yaml",
858
+ "pyproject.toml",
859
+ "requirements.txt",
860
+ "settings.gradle",
861
+ "settings.gradle.kts",
862
+ "tsconfig.json",
863
+ "yarn.lock"
864
+ ]);
865
+ function isTestFileForCoverage(filePath) {
866
+ const normalized = filePath.replaceAll("\\", "/");
867
+ return /(?:^|\/)(?:__tests__|tests?|spec|integration_test)(?:\/|$)/i.test(normalized) || COVERAGE_TEST_BASENAME.test(normalized) || /(?:^|\/)[^/]+(?:Test|Tests|Spec)\.(?:cs|java|kt|kts|scala|swift)$/.test(normalized);
868
+ }
869
+ function isProductSourceForCoverage(filePath, sample = "", binary = false) {
870
+ if (binary || isTestFileForCoverage(filePath)) return false;
871
+ const normalized = filePath.replaceAll("\\", "/");
872
+ const base = path5.posix.basename(normalized);
873
+ if (/(?:^|\/)(?:third_party|vendor|external|docs?|adr|changelogs?)(?:\/|$)/i.test(normalized)) return false;
874
+ if (/(?:^|\/)generated(?:\/|$)/i.test(normalized) || /\.(?:g|freezed)\.dart$|\.pb\.go$|\.generated\.[^.]+$|\.min\.(?:js|css)$/i.test(base) || /generated|do not edit/i.test(sample.split(/\r?\n/).slice(0, 3).join("\n"))) return false;
875
+ if (COVERAGE_STARLARK_BASENAMES.has(base) || /\.(?:bzl|bazel|star)$/i.test(base)) return true;
876
+ if (COVERAGE_CONFIG_BASENAMES.has(base) || /\.(?:lock|config|conf|ini|properties)$/i.test(base) || /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(normalized) || /^(?:Dockerfile(?:\..+)?|requirements(?:[-_.][^/]*)?\.txt|tsconfig(?:\.[^/]+)?\.json|(?:docker-)?compose(?:\.[^/]+)?\.ya?ml)$/i.test(base)) return false;
877
+ if (COVERAGE_SOURCE_EXTENSIONS.has(path5.posix.extname(base).toLowerCase())) return true;
878
+ const firstLine = sample.split(/\r?\n/, 1)[0] ?? "";
879
+ return /^#!.*\b(?:python3?|bash|sh|zsh|fish|node|ruby)\b/.test(firstLine);
880
+ }
881
+ function coverageSample(snapshot, filePath, binary) {
882
+ if (binary) return "";
883
+ try {
884
+ return fs5.readFileSync(path5.join(snapshot.repositoryRoot, filePath), "utf8").slice(0, 8192);
885
+ } catch {
886
+ return "";
887
+ }
888
+ }
889
+ function changedSymbolIdentity(symbol) {
890
+ return `${symbol.path}::${symbol.name} (${symbol.kind}, ${symbol.id})`;
891
+ }
892
+ function summarizeChangedSymbols(symbols) {
893
+ const identities = symbols.map(changedSymbolIdentity);
894
+ const shown = identities.slice(0, 20);
895
+ return `${shown.join(", ")}${identities.length > shown.length ? `, +${identities.length - shown.length} more (see evidence)` : ""}`;
896
+ }
897
+ function testCoverageCheck(snapshot, impactedTests, mappedSymbols) {
898
+ const tests = snapshot.files.filter((file) => isTestFileForCoverage(file.path));
899
+ const source = snapshot.files.filter((file) => isProductSourceForCoverage(file.path, coverageSample(snapshot, file.path, file.binary), file.binary));
900
+ const sourcePaths = new Set(source.map((file) => file.path.replaceAll("\\", "/")));
901
+ const changedSymbols = mappedSymbols.filter((symbol) => sourcePaths.has(symbol.path.replaceAll("\\", "/")));
902
+ const changedTestPaths = new Set(tests.map((file) => file.path.replaceAll("\\", "/")));
903
+ const changedRelatedTests = impactedTests.filter((file) => changedTestPaths.has(file.replaceAll("\\", "/")));
904
+ const state = !source.length || changedRelatedTests.length ? "PASS" : impactedTests.length ? "INCONCLUSIVE" : "FAIL";
905
+ const evidence = state === "PASS" ? (source.length ? changedRelatedTests : tests.map((file) => file.path)).map((file) => ({ kind: "path", value: file })) : impactedTests.length ? [
906
+ ...changedSymbols.map((symbol) => ({ kind: "changed_symbol", value: changedSymbolIdentity(symbol) })),
907
+ ...impactedTests.map((file) => ({ kind: "related_test", value: file }))
908
+ ] : changedSymbols.length ? changedSymbols.map((symbol) => ({ kind: "uncovered_symbol", value: changedSymbolIdentity(symbol) })) : source.map((file) => ({ kind: "path", value: file.path }));
909
+ const subject = changedSymbols.length ? `changed symbols ${summarizeChangedSymbols(changedSymbols)}` : `changed product source ${source.map((file) => file.path).join(", ")}`;
910
+ const reason = state === "FAIL" ? `No changed or statically related test covers ${subject}.` : state === "INCONCLUSIVE" ? `Related tests exist for ${subject}, but no result artifact proves they ran.` : source.length ? "A statically related test changed with the product behavior." : "No product code changed.";
911
+ return makeCheck({
912
+ evaluator: "test-coverage",
913
+ evaluatorVersion: "2",
914
+ title: "Changed behavior has test evidence",
915
+ state,
916
+ enforcement: "warn",
917
+ reason,
918
+ evidence,
919
+ refs: changedSymbols.map((symbol) => symbol.id)
920
+ });
1212
921
  }
1213
922
  function githubCheck(evidence) {
1214
923
  return makeCheck({ evaluator: "github-check", title: `CI check: ${String(evidence.payload.name)}`, state: evidence.state === "available" ? "PASS" : evidence.state === "failed" ? "FAIL" : evidence.state === "error" ? "ERROR" : "INCONCLUSIVE", enforcement: "block", reason: evidence.state === "available" ? "GitHub job succeeded." : evidence.state === "failed" ? `GitHub job concluded ${String(evidence.payload.conclusion)}.` : "GitHub job conclusion is unavailable.", evidence: [{ id: evidence.id, kind: evidence.kind, value: evidence.source }], refs: [] });
@@ -1224,8 +933,9 @@ async function evaluateSemanticCriteria(plan, snapshot, config) {
1224
933
  return results;
1225
934
  }
1226
935
  function makeCheck(input) {
1227
- const identity = { evaluator: input.evaluator, version: "1", title: input.title, state: input.state, evidence: input.evidence ?? [], refs: input.refs };
1228
- return { id: input.id ?? createId("check"), evaluator: input.evaluator, evaluatorVersion: "1", title: input.title, state: input.state, enforcement: input.enforcement, reason: input.reason, evidence: input.evidence ?? [], refs: input.refs, ...input.owner ? { owner: input.owner } : {}, fingerprint: hashObject(identity) };
936
+ const evaluatorVersion = input.evaluatorVersion ?? "1";
937
+ const identity = { evaluator: input.evaluator, version: evaluatorVersion, title: input.title, state: input.state, evidence: input.evidence ?? [], refs: input.refs };
938
+ return { id: input.id ?? createId("check"), evaluator: input.evaluator, evaluatorVersion, title: input.title, state: input.state, enforcement: input.enforcement, reason: input.reason, evidence: input.evidence ?? [], refs: input.refs, ...input.owner ? { owner: input.owner } : {}, fingerprint: hashObject(identity) };
1229
939
  }
1230
940
  function escapeRegex(value) {
1231
941
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1253,10 +963,10 @@ function learnFromFinding(root, findingId) {
1253
963
  enforcement: "observe",
1254
964
  fingerprint: payload.fingerprint
1255
965
  });
1256
- const filename = path8.join(root, ".scriptonia", "evals", `${id}.yaml`);
1257
- if (fs7.existsSync(filename)) throw new Error(`draft eval already exists: ${path8.relative(root, filename)}`);
1258
- fs7.mkdirSync(path8.dirname(filename), { recursive: true });
1259
- fs7.writeFileSync(filename, YAML2.stringify(evalCase));
966
+ const filename = path5.join(root, ".scriptonia", "evals", `${id}.yaml`);
967
+ if (fs5.existsSync(filename)) throw new Error(`draft eval already exists: ${path5.relative(root, filename)}`);
968
+ fs5.mkdirSync(path5.dirname(filename), { recursive: true });
969
+ fs5.writeFileSync(filename, YAML2.stringify(evalCase));
1260
970
  return { path: filename, evalCase };
1261
971
  } finally {
1262
972
  store.close();
@@ -1293,7 +1003,7 @@ function evalDoctor(root) {
1293
1003
  }
1294
1004
  function toSarif(result) {
1295
1005
  const actionable = result.checks.filter((check) => check.state === "FAIL" || check.state === "ERROR" || check.state === "INCONCLUSIVE");
1296
- return { version: "2.1.0", $schema: "https://json.schemastore.org/sarif-2.1.0.json", runs: [{ tool: { driver: { name: "Scriptonia", version: "0.9.0-rc.1", informationUri: "https://scriptonia.dev", rules: [...new Map(actionable.map((check) => [check.evaluator, { id: check.evaluator, name: check.evaluator, shortDescription: { text: check.title } }])).values()] } }, results: actionable.map((check) => ({ ruleId: check.evaluator, level: check.enforcement === "block" ? "error" : "warning", message: { text: `${check.title}: ${check.reason}` }, properties: { state: check.state, enforcement: check.enforcement, fingerprint: check.fingerprint, refs: check.refs } })) }] };
1006
+ return { version: "2.1.0", $schema: "https://json.schemastore.org/sarif-2.1.0.json", runs: [{ tool: { driver: { name: "Scriptonia", version: "0.9.0-rc.2", informationUri: "https://scriptonia.dev", rules: [...new Map(actionable.map((check) => [check.evaluator, { id: check.evaluator, name: check.evaluator, shortDescription: { text: check.title } }])).values()] } }, results: actionable.map((check) => ({ ruleId: check.evaluator, level: check.enforcement === "block" ? "error" : "warning", message: { text: `${check.title}: ${check.reason}` }, properties: { state: check.state, enforcement: check.enforcement, fingerprint: check.fingerprint, refs: check.refs } })) }] };
1297
1007
  }
1298
1008
 
1299
1009
  // ../packages/github/src/index.ts
@@ -1309,8 +1019,8 @@ function escapeCell(value) {
1309
1019
 
1310
1020
  // ../packages/run-capture/src/index.ts
1311
1021
  import { spawn } from "child_process";
1312
- import fs8 from "fs";
1313
- import path9 from "path";
1022
+ import fs6 from "fs";
1023
+ import path6 from "path";
1314
1024
 
1315
1025
  // ../packages/run-protocol/src/index.ts
1316
1026
  var BUILTIN_PATTERNS = [
@@ -1420,7 +1130,7 @@ function snapshotFileHashes(root, files) {
1420
1130
  continue;
1421
1131
  }
1422
1132
  try {
1423
- hashes.set(file.path, sha256(fs8.readFileSync(path9.join(root, file.path))));
1133
+ hashes.set(file.path, sha256(fs6.readFileSync(path6.join(root, file.path))));
1424
1134
  } catch {
1425
1135
  hashes.set(file.path, "unreadable");
1426
1136
  }
@@ -1442,8 +1152,1006 @@ function ingestJsonl(root, text, adapter = "normalized") {
1442
1152
  }
1443
1153
  }
1444
1154
 
1155
+ // src/brain-commands.ts
1156
+ import fs8 from "fs";
1157
+ import path8 from "path";
1158
+ import { spawnSync } from "child_process";
1159
+ import pc from "picocolors";
1160
+
1161
+ // src/brain-knowledge-http.ts
1162
+ function apiOrigin(value) {
1163
+ const url = new URL(value);
1164
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
1165
+ throw new TypeError("Scriptonia URL must use http or https");
1166
+ }
1167
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
1168
+ if (url.protocol === "http:" && !loopback) {
1169
+ throw new TypeError("Scriptonia URL must use HTTPS except for an explicit loopback address");
1170
+ }
1171
+ return url.toString().replace(/\/$/, "");
1172
+ }
1173
+ function responseIssue(value) {
1174
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "unexpected response";
1175
+ const record3 = value;
1176
+ const error = typeof record3.error === "string" ? record3.error : "request_failed";
1177
+ const message = typeof record3.message === "string" ? record3.message : void 0;
1178
+ const issues = Array.isArray(record3.issues) ? record3.issues.map((entry) => typeof entry === "string" ? entry : JSON.stringify(entry)).join("; ") : void 0;
1179
+ return [error, message, issues].filter(Boolean).join(": ");
1180
+ }
1181
+ function createBrainKnowledgeHttpProvider(options) {
1182
+ const baseUrl = apiOrigin(options.baseUrl.trim());
1183
+ const apiKey = options.apiKey.trim();
1184
+ if (!apiKey) throw new TypeError("Scriptonia API key cannot be empty");
1185
+ const fetchImpl = options.fetchImpl ?? fetch;
1186
+ return {
1187
+ id: "scriptonia-brain-extract-http",
1188
+ version: "1",
1189
+ async extract(call) {
1190
+ const response = await fetchImpl(`${baseUrl}/api/brain/extract`, {
1191
+ method: "POST",
1192
+ headers: {
1193
+ authorization: `Bearer ${apiKey}`,
1194
+ "content-type": "application/json"
1195
+ },
1196
+ // The server endpoint owns model selection and repair. It accepts the
1197
+ // stable BrainExtractRequestV1 contract, never provider internals.
1198
+ body: JSON.stringify(call.request),
1199
+ signal: call.signal
1200
+ });
1201
+ const value = await response.json().catch(() => void 0);
1202
+ if (!response.ok) {
1203
+ throw new Error(`Scriptonia brain extraction failed (${response.status}): ${responseIssue(value)}`);
1204
+ }
1205
+ const parsed = brainExtractResponseV1Schema.safeParse(value);
1206
+ if (!parsed.success) {
1207
+ throw new Error(`Scriptonia brain extraction returned an invalid response: ${parsed.error.message}`);
1208
+ }
1209
+ return {
1210
+ output: parsed.data,
1211
+ usage: {
1212
+ inputTokens: parsed.data.usage.input_tokens,
1213
+ outputTokens: parsed.data.usage.output_tokens,
1214
+ modelCalls: parsed.data.usage.calls
1215
+ },
1216
+ metadata: {
1217
+ ...response.headers.get("x-scriptonia-model")?.trim() ? { model: response.headers.get("x-scriptonia-model").trim() } : {},
1218
+ ...response.headers.get("x-scriptonia-provider-revision")?.trim() ? { providerRevision: response.headers.get("x-scriptonia-provider-revision").trim() } : {},
1219
+ ...response.headers.get("x-scriptonia-response-id")?.trim() ? { responseId: response.headers.get("x-scriptonia-response-id").trim() } : {}
1220
+ }
1221
+ };
1222
+ }
1223
+ };
1224
+ }
1225
+
1226
+ // src/local-binding.ts
1227
+ import fs7 from "fs";
1228
+ import os from "os";
1229
+ import path7 from "path";
1230
+ function readJson(filename) {
1231
+ try {
1232
+ return JSON.parse(fs7.readFileSync(filename, "utf8"));
1233
+ } catch {
1234
+ return void 0;
1235
+ }
1236
+ }
1237
+ function record(value) {
1238
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1239
+ }
1240
+ function realPath(value) {
1241
+ try {
1242
+ return fs7.realpathSync(value);
1243
+ } catch {
1244
+ return path7.resolve(value);
1245
+ }
1246
+ }
1247
+ function findLocalProjectBinding(repoRoot, home = os.homedir()) {
1248
+ const configRoot = path7.join(home, ".scriptonia");
1249
+ const global = record(readJson(path7.join(configRoot, "config.json")));
1250
+ if (typeof global?.url !== "string" || !global.url.trim()) return void 0;
1251
+ const projectsRoot = path7.join(configRoot, "projects");
1252
+ if (!fs7.existsSync(projectsRoot)) return void 0;
1253
+ const target = realPath(repoRoot);
1254
+ let selected;
1255
+ for (const entry of fs7.readdirSync(projectsRoot, { withFileTypes: true })) {
1256
+ if (!entry.isDirectory()) continue;
1257
+ const value = record(readJson(path7.join(projectsRoot, entry.name, "project.json")));
1258
+ if (typeof value?.repoPath !== "string" || typeof value.token !== "string" || !value.token.trim()) continue;
1259
+ const candidate = realPath(value.repoPath);
1260
+ if (target !== candidate && !target.startsWith(`${candidate}${path7.sep}`)) continue;
1261
+ if (!selected || candidate.length > selected.root.length) selected = { root: candidate, value };
1262
+ }
1263
+ if (!selected || typeof selected.value.token !== "string") return void 0;
1264
+ return {
1265
+ baseUrl: global.url.trim(),
1266
+ apiKey: selected.value.token.trim(),
1267
+ ...typeof selected.value.projectId === "string" ? { projectId: selected.value.projectId } : {},
1268
+ ...typeof selected.value.name === "string" ? { projectName: selected.value.name } : {}
1269
+ };
1270
+ }
1271
+
1272
+ // src/brain-commands.ts
1273
+ var STATUS_CHANGED_PATH_LIMIT = 50;
1274
+ var STATUS_GIT_TIMEOUT_MS = 125;
1275
+ var STATUS_GIT_OUTPUT_LIMIT = 512 * 1024;
1276
+ var STATUS_STAGE_LIMIT = 64;
1277
+ var WHY_FACT_LIMIT = 12;
1278
+ function registerBrainCommands(program) {
1279
+ const brain = program.command("brain").description("Build and inspect the deterministic repository Deep Brain");
1280
+ brain.command("build").description("Build a new immutable Deep Brain generation").option("--fast", "run the deterministic fast pipeline").option("--deep", "run the full pipeline, including optional enrichment when available").option("--offline", "forbid network/model enrichment and use local deterministic evidence only").option("--github", "mine bounded GitHub pull-request evidence when credentials are available").option("--show-prompts", "store and reveal the exact credential-screened model inputs").option("--force", "request a hard-gate bypass (always rejected; faulty brains are never activated)").option("--json", "print a machine-readable build summary").action(async (options) => {
1281
+ if (options.fast && options.deep) {
1282
+ emitBuildError("--fast and --deep are mutually exclusive", Boolean(options.json));
1283
+ process.exitCode = 2;
1284
+ return;
1285
+ }
1286
+ if (options.force) {
1287
+ emitBuildError("--force is intentionally rejected: Deep Brain hard gates cannot be bypassed, and a faulty generation is never activated", Boolean(options.json), 1);
1288
+ process.exitCode = 1;
1289
+ return;
1290
+ }
1291
+ const root = findRepositoryRoot();
1292
+ const mode = options.fast ? "fast" : options.deep ? "deep" : "incremental";
1293
+ try {
1294
+ const offline = Boolean(options.offline);
1295
+ const binding = mode === "deep" && !offline ? findLocalProjectBinding(root) : void 0;
1296
+ if (mode === "deep" && !offline && !binding) {
1297
+ throw new Error("Online deep build needs this repository's Scriptonia project binding. Run `npx scriptonia login` and `npx scriptonia init`, or use `--offline`.");
1298
+ }
1299
+ const result = await buildBrain(root, {
1300
+ mode,
1301
+ offline,
1302
+ github: { enabled: Boolean(options.github), offline },
1303
+ ...binding ? { knowledgeProvider: createBrainKnowledgeHttpProvider(binding) } : {},
1304
+ showPrompts: Boolean(options.showPrompts),
1305
+ ...options.showPrompts && !options.json ? {
1306
+ onKnowledgeInspection(artifact) {
1307
+ const attempt = artifact.attempt === void 0 ? "" : ` \xB7 attempt ${artifact.attempt}`;
1308
+ console.log(` ${pc.magenta("model input").padEnd(12)} ${artifact.kind} \xB7 ${artifact.content}${attempt} \xB7 ${path8.relative(root, artifact.path)}`);
1309
+ }
1310
+ } : {},
1311
+ ...options.json ? {} : {
1312
+ onProgress(event) {
1313
+ const detail = event.detail ? ` \xB7 ${event.detail}` : "";
1314
+ const elapsed = event.elapsedMs === void 0 ? "" : ` \xB7 ${Math.round(event.elapsedMs)}ms`;
1315
+ const state = event.status === "failed" ? pc.red(event.status) : event.status === "skipped" || event.status === "partial" ? pc.yellow(event.status) : event.status === "succeeded" ? pc.green(event.status) : pc.cyan(event.status);
1316
+ console.log(` ${state.padEnd(12)} ${event.stage}${elapsed}${detail}`);
1317
+ }
1318
+ }
1319
+ });
1320
+ if (options.json) console.log(JSON.stringify(summarizeBuild(result, mode, offline), null, 2));
1321
+ else emitBuildResult(result, mode, offline);
1322
+ if (!result.activated) process.exitCode = 1;
1323
+ } catch (error) {
1324
+ emitBuildError(errorMessage(error), Boolean(options.json));
1325
+ process.exitCode = 2;
1326
+ }
1327
+ });
1328
+ brain.command("status").description("Show the active generation, storage, coverage, staleness, and failures").option("--json", "print machine-readable status").option("--check", "exhaustively compare every repository file with the active generation").action((options) => {
1329
+ const root = findRepositoryRoot();
1330
+ const report = readBrainStatus(root, { check: Boolean(options.check) });
1331
+ if (options.json) console.log(JSON.stringify(report, null, 2));
1332
+ else emitBrainStatus(report);
1333
+ if (report.state === "MISSING") process.exitCode = 1;
1334
+ });
1335
+ }
1336
+ function readBrainStatus(root, options = {}) {
1337
+ const filename = path8.join(root, ".scriptonia", "brain.db");
1338
+ if (!fs8.existsSync(filename)) return missingStatus(0, []);
1339
+ const store = new BrainStore(root);
1340
+ try {
1341
+ return readBrainStatusFromStore(root, store, { ...options, quickCheck: true });
1342
+ } finally {
1343
+ store.close();
1344
+ }
1345
+ }
1346
+ function readBrainStatusFromStore(root, store, options = {}) {
1347
+ const activeBuild = store.deep.getActiveBuild();
1348
+ const activeGeneration = store.deep.getActiveGeneration();
1349
+ const recentBuilds = store.deep.listBuilds(10);
1350
+ const emptyStorage = storageSummary(store, 0);
1351
+ if (!activeBuild || !activeGeneration || activeGeneration.buildId !== activeBuild.id) {
1352
+ const failures2 = recentBuildFailures(recentBuilds);
1353
+ if (!activeBuild && failures2.length === 0) failures2.push({ source: "build", code: "NO_ACTIVE_GENERATION", message: "No READY Deep Brain generation is active." });
1354
+ return missingStatus(storageTotal(emptyStorage), failures2, emptyStorage);
1355
+ }
1356
+ const aggregate = readStatusAggregate(store, activeBuild.id);
1357
+ const storage = storageSummary(store, aggregate.activeObjectBytes);
1358
+ const coverage = parseCoverage(activeBuild.coverage);
1359
+ const failures = [];
1360
+ if (!coverage) failures.push({ source: "coverage", code: "COVERAGE_MISSING", message: "The active generation has no readable coverage report.", build_id: activeBuild.id });
1361
+ else for (const check of coverage.checks.filter((entry) => !entry.passed)) {
1362
+ failures.push({ source: "coverage", code: check.id, message: check.message, build_id: activeBuild.id, diagnostics: check.diagnostics });
1363
+ }
1364
+ failures.push(...recentBuildFailures(recentBuilds.filter((build) => build.startedAt > activeBuild.startedAt)));
1365
+ const stageTimings = readStageTimings(store, activeBuild.id);
1366
+ const buildTiming = buildTimingFor(activeBuild);
1367
+ const knowledgeFreshness = knowledgeFreshnessFor(stageTimings, coverage);
1368
+ const planQualityEvidence = store.planQuality.latestForActiveGeneration();
1369
+ const brainScore = coverage ? scoreBrain(coverage, buildTiming, planQualityEvidence) : null;
1370
+ let staleness;
1371
+ if (options.check) {
1372
+ try {
1373
+ const current = inspectRepository(root, store.deep.listFiles(activeBuild.id));
1374
+ const stale = current.snapshotHash !== activeBuild.identity.repositorySnapshotId;
1375
+ const visible = current.changedPaths.slice(0, STATUS_CHANGED_PATH_LIMIT);
1376
+ staleness = {
1377
+ is_stale: stale,
1378
+ checked_snapshot_hash: current.snapshotHash,
1379
+ changed_file_count: current.changedPaths.length,
1380
+ changed_paths: visible,
1381
+ changed_paths_omitted: current.changedPaths.length - visible.length,
1382
+ check: "EXHAUSTIVE",
1383
+ proof: "EXACT",
1384
+ detail: stale ? "Every repository file was hashed; the active generation differs." : "Every repository file was hashed; the active generation matches exactly.",
1385
+ current_head: null,
1386
+ working_tree_dirty: null,
1387
+ candidate_changed_file_count: 0,
1388
+ candidate_changed_paths: [],
1389
+ candidate_changed_paths_omitted: 0
1390
+ };
1391
+ if (stale && current.changedPaths.length === 0) {
1392
+ failures.push({
1393
+ source: "staleness",
1394
+ code: "SNAPSHOT_METADATA_CHANGED",
1395
+ message: "The repository snapshot changed without a content-hash delta; rebuild to refresh classification/configuration evidence.",
1396
+ build_id: activeBuild.id
1397
+ });
1398
+ }
1399
+ } catch (error) {
1400
+ staleness = unknownStaleness("EXHAUSTIVE", `Exhaustive staleness check failed: ${errorMessage(error)}`);
1401
+ failures.push({ source: "staleness", code: "STALENESS_CHECK_FAILED", message: errorMessage(error), build_id: activeBuild.id });
1402
+ }
1403
+ } else if (options.quickCheck === false) {
1404
+ staleness = unknownStaleness("FAST", "Quick Git observation was disabled; run `scriptonia brain status --check` for an exact result.");
1405
+ } else {
1406
+ staleness = inspectRepositoryFast(root);
1407
+ }
1408
+ return {
1409
+ schema_version: "1",
1410
+ state: "READY",
1411
+ size_bytes: storageTotal(storage),
1412
+ generation: {
1413
+ number: activeGeneration.generation,
1414
+ build_id: activeBuild.id,
1415
+ build_snapshot_hash: activeBuild.snapshotId,
1416
+ repository_snapshot_hash: activeBuild.identity.repositorySnapshotId,
1417
+ git_evidence_anchor: aggregate.gitEvidenceAnchor
1418
+ },
1419
+ counts: aggregate.counts,
1420
+ coverage,
1421
+ parser_runtime: parserRuntimeStatus(aggregate.astRuntime),
1422
+ storage,
1423
+ stage_timings: stageTimings.map(({ metrics: _metrics, ...stage }) => stage),
1424
+ build_timing: buildTiming,
1425
+ brain_score: brainScore,
1426
+ knowledge_freshness: knowledgeFreshness,
1427
+ staleness,
1428
+ failures
1429
+ };
1430
+ }
1431
+ function explainDeepBrain(root, inputTarget) {
1432
+ const status = readBrainStatus(root);
1433
+ if (status.state !== "READY" || !status.generation) {
1434
+ return { available: false, generation: null, stale: status.staleness.is_stale, target: null, role: null, ownership: [], signals: [], plans: [], facts: [], dependents: [], related_tests: [] };
1435
+ }
1436
+ const store = new BrainStore(root);
1437
+ try {
1438
+ const activeBuild = store.deep.getActiveBuild();
1439
+ if (!activeBuild) return { available: false, generation: null, stale: null, target: null, role: null, ownership: [], signals: [], plans: [], facts: [], dependents: [], related_tests: [] };
1440
+ const files = store.deep.listFiles(activeBuild.id);
1441
+ const target = resolveWhyTarget(root, store, activeBuild.id, files, inputTarget);
1442
+ if (!target) {
1443
+ return {
1444
+ available: false,
1445
+ generation: status.generation,
1446
+ stale: status.staleness.is_stale,
1447
+ target: null,
1448
+ role: null,
1449
+ ownership: [],
1450
+ signals: [],
1451
+ plans: [],
1452
+ facts: [],
1453
+ dependents: [],
1454
+ related_tests: [],
1455
+ context_error: `No repository file or symbol matched "${inputTarget.trim()}".`
1456
+ };
1457
+ }
1458
+ const relativePath = target.path;
1459
+ let repositoryState;
1460
+ let stale = status.staleness.is_stale;
1461
+ try {
1462
+ repositoryState = inspectRepository(root, files);
1463
+ } catch {
1464
+ }
1465
+ if (repositoryState) stale = repositoryState.snapshotHash !== activeBuild.identity.repositorySnapshotId;
1466
+ const impact = createBrainImpactProvider(store.deep, { root });
1467
+ const traversal = impact?.dependents({ files: [relativePath] }, 2);
1468
+ const dependents = [...new Set((traversal?.files ?? []).filter((entry) => entry !== relativePath))].sort();
1469
+ const relatedTests2 = [...new Set(impact?.relatedTests({ files: [relativePath] }) ?? [])].sort();
1470
+ const metric = store.deep.getFileGraphMetric(relativePath, activeBuild.id);
1471
+ const rank = store.deep.getFileGraphRank(relativePath, activeBuild.id);
1472
+ const role = metric && rank ? {
1473
+ cluster: metric.cluster,
1474
+ component_id: metric.componentId,
1475
+ core_score: metric.coreScore,
1476
+ hotspot_score: metric.hotspotScore,
1477
+ churn_score: metric.churnScore,
1478
+ core_rank: rank.coreRank,
1479
+ hotspot_rank: rank.hotspotRank,
1480
+ total_files: rank.totalFiles
1481
+ } : null;
1482
+ const exactFacts = store.deep.findContextFactsByExactPaths([relativePath], activeBuild.id);
1483
+ const ownership = exactFacts.filter((fact) => fact.kind === "ownership").flatMap((fact) => ownershipFromPayload(fact.payload));
1484
+ const signals = store.searchSignalsMentioning(relativePath, 10).map((entry) => ({
1485
+ id: entry.id,
1486
+ source: entry.source,
1487
+ excerpt: entry.excerpt,
1488
+ created_at: entry.createdAt
1489
+ }));
1490
+ const plans = store.searchPlansMentioning(relativePath, 10).map((entry) => ({
1491
+ id: entry.id,
1492
+ version: entry.version,
1493
+ state: entry.state,
1494
+ excerpt: entry.excerpt,
1495
+ created_at: entry.createdAt
1496
+ }));
1497
+ const localPlan = localPlanMention(root, relativePath);
1498
+ if (localPlan && !plans.some((entry) => entry.id === localPlan.id && entry.version === localPlan.version)) plans.unshift(localPlan);
1499
+ let pack;
1500
+ let contextError;
1501
+ if (repositoryState && repositoryState.snapshotHash !== activeBuild.identity.repositorySnapshotId && repositoryState.changedPaths.length === 0) {
1502
+ contextError = "Deep Brain is stale because repository metadata changed; rebuild before trusting focused context.";
1503
+ } else {
1504
+ try {
1505
+ const focusedQuery = target.kind === "symbol" && target.symbol ? `Explain symbol "${target.symbol.name}" and the impact of "${relativePath}"` : `Explain the impact of "${relativePath}"`;
1506
+ pack = buildContextPack(store.deep, focusedQuery, {
1507
+ repositoryRoot: root,
1508
+ maxItems: WHY_FACT_LIMIT,
1509
+ maxBytes: 6e4,
1510
+ graph: createContextGraphExpansionAdapter(store.deep),
1511
+ ...repositoryState ? {
1512
+ currentSnapshotHash: repositoryState.snapshotHash,
1513
+ changedPaths: repositoryState.changedPaths
1514
+ } : {}
1515
+ });
1516
+ } catch (error) {
1517
+ contextError = errorMessage(error);
1518
+ }
1519
+ }
1520
+ const facts = pack?.sections.flatMap((section) => section.facts.map((fact) => ({
1521
+ section: section.kind,
1522
+ id: fact.fact_id,
1523
+ title: fact.title,
1524
+ kind: fact.kind,
1525
+ path: fact.source.path,
1526
+ start_line: fact.source.start_line,
1527
+ end_line: fact.source.end_line,
1528
+ relevance: fact.relevance
1529
+ }))) ?? [];
1530
+ return {
1531
+ available: true,
1532
+ generation: status.generation,
1533
+ stale,
1534
+ target,
1535
+ role,
1536
+ ownership,
1537
+ signals,
1538
+ plans,
1539
+ facts,
1540
+ dependents,
1541
+ related_tests: relatedTests2,
1542
+ ...contextError ? { context_error: contextError } : {}
1543
+ };
1544
+ } finally {
1545
+ store.close();
1546
+ }
1547
+ }
1548
+ function resolveWhyTarget(root, store, buildId, files, input) {
1549
+ const query = input.trim();
1550
+ if (!query || query.length > 500) return void 0;
1551
+ const knownPaths = new Set(files.map((file) => file.path));
1552
+ try {
1553
+ const relative = path8.relative(root, assertInside(root, path8.resolve(root, query))).replaceAll("\\", "/");
1554
+ if (knownPaths.has(relative)) return { input: query, kind: "file", path: relative, alternatives: [] };
1555
+ } catch {
1556
+ }
1557
+ const exact = store.deep.findContextSymbolsByExactNames([query], buildId);
1558
+ const match = exact.length > 0 ? "exact" : "trigram";
1559
+ const candidates = exact.length > 0 ? exact : store.deep.findContextSymbolsByTrigram([query], 20, buildId);
1560
+ if (candidates.length === 0) return void 0;
1561
+ const ranked = rankWhySymbols(store, buildId, candidates);
1562
+ const selected = ranked[0];
1563
+ return {
1564
+ input: query,
1565
+ kind: "symbol",
1566
+ path: selected.filePath,
1567
+ symbol: { id: selected.id, name: selected.name, kind: selected.kind, match },
1568
+ alternatives: ranked.slice(1, 6).map((symbol) => ({ id: symbol.id, name: symbol.name, kind: symbol.kind, path: symbol.filePath }))
1569
+ };
1570
+ }
1571
+ function rankWhySymbols(store, buildId, symbols) {
1572
+ const scores = /* @__PURE__ */ new Map();
1573
+ for (const symbol of symbols) {
1574
+ if (!scores.has(symbol.filePath)) scores.set(symbol.filePath, store.deep.getFileGraphMetric(symbol.filePath, buildId)?.coreScore ?? 0);
1575
+ }
1576
+ return [...symbols].sort((left, right) => (scores.get(right.filePath) ?? 0) - (scores.get(left.filePath) ?? 0) || left.name.localeCompare(right.name) || left.filePath.localeCompare(right.filePath) || left.id.localeCompare(right.id));
1577
+ }
1578
+ function ownershipFromPayload(payload) {
1579
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return [];
1580
+ const record3 = payload;
1581
+ if (record3.advisory !== true || record3.source !== "codeowners" && record3.source !== "git_author") return [];
1582
+ if (typeof record3.confidence !== "number" || !Number.isFinite(record3.confidence) || !Array.isArray(record3.principals)) return [];
1583
+ const principals = record3.principals.flatMap((principal) => {
1584
+ if (!principal || typeof principal !== "object" || Array.isArray(principal)) return [];
1585
+ const value = principal;
1586
+ if (value.kind !== "codeowners" && value.kind !== "git_author" || typeof value.value !== "string" || !value.value.trim()) return [];
1587
+ return [{ kind: value.kind, value: value.value }];
1588
+ });
1589
+ if (!principals.length) return [];
1590
+ return [{ source: record3.source, principals, confidence: record3.confidence, advisory: true, evidence: record3.evidence ?? null }];
1591
+ }
1592
+ function localPlanMention(root, relativePath) {
1593
+ const filename = path8.join(root, "PLAN.md");
1594
+ if (!fs8.existsSync(filename)) return void 0;
1595
+ let body;
1596
+ try {
1597
+ body = fs8.readFileSync(filename, "utf8");
1598
+ } catch {
1599
+ return void 0;
1600
+ }
1601
+ const index = body.indexOf(relativePath);
1602
+ if (index < 0) return void 0;
1603
+ const start = Math.max(0, index - 160);
1604
+ const end = Math.min(body.length, index + relativePath.length + 240);
1605
+ const excerpt = body.slice(start, end).replace(/[\r\n]+/g, " ").trim();
1606
+ let createdAt = (/* @__PURE__ */ new Date(0)).toISOString();
1607
+ try {
1608
+ createdAt = fs8.statSync(filename).mtime.toISOString();
1609
+ } catch {
1610
+ }
1611
+ return { id: "PLAN.md", version: 1, state: "local", excerpt, created_at: createdAt };
1612
+ }
1613
+ function summarizeBuild(result, mode, offline) {
1614
+ return {
1615
+ schema_version: "1",
1616
+ state: result.activated ? "READY" : "FAILED",
1617
+ exit_code: result.activated ? 0 : 1,
1618
+ mode,
1619
+ offline,
1620
+ changed: result.changed,
1621
+ activated: result.activated,
1622
+ build_id: result.build.id,
1623
+ active_build_id: result.activeBuildId ?? null,
1624
+ repository_snapshot_hash: result.repositorySnapshotId,
1625
+ files: result.census.files.length,
1626
+ ast: {
1627
+ candidates: result.ast.candidates,
1628
+ parsed: result.ast.parsed,
1629
+ partial: result.ast.partial,
1630
+ failed: result.ast.failed,
1631
+ unsupported: result.ast.unsupported,
1632
+ parser_runtimes: result.ast.parserRuntimes,
1633
+ by_language: result.ast.byLanguage
1634
+ },
1635
+ coverage: result.coverage,
1636
+ stages: result.stages.map((stage) => ({ name: stage.name, required: stage.required, status: stage.status, elapsed_ms: Math.round(stage.elapsedMs), ...stage.error ? { error: stage.error } : {} })),
1637
+ github: result.github ? { status: result.github.status, result_id: result.github.resultId } : null,
1638
+ model_inputs: result.knowledge?.inputInspections?.map((artifact) => ({
1639
+ kind: artifact.kind,
1640
+ content: artifact.content,
1641
+ attempt: artifact.attempt ?? null,
1642
+ hash: artifact.hash,
1643
+ size_bytes: artifact.sizeBytes,
1644
+ path: path8.relative(result.census.root, artifact.path)
1645
+ })) ?? [],
1646
+ projections: result.projections.map((filename) => path8.basename(filename))
1647
+ };
1648
+ }
1649
+ function emitBuildResult(result, mode, offline) {
1650
+ const state = result.activated ? pc.green("READY") : pc.red("FAILED");
1651
+ console.log(`
1652
+ ${pc.bold("DEEP BRAIN BUILD")} \xB7 ${state}`);
1653
+ console.log(` mode ${mode}${offline ? " \xB7 offline" : ""}${result.changed ? "" : " \xB7 unchanged"}`);
1654
+ console.log(` files ${result.census.files.length} \xB7 syntax ${result.ast.parsed + result.ast.partial}/${result.ast.candidates} successful`);
1655
+ console.log(` Tree-sitter ${result.ast.parserRuntimes["web-tree-sitter"].parsed + result.ast.parserRuntimes["web-tree-sitter"].partial}/${result.ast.parserRuntimes["web-tree-sitter"].candidates}`);
1656
+ console.log(` structural ${result.ast.parserRuntimes["structural-fallback"].parsed + result.ast.parserRuntimes["structural-fallback"].partial}/${result.ast.parserRuntimes["structural-fallback"].candidates} \xB7 explicitly non-Tree-sitter`);
1657
+ if (result.ast.parserRuntimes.unavailable.candidates > 0) {
1658
+ console.log(` unavailable ${result.ast.parserRuntimes.unavailable.candidates}`);
1659
+ }
1660
+ console.log(` coverage ${result.coverage.passed ? pc.green("PASS") : pc.red("FAIL")}`);
1661
+ for (const check of result.coverage.checks.filter((entry) => !entry.passed)) {
1662
+ console.log(` ${pc.red(check.id)} \xB7 ${check.message}`);
1663
+ emitCoverageDiagnostics(check.diagnostics, " ");
1664
+ }
1665
+ console.log(` snapshot ${result.repositorySnapshotId}`);
1666
+ console.log(` build ${result.build.id}`);
1667
+ if (!result.activated) console.log(`
1668
+ ${pc.red("Not activated.")} The previous READY generation, if any, remains untouched.`);
1669
+ console.log("");
1670
+ }
1671
+ function emitBuildError(message, json, exitCode = 2) {
1672
+ if (json) console.log(JSON.stringify({ schema_version: "1", state: "FAILED", exit_code: exitCode, error: message }, null, 2));
1673
+ else console.error(`
1674
+ ${pc.bold("DEEP BRAIN BUILD")} \xB7 ${pc.red("FAILED")}
1675
+ ${message}
1676
+ No previous READY generation was deleted.
1677
+ `);
1678
+ }
1679
+ function emitBrainStatus(report) {
1680
+ if (report.state === "MISSING" || !report.generation) {
1681
+ console.log(`
1682
+ ${pc.bold("DEEP BRAIN STATUS")} \xB7 ${pc.yellow("NOT BUILT")}`);
1683
+ console.log(" Run: scriptonia brain build --fast --offline");
1684
+ for (const failure of report.failures) {
1685
+ console.log(` ${pc.red(failure.code)} \xB7 ${failure.message}`);
1686
+ if (failure.diagnostics) emitCoverageDiagnostics(failure.diagnostics, " ");
1687
+ }
1688
+ console.log("");
1689
+ return;
1690
+ }
1691
+ const freshness = report.staleness.is_stale === false ? pc.green("fresh") : report.staleness.is_stale === true ? pc.yellow("stale") : pc.yellow("unknown freshness");
1692
+ const coverage = report.coverage?.passed ? pc.green("PASS") : pc.red("FAIL");
1693
+ console.log(`
1694
+ ${pc.bold("DEEP BRAIN STATUS")} \xB7 ${pc.green(report.state)}`);
1695
+ console.log(` generation ${report.generation.number} \xB7 ${report.generation.build_id}`);
1696
+ console.log(` snapshot ${report.generation.repository_snapshot_hash} \xB7 ${freshness}`);
1697
+ if (report.generation.git_evidence_anchor) console.log(` git anchor ${report.generation.git_evidence_anchor} \xB7 bounded history evidence, not a freshness proof`);
1698
+ console.log(` size ${formatBytes(report.size_bytes)}`);
1699
+ console.log(` contents ${report.counts.files} files \xB7 ${report.counts.symbols} symbols \xB7 ${report.counts.facts} facts \xB7 ${report.counts.edges} edges`);
1700
+ console.log(` coverage ${coverage}${report.coverage ? ` \xB7 ${report.coverage.checks.filter((entry) => entry.passed).length}/${report.coverage.checks.length} checks` : " \xB7 missing"}`);
1701
+ if (report.parser_runtime) {
1702
+ const treeSitter = report.parser_runtime.overall["web-tree-sitter"];
1703
+ const structural = report.parser_runtime.overall["structural-fallback"];
1704
+ console.log(` parsers Tree-sitter ${treeSitter ? `${treeSitter.parsed + treeSitter.partial}/${treeSitter.candidates}` : "0/0"} \xB7 structural ${structural ? `${structural.parsed + structural.partial}/${structural.candidates}` : "0/0"} (non-Tree-sitter)`);
1705
+ }
1706
+ if (report.brain_score) {
1707
+ const measured = report.brain_score.measured_score === null ? "unavailable" : `${report.brain_score.measured_score.toFixed(1)}/100 measured`;
1708
+ const release = report.brain_score.complete ? ` \xB7 release score ${report.brain_score.release_score?.toFixed(1)}` : ` \xB7 ${report.brain_score.available_points}/100 points have build-scoped evidence`;
1709
+ console.log(` brain score ${measured}${release}`);
1710
+ for (const component of report.brain_score.components.filter((entry) => entry.status === "UNAVAILABLE")) {
1711
+ console.log(` ${pc.yellow("unavailable")} ${component.id} \xB7 ${component.detail}`);
1712
+ }
1713
+ }
1714
+ console.log(` knowledge ${knowledgeLabel(report.knowledge_freshness.status)} \xB7 ${report.knowledge_freshness.detail}`);
1715
+ if (report.build_timing) {
1716
+ const timing = report.build_timing.elapsed_ms === null ? "elapsed unavailable" : `${formatDuration(report.build_timing.elapsed_ms)} / ${formatDuration(report.build_timing.budget_ms)} budget`;
1717
+ console.log(` build time ${report.build_timing.mode.toLowerCase()} \xB7 ${timing}`);
1718
+ }
1719
+ if (report.stage_timings.length > 0) {
1720
+ console.log(" stages");
1721
+ for (const stage of report.stage_timings) {
1722
+ console.log(` ${stage.status.padEnd(9)} ${stage.name.padEnd(12)} ${stage.elapsed_ms === null ? "unknown" : formatDuration(stage.elapsed_ms)}`);
1723
+ }
1724
+ }
1725
+ if (report.staleness.changed_file_count > 0) {
1726
+ console.log(` changed ${report.staleness.changed_file_count} files${report.staleness.changed_paths_omitted ? ` \xB7 ${report.staleness.changed_paths_omitted} omitted` : ""}`);
1727
+ for (const filename of report.staleness.changed_paths.slice(0, 10)) console.log(` ${filename}`);
1728
+ }
1729
+ if (report.staleness.candidate_changed_file_count > 0) {
1730
+ console.log(` git status ${report.staleness.candidate_changed_file_count} current paths observed${report.staleness.candidate_changed_paths_omitted ? ` \xB7 ${report.staleness.candidate_changed_paths_omitted} omitted` : ""}`);
1731
+ for (const filename of report.staleness.candidate_changed_paths.slice(0, 10)) console.log(` ${filename}`);
1732
+ }
1733
+ if (report.staleness.proof === "UNAVAILABLE") console.log(` freshness ${pc.yellow("UNKNOWN")} \xB7 ${report.staleness.detail}`);
1734
+ if (report.failures.length > 0) {
1735
+ console.log(" failures");
1736
+ for (const failure of report.failures) {
1737
+ console.log(` ${pc.red(failure.code)} \xB7 ${failure.message}`);
1738
+ if (failure.diagnostics) emitCoverageDiagnostics(failure.diagnostics, " ");
1739
+ }
1740
+ } else console.log(` failures ${pc.green("none")}`);
1741
+ console.log("");
1742
+ }
1743
+ function missingStatus(sizeBytes, failures, storage = emptyStorageSummary(sizeBytes)) {
1744
+ return {
1745
+ schema_version: "1",
1746
+ state: "MISSING",
1747
+ size_bytes: sizeBytes,
1748
+ generation: null,
1749
+ counts: { files: 0, symbols: 0, facts: 0, edges: 0, routes: 0, manifests: 0 },
1750
+ coverage: null,
1751
+ parser_runtime: null,
1752
+ storage,
1753
+ stage_timings: [],
1754
+ build_timing: null,
1755
+ brain_score: null,
1756
+ knowledge_freshness: { status: "NOT_BUILT", changed_files_since_extract: null, detail: "No active Deep Brain generation exists." },
1757
+ staleness: unknownStaleness("FAST", "No active Deep Brain generation exists."),
1758
+ failures
1759
+ };
1760
+ }
1761
+ function readStatusAggregate(store, buildId) {
1762
+ const summary = store.deep.getBuildSummary(buildId);
1763
+ if (!summary) throw new Error(`Deep Brain status aggregate returned no row for ${buildId}`);
1764
+ return {
1765
+ counts: {
1766
+ files: summary.files,
1767
+ symbols: summary.symbols,
1768
+ facts: summary.facts,
1769
+ edges: summary.edges,
1770
+ routes: summary.routes,
1771
+ manifests: summary.manifests
1772
+ },
1773
+ activeObjectBytes: summary.activeObjectBytes,
1774
+ gitEvidenceAnchor: summary.gitEvidenceAnchor ?? null,
1775
+ ...summary.astRuntime ? { astRuntime: summary.astRuntime } : {}
1776
+ };
1777
+ }
1778
+ function parserRuntimeStatus(summary) {
1779
+ if (!summary) return null;
1780
+ return {
1781
+ schema_version: summary.schemaVersion,
1782
+ overall: summary.overall,
1783
+ by_language: summary.byLanguage
1784
+ };
1785
+ }
1786
+ function readStageTimings(store, buildId) {
1787
+ return store.deep.listBuildStages(buildId, STATUS_STAGE_LIMIT).map((row) => {
1788
+ const metrics = record2(row.metrics) ?? null;
1789
+ const error = record2(row.error) ?? null;
1790
+ const elapsedMetric = finiteNonnegative(metrics?.elapsedMs);
1791
+ const elapsedWall = elapsedBetween(row.startedAt, row.finishedAt);
1792
+ return {
1793
+ name: row.name,
1794
+ required: row.required,
1795
+ status: row.status,
1796
+ elapsed_ms: elapsedMetric ?? elapsedWall,
1797
+ error: typeof error?.message === "string" ? error.message : row.error === void 0 ? null : String(row.error),
1798
+ metrics
1799
+ };
1800
+ });
1801
+ }
1802
+ function buildTimingFor(build) {
1803
+ const mode = build.identity.mode;
1804
+ const budgetMs = mode === "DEEP" ? 7 * 6e4 : mode === "FAST" ? 9e4 : 3e4;
1805
+ const elapsedMs = elapsedBetween(build.startedAt, build.finishedAt ?? null);
1806
+ return {
1807
+ mode,
1808
+ elapsed_ms: elapsedMs,
1809
+ budget_ms: budgetMs,
1810
+ within_budget: elapsedMs === null ? null : elapsedMs <= budgetMs
1811
+ };
1812
+ }
1813
+ function knowledgeFreshnessFor(stages, coverage) {
1814
+ const stage = stages.find((entry) => entry.name === "knowledge");
1815
+ if (!stage) return { status: "UNKNOWN", changed_files_since_extract: null, detail: "The generation has no knowledge-stage metadata." };
1816
+ const status = typeof stage.metrics?.status === "string" ? stage.metrics.status.toUpperCase() : void 0;
1817
+ const reason = typeof stage.metrics?.reason === "string" ? stage.metrics.reason : void 0;
1818
+ const staleFiles = finiteNonnegative(stage.metrics?.knowledgeStaleByFiles);
1819
+ if (status === "REUSED") {
1820
+ const count = staleFiles ?? 0;
1821
+ return count > 0 ? { status: "STALE", changed_files_since_extract: count, detail: `Citation-valid knowledge was reused and is stale by ${count} changed file${count === 1 ? "" : "s"}.` } : { status: "CURRENT", changed_files_since_extract: 0, detail: "Citation-valid knowledge was reused with no changed source files." };
1822
+ }
1823
+ if (status === "VALIDATED") return { status: "CURRENT", changed_files_since_extract: 0, detail: "Knowledge claims were validated for this generation." };
1824
+ if (status === "SKIPPED" || reason === "offline" || reason === "fast-or-incremental") {
1825
+ return { status: "NOT_BUILT", changed_files_since_extract: null, detail: reason ? `Knowledge extraction was skipped (${reason}).` : "Knowledge extraction was skipped." };
1826
+ }
1827
+ const llmClaims = finiteNonnegative(coverage?.metrics.llmClaims);
1828
+ if (stage.status === "SUCCEEDED" && llmClaims !== null && llmClaims > 0) {
1829
+ return { status: "CURRENT", changed_files_since_extract: 0, detail: "Stored knowledge claims belong to this completed generation." };
1830
+ }
1831
+ return { status: "UNKNOWN", changed_files_since_extract: null, detail: "Knowledge freshness cannot be proven from this generation's stage metadata." };
1832
+ }
1833
+ function scoreBrain(coverage, timing, planEvidence) {
1834
+ const metric = (name) => finiteNonnegative(coverage.metrics[name]);
1835
+ const sourceFiles = metric("sourceFiles");
1836
+ const languageFiles = metric("identifiedLanguageFiles");
1837
+ const supportedAst = metric("supportedAstFiles");
1838
+ const parsedAst = metric("parsedAstFiles");
1839
+ const internalImports = metric("internalImports");
1840
+ const resolvedImports = metric("resolvedInternalImports");
1841
+ const coverageRatios = [
1842
+ safeRatio(languageFiles, sourceFiles, false),
1843
+ safeRatio(parsedAst, supportedAst, true),
1844
+ safeRatio(resolvedImports, internalImports, true)
1845
+ ];
1846
+ const coverageValue = coverageRatios.every((value) => value !== null) ? coverageRatios.reduce((sum, value) => sum + (value ?? 0), 0) / coverageRatios.length : null;
1847
+ const coverageComponent = coverageValue === null ? unavailableScoreComponent("coverage", 40, "Language, AST, or import coverage metrics are missing.") : measuredScoreComponent("coverage", 40, coverageValue, "Mean of language identification, AST parse, and internal-import resolution rates.");
1848
+ const llmClaims = metric("llmClaims");
1849
+ const citedClaims = metric("citedLlmClaims");
1850
+ const citationValue = llmClaims !== null && citedClaims !== null && llmClaims > 0 ? Math.min(1, citedClaims / llmClaims) : null;
1851
+ const citationComponent = citationValue === null ? unavailableScoreComponent("citation_rate", 20, llmClaims === 0 ? "No LLM knowledge claims exist in this generation." : "Citation metrics are missing.") : measuredScoreComponent("citation_rate", 20, citationValue, "Cited LLM knowledge claims divided by all LLM knowledge claims.");
1852
+ const planComponent = planEvidence ? measuredScoreComponent(
1853
+ "plan_file_validity",
1854
+ 25,
1855
+ planEvidence.quality.metrics.plan_file_validity,
1856
+ `Latest valid ${planEvidence.flow} plan ${planEvidence.planSlug} v${planEvidence.planVersion} is bound to this generation (quality ${roundScore(planEvidence.quality.score)}/100).`
1857
+ ) : unavailableScoreComponent(
1858
+ "plan_file_validity",
1859
+ 25,
1860
+ "No schema-valid plan-quality evidence is stored for this active generation."
1861
+ );
1862
+ const budgetComponent = timing.within_budget === null ? unavailableScoreComponent("budget_compliance", 15, "The build has no trustworthy finished timestamp.") : measuredScoreComponent(
1863
+ "budget_compliance",
1864
+ 15,
1865
+ timing.within_budget ? 1 : 0,
1866
+ `${timing.mode.toLowerCase()} build ${timing.within_budget ? "met" : "exceeded"} the ${formatDuration(timing.budget_ms)} published budget.`
1867
+ );
1868
+ const components = [coverageComponent, citationComponent, planComponent, budgetComponent];
1869
+ const measured = components.filter((component) => component.status === "MEASURED");
1870
+ const earnedPoints = roundScore(measured.reduce((sum, component) => sum + (component.earned_points ?? 0), 0));
1871
+ const availablePoints = measured.reduce((sum, component) => sum + component.weight, 0);
1872
+ const complete = availablePoints === 100;
1873
+ return {
1874
+ measured_score: availablePoints === 0 ? null : roundScore(earnedPoints / availablePoints * 100),
1875
+ earned_points: earnedPoints,
1876
+ available_points: availablePoints,
1877
+ total_points: 100,
1878
+ complete,
1879
+ release_score: complete ? earnedPoints : null,
1880
+ components
1881
+ };
1882
+ }
1883
+ function measuredScoreComponent(id, weight, value, detail) {
1884
+ const bounded = Math.max(0, Math.min(1, value));
1885
+ return { id, weight, status: "MEASURED", value: roundScore(bounded), earned_points: roundScore(bounded * weight), detail };
1886
+ }
1887
+ function unavailableScoreComponent(id, weight, detail) {
1888
+ return { id, weight, status: "UNAVAILABLE", value: null, earned_points: null, detail };
1889
+ }
1890
+ function inspectRepositoryFast(root) {
1891
+ const result = spawnSync("git", ["status", "--porcelain=v2", "--branch", "-z", "--untracked-files=all", "--ignore-submodules=dirty"], {
1892
+ cwd: root,
1893
+ encoding: "utf8",
1894
+ timeout: STATUS_GIT_TIMEOUT_MS,
1895
+ maxBuffer: STATUS_GIT_OUTPUT_LIMIT,
1896
+ stdio: ["ignore", "pipe", "pipe"]
1897
+ });
1898
+ if (result.error || result.status !== 0) {
1899
+ const reason = result.error?.message ?? (result.stderr.trim() || `git status exited ${result.status ?? "without a status"}`);
1900
+ return unknownStaleness("FAST", `Bounded Git observation was unavailable: ${reason}. Run \`scriptonia brain status --check\` for an exact result.`);
1901
+ }
1902
+ const parsed = parseGitStatusV2(result.stdout);
1903
+ const visible = parsed.paths.slice(0, STATUS_CHANGED_PATH_LIMIT);
1904
+ return {
1905
+ is_stale: null,
1906
+ checked_snapshot_hash: null,
1907
+ changed_file_count: 0,
1908
+ changed_paths: [],
1909
+ changed_paths_omitted: 0,
1910
+ check: "FAST",
1911
+ proof: "UNAVAILABLE",
1912
+ detail: "Git status describes the current checkout, but this generation does not persist the Git tree and dirty-state proof needed to compare it exactly. Run `scriptonia brain status --check`.",
1913
+ current_head: parsed.head,
1914
+ working_tree_dirty: parsed.paths.length > 0,
1915
+ candidate_changed_file_count: parsed.paths.length,
1916
+ candidate_changed_paths: visible,
1917
+ candidate_changed_paths_omitted: parsed.paths.length - visible.length
1918
+ };
1919
+ }
1920
+ function parseGitStatusV2(output) {
1921
+ const records = output.split("\0");
1922
+ const paths = /* @__PURE__ */ new Set();
1923
+ let head = null;
1924
+ for (let index = 0; index < records.length; index += 1) {
1925
+ const record3 = records[index] ?? "";
1926
+ if (record3.startsWith("# branch.oid ")) {
1927
+ const candidate = record3.slice("# branch.oid ".length).trim();
1928
+ if (/^[0-9a-f]{40,64}$/.test(candidate)) head = candidate;
1929
+ continue;
1930
+ }
1931
+ let filename;
1932
+ if (record3.startsWith("1 ")) filename = record3.split(" ").slice(8).join(" ");
1933
+ else if (record3.startsWith("2 ")) {
1934
+ filename = record3.split(" ").slice(9).join(" ");
1935
+ const oldPath = records[index + 1];
1936
+ if (oldPath) addGitCandidate(paths, oldPath);
1937
+ index += 1;
1938
+ } else if (record3.startsWith("u ")) filename = record3.split(" ").slice(10).join(" ");
1939
+ else if (record3.startsWith("? ")) filename = record3.slice(2);
1940
+ if (filename) addGitCandidate(paths, filename);
1941
+ }
1942
+ return { head, paths: [...paths].sort() };
1943
+ }
1944
+ function addGitCandidate(paths, filename) {
1945
+ const normalized = filename.replaceAll("\\", "/");
1946
+ if (!normalized || normalized === ".scriptonia" || normalized.startsWith(".scriptonia/") || normalized === ".git" || normalized.startsWith(".git/")) return;
1947
+ paths.add(normalized);
1948
+ }
1949
+ function unknownStaleness(check, detail) {
1950
+ return {
1951
+ is_stale: null,
1952
+ checked_snapshot_hash: null,
1953
+ changed_file_count: 0,
1954
+ changed_paths: [],
1955
+ changed_paths_omitted: 0,
1956
+ check,
1957
+ proof: "UNAVAILABLE",
1958
+ detail,
1959
+ current_head: null,
1960
+ working_tree_dirty: null,
1961
+ candidate_changed_file_count: 0,
1962
+ candidate_changed_paths: [],
1963
+ candidate_changed_paths_omitted: 0
1964
+ };
1965
+ }
1966
+ function inspectRepository(root, storedFiles) {
1967
+ const census = walkRepo(root);
1968
+ const snapshotHash = hashObject(census.files.map((file) => ({
1969
+ path: file.path,
1970
+ sha256: file.sha256,
1971
+ class: file.class,
1972
+ language: file.language,
1973
+ analysisMode: file.analysisMode
1974
+ })));
1975
+ const current = new Map(census.files.map((file) => [file.path, file.sha256]));
1976
+ const stored = new Map(storedFiles.map((file) => [file.path, file.contentHash]));
1977
+ const changedPaths = [.../* @__PURE__ */ new Set([...current.keys(), ...stored.keys()])].filter((filename) => current.get(filename) !== stored.get(filename)).sort();
1978
+ return { snapshotHash, changedPaths };
1979
+ }
1980
+ function parseCoverage(value) {
1981
+ const envelope = record2(value);
1982
+ const report = record2(envelope?.report);
1983
+ const metrics = record2(envelope?.metrics);
1984
+ if (typeof report?.passed !== "boolean" || !metrics || !Array.isArray(report.checks)) return null;
1985
+ const checks = [];
1986
+ for (const value2 of report.checks) {
1987
+ const check = record2(value2);
1988
+ if (!check || typeof check.id !== "string" || typeof check.passed !== "boolean" || typeof check.message !== "string") continue;
1989
+ checks.push({
1990
+ id: check.id,
1991
+ passed: check.passed,
1992
+ message: check.message,
1993
+ ...typeof check.actual === "number" ? { actual: check.actual } : {},
1994
+ ...typeof check.total === "number" ? { total: check.total } : {},
1995
+ ...typeof check.threshold === "number" ? { threshold: check.threshold } : {},
1996
+ diagnostics: parseCoverageDiagnostics(check.diagnostics)
1997
+ });
1998
+ }
1999
+ return { passed: report.passed, metrics, checks };
2000
+ }
2001
+ function recentBuildFailures(builds) {
2002
+ const failures = [];
2003
+ for (const build of builds.filter((candidate) => candidate.status === "FAILED").slice(0, 5)) {
2004
+ failures.push({ source: "build", code: "BUILD_FAILED", message: failureMessage(build.failure), build_id: build.id });
2005
+ const coverage = parseCoverage(build.coverage);
2006
+ for (const check of coverage?.checks.filter((candidate) => !candidate.passed) ?? []) {
2007
+ failures.push({
2008
+ source: "coverage",
2009
+ code: check.id,
2010
+ message: check.message,
2011
+ build_id: build.id,
2012
+ diagnostics: check.diagnostics
2013
+ });
2014
+ }
2015
+ }
2016
+ return failures;
2017
+ }
2018
+ function emptyCoverageDiagnostics() {
2019
+ return { schemaVersion: "1", items: [], omittedCount: 0 };
2020
+ }
2021
+ function parseCoverageDiagnostics(value) {
2022
+ const object = record2(value);
2023
+ if (object?.schemaVersion !== "1" || !Array.isArray(object.items)) return emptyCoverageDiagnostics();
2024
+ const items = [];
2025
+ let invalidOrCapped = Math.max(0, object.items.length - MAX_DEEP_COVERAGE_DIAGNOSTIC_ITEMS);
2026
+ for (const raw of object.items.slice(0, MAX_DEEP_COVERAGE_DIAGNOSTIC_ITEMS)) {
2027
+ const parsed = parseCoverageGapItem(raw);
2028
+ if (parsed) items.push(parsed);
2029
+ else invalidOrCapped += 1;
2030
+ }
2031
+ const omitted = finiteNonnegative(object.omittedCount) ?? 0;
2032
+ return { schemaVersion: "1", items, omittedCount: Math.floor(omitted) + invalidOrCapped };
2033
+ }
2034
+ function parseCoverageGapItem(value) {
2035
+ const item2 = record2(value);
2036
+ if (!item2 || typeof item2.kind !== "string") return void 0;
2037
+ if (item2.kind === "source_file_shortfall" && typeof item2.actual === "number" && typeof item2.required === "number") {
2038
+ return { kind: item2.kind, actual: item2.actual, required: item2.required };
2039
+ }
2040
+ if (item2.kind === "required_stage" && typeof item2.stage === "string" && typeof item2.status === "string") {
2041
+ return { kind: item2.kind, stage: item2.stage, status: item2.status, ...typeof item2.reason === "string" ? { reason: item2.reason } : {} };
2042
+ }
2043
+ if (item2.kind === "unknown_language" && typeof item2.path === "string" && (item2.category === "SOURCE" || item2.category === "TEST")) {
2044
+ return { kind: item2.kind, path: item2.path, category: item2.category };
2045
+ }
2046
+ if (item2.kind === "ast_required_file" && typeof item2.path === "string" && (item2.status === "FAILED" || item2.status === "PENDING" || item2.status === "UNSUPPORTED") && typeof item2.reason === "string") {
2047
+ return { kind: item2.kind, path: item2.path, status: item2.status, reason: item2.reason };
2048
+ }
2049
+ if (item2.kind === "unresolved_internal_import" && typeof item2.sourcePath === "string" && typeof item2.specifier === "string") {
2050
+ return { kind: item2.kind, sourcePath: item2.sourcePath, specifier: item2.specifier, ...typeof item2.reason === "string" ? { reason: item2.reason } : {} };
2051
+ }
2052
+ if (item2.kind === "failed_manifest" && typeof item2.path === "string" && typeof item2.manifestKind === "string" && typeof item2.error === "string") {
2053
+ return { kind: item2.kind, path: item2.path, manifestKind: item2.manifestKind, error: item2.error };
2054
+ }
2055
+ if (item2.kind === "knowledge_claim" && typeof item2.claimId === "string" && typeof item2.claimKind === "string" && (item2.status === "UNCITED" || item2.status === "REJECTED") && typeof item2.reason === "string") {
2056
+ return { kind: item2.kind, claimId: item2.claimId, claimKind: item2.claimKind, status: item2.status, reason: item2.reason };
2057
+ }
2058
+ return void 0;
2059
+ }
2060
+ function emitCoverageDiagnostics(diagnostics, indent) {
2061
+ for (const item2 of diagnostics.items) console.log(`${indent}${coverageDiagnosticLabel(item2)}`);
2062
+ if (diagnostics.omittedCount > 0) console.log(`${indent}+${diagnostics.omittedCount} additional gap${diagnostics.omittedCount === 1 ? "" : "s"} omitted`);
2063
+ }
2064
+ function coverageDiagnosticLabel(item2) {
2065
+ if (item2.kind === "source_file_shortfall") return `source files ${item2.actual}/${item2.required}`;
2066
+ if (item2.kind === "required_stage") return `${item2.stage} \xB7 ${item2.status}${item2.reason ? ` \xB7 ${item2.reason}` : ""}`;
2067
+ if (item2.kind === "unknown_language") return `${item2.path} \xB7 ${item2.category.toLowerCase()} language unknown`;
2068
+ if (item2.kind === "ast_required_file") return `${item2.path} \xB7 AST ${item2.status.toLowerCase()} \xB7 ${item2.reason}`;
2069
+ if (item2.kind === "unresolved_internal_import") return `${item2.sourcePath} imports ${item2.specifier} \xB7 ${item2.reason ?? "unresolved"}`;
2070
+ if (item2.kind === "failed_manifest") return `${item2.path} \xB7 ${item2.manifestKind} \xB7 ${item2.error}`;
2071
+ return `${item2.claimId} \xB7 ${item2.claimKind} ${item2.status.toLowerCase()} \xB7 ${item2.reason}`;
2072
+ }
2073
+ function failureMessage(value) {
2074
+ const object = record2(value);
2075
+ if (typeof object?.message === "string") return object.message;
2076
+ if (typeof object?.code === "string") return object.code;
2077
+ if (value === void 0) return "A Deep Brain build failed without a recorded reason.";
2078
+ try {
2079
+ return JSON.stringify(value);
2080
+ } catch {
2081
+ return String(value);
2082
+ }
2083
+ }
2084
+ function storageSummary(store, activeObjectBytes) {
2085
+ return {
2086
+ database_bytes: fileSize(store.filename),
2087
+ wal_bytes: fileSize(`${store.filename}-wal`),
2088
+ shared_memory_bytes: fileSize(`${store.filename}-shm`),
2089
+ active_object_bytes: activeObjectBytes,
2090
+ inactive_object_bytes_included: false
2091
+ };
2092
+ }
2093
+ function emptyStorageSummary(databaseBytes = 0) {
2094
+ return {
2095
+ database_bytes: databaseBytes,
2096
+ wal_bytes: 0,
2097
+ shared_memory_bytes: 0,
2098
+ active_object_bytes: 0,
2099
+ inactive_object_bytes_included: false
2100
+ };
2101
+ }
2102
+ function storageTotal(storage) {
2103
+ return storage.database_bytes + storage.wal_bytes + storage.shared_memory_bytes + storage.active_object_bytes;
2104
+ }
2105
+ function fileSize(filename) {
2106
+ try {
2107
+ const stat = fs8.lstatSync(filename);
2108
+ return stat.isFile() && !stat.isSymbolicLink() ? stat.size : 0;
2109
+ } catch {
2110
+ return 0;
2111
+ }
2112
+ }
2113
+ function formatBytes(value) {
2114
+ if (value < 1024) return `${value} B`;
2115
+ if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KiB`;
2116
+ if (value < 1024 ** 3) return `${(value / 1024 ** 2).toFixed(1)} MiB`;
2117
+ return `${(value / 1024 ** 3).toFixed(1)} GiB`;
2118
+ }
2119
+ function formatDuration(value) {
2120
+ if (value < 1e3) return `${Math.round(value)}ms`;
2121
+ if (value < 6e4) return `${(value / 1e3).toFixed(1)}s`;
2122
+ return `${(value / 6e4).toFixed(1)}m`;
2123
+ }
2124
+ function knowledgeLabel(status) {
2125
+ if (status === "CURRENT") return pc.green(status.toLowerCase());
2126
+ if (status === "STALE") return pc.yellow(status.toLowerCase());
2127
+ return pc.dim(status.toLowerCase().replace("_", " "));
2128
+ }
2129
+ function finiteNonnegative(value) {
2130
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
2131
+ }
2132
+ function elapsedBetween(startedAt, finishedAt) {
2133
+ if (typeof startedAt !== "string" || typeof finishedAt !== "string") return null;
2134
+ const started = Date.parse(startedAt);
2135
+ const finished = Date.parse(finishedAt);
2136
+ return Number.isFinite(started) && Number.isFinite(finished) && finished >= started ? finished - started : null;
2137
+ }
2138
+ function safeRatio(actual, total, zeroIsComplete) {
2139
+ if (actual === null || total === null || actual > total) return null;
2140
+ if (total === 0) return zeroIsComplete ? 1 : null;
2141
+ return actual / total;
2142
+ }
2143
+ function roundScore(value) {
2144
+ return Math.round(value * 100) / 100;
2145
+ }
2146
+ function record2(value) {
2147
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2148
+ }
2149
+ function errorMessage(error) {
2150
+ return error instanceof Error ? error.message : String(error);
2151
+ }
2152
+
1445
2153
  // src/index.ts
1446
- var V2_COMMANDS = /* @__PURE__ */ new Set(["run", "runs", "verify", "gate", "learn", "eval", "acknowledge", "events", "v2-init", "report-summary", "diff-check", "why", "recall", "decide", "pulse"]);
2154
+ var V2_COMMANDS = /* @__PURE__ */ new Set(["--version", "-V", "run", "runs", "verify", "gate", "learn", "eval", "acknowledge", "events", "v2-init", "report-summary", "diff-check", "why", "recall", "decide", "pulse", "brain"]);
1447
2155
  var requested = process.argv[2];
1448
2156
  if (!requested || !V2_COMMANDS.has(requested)) {
1449
2157
  await import(new URL("../bin/scriptonia.mjs", import.meta.url).href);
@@ -1453,31 +2161,32 @@ if (!requested || !V2_COMMANDS.has(requested)) {
1453
2161
  async function runV2() {
1454
2162
  await initializeTelemetry();
1455
2163
  const program = new Command();
1456
- program.name("scriptonia").description("Evidence-based product memory and verification for AI coding agents").version("0.9.0-rc.1");
2164
+ program.name("scriptonia").description("Evidence-based product memory and verification for AI coding agents").version("0.9.0-rc.2");
2165
+ registerBrainCommands(program);
1457
2166
  program.command("v2-init").description("Create inspectable V2 configuration directories").action(() => {
1458
2167
  const root = findRepositoryRoot();
1459
2168
  const filename = ensureConfig(root);
1460
- console.log(`${pc.green("\u2713")} V2 brain configured at ${path10.relative(root, filename)}`);
2169
+ console.log(`${pc2.green("\u2713")} V2 brain configured at ${path9.relative(root, filename)}`);
1461
2170
  });
1462
2171
  program.command("diff-check").description("Run only sub-second deterministic changed-file policies").option("--base <ref>", "base Git reference", "HEAD").action((options) => {
1463
- const started2 = performance.now();
2172
+ const started = performance.now();
1464
2173
  const root = findRepositoryRoot();
1465
2174
  const snapshot = buildSnapshot({ cwd: root, base: options.base });
1466
2175
  const definitions = loadDecisions(root);
1467
2176
  const context = buildScopeContext(root, snapshot.files.map((file) => file.path));
1468
2177
  const results = evaluateDecisions(definitions.decisions, context).filter((result) => result.state !== "SKIPPED");
1469
- const plan = path10.join(root, "PLAN.md");
2178
+ const plan = path9.join(root, "PLAN.md");
1470
2179
  const unresolved = fs9.existsSync(plan) ? fs9.readFileSync(plan, "utf8").split("\n").filter((line) => /(?:⚠\s*)?UNRESOLVED/i.test(line)) : [];
1471
2180
  console.log(`
1472
- ${pc.bold("SCRIPTONIA DIFF CHECK")} \xB7 ${snapshot.files.length} changed files
2181
+ ${pc2.bold("SCRIPTONIA DIFF CHECK")} \xB7 ${snapshot.files.length} changed files
1473
2182
  `);
1474
- for (const error of definitions.errors) console.log(` ${pc.red("ERROR")} ${error}`);
1475
- for (const result of results) console.log(` ${result.state === "PASS" ? pc.green(result.state) : pc.red(result.state)} ${result.title} \xB7 ${result.reason}`);
1476
- if (unresolved.length) console.log(` ${pc.red("FAIL")} PLAN.md contains ${unresolved.length} unresolved constraint(s).`);
2183
+ for (const error of definitions.errors) console.log(` ${pc2.red("ERROR")} ${error}`);
2184
+ for (const result of results) console.log(` ${result.state === "PASS" ? pc2.green(result.state) : pc2.red(result.state)} ${result.title} \xB7 ${result.reason}`);
2185
+ if (unresolved.length) console.log(` ${pc2.red("FAIL")} PLAN.md contains ${unresolved.length} unresolved constraint(s).`);
1477
2186
  const blocked = unresolved.length > 0 || results.some((result) => result.state === "FAIL" && result.enforcement === "block");
1478
2187
  const errored = definitions.errors.length > 0 || results.some((result) => result.state === "ERROR");
1479
2188
  console.log(`
1480
- ${errored ? "ERROR" : blocked ? "BLOCKED" : "PASS"} \xB7 ${Math.round(performance.now() - started2)}ms
2189
+ ${errored ? "ERROR" : blocked ? "BLOCKED" : "PASS"} \xB7 ${Math.round(performance.now() - started)}ms
1481
2190
  `);
1482
2191
  process.exitCode = errored ? 4 : blocked ? 3 : 0;
1483
2192
  });
@@ -1486,23 +2195,59 @@ ${errored ? "ERROR" : blocked ? "BLOCKED" : "PASS"} \xB7 ${Math.round(performanc
1486
2195
  ensureConfig(root);
1487
2196
  const id = `decision_${slug(title)}_${Date.now().toString(36)}`;
1488
2197
  const decision = decisionSchema.parse({ schema_version: "1", id, version: 1, title, state: options.confirm ? "active" : "draft", owner: options.owner, source_refs: [], scope: { paths: { include: csv(options.include), exclude: csv(options.exclude) } }, predicate: { kind: options.kind, names: csv(options.names) }, enforcement: options.enforcement, created_at: (/* @__PURE__ */ new Date()).toISOString(), confirmed_by: options.confirm });
1489
- const filename = path10.join(root, ".scriptonia", "decisions", `${id}.yaml`);
2198
+ const filename = path9.join(root, ".scriptonia", "decisions", `${id}.yaml`);
1490
2199
  fs9.writeFileSync(filename, YAML3.stringify(decision));
1491
- console.log(`${pc.green("\u2713")} ${decision.state} decision written to ${path10.relative(root, filename)}`);
2200
+ console.log(`${pc2.green("\u2713")} ${decision.state} decision written to ${path9.relative(root, filename)}`);
1492
2201
  if (decision.state === "draft") console.log(" Review its scope and predicate, then confirm it before enforcement.");
1493
2202
  });
1494
- program.command("why <file>").description("Explain the active product memory affecting a file").action((file) => {
2203
+ program.command("why <target>").description("Explain Deep Brain evidence, impact, tests, decisions, and evals affecting a file or symbol").option("--json", "print a machine-readable explanation").action((target, options) => {
1495
2204
  const root = findRepositoryRoot();
1496
- const relative = path10.relative(root, assertInside(root, path10.resolve(root, file))).replaceAll("\\", "/");
1497
- const context = buildScopeContext(root, [relative]);
1498
- const decisions = loadDecisions(root).decisions.filter((entry) => entry.state === "active" && matchScope(entry.scope, context).matched);
1499
- const evals = loadEvals(root).evals.filter((entry) => entry.state === "active" && matchScope(entry.scope, context).matched);
2205
+ const deep = explainDeepBrain(root, target);
2206
+ const relative = deep.target?.path;
2207
+ const context = relative ? buildScopeContext(root, [relative]) : void 0;
2208
+ const decisions = context ? loadDecisions(root).decisions.filter((entry) => entry.state === "active" && matchScope(entry.scope, context).matched) : [];
2209
+ const evals = context ? loadEvals(root).evals.filter((entry) => entry.state === "active" && matchScope(entry.scope, context).matched) : [];
2210
+ if (options.json) {
2211
+ console.log(JSON.stringify({
2212
+ schema_version: "1",
2213
+ query: target,
2214
+ file: relative ?? null,
2215
+ deep_brain: deep,
2216
+ decisions: decisions.map((entry) => ({ id: entry.id, version: entry.version, title: entry.title, enforcement: entry.enforcement })),
2217
+ evals: evals.map((entry) => ({ id: entry.id, version: entry.version, title: entry.title, enforcement: entry.enforcement }))
2218
+ }, null, 2));
2219
+ return;
2220
+ }
1500
2221
  console.log(`
1501
- ${pc.bold("WHY")} ${relative}
2222
+ ${pc2.bold("WHY")} ${target}${relative && relative !== target ? ` \u2192 ${relative}` : ""}
1502
2223
  `);
2224
+ if (deep.available && deep.generation) {
2225
+ const freshness = deep.stale === false ? pc2.green("fresh") : deep.stale === true ? pc2.yellow("stale") : pc2.yellow("freshness unknown");
2226
+ console.log(` brain generation ${deep.generation.number} \xB7 ${freshness} \xB7 ${deep.generation.build_id}`);
2227
+ if (deep.target?.symbol) {
2228
+ console.log(` symbol ${deep.target.symbol.name} \xB7 ${deep.target.symbol.kind} \xB7 ${deep.target.symbol.match} match \xB7 ${deep.target.symbol.id}`);
2229
+ for (const alternative of deep.target.alternatives) console.log(` also ${alternative.name} \xB7 ${alternative.kind} \xB7 ${alternative.path}`);
2230
+ }
2231
+ if (deep.role) {
2232
+ console.log(` role ${deep.role.cluster} \xB7 core #${deep.role.core_rank}/${deep.role.total_files} (${deep.role.core_score.toFixed(2)}) \xB7 hotspot #${deep.role.hotspot_rank} (${deep.role.hotspot_score.toFixed(2)}) \xB7 churn ${deep.role.churn_score.toFixed(2)}`);
2233
+ }
2234
+ for (const owner of deep.ownership) console.log(` owner ${owner.principals.map((principal) => principal.value).join(", ")} \xB7 ${owner.source} \xB7 ${(owner.confidence * 100).toFixed(0)}% \xB7 advisory`);
2235
+ for (const fact of deep.facts) {
2236
+ console.log(` evidence ${fact.title} \xB7 ${fact.path}:${fact.start_line} \xB7 ${(fact.relevance * 100).toFixed(0)}% \xB7 ${fact.id}`);
2237
+ }
2238
+ for (const dependent of deep.dependents) console.log(` dependent ${dependent}`);
2239
+ for (const test of deep.related_tests) console.log(` test ${test}`);
2240
+ for (const signal of deep.signals) console.log(` signal ${signal.id} \xB7 ${signal.source} \xB7 ${signal.excerpt.replace(/[\r\n]+/g, " ").slice(0, 180)}`);
2241
+ for (const plan of deep.plans) console.log(` plan ${plan.id}@${plan.version} \xB7 ${plan.state} \xB7 ${plan.excerpt.replace(/[\r\n]+/g, " ").slice(0, 180)}`);
2242
+ if (deep.context_error) console.log(` ${pc2.yellow("context")} ${deep.context_error}`);
2243
+ } else if (deep.generation) {
2244
+ console.log(` brain ${pc2.yellow(deep.context_error ?? `no file or symbol matched "${target}"`)}`);
2245
+ } else {
2246
+ console.log(` brain ${pc2.dim("not built \xB7 run scriptonia brain build --fast --offline")}`);
2247
+ }
1503
2248
  for (const decision of decisions) console.log(` decision ${decision.id}@${decision.version} \xB7 ${decision.title} \xB7 ${decision.enforcement}`);
1504
2249
  for (const evaluation2 of evals) console.log(` eval ${evaluation2.id}@${evaluation2.version} \xB7 ${evaluation2.title} \xB7 ${evaluation2.enforcement}`);
1505
- if (!decisions.length && !evals.length) console.log(" No active scoped decisions or evals apply.");
2250
+ if (!deep.facts.length && !deep.dependents.length && !deep.related_tests.length && !deep.ownership.length && !deep.signals.length && !deep.plans.length && !decisions.length && !evals.length) console.log(" No active Deep Brain evidence, scoped decisions, signals, plans, or evals apply.");
1506
2251
  console.log("");
1507
2252
  });
1508
2253
  program.command("recall <query>").description("Search inspectable local decisions and evals").option("--limit <number>", "maximum matches", "10").action((query, options) => {
@@ -1514,7 +2259,7 @@ ${pc.bold("WHY")} ${relative}
1514
2259
  const tokens = query.toLowerCase().split(/\W+/).filter(Boolean);
1515
2260
  const matches = entries.map((entry) => ({ ...entry, score: tokens.reduce((score, token) => score + (entry.body.toLowerCase().split(token).length - 1), 0) })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, Math.max(1, Number(options.limit) || 10));
1516
2261
  console.log(`
1517
- ${pc.bold("RECALL")} \u201C${query}\u201D
2262
+ ${pc2.bold("RECALL")} \u201C${query}\u201D
1518
2263
  `);
1519
2264
  for (const entry of matches) console.log(` ${entry.kind.padEnd(9)} ${entry.id} \xB7 ${entry.title}`);
1520
2265
  if (!matches.length) console.log(" No matching local memory.");
@@ -1531,13 +2276,13 @@ ${pc.bold("RECALL")} \u201C${query}\u201D
1531
2276
  try {
1532
2277
  const runs = store.listRuns(5);
1533
2278
  console.log(`
1534
- ${pc.bold("SCRIPTONIA PULSE")}
2279
+ ${pc2.bold("SCRIPTONIA PULSE")}
1535
2280
  `);
1536
- console.log(` repository ${snapshot.dirty ? pc.yellow("dirty") : pc.green("clean")} \xB7 ${snapshot.files.length} changed files`);
2281
+ console.log(` repository ${snapshot.dirty ? pc2.yellow("dirty") : pc2.green("clean")} \xB7 ${snapshot.files.length} changed files`);
1537
2282
  console.log(` decisions ${decisions.decisions.filter((entry) => entry.state === "active").length} active \xB7 ${decisions.errors.length} invalid`);
1538
2283
  console.log(` evals ${evals.evals.filter((entry) => entry.state === "active").length} active \xB7 ${health.issues.length} health issues`);
1539
2284
  console.log(` runs ${runs.length} recent captured`);
1540
- console.log(` semantic ${process.env.SCRIPTONIA_MODEL_GATEWAY_URL && process.env.SCRIPTONIA_API_TOKEN || process.env.LITELLM_BASE_URL && process.env.LITELLM_MASTER_KEY ? pc.green("configured") : pc.dim("disabled until credentials are added")}`);
2285
+ console.log(` semantic ${process.env.SCRIPTONIA_MODEL_GATEWAY_URL && process.env.SCRIPTONIA_API_TOKEN || process.env.LITELLM_BASE_URL && process.env.LITELLM_MASTER_KEY ? pc2.green("configured") : pc2.dim("disabled until credentials are added")}`);
1541
2286
  console.log("");
1542
2287
  } finally {
1543
2288
  store.close();
@@ -1561,7 +2306,7 @@ ${pc.bold("SCRIPTONIA PULSE")}
1561
2306
  const exact = separator >= 0 ? process.argv.slice(separator + 1) : argv;
1562
2307
  const result = await captureCommand({ root, command: exact, adapter: options.adapter, agent: options.agent });
1563
2308
  console.log(`
1564
- ${pc.green("\u2713")} captured ${result.runId} \xB7 child exit ${result.exitCode}`);
2309
+ ${pc2.green("\u2713")} captured ${result.runId} \xB7 child exit ${result.exitCode}`);
1565
2310
  process.exitCode = result.exitCode;
1566
2311
  });
1567
2312
  program.command("runs").description("List recent captured agent runs").option("--json", "print JSON").option("--limit <number>", "maximum rows", "20").action((options) => {
@@ -1572,7 +2317,7 @@ ${pc.green("\u2713")} captured ${result.runId} \xB7 child exit ${result.exitCode
1572
2317
  if (options.json) console.log(JSON.stringify(runs, null, 2));
1573
2318
  else {
1574
2319
  console.log(`
1575
- ${pc.bold("RECENT AGENT RUNS")}
2320
+ ${pc2.bold("RECENT AGENT RUNS")}
1576
2321
  `);
1577
2322
  if (!runs.length) console.log(" No captured runs yet. Use: scriptonia run -- <agent-command>");
1578
2323
  for (const run of runs) console.log(` ${String(run.status).padEnd(10)} ${run.id} ${JSON.stringify(run.command)} exit=${run.exit_code ?? "-"}`);
@@ -1594,7 +2339,7 @@ ${pc.bold("RECENT AGENT RUNS")}
1594
2339
  program.command("learn <finding-id>").description("Convert a confirmed finding into a reviewable draft eval").action((findingId) => {
1595
2340
  const root = findRepositoryRoot();
1596
2341
  const learned = learnFromFinding(root, findingId);
1597
- console.log(`${pc.green("\u2713")} draft eval ${learned.evalCase.id} written to ${path10.relative(root, learned.path)}`);
2342
+ console.log(`${pc2.green("\u2713")} draft eval ${learned.evalCase.id} written to ${path9.relative(root, learned.path)}`);
1598
2343
  console.log(" Review its scope and evidence, add an owner, then change state to active.");
1599
2344
  });
1600
2345
  const evaluation = program.command("eval").description("Run and maintain versioned regression cases");
@@ -1608,7 +2353,7 @@ ${pc.bold("RECENT AGENT RUNS")}
1608
2353
  const root = findRepositoryRoot();
1609
2354
  const report = evalDoctor(root);
1610
2355
  console.log(`
1611
- ${pc.bold("EVAL DOCTOR")} \xB7 ${report.healthy ? pc.green("HEALTHY") : pc.yellow("NEEDS ATTENTION")}
2356
+ ${pc2.bold("EVAL DOCTOR")} \xB7 ${report.healthy ? pc2.green("HEALTHY") : pc2.yellow("NEEDS ATTENTION")}
1612
2357
  `);
1613
2358
  for (const issue of report.issues) console.log(` ${issue.kind.padEnd(16)} ${issue.id} \xB7 ${issue.message}`);
1614
2359
  if (!report.issues.length) console.log(" No duplicate, stale, ownerless, invalid, or expired evals.");
@@ -1620,19 +2365,19 @@ ${pc.bold("EVAL DOCTOR")} \xB7 ${report.healthy ? pc.green("HEALTHY") : pc.yello
1620
2365
  if (!allowed.includes(options.reason)) throw new Error(`invalid reason: ${options.reason}`);
1621
2366
  const root = findRepositoryRoot();
1622
2367
  const id = acknowledgeFinding(root, findingId, options.reason, options.actor, options.comment);
1623
- console.log(`${pc.green("\u2713")} acknowledgement recorded: ${id}`);
2368
+ console.log(`${pc2.green("\u2713")} acknowledgement recorded: ${id}`);
1624
2369
  });
1625
2370
  const events = program.command("events").description("Ingest normalized adapter events");
1626
2371
  events.command("ingest <file>").option("--adapter <name>", "normalized or codex", "normalized").action((filename, options) => {
1627
2372
  const root = findRepositoryRoot();
1628
- const full = assertInside(root, path10.resolve(root, filename));
2373
+ const full = assertInside(root, path9.resolve(root, filename));
1629
2374
  if (options.adapter !== "normalized" && options.adapter !== "codex") throw new Error(`unsupported event adapter: ${options.adapter}`);
1630
2375
  const count = ingestJsonl(root, fs9.readFileSync(full, "utf8"), options.adapter);
1631
- console.log(`${pc.green("\u2713")} ingested ${count} normalized run events`);
2376
+ console.log(`${pc2.green("\u2713")} ingested ${count} normalized run events`);
1632
2377
  });
1633
2378
  program.command("report-summary <file>").action((filename) => {
1634
2379
  const root = findRepositoryRoot();
1635
- const full = assertInside(root, path10.resolve(root, filename));
2380
+ const full = assertInside(root, path9.resolve(root, filename));
1636
2381
  const result = JSON.parse(fs9.readFileSync(full, "utf8"));
1637
2382
  const markdown = githubStepSummary(result);
1638
2383
  if (process.env.GITHUB_STEP_SUMMARY) fs9.appendFileSync(process.env.GITHUB_STEP_SUMMARY, markdown);
@@ -1643,16 +2388,16 @@ ${pc.bold("EVAL DOCTOR")} \xB7 ${report.healthy ? pc.green("HEALTHY") : pc.yello
1643
2388
  function emitVerification(root, result, options) {
1644
2389
  if (options.json === true) console.log(JSON.stringify(result, null, 2));
1645
2390
  else {
1646
- const color = result.gate === "PASS" ? pc.green : result.gate === "PASS_WITH_WARNINGS" ? pc.yellow : pc.red;
2391
+ const color = result.gate === "PASS" ? pc2.green : result.gate === "PASS_WITH_WARNINGS" ? pc2.yellow : pc2.red;
1647
2392
  console.log(`
1648
- ${pc.bold("SCRIPTONIA VERIFY")} \xB7 ${color(result.gate)}${result.cached ? pc.dim(" \xB7 cached") : ""}`);
1649
- console.log(pc.dim(`snapshot ${result.snapshotId}`));
2393
+ ${pc2.bold("SCRIPTONIA VERIFY")} \xB7 ${color(result.gate)}${result.cached ? pc2.dim(" \xB7 cached") : ""}`);
2394
+ console.log(pc2.dim(`snapshot ${result.snapshotId}`));
1650
2395
  console.log(`coverage plan=${result.coverage.plan} decisions=${result.coverage.decisions.evaluated}/${result.coverage.decisions.total} evals=${result.coverage.evals.evaluated}/${result.coverage.evals.total} semantic=${result.coverage.semantic}
1651
2396
  `);
1652
2397
  for (const check of result.checks) {
1653
- const state = check.state === "PASS" ? pc.green(check.state) : check.state === "SKIPPED" ? pc.dim(check.state) : check.state === "INCONCLUSIVE" ? pc.yellow(check.state) : pc.red(check.state);
2398
+ const state = check.state === "PASS" ? pc2.green(check.state) : check.state === "SKIPPED" ? pc2.dim(check.state) : check.state === "INCONCLUSIVE" ? pc2.yellow(check.state) : pc2.red(check.state);
1654
2399
  console.log(` ${state.padEnd(18)} ${check.title}`);
1655
- if (check.state !== "PASS" && check.state !== "SKIPPED") console.log(` ${pc.dim(check.reason)} \xB7 ${check.findingId ?? check.id}`);
2400
+ if (check.state !== "PASS" && check.state !== "SKIPPED") console.log(` ${pc2.dim(check.reason)} \xB7 ${check.findingId ?? check.id}`);
1656
2401
  }
1657
2402
  console.log(`
1658
2403
  exit ${result.exitCode}
@@ -1664,8 +2409,8 @@ exit ${result.exitCode}
1664
2409
  `);
1665
2410
  }
1666
2411
  function writeOutput(root, filename, content) {
1667
- const full = assertInside(root, path10.resolve(root, filename));
1668
- fs9.mkdirSync(path10.dirname(full), { recursive: true });
2412
+ const full = assertInside(root, path9.resolve(root, filename));
2413
+ fs9.mkdirSync(path9.dirname(full), { recursive: true });
1669
2414
  fs9.writeFileSync(full, content);
1670
2415
  }
1671
2416
  function csv(value) {
@@ -1677,7 +2422,7 @@ function slug(value) {
1677
2422
  function configureGatewayFromLegacyLogin() {
1678
2423
  if (process.env.SCRIPTONIA_MODEL_GATEWAY_URL && process.env.SCRIPTONIA_API_TOKEN) return;
1679
2424
  try {
1680
- const config = JSON.parse(fs9.readFileSync(path10.join(os.homedir(), ".scriptonia", "config.json"), "utf8"));
2425
+ const config = JSON.parse(fs9.readFileSync(path9.join(os2.homedir(), ".scriptonia", "config.json"), "utf8"));
1681
2426
  if (config.url && !process.env.SCRIPTONIA_MODEL_GATEWAY_URL) process.env.SCRIPTONIA_MODEL_GATEWAY_URL = config.url;
1682
2427
  if (config.token && !process.env.SCRIPTONIA_API_TOKEN) process.env.SCRIPTONIA_API_TOKEN = config.token;
1683
2428
  } catch {