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.
- package/LICENSE +21 -0
- package/README.md +32 -11
- package/assets/grammar-manifest.json +32 -0
- package/assets/grammars/tree-sitter-bash.wasm +0 -0
- package/assets/grammars/tree-sitter-c.wasm +0 -0
- package/assets/grammars/tree-sitter-cpp.wasm +0 -0
- package/assets/grammars/tree-sitter-dart.wasm +0 -0
- package/assets/grammars/tree-sitter-go.wasm +0 -0
- package/assets/grammars/tree-sitter-java.wasm +0 -0
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-kotlin.wasm +0 -0
- package/assets/grammars/tree-sitter-objc.wasm +0 -0
- package/assets/grammars/tree-sitter-python.wasm +0 -0
- package/assets/grammars/tree-sitter-rust.wasm +0 -0
- package/assets/grammars/tree-sitter-swift.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/licenses/tree-sitter-wasms-LICENSE +24 -0
- package/assets/licenses/web-tree-sitter-LICENSE +21 -0
- package/assets/queries/manifest.json +12 -0
- package/assets/queries/v1/dart.scm +7 -0
- package/assets/queries/v1/go.scm +5 -0
- package/assets/queries/v1/javascript.scm +15 -0
- package/assets/queries/v1/python.scm +5 -0
- package/assets/queries/v1/typescript.scm +17 -0
- package/assets/tree-sitter.wasm +0 -0
- package/bin/scriptonia.mjs +243 -76
- package/bin/verify.mjs +40 -6
- package/dist/brain-ast-worker.js +62 -0
- package/dist/brain-ast-worker.js.map +1 -0
- package/dist/chunk-6TLQP3LV.js +4048 -0
- package/dist/chunk-6TLQP3LV.js.map +1 -0
- package/dist/chunk-ZLKAW77A.js +16613 -0
- package/dist/chunk-ZLKAW77A.js.map +1 -0
- package/dist/index.js +1407 -662
- package/dist/index.js.map +1 -1
- package/dist/legacy-brain-bridge.js +156 -0
- package/dist/legacy-brain-bridge.js.map +1 -0
- 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
|
|
6
|
-
import
|
|
38
|
+
import os2 from "os";
|
|
39
|
+
import path9 from "path";
|
|
7
40
|
import { Command } from "commander";
|
|
8
|
-
import
|
|
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
|
|
68
|
-
import
|
|
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
|
|
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",
|
|
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",
|
|
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
|
|
117
|
-
const currentSuite = String(
|
|
118
|
-
const tests = asArray(
|
|
119
|
-
for (const key of ["testsuite", "testsuites"]) for (const child of asArray(
|
|
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 =
|
|
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
|
|
274
|
-
import
|
|
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 =
|
|
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(
|
|
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 =
|
|
328
|
-
if (!
|
|
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(
|
|
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
|
|
251
|
+
const configuredGatewayUrl = process.env.SCRIPTONIA_MODEL_GATEWAY_URL;
|
|
353
252
|
const gatewayToken = process.env.SCRIPTONIA_API_TOKEN;
|
|
354
|
-
if (
|
|
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)(`${
|
|
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 = `${
|
|
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
|
|
441
|
-
import
|
|
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
|
|
555
|
-
import
|
|
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 =
|
|
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(
|
|
565
|
-
const pkgFile =
|
|
566
|
-
if (
|
|
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(
|
|
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 (
|
|
339
|
+
if (fs3.existsSync(path3.join(root, "package.json"))) {
|
|
577
340
|
try {
|
|
578
|
-
result.set(".", String(JSON.parse(
|
|
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
|
|
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) =>
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
672
|
-
if (!
|
|
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
|
|
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(
|
|
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 =
|
|
994
|
-
if (!
|
|
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(
|
|
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 =
|
|
1011
|
-
const filename =
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (!
|
|
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 =
|
|
1019
|
-
if (!
|
|
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
|
|
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(
|
|
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 =
|
|
1050
|
-
const plan =
|
|
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
|
|
1063
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
|
1228
|
-
|
|
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 =
|
|
1257
|
-
if (
|
|
1258
|
-
|
|
1259
|
-
|
|
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.
|
|
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
|
|
1313
|
-
import
|
|
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(
|
|
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.
|
|
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(`${
|
|
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
|
|
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 =
|
|
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
|
-
${
|
|
2181
|
+
${pc2.bold("SCRIPTONIA DIFF CHECK")} \xB7 ${snapshot.files.length} changed files
|
|
1473
2182
|
`);
|
|
1474
|
-
for (const error of definitions.errors) console.log(` ${
|
|
1475
|
-
for (const result of results) console.log(` ${result.state === "PASS" ?
|
|
1476
|
-
if (unresolved.length) console.log(` ${
|
|
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() -
|
|
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 =
|
|
2198
|
+
const filename = path9.join(root, ".scriptonia", "decisions", `${id}.yaml`);
|
|
1490
2199
|
fs9.writeFileSync(filename, YAML3.stringify(decision));
|
|
1491
|
-
console.log(`${
|
|
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 <
|
|
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
|
|
1497
|
-
const
|
|
1498
|
-
const
|
|
1499
|
-
const
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
2279
|
+
${pc2.bold("SCRIPTONIA PULSE")}
|
|
1535
2280
|
`);
|
|
1536
|
-
console.log(` repository ${snapshot.dirty ?
|
|
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 ?
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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(`${
|
|
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
|
-
${
|
|
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(`${
|
|
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,
|
|
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(`${
|
|
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,
|
|
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" ?
|
|
2391
|
+
const color = result.gate === "PASS" ? pc2.green : result.gate === "PASS_WITH_WARNINGS" ? pc2.yellow : pc2.red;
|
|
1647
2392
|
console.log(`
|
|
1648
|
-
${
|
|
1649
|
-
console.log(
|
|
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" ?
|
|
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(` ${
|
|
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,
|
|
1668
|
-
fs9.mkdirSync(
|
|
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(
|
|
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 {
|