scriptonia 0.8.0 → 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 +43 -13
- 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 +2431 -0
- package/dist/index.js.map +1 -0
- package/dist/legacy-brain-bridge.js +156 -0
- package/dist/legacy-brain-bridge.js.map +1 -0
- package/package.json +78 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,2431 @@
|
|
|
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";
|
|
35
|
+
|
|
36
|
+
// src/index.ts
|
|
37
|
+
import fs9 from "fs";
|
|
38
|
+
import os2 from "os";
|
|
39
|
+
import path9 from "path";
|
|
40
|
+
import { Command } from "commander";
|
|
41
|
+
import pc2 from "picocolors";
|
|
42
|
+
import YAML3 from "yaml";
|
|
43
|
+
|
|
44
|
+
// ../packages/eval-engine/src/index.ts
|
|
45
|
+
import fs5 from "fs";
|
|
46
|
+
import path5 from "path";
|
|
47
|
+
import YAML2 from "yaml";
|
|
48
|
+
|
|
49
|
+
// ../packages/evidence/src/index.ts
|
|
50
|
+
import fs from "fs";
|
|
51
|
+
import path from "path";
|
|
52
|
+
import fg from "fast-glob";
|
|
53
|
+
import { XMLParser } from "fast-xml-parser";
|
|
54
|
+
function item(kind, collector, source, state, payload) {
|
|
55
|
+
return { id: createId("evidence"), kind, collector, collectorVersion: "1", source, state, contentHash: hashObject({ kind, source, state, payload }), payload, collectedAt: nowIso(), redaction: { applied: false, policyVersion: "1" } };
|
|
56
|
+
}
|
|
57
|
+
function collectGitHubNeeds(raw = process.env.SCRIPTONIA_NEEDS_JSON) {
|
|
58
|
+
if (!raw) return [];
|
|
59
|
+
try {
|
|
60
|
+
const needs = JSON.parse(raw);
|
|
61
|
+
return Object.entries(needs).map(([name, value]) => {
|
|
62
|
+
const result = String(value?.result ?? "unknown").toLowerCase();
|
|
63
|
+
const state = result === "success" ? "available" : result === "failure" || result === "cancelled" ? "failed" : "missing";
|
|
64
|
+
return item("github_check", "github-needs", `github:${name}`, state, { name, conclusion: result, outputs: value?.outputs ?? {} });
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return [item("github_check", "github-needs", "env:SCRIPTONIA_NEEDS_JSON", "error", { error: error instanceof Error ? error.message : String(error) })];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function collectJUnit(root, patterns) {
|
|
71
|
+
const files = fg.sync(patterns, { cwd: root, absolute: true, onlyFiles: true, unique: true, suppressErrors: true });
|
|
72
|
+
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", allowBooleanAttributes: true });
|
|
73
|
+
const items = [];
|
|
74
|
+
for (const filename of files) {
|
|
75
|
+
try {
|
|
76
|
+
const xml = fs.readFileSync(filename, "utf8");
|
|
77
|
+
if (Buffer.byteLength(xml) > 1e7) throw new Error("JUnit artifact exceeds 10 MB limit");
|
|
78
|
+
const parsed = parser.parse(xml);
|
|
79
|
+
for (const test of flattenTests(parsed)) {
|
|
80
|
+
const failed = Boolean(test.failure || test.error);
|
|
81
|
+
const skipped = Boolean(test.skipped);
|
|
82
|
+
const suite = String(test.classname ?? test.suite ?? "");
|
|
83
|
+
const name = String(test.name ?? "unnamed");
|
|
84
|
+
items.push(item("junit_test", "junit-xml", path.relative(root, filename), skipped ? "missing" : failed ? "failed" : "available", { suite, test: name, failed, skipped }));
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
items.push(item("junit_test", "junit-xml", path.relative(root, filename), "error", { error: error instanceof Error ? error.message : String(error) }));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return items;
|
|
91
|
+
}
|
|
92
|
+
function flattenTests(value, suite = "") {
|
|
93
|
+
if (!value || typeof value !== "object") return [];
|
|
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));
|
|
98
|
+
return tests;
|
|
99
|
+
}
|
|
100
|
+
function asArray(value) {
|
|
101
|
+
return value === void 0 ? [] : Array.isArray(value) ? value : [value];
|
|
102
|
+
}
|
|
103
|
+
function collectRepositoryFacts(root, changedFiles) {
|
|
104
|
+
return changedFiles.map((file) => {
|
|
105
|
+
const full = path.join(root, file);
|
|
106
|
+
let exists = false;
|
|
107
|
+
let size = 0;
|
|
108
|
+
try {
|
|
109
|
+
const stat = fs.statSync(full);
|
|
110
|
+
exists = stat.isFile();
|
|
111
|
+
size = stat.size;
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
return item("repository_fact", "repository-fact", `path:${file}`, "available", { path: file, exists, size });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function evaluateRequirement(requirement, evidence) {
|
|
118
|
+
const relevant = evidence.filter((candidate) => candidate.kind === requirement.kind && requirementMatches(requirement, candidate));
|
|
119
|
+
if (!relevant.length) return { state: "INCONCLUSIVE", reason: `Required ${requirement.kind} evidence is unavailable.`, evidence: [] };
|
|
120
|
+
if (relevant.some((candidate) => candidate.state === "error")) return { state: "ERROR", reason: `${requirement.kind} collector failed.`, evidence: relevant };
|
|
121
|
+
if (relevant.some((candidate) => candidate.state === "failed")) return { state: "FAIL", reason: `Required ${requirement.kind} evidence failed.`, evidence: relevant };
|
|
122
|
+
if (relevant.some((candidate) => candidate.state === "available")) return { state: "PASS", reason: `Required ${requirement.kind} evidence passed.`, evidence: relevant };
|
|
123
|
+
return { state: "INCONCLUSIVE", reason: `Required ${requirement.kind} evidence is incomplete.`, evidence: relevant };
|
|
124
|
+
}
|
|
125
|
+
function requirementMatches(requirement, candidate) {
|
|
126
|
+
const payload = candidate.payload;
|
|
127
|
+
if (requirement.name && String(payload.name ?? payload.test ?? "") !== requirement.name) return false;
|
|
128
|
+
if (requirement.suite && String(payload.suite ?? "") !== requirement.suite) return false;
|
|
129
|
+
if (requirement.test && String(payload.test ?? "") !== requirement.test) return false;
|
|
130
|
+
if (requirement.path && String(payload.path ?? "") !== requirement.path) return false;
|
|
131
|
+
if (requirement.value && !Object.values(payload).map(String).includes(requirement.value)) return false;
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ../packages/impact/src/index.ts
|
|
136
|
+
import fs2 from "fs";
|
|
137
|
+
import path2 from "path";
|
|
138
|
+
import fg2 from "fast-glob";
|
|
139
|
+
var SOURCE = ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}", "!**/node_modules/**", "!**/dist/**", "!**/.next/**", "!**/coverage/**"];
|
|
140
|
+
var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
|
|
141
|
+
function buildFallbackImpactIndex(root) {
|
|
142
|
+
const files = fg2.sync(SOURCE, { cwd: root, onlyFiles: true, unique: true }).map(normalizeRepoPath).sort();
|
|
143
|
+
const known = new Set(files);
|
|
144
|
+
const edges = [];
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
let text;
|
|
147
|
+
try {
|
|
148
|
+
text = fs2.readFileSync(path2.join(root, file), "utf8");
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const imports = [
|
|
153
|
+
...text.matchAll(/(?:import|export)\s+(?:[^'";]+?\s+from\s+)?["']([^"']+)["']/g),
|
|
154
|
+
...text.matchAll(/(?:require|import)\(\s*["']([^"']+)["']\s*\)/g)
|
|
155
|
+
];
|
|
156
|
+
for (const match of imports) {
|
|
157
|
+
const specifier = match[1];
|
|
158
|
+
if (!specifier?.startsWith(".")) continue;
|
|
159
|
+
const base = normalizeRepoPath(path2.posix.join(path2.posix.dirname(file), specifier));
|
|
160
|
+
const target = EXTENSIONS.map((extension) => `${base}${extension}`).find((candidate) => known.has(candidate));
|
|
161
|
+
if (target) edges.push({ from: file, to: target, kind: "import", confidence: "high", provenance: `static-import:${file}` });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const stable = edges.sort((a, b) => `${a.from}:${a.to}`.localeCompare(`${b.from}:${b.to}`));
|
|
165
|
+
return { provider: "fallback-import-graph", version: "1", id: hashObject(stable), edges: stable };
|
|
166
|
+
}
|
|
167
|
+
function impactedFiles(index, changedFiles, depth = 2) {
|
|
168
|
+
const changed = new Set(changedFiles.map(normalizeRepoPath));
|
|
169
|
+
const files = new Set(changed);
|
|
170
|
+
const used = [];
|
|
171
|
+
let frontier = new Set(changed);
|
|
172
|
+
for (let step = 0; step < depth; step++) {
|
|
173
|
+
const next = /* @__PURE__ */ new Set();
|
|
174
|
+
for (const edge of index.edges) {
|
|
175
|
+
if (edge.confidence !== "high" || !frontier.has(edge.to) || files.has(edge.from)) continue;
|
|
176
|
+
files.add(edge.from);
|
|
177
|
+
next.add(edge.from);
|
|
178
|
+
used.push(edge);
|
|
179
|
+
}
|
|
180
|
+
frontier = next;
|
|
181
|
+
if (!frontier.size) break;
|
|
182
|
+
}
|
|
183
|
+
return { files: [...files].sort(), edges: used };
|
|
184
|
+
}
|
|
185
|
+
function relatedTests(index, changedFiles) {
|
|
186
|
+
const impacted = impactedFiles(index, changedFiles, 3).files;
|
|
187
|
+
return impacted.filter((file) => /(?:^|\/)(?:__tests__|tests?|spec)(?:\/|$)|\.(?:test|spec)\.[^.]+$/i.test(file));
|
|
188
|
+
}
|
|
189
|
+
function loadCodeGraphIndex(root) {
|
|
190
|
+
const filename = path2.join(root, ".scriptonia", "codegraph.json");
|
|
191
|
+
if (!fs2.existsSync(filename)) return void 0;
|
|
192
|
+
try {
|
|
193
|
+
const value = JSON.parse(fs2.readFileSync(filename, "utf8"));
|
|
194
|
+
const edges = (value.edges ?? []).filter((edge) => edge && typeof edge.from === "string" && typeof edge.to === "string" && ["high", "medium", "low"].includes(edge.confidence));
|
|
195
|
+
return { provider: "codegraph-file-adapter", version: "1", id: hashObject({ sourceVersion: value.version, edges }), edges };
|
|
196
|
+
} catch {
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
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
|
+
}
|
|
223
|
+
|
|
224
|
+
// ../packages/model-gateway/src/index.ts
|
|
225
|
+
import { z } from "zod";
|
|
226
|
+
var judgeResultSchema = z.object({
|
|
227
|
+
result: z.enum(["supports", "contradicts", "uncertain"]),
|
|
228
|
+
confidence: z.number().min(0).max(1),
|
|
229
|
+
evidenceRefs: z.array(z.string()).default([]),
|
|
230
|
+
reason: z.string()
|
|
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
|
+
}
|
|
245
|
+
async function semanticJudge(input) {
|
|
246
|
+
const model = input.model ?? process.env.OPENAI_OUTPUT_MODEL ?? "gpt-5.6-terra";
|
|
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.";
|
|
248
|
+
const serialized = JSON.stringify(input.evidenceBundle);
|
|
249
|
+
const maxBytes = input.maxBytes ?? 12e4;
|
|
250
|
+
if (Buffer.byteLength(serialized) > maxBytes) return { available: false, error: `evidence bundle exceeds ${maxBytes} byte limit` };
|
|
251
|
+
const configuredGatewayUrl = process.env.SCRIPTONIA_MODEL_GATEWAY_URL;
|
|
252
|
+
const gatewayToken = process.env.SCRIPTONIA_API_TOKEN;
|
|
253
|
+
if (configuredGatewayUrl && gatewayToken) {
|
|
254
|
+
const gateway = secureBaseUrl(configuredGatewayUrl, "Scriptonia model gateway URL");
|
|
255
|
+
if (!gateway.url) return { available: false, error: gateway.error };
|
|
256
|
+
const controller2 = new AbortController();
|
|
257
|
+
const timeout2 = setTimeout(() => controller2.abort(), input.timeoutMs ?? 2e4);
|
|
258
|
+
try {
|
|
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 }) });
|
|
260
|
+
const body = await response.json();
|
|
261
|
+
if (!response.ok) return { available: false, error: `Scriptonia model gateway returned ${response.status}` };
|
|
262
|
+
const parsed = judgeResultSchema.safeParse(body);
|
|
263
|
+
if (!parsed.success) return { available: false, error: "Scriptonia model gateway returned invalid structured output" };
|
|
264
|
+
return { available: true, result: { ...parsed.data, schemaVersion: "1", evaluatorVersion: "1", model: String(body.model ?? model), ...typeof body.providerRevision === "string" ? { providerRevision: body.providerRevision } : {}, rubricHash: String(body.rubricHash ?? hashObject(rubric)) } };
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return { available: false, error: error instanceof Error ? error.message : String(error) };
|
|
267
|
+
} finally {
|
|
268
|
+
clearTimeout(timeout2);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const endpoint = input.endpoint ?? process.env.LITELLM_BASE_URL;
|
|
272
|
+
const apiKey = input.apiKey ?? process.env.LITELLM_MASTER_KEY;
|
|
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 };
|
|
276
|
+
const directModel = input.model ?? process.env.LITELLM_MODEL ?? "semantic-judge";
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timeout = setTimeout(() => controller.abort(), input.timeoutMs ?? 2e4);
|
|
279
|
+
const url = `${liteLLM.url}/v1/chat/completions`;
|
|
280
|
+
try {
|
|
281
|
+
const response = await (input.fetchImpl ?? fetch)(url, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
|
|
284
|
+
signal: controller.signal,
|
|
285
|
+
body: JSON.stringify({
|
|
286
|
+
model: directModel,
|
|
287
|
+
temperature: 0,
|
|
288
|
+
response_format: { type: "json_object" },
|
|
289
|
+
messages: [
|
|
290
|
+
{ role: "system", content: `${rubric}
|
|
291
|
+
Return JSON with result, confidence, evidenceRefs, reason.` },
|
|
292
|
+
{ role: "user", content: JSON.stringify({ criterion: input.criterion, evidence: input.evidenceBundle }) }
|
|
293
|
+
]
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
const body = await response.json();
|
|
297
|
+
if (!response.ok) return { available: false, error: `LiteLLM returned ${response.status}` };
|
|
298
|
+
const content = body.choices?.[0]?.message?.content;
|
|
299
|
+
if (typeof content !== "string") return { available: false, error: "LiteLLM response contains no message content" };
|
|
300
|
+
const parsed = judgeResultSchema.safeParse(JSON.parse(content));
|
|
301
|
+
if (!parsed.success) return { available: false, error: "semantic judge returned invalid structured output" };
|
|
302
|
+
return { available: true, result: { ...parsed.data, schemaVersion: "1", evaluatorVersion: "1", model: directModel, rubricHash: hashObject(rubric) } };
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return { available: false, error: error instanceof Error ? error.message : String(error) };
|
|
305
|
+
} finally {
|
|
306
|
+
clearTimeout(timeout);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ../packages/policy-engine/src/index.ts
|
|
311
|
+
import fs4 from "fs";
|
|
312
|
+
import path4 from "path";
|
|
313
|
+
import picomatch2 from "picomatch";
|
|
314
|
+
import YAML from "yaml";
|
|
315
|
+
|
|
316
|
+
// ../packages/scope-engine/src/index.ts
|
|
317
|
+
import fs3 from "fs";
|
|
318
|
+
import path3 from "path";
|
|
319
|
+
import picomatch from "picomatch";
|
|
320
|
+
function discoverPackages(root) {
|
|
321
|
+
const result = /* @__PURE__ */ new Map();
|
|
322
|
+
const visit = (relative, depth) => {
|
|
323
|
+
if (depth > 4) return;
|
|
324
|
+
const absolute = path3.join(root, relative);
|
|
325
|
+
for (const entry of safeReadDir(absolute)) {
|
|
326
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
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)) {
|
|
330
|
+
try {
|
|
331
|
+
result.set(next, String(JSON.parse(fs3.readFileSync(pkgFile, "utf8")).name || next));
|
|
332
|
+
} catch {
|
|
333
|
+
result.set(next, next);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
visit(next, depth + 1);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
if (fs3.existsSync(path3.join(root, "package.json"))) {
|
|
340
|
+
try {
|
|
341
|
+
result.set(".", String(JSON.parse(fs3.readFileSync(path3.join(root, "package.json"), "utf8")).name || "."));
|
|
342
|
+
} catch {
|
|
343
|
+
result.set(".", ".");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
visit(".", 0);
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
function safeReadDir(dir) {
|
|
350
|
+
try {
|
|
351
|
+
return fs3.readdirSync(dir, { withFileTypes: true });
|
|
352
|
+
} catch {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function parseCodeowners(root) {
|
|
357
|
+
const candidates = ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS"];
|
|
358
|
+
const file = candidates.map((name) => path3.join(root, name)).find(fs3.existsSync);
|
|
359
|
+
const owners = /* @__PURE__ */ new Map();
|
|
360
|
+
if (!file) return owners;
|
|
361
|
+
for (const raw of fs3.readFileSync(file, "utf8").split("\n")) {
|
|
362
|
+
const line = raw.trim();
|
|
363
|
+
if (!line || line.startsWith("#")) continue;
|
|
364
|
+
const [pattern, ...names] = line.split(/\s+/);
|
|
365
|
+
if (pattern && names.length) owners.set(pattern.replace(/^\//, ""), names);
|
|
366
|
+
}
|
|
367
|
+
return owners;
|
|
368
|
+
}
|
|
369
|
+
function ownerFor(file, owners) {
|
|
370
|
+
let found;
|
|
371
|
+
for (const [pattern, names] of owners) {
|
|
372
|
+
try {
|
|
373
|
+
if (picomatch.isMatch(file, pattern, { dot: true, basename: !pattern.includes("/") })) found = names;
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return found?.join(",");
|
|
378
|
+
}
|
|
379
|
+
function buildScopeContext(root, files, extras = {}) {
|
|
380
|
+
return {
|
|
381
|
+
root,
|
|
382
|
+
files: files.map(normalizeRepoPath),
|
|
383
|
+
symbols: extras.symbols ?? extractSymbols(root, files),
|
|
384
|
+
dependencies: extras.dependencies ?? discoverDependencies(root),
|
|
385
|
+
checks: extras.checks ?? [],
|
|
386
|
+
permissions: extras.permissions ?? [],
|
|
387
|
+
owners: parseCodeowners(root),
|
|
388
|
+
packages: discoverPackages(root)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function matchScope(scope, context) {
|
|
392
|
+
const include = scope.paths.include.length ? scope.paths.include : ["**"];
|
|
393
|
+
const included = picomatch(include, { dot: true });
|
|
394
|
+
const excluded = scope.paths.exclude.length ? picomatch(scope.paths.exclude, { dot: true }) : () => false;
|
|
395
|
+
const pathFiles = context.files.filter((file) => included(file) && !excluded(file));
|
|
396
|
+
const packageRoots = [...context.packages.entries()].filter(([, name]) => scope.packages.includes(name)).map(([root]) => root);
|
|
397
|
+
const packageFiles = scope.packages.length ? context.files.filter((file) => packageRoots.some((root) => root === "." || file === root || file.startsWith(`${root}/`))) : [];
|
|
398
|
+
const symbols = scope.symbols.filter((symbol) => context.symbols.includes(symbol));
|
|
399
|
+
const deps = scope.dependencies.filter((dependency) => context.dependencies.includes(dependency));
|
|
400
|
+
const hasConstraint = scope.packages.length + scope.services.length + scope.symbols.length + scope.dependencies.length > 0 || scope.paths.include.some((item2) => item2 !== "**");
|
|
401
|
+
const matched = hasConstraint ? Boolean(pathFiles.length || packageFiles.length || symbols.length || deps.length) : Boolean(context.files.length);
|
|
402
|
+
const specificity = symbols.length ? 5 : pathFiles.some((file) => scope.paths.include.includes(file)) ? 4 : pathFiles.length ? 3 : packageFiles.length ? 2 : matched ? 1 : 0;
|
|
403
|
+
const files = [.../* @__PURE__ */ new Set([...pathFiles, ...packageFiles])];
|
|
404
|
+
const reasons = [files.length && `paths:${files.length}`, symbols.length && `symbols:${symbols.join(",")}`, deps.length && `dependencies:${deps.join(",")}`].filter(Boolean);
|
|
405
|
+
return { matched, specificity, files, reasons, owner: files.map((file) => ownerFor(file, context.owners)).find(Boolean) };
|
|
406
|
+
}
|
|
407
|
+
function discoverDependencies(root) {
|
|
408
|
+
const names = /* @__PURE__ */ new Set();
|
|
409
|
+
for (const [directory] of discoverPackages(root)) {
|
|
410
|
+
try {
|
|
411
|
+
const pkg = JSON.parse(fs3.readFileSync(path3.join(root, directory, "package.json"), "utf8"));
|
|
412
|
+
for (const section of [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies]) for (const name of Object.keys(section || {})) names.add(name);
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return [...names].sort();
|
|
417
|
+
}
|
|
418
|
+
function extractSymbols(root, files) {
|
|
419
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
420
|
+
const pattern = /\b(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const|let|var)\s+([A-Za-z_$][\w$]*)/g;
|
|
421
|
+
for (const file of files) {
|
|
422
|
+
if (!/\.[cm]?[jt]sx?$/.test(file)) continue;
|
|
423
|
+
try {
|
|
424
|
+
const text = fs3.readFileSync(path3.join(root, file), "utf8");
|
|
425
|
+
for (const match of text.matchAll(pattern)) if (match[1]) symbols.add(match[1]);
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return [...symbols].sort();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ../packages/policy-engine/src/index.ts
|
|
433
|
+
function loadDecisions(root) {
|
|
434
|
+
const directory = path4.join(root, ".scriptonia", "decisions");
|
|
435
|
+
if (!fs4.existsSync(directory)) return { decisions: [], errors: [] };
|
|
436
|
+
const decisions = [];
|
|
437
|
+
const errors = [];
|
|
438
|
+
for (const name of fs4.readdirSync(directory).filter((file) => /\.ya?ml$/i.test(file)).sort()) {
|
|
439
|
+
try {
|
|
440
|
+
decisions.push(decisionSchema.parse(YAML.parse(fs4.readFileSync(path4.join(directory, name), "utf8"))));
|
|
441
|
+
} catch (error) {
|
|
442
|
+
errors.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { decisions, errors };
|
|
446
|
+
}
|
|
447
|
+
function parseDecisionDocuments(documents) {
|
|
448
|
+
const decisions = [];
|
|
449
|
+
const errors = [];
|
|
450
|
+
for (const document of documents) {
|
|
451
|
+
try {
|
|
452
|
+
decisions.push(decisionSchema.parse(YAML.parse(document.text)));
|
|
453
|
+
} catch (error) {
|
|
454
|
+
errors.push(`${document.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return { decisions, errors };
|
|
458
|
+
}
|
|
459
|
+
function evaluateDecisions(decisions, context) {
|
|
460
|
+
return decisions.map((decision) => evaluateDecision(decision, context));
|
|
461
|
+
}
|
|
462
|
+
function evaluateDecision(decision, context) {
|
|
463
|
+
const base = { id: `decision:${decision.id}@${decision.version}`, evaluator: `predicate:${decision.predicate.kind}`, evaluatorVersion: "1", enforcement: decision.enforcement, title: decision.title, decisionRef: `${decision.id}@${decision.version}` };
|
|
464
|
+
const finish = (state, reason, evidence, owner) => {
|
|
465
|
+
const identity = { decision: base.decisionRef, state, evidence, evaluator: base.evaluator, version: base.evaluatorVersion };
|
|
466
|
+
return { ...base, state, reason, evidence, ...owner ? { owner } : {}, fingerprint: hashObject(identity) };
|
|
467
|
+
};
|
|
468
|
+
if (decision.state !== "active") return finish("SKIPPED", `Decision is ${decision.state}; only active decisions enforce.`, []);
|
|
469
|
+
if (!decision.confirmed_by) return finish("INCONCLUSIVE", "Active decision has no human confirmation.", []);
|
|
470
|
+
const scoped = matchScope(decision.scope, context);
|
|
471
|
+
if (!scoped.matched) return finish("SKIPPED", "Decision scope does not intersect this change.", []);
|
|
472
|
+
const p = decision.predicate;
|
|
473
|
+
const names = p.names.length ? p.names : p.patterns;
|
|
474
|
+
const matches = (patterns) => context.files.filter((file) => picomatch2.isMatch(file, patterns, { dot: true }));
|
|
475
|
+
switch (p.kind) {
|
|
476
|
+
case "path_forbidden": {
|
|
477
|
+
const found = matches(names);
|
|
478
|
+
return finish(found.length ? "FAIL" : "PASS", found.length ? `Forbidden paths changed: ${found.join(", ")}` : "No forbidden path changed.", found.map((value) => ({ kind: "path", value })), decision.owner ?? scoped.owner);
|
|
479
|
+
}
|
|
480
|
+
case "path_requires_owner": {
|
|
481
|
+
const target = matches(names.length ? names : decision.scope.paths.include);
|
|
482
|
+
const missing = target.filter((file) => !ownerFor(file, context.owners) && !decision.owner && !p.owner);
|
|
483
|
+
return finish(missing.length ? "FAIL" : "PASS", missing.length ? `Changed paths have no owner: ${missing.join(", ")}` : "All applicable paths have an owner.", target.map((value) => ({ kind: "path", value })), decision.owner ?? p.owner ?? scoped.owner);
|
|
484
|
+
}
|
|
485
|
+
case "dependency_forbidden": {
|
|
486
|
+
const found = names.filter((name) => context.dependencies.includes(name));
|
|
487
|
+
return finish(found.length ? "FAIL" : "PASS", found.length ? `Forbidden dependencies present: ${found.join(", ")}` : "No forbidden dependency is present.", found.map((value) => ({ kind: "dependency", value })), decision.owner ?? scoped.owner);
|
|
488
|
+
}
|
|
489
|
+
case "dependency_required": {
|
|
490
|
+
const missing = names.filter((name) => !context.dependencies.includes(name));
|
|
491
|
+
return finish(missing.length ? "FAIL" : "PASS", missing.length ? `Required dependencies missing: ${missing.join(", ")}` : "Required dependencies are present.", names.map((value) => ({ kind: "dependency", value })), decision.owner ?? scoped.owner);
|
|
492
|
+
}
|
|
493
|
+
case "symbol_forbidden": {
|
|
494
|
+
const found = names.filter((name) => context.symbols.includes(name));
|
|
495
|
+
return finish(found.length ? "FAIL" : "PASS", found.length ? `Forbidden symbols changed: ${found.join(", ")}` : "No forbidden symbol changed.", found.map((value) => ({ kind: "symbol", value })), decision.owner ?? scoped.owner);
|
|
496
|
+
}
|
|
497
|
+
case "symbol_requires_test": {
|
|
498
|
+
const targeted = names.some((name) => context.symbols.includes(name));
|
|
499
|
+
const tests = context.files.filter((file) => /(?:^|\/)(?:__tests__|tests?|spec)(?:\/|$)|\.(?:test|spec)\.[^.]+$/i.test(file));
|
|
500
|
+
return finish(targeted && !tests.length ? "FAIL" : "PASS", targeted && !tests.length ? "Changed symbol requires a changed test." : "Required test-file evidence is present or the symbol is unchanged.", tests.map((value) => ({ kind: "path", value })), decision.owner ?? scoped.owner);
|
|
501
|
+
}
|
|
502
|
+
case "file_pattern_required": {
|
|
503
|
+
const found = matches(names);
|
|
504
|
+
return finish(found.length ? "PASS" : "FAIL", found.length ? "Required file pattern is present in the change." : `No changed file matches: ${names.join(", ")}`, found.map((value) => ({ kind: "path", value })), decision.owner ?? scoped.owner);
|
|
505
|
+
}
|
|
506
|
+
case "permission_required": {
|
|
507
|
+
const required = p.permission ?? names[0];
|
|
508
|
+
if (!required) return finish("ERROR", "permission_required has no permission value.", []);
|
|
509
|
+
return finish(context.permissions.includes(required) ? "PASS" : "INCONCLUSIVE", context.permissions.includes(required) ? `Permission confirmed: ${required}` : `Permission not confirmed: ${required}`, [{ kind: "permission", value: required }], decision.owner ?? scoped.owner);
|
|
510
|
+
}
|
|
511
|
+
case "check_required": {
|
|
512
|
+
const required = p.check ?? names[0];
|
|
513
|
+
if (!required) return finish("ERROR", "check_required has no check name.", []);
|
|
514
|
+
return finish(context.checks.includes(required) ? "PASS" : "INCONCLUSIVE", context.checks.includes(required) ? `Required check passed: ${required}` : `Required check evidence unavailable: ${required}`, [{ kind: "check", value: required }], decision.owner ?? scoped.owner);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ../packages/eval-engine/src/index.ts
|
|
520
|
+
var DEFAULT_CONFIG = configSchema.parse({});
|
|
521
|
+
function loadConfig(root) {
|
|
522
|
+
const filename = path5.join(root, ".scriptonia", "config.yaml");
|
|
523
|
+
if (!fs5.existsSync(filename)) return { config: DEFAULT_CONFIG, errors: [] };
|
|
524
|
+
try {
|
|
525
|
+
return { config: configSchema.parse(YAML2.parse(fs5.readFileSync(filename, "utf8"))), errors: [] };
|
|
526
|
+
} catch (error) {
|
|
527
|
+
return { config: DEFAULT_CONFIG, errors: [`config.yaml: ${error instanceof Error ? error.message : String(error)}`] };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function parseConfigDocument(text, name = "config.yaml") {
|
|
531
|
+
if (text === void 0) return { config: DEFAULT_CONFIG, errors: [] };
|
|
532
|
+
try {
|
|
533
|
+
return { config: configSchema.parse(YAML2.parse(text)), errors: [] };
|
|
534
|
+
} catch (error) {
|
|
535
|
+
return { config: DEFAULT_CONFIG, errors: [`${name}: ${error instanceof Error ? error.message : String(error)}`] };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function ensureConfig(root) {
|
|
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));
|
|
544
|
+
return filename;
|
|
545
|
+
}
|
|
546
|
+
function loadEvals(root) {
|
|
547
|
+
const directory = path5.join(root, ".scriptonia", "evals");
|
|
548
|
+
if (!fs5.existsSync(directory)) return { evals: [], errors: [] };
|
|
549
|
+
const evals = [];
|
|
550
|
+
const errors = [];
|
|
551
|
+
for (const filename of fs5.readdirSync(directory).filter((file) => /\.ya?ml$/i.test(file)).sort()) {
|
|
552
|
+
try {
|
|
553
|
+
evals.push(evalCaseSchema.parse(YAML2.parse(fs5.readFileSync(path5.join(directory, filename), "utf8"))));
|
|
554
|
+
} catch (error) {
|
|
555
|
+
errors.push(`${filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { evals, errors };
|
|
559
|
+
}
|
|
560
|
+
function parseEvalDocuments(documents) {
|
|
561
|
+
const evals = [];
|
|
562
|
+
const errors = [];
|
|
563
|
+
for (const document of documents) {
|
|
564
|
+
try {
|
|
565
|
+
evals.push(evalCaseSchema.parse(YAML2.parse(document.text)));
|
|
566
|
+
} catch (error) {
|
|
567
|
+
errors.push(`${document.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { evals, errors };
|
|
571
|
+
}
|
|
572
|
+
async function verifyRepository(options) {
|
|
573
|
+
if (!options.trustedBase) ensureConfig(options.root);
|
|
574
|
+
const trustedRef = options.trustedBase ? options.base : void 0;
|
|
575
|
+
const loadedConfig = trustedRef ? parseConfigDocument(readFileAtRef(options.root, trustedRef, ".scriptonia/config.yaml"), `${trustedRef}:.scriptonia/config.yaml`) : loadConfig(options.root);
|
|
576
|
+
const config = loadedConfig.config;
|
|
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 }));
|
|
578
|
+
const planPath = path5.resolve(options.root, options.plan ?? "PLAN.md");
|
|
579
|
+
const plan = fs5.existsSync(planPath) ? fs5.readFileSync(planPath, "utf8") : void 0;
|
|
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);
|
|
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);
|
|
582
|
+
const evals = options.evalFilter ? loadedEvals.evals.filter((entry) => entry.id === options.evalFilter || entry.title.toLowerCase().includes(options.evalFilter.toLowerCase())) : loadedEvals.evals;
|
|
583
|
+
const collected = await withSpan("evidence.collect", { changed_file_count: snapshot.files.length }, () => {
|
|
584
|
+
const github2 = collectGitHubNeeds(options.githubNeeds ?? process.env[config.evidence.github_needs_env]);
|
|
585
|
+
const junit = collectJUnit(options.root, config.evidence.junit);
|
|
586
|
+
const repositoryFacts = collectRepositoryFacts(options.root, snapshot.files.map((file) => file.path));
|
|
587
|
+
return { github: github2, evidence: [...github2, ...junit, ...repositoryFacts] };
|
|
588
|
+
});
|
|
589
|
+
const { github, evidence } = collected;
|
|
590
|
+
const passedChecks = github.filter((item2) => item2.state === "available").map((item2) => String(item2.payload.name));
|
|
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);
|
|
602
|
+
const scope = await withSpan("scope.resolve", { impacted_file_count: impact.files.length }, () => buildScopeContext(options.root, impact.files, { checks: passedChecks }));
|
|
603
|
+
const policy = await withSpan("policy.evaluate", { decision_count: loadedDecisions.decisions.length }, () => evaluateDecisions(loadedDecisions.decisions, scope));
|
|
604
|
+
const identity = {
|
|
605
|
+
schemaVersion: "1",
|
|
606
|
+
baseSha: snapshot.baseSha,
|
|
607
|
+
headSha: snapshot.headSha,
|
|
608
|
+
diffHash: snapshot.diffHash,
|
|
609
|
+
config,
|
|
610
|
+
plan: plan ? hashObject(plan) : null,
|
|
611
|
+
decisions: loadedDecisions.decisions.map((entry) => ({ id: entry.id, version: entry.version, hash: hashObject(entry) })),
|
|
612
|
+
evals: evals.map((entry) => ({ id: entry.id, version: entry.version, hash: hashObject(entry) })),
|
|
613
|
+
evidence: evidence.map((entry) => ({ hash: entry.contentHash, state: entry.state })),
|
|
614
|
+
impact: { ...impactProviderIdentity(impactProvider), edges: impact.edges },
|
|
615
|
+
evaluators: { policy: "1", eval: "1", plan: "1", gate: "1", testCoverage: "2" },
|
|
616
|
+
semantic: config.semantic.enabled || options.semantic ? {
|
|
617
|
+
model: config.semantic.model ?? process.env.OPENAI_OUTPUT_MODEL ?? process.env.LITELLM_MODEL ?? "gpt-5.6-terra",
|
|
618
|
+
endpoint: config.semantic.endpoint ?? process.env.SCRIPTONIA_MODEL_GATEWAY_URL ?? process.env.LITELLM_BASE_URL ?? "unconfigured"
|
|
619
|
+
} : null,
|
|
620
|
+
trustedConfigurationRef: trustedRef ?? "working-tree"
|
|
621
|
+
};
|
|
622
|
+
const snapshotId = hashObject(identity);
|
|
623
|
+
const store = new BrainStore(options.root);
|
|
624
|
+
try {
|
|
625
|
+
for (const decision of loadedDecisions.decisions) store.indexDecision({ ...decision });
|
|
626
|
+
for (const evaluation of loadedEvals.evals) store.indexEval({ ...evaluation });
|
|
627
|
+
store.saveSnapshot({ id: snapshotId, baseSha: snapshot.baseSha, headSha: snapshot.headSha, diffHash: snapshot.diffHash, payload: identity, files: snapshot.files });
|
|
628
|
+
store.saveImpact({ id: impactIndex.id, provider: impactIndex.provider, version: impactIndex.version, snapshotId, edges: impact.edges });
|
|
629
|
+
store.saveEvidence(evidence.map((entry) => ({ id: entry.id, contentHash: entry.contentHash, kind: entry.kind, state: entry.state, payload: entry.payload, collectedAt: entry.collectedAt })));
|
|
630
|
+
if (!options.refresh) {
|
|
631
|
+
const cached = await withSpan("cache.lookup", { refresh: false }, () => store.getCached(snapshotId));
|
|
632
|
+
if (cached) return { ...cached, cached: true };
|
|
633
|
+
}
|
|
634
|
+
const checks = [];
|
|
635
|
+
for (const error of [...loadedConfig.errors, ...loadedDecisions.errors, ...loadedEvals.errors]) checks.push(makeCheck({ evaluator: "definition-loader", title: "Definition validation", state: "ERROR", enforcement: "block", reason: error, refs: [] }));
|
|
636
|
+
checks.push(...policy.map(policyToCheck));
|
|
637
|
+
checks.push(...evaluateEvals(evals, scope, evidence, policy));
|
|
638
|
+
checks.push(...evaluatePlan(plan, config.required_plan, evidence));
|
|
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)));
|
|
645
|
+
checks.push(...github.map(githubCheck));
|
|
646
|
+
let semanticCoverage = config.semantic.enabled || options.semantic ? "unavailable" : "disabled";
|
|
647
|
+
if ((config.semantic.enabled || options.semantic) && plan) {
|
|
648
|
+
const semanticChecks = await evaluateSemanticCriteria(plan, snapshot, config);
|
|
649
|
+
if (semanticChecks.some((check) => !check.reason.includes("unavailable"))) semanticCoverage = "available";
|
|
650
|
+
checks.push(...semanticChecks);
|
|
651
|
+
}
|
|
652
|
+
const gate = await withSpan("verdict.aggregate", { check_count: checks.length }, () => aggregateGate(checks, config.incomplete_blocks));
|
|
653
|
+
for (const check of checks) {
|
|
654
|
+
if (check.state === "FAIL" || check.state === "ERROR" || check.state === "INCONCLUSIVE" && check.enforcement === "block") {
|
|
655
|
+
check.findingId = store.findFindingId(check.fingerprint) ?? `finding_${hashObject({ snapshotId, fingerprint: check.fingerprint }).slice(0, 24)}`;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const verificationId = createId("verification");
|
|
659
|
+
const decisionEvaluated = policy.filter((entry) => entry.state !== "SKIPPED").length;
|
|
660
|
+
const evalEvaluated = checks.filter((entry) => entry.evaluator === "eval-evidence" && entry.state !== "SKIPPED").length;
|
|
661
|
+
const result = {
|
|
662
|
+
schemaVersion: "1",
|
|
663
|
+
verificationId,
|
|
664
|
+
snapshotId,
|
|
665
|
+
createdAt: nowIso(),
|
|
666
|
+
cached: false,
|
|
667
|
+
gate,
|
|
668
|
+
exitCode: exitCodeForGate(gate),
|
|
669
|
+
coverage: { plan: plan ? "present" : "absent", decisions: { total: policy.length, evaluated: decisionEvaluated, skipped: policy.length - decisionEvaluated }, evals: { total: evals.length, evaluated: evalEvaluated, skipped: evals.length - evalEvaluated }, semantic: semanticCoverage },
|
|
670
|
+
snapshot: { schemaVersion: snapshot.schemaVersion, baseRef: snapshot.baseRef, baseSha: snapshot.baseSha, headSha: snapshot.headSha, dirty: snapshot.dirty, diffHash: snapshot.diffHash, files: snapshot.files, snapshotId: snapshot.snapshotId, repositoryRoot: "." },
|
|
671
|
+
checks,
|
|
672
|
+
evidence: evidence.map(({ id, kind, source, state, contentHash }) => ({ id, kind, source, state, contentHash })),
|
|
673
|
+
configurationHash: hashObject(config)
|
|
674
|
+
};
|
|
675
|
+
store.saveVerification({ id: verificationId, snapshotId, gateState: gate, result, refresh: options.refresh });
|
|
676
|
+
store.saveChecks(verificationId, checks);
|
|
677
|
+
for (const check of checks.filter((entry) => entry.findingId)) {
|
|
678
|
+
store.saveFinding({ id: check.findingId, snapshotId, fingerprint: check.fingerprint, evaluator: check.evaluator, state: check.state, enforcement: check.enforcement, owner: check.owner, payload: check });
|
|
679
|
+
}
|
|
680
|
+
return result;
|
|
681
|
+
} finally {
|
|
682
|
+
store.close();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function policyToCheck(result) {
|
|
686
|
+
return { id: result.id, evaluator: result.evaluator, evaluatorVersion: result.evaluatorVersion, title: result.title, state: result.state, enforcement: result.enforcement, reason: result.reason, evidence: result.evidence.map((entry) => ({ kind: entry.kind, value: entry.value })), refs: [result.decisionRef], ...result.owner ? { owner: result.owner } : {}, fingerprint: result.fingerprint };
|
|
687
|
+
}
|
|
688
|
+
function evaluateEvals(evals, scope, evidence, policies) {
|
|
689
|
+
return evals.map((evaluation) => {
|
|
690
|
+
if (evaluation.state !== "active") return makeCheck({ id: `eval:${evaluation.id}@${evaluation.version}`, evaluator: "eval-evidence", title: evaluation.title, state: "SKIPPED", enforcement: evaluation.enforcement, reason: `Eval is ${evaluation.state}; only active evals enforce.`, refs: [evaluation.id], owner: evaluation.owner });
|
|
691
|
+
const match = matchScope(evaluation.scope, scope);
|
|
692
|
+
if (!match.matched) return makeCheck({ id: `eval:${evaluation.id}@${evaluation.version}`, evaluator: "eval-evidence", title: evaluation.title, state: "SKIPPED", enforcement: evaluation.enforcement, reason: "Eval scope does not intersect this change.", refs: [evaluation.id], owner: evaluation.owner ?? match.owner });
|
|
693
|
+
const withPolicies = [...evidence, ...policies.map((policy) => ({ id: policy.id, kind: "decision_predicate", collector: "policy-engine", collectorVersion: "1", source: policy.decisionRef, state: policy.state === "PASS" ? "available" : policy.state === "FAIL" ? "failed" : "missing", contentHash: policy.fingerprint, payload: { name: policy.decisionRef, result: policy.state }, collectedAt: nowIso(), redaction: { applied: false, policyVersion: "1" } }))];
|
|
694
|
+
const all = evaluation.evidence.all.map((requirement) => evaluateRequirement(requirement, withPolicies));
|
|
695
|
+
const any = evaluation.evidence.any.map((requirement) => evaluateRequirement(requirement, withPolicies));
|
|
696
|
+
let state = "PASS";
|
|
697
|
+
if (all.some((entry) => entry.state === "ERROR") || any.some((entry) => entry.state === "ERROR")) state = "ERROR";
|
|
698
|
+
else if (all.some((entry) => entry.state === "FAIL") || any.length > 0 && any.every((entry) => entry.state === "FAIL")) state = "FAIL";
|
|
699
|
+
else if (all.some((entry) => entry.state === "INCONCLUSIVE") || any.length > 0 && !any.some((entry) => entry.state === "PASS")) state = "INCONCLUSIVE";
|
|
700
|
+
else if (!all.length && !any.length) state = "INCONCLUSIVE";
|
|
701
|
+
const used = [...all, ...any].flatMap((entry) => entry.evidence);
|
|
702
|
+
return makeCheck({ id: `eval:${evaluation.id}@${evaluation.version}`, evaluator: "eval-evidence", title: evaluation.title, state, enforcement: evaluation.enforcement, reason: state === "PASS" ? "All declared eval evidence passed." : state === "FAIL" ? "Declared eval evidence failed." : state === "ERROR" ? "An evidence collector failed." : "Declared eval evidence is unavailable or incomplete.", evidence: used.map((entry) => ({ id: entry.id, kind: entry.kind, value: entry.source })), refs: [evaluation.id, ...evaluation.decision_refs, ...evaluation.signal_refs], owner: evaluation.owner ?? match.owner });
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function evaluatePlan(plan, required, evidence) {
|
|
706
|
+
if (!plan) return [makeCheck({ evaluator: "plan-coverage", title: "Approved plan", state: required ? "INCONCLUSIVE" : "SKIPPED", enforcement: required ? "block" : "observe", reason: required ? "PLAN.md is required but unavailable." : "No PLAN.md is present; policy-only mode is active.", refs: [] })];
|
|
707
|
+
const checks = [];
|
|
708
|
+
const unresolved = plan.split("\n").filter((line) => /(?:⚠\s*)?UNRESOLVED/i.test(line));
|
|
709
|
+
checks.push(makeCheck({ evaluator: "plan-unresolved", title: "Unresolved constraints", state: unresolved.length ? "FAIL" : "PASS", enforcement: "block", reason: unresolved.length ? "PLAN.md contains unresolved approval gates." : "No unresolved approval gate remains.", evidence: unresolved.map((value) => ({ kind: "plan_line", value: value.trim() })), refs: ["PLAN.md"] }));
|
|
710
|
+
for (const criterion of parseSectionItems(plan, "acceptance criteria")) {
|
|
711
|
+
const requirements = parseInlineEvidence(criterion);
|
|
712
|
+
checks.push(planEvidenceCheck("plan-criterion", criterion, requirements, evidence));
|
|
713
|
+
}
|
|
714
|
+
for (const nonGoal of parseSectionItems(plan, "non-goals")) {
|
|
715
|
+
const requirements = parseInlineEvidence(nonGoal);
|
|
716
|
+
checks.push(planEvidenceCheck("plan-non-goal", nonGoal, requirements, evidence));
|
|
717
|
+
}
|
|
718
|
+
return checks;
|
|
719
|
+
}
|
|
720
|
+
function planEvidenceCheck(evaluator, text, requirements, evidence) {
|
|
721
|
+
const title = text.replace(/\s*\[evidence:[^\]]+\]/g, "");
|
|
722
|
+
if (!requirements.length) return makeCheck({ evaluator, title, state: "INCONCLUSIVE", enforcement: "warn", reason: evaluator === "plan-criterion" ? "Acceptance criterion has no declared evidence mapping." : "Non-goal has no deterministic evidence mapping.", refs: ["PLAN.md"] });
|
|
723
|
+
const results = requirements.map((requirement) => evaluateRequirement(requirement, evidence));
|
|
724
|
+
const state = results.some((entry) => entry.state === "ERROR") ? "ERROR" : results.some((entry) => entry.state === "FAIL") ? "FAIL" : results.some((entry) => entry.state === "INCONCLUSIVE") ? "INCONCLUSIVE" : "PASS";
|
|
725
|
+
return makeCheck({ evaluator, title, state, enforcement: "warn", reason: state === "PASS" ? "All declared plan evidence passed." : state === "FAIL" ? "Declared plan evidence failed." : state === "ERROR" ? "A declared plan evidence collector failed." : "Declared plan evidence is unavailable or incomplete.", evidence: results.flatMap((entry) => entry.evidence).map((entry) => ({ id: entry.id, kind: entry.kind, value: entry.source })), refs: ["PLAN.md"] });
|
|
726
|
+
}
|
|
727
|
+
function parseSectionItems(markdown, title) {
|
|
728
|
+
const lines = markdown.split("\n");
|
|
729
|
+
const start = lines.findIndex((line) => new RegExp(`^#{1,6}\\s+${escapeRegex(title)}\\s*$`, "i").test(line.trim()));
|
|
730
|
+
if (start < 0) return [];
|
|
731
|
+
const items = [];
|
|
732
|
+
for (const line of lines.slice(start + 1)) {
|
|
733
|
+
if (/^#{1,6}\s+/.test(line.trim())) break;
|
|
734
|
+
const match = line.match(/^\s*(?:[-*]|\d+[.)])\s+(?:\[[ xX]\]\s*)?(.+)/);
|
|
735
|
+
if (match?.[1]) items.push(match[1].trim());
|
|
736
|
+
}
|
|
737
|
+
return items;
|
|
738
|
+
}
|
|
739
|
+
function parseInlineEvidence(text) {
|
|
740
|
+
return [...text.matchAll(/\[evidence:([a-z_]+)(?::([^:\]]+))?(?::([^\]]+))?\]/gi)].flatMap((match) => {
|
|
741
|
+
const kind = match[1];
|
|
742
|
+
const first = match[2];
|
|
743
|
+
const second = match[3];
|
|
744
|
+
const candidate = kind === "junit_test" ? { kind, suite: first, test: second } : kind === "repository_fact" ? { kind, path: first } : { kind, name: first };
|
|
745
|
+
const parsed = evidenceRequirementSchema.safeParse(candidate);
|
|
746
|
+
return parsed.success ? [parsed.data] : [];
|
|
747
|
+
});
|
|
748
|
+
}
|
|
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
|
+
});
|
|
921
|
+
}
|
|
922
|
+
function githubCheck(evidence) {
|
|
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: [] });
|
|
924
|
+
}
|
|
925
|
+
async function evaluateSemanticCriteria(plan, snapshot, config) {
|
|
926
|
+
const criteria = parseSectionItems(plan, "acceptance criteria").filter((line) => !parseInlineEvidence(line).length);
|
|
927
|
+
const results = [];
|
|
928
|
+
for (const criterion of criteria.slice(0, 20)) {
|
|
929
|
+
const judged = await withSpan("judge.evaluate", { criterion_length: criterion.length }, () => semanticJudge({ criterion, evidenceBundle: { changedFiles: snapshot.files, diff: snapshot.diff.slice(0, config.semantic.max_bytes) }, endpoint: config.semantic.endpoint, model: config.semantic.model, timeoutMs: config.semantic.timeout_ms, maxBytes: config.semantic.max_bytes }));
|
|
930
|
+
const state = !judged.available || !judged.result || judged.result.result === "uncertain" ? "INCONCLUSIVE" : judged.result.result === "supports" ? "PASS" : "FAIL";
|
|
931
|
+
results.push(makeCheck({ evaluator: "semantic-judge", title: criterion, state, enforcement: "warn", reason: judged.result?.reason ?? `Semantic evaluation unavailable: ${judged.error ?? "unknown error"}`, evidence: judged.result?.evidenceRefs.map((value) => ({ kind: "semantic_ref", value })) ?? [], refs: ["PLAN.md"] }));
|
|
932
|
+
}
|
|
933
|
+
return results;
|
|
934
|
+
}
|
|
935
|
+
function makeCheck(input) {
|
|
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) };
|
|
939
|
+
}
|
|
940
|
+
function escapeRegex(value) {
|
|
941
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
942
|
+
}
|
|
943
|
+
function learnFromFinding(root, findingId) {
|
|
944
|
+
const store = new BrainStore(root);
|
|
945
|
+
try {
|
|
946
|
+
const finding = store.getFinding(findingId);
|
|
947
|
+
if (!finding) throw new Error(`finding not found: ${findingId}`);
|
|
948
|
+
const payload = finding.payload;
|
|
949
|
+
const id = `eval_${payload.fingerprint.slice(0, 16)}`;
|
|
950
|
+
const evalCase = evalCaseSchema.parse({
|
|
951
|
+
schema_version: "1",
|
|
952
|
+
id,
|
|
953
|
+
version: 1,
|
|
954
|
+
title: payload.title,
|
|
955
|
+
state: "draft",
|
|
956
|
+
owner: payload.owner,
|
|
957
|
+
origin: { kind: "confirmed_failure", finding_ref: findingId },
|
|
958
|
+
decision_refs: payload.refs.filter((ref) => ref.startsWith("decision_")),
|
|
959
|
+
signal_refs: payload.refs.filter((ref) => ref.startsWith("signal_")),
|
|
960
|
+
scope: { paths: { include: payload.evidence.filter((entry) => entry.kind === "path").map((entry) => entry.value).slice(0, 20).length ? payload.evidence.filter((entry) => entry.kind === "path").map((entry) => entry.value).slice(0, 20) : ["**"], exclude: [] } },
|
|
961
|
+
assertion: { kind: "required_evidence", description: payload.reason },
|
|
962
|
+
evidence: { all: [], any: [] },
|
|
963
|
+
enforcement: "observe",
|
|
964
|
+
fingerprint: payload.fingerprint
|
|
965
|
+
});
|
|
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));
|
|
970
|
+
return { path: filename, evalCase };
|
|
971
|
+
} finally {
|
|
972
|
+
store.close();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function acknowledgeFinding(root, findingId, reason, actor, comment) {
|
|
976
|
+
const store = new BrainStore(root);
|
|
977
|
+
try {
|
|
978
|
+
const finding = store.getFinding(findingId);
|
|
979
|
+
if (!finding) throw new Error(`finding not found: ${findingId}`);
|
|
980
|
+
const id = createId("ack");
|
|
981
|
+
store.acknowledge({ id, findingId, reason, comment, actor, snapshotId: String(finding.snapshot_id) });
|
|
982
|
+
return id;
|
|
983
|
+
} finally {
|
|
984
|
+
store.close();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function evalDoctor(root) {
|
|
988
|
+
const { evals, errors } = loadEvals(root);
|
|
989
|
+
const { decisions } = loadDecisions(root);
|
|
990
|
+
const issues = errors.map((message) => ({ kind: "invalid", id: "definition", message }));
|
|
991
|
+
const activeDecisions = new Map(decisions.filter((entry) => entry.state === "active").map((entry) => [entry.id, entry]));
|
|
992
|
+
const fingerprints = /* @__PURE__ */ new Map();
|
|
993
|
+
for (const evaluation of evals) {
|
|
994
|
+
if (!evaluation.owner) issues.push({ kind: "missing_owner", id: evaluation.id, message: "Eval has no owner." });
|
|
995
|
+
if (evaluation.review_after && Date.parse(evaluation.review_after) < Date.now() && evaluation.state === "active") issues.push({ kind: "expired_review", id: evaluation.id, message: `Review date passed: ${evaluation.review_after}` });
|
|
996
|
+
for (const ref of evaluation.decision_refs) if (!activeDecisions.has(ref) && evaluation.state === "active") issues.push({ kind: "stale_decision", id: evaluation.id, message: `Referenced decision is not active: ${ref}` });
|
|
997
|
+
const fingerprint = evaluation.fingerprint ?? hashObject({ assertion: evaluation.assertion, scope: evaluation.scope, origin: evaluation.origin, evidence: evaluation.evidence });
|
|
998
|
+
const duplicate = fingerprints.get(fingerprint);
|
|
999
|
+
if (duplicate) issues.push({ kind: "duplicate", id: evaluation.id, message: `Duplicates ${duplicate}.` });
|
|
1000
|
+
else fingerprints.set(fingerprint, evaluation.id);
|
|
1001
|
+
}
|
|
1002
|
+
return { healthy: issues.length === 0, issues };
|
|
1003
|
+
}
|
|
1004
|
+
function toSarif(result) {
|
|
1005
|
+
const actionable = result.checks.filter((check) => check.state === "FAIL" || check.state === "ERROR" || check.state === "INCONCLUSIVE");
|
|
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 } })) }] };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// ../packages/github/src/index.ts
|
|
1010
|
+
function githubStepSummary(result) {
|
|
1011
|
+
const lines = [`# Scriptonia gate: ${result.gate}`, "", `Snapshot: \`${result.snapshotId}\``, "", "| State | Check | Reason |", "|---|---|---|"];
|
|
1012
|
+
for (const check of result.checks) lines.push(`| ${check.state} | ${escapeCell(check.title)} | ${escapeCell(check.reason)} |`);
|
|
1013
|
+
return `${lines.join("\n")}
|
|
1014
|
+
`;
|
|
1015
|
+
}
|
|
1016
|
+
function escapeCell(value) {
|
|
1017
|
+
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// ../packages/run-capture/src/index.ts
|
|
1021
|
+
import { spawn } from "child_process";
|
|
1022
|
+
import fs6 from "fs";
|
|
1023
|
+
import path6 from "path";
|
|
1024
|
+
|
|
1025
|
+
// ../packages/run-protocol/src/index.ts
|
|
1026
|
+
var BUILTIN_PATTERNS = [
|
|
1027
|
+
/\b(?:sk|sk-ant|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{12,}\b/g,
|
|
1028
|
+
/\bBearer\s+[A-Za-z0-9._~+\/-]+=*\b/gi,
|
|
1029
|
+
/\b[A-Za-z0-9_-]*(?:TOKEN|SECRET|PASSWORD|API_KEY)\s*[=:]\s*[^\s,;]+/gi
|
|
1030
|
+
];
|
|
1031
|
+
function redact(value, options = {}) {
|
|
1032
|
+
const secrets = (options.envNames ?? []).map((name) => process.env[name]).filter((entry) => Boolean(entry && entry.length >= 4));
|
|
1033
|
+
const custom = (options.patterns ?? []).map((pattern) => new RegExp(pattern, "gi"));
|
|
1034
|
+
let text = JSON.stringify(value);
|
|
1035
|
+
const original = text;
|
|
1036
|
+
for (const secret of secrets) text = text.replaceAll(secret, "[REDACTED]");
|
|
1037
|
+
for (const pattern of [...BUILTIN_PATTERNS, ...custom]) text = text.replace(pattern, "[REDACTED]");
|
|
1038
|
+
const maxBytes = options.maxBytes ?? 128e3;
|
|
1039
|
+
if (Buffer.byteLength(text) > maxBytes) text = JSON.stringify({ truncated: true, bytes: Buffer.byteLength(text), preview: text.slice(0, maxBytes - 128) });
|
|
1040
|
+
try {
|
|
1041
|
+
return { value: JSON.parse(text), applied: text !== original };
|
|
1042
|
+
} catch {
|
|
1043
|
+
return { value: "[REDACTED:invalid-payload]", applied: true };
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function createEvent(input) {
|
|
1047
|
+
const sanitized = redact(input.payload, input.redaction);
|
|
1048
|
+
return runEventSchema.parse({
|
|
1049
|
+
schemaVersion: "1",
|
|
1050
|
+
eventId: createId("event"),
|
|
1051
|
+
runId: input.runId,
|
|
1052
|
+
sequence: input.sequence,
|
|
1053
|
+
occurredAt: nowIso(),
|
|
1054
|
+
source: { adapter: input.adapter ?? "explicit-wrapper", adapterVersion: "1", ...input.agent ? { agent: input.agent } : {} },
|
|
1055
|
+
type: input.type,
|
|
1056
|
+
payload: sanitized.value,
|
|
1057
|
+
redaction: { applied: sanitized.applied, policyVersion: "1" }
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
function parseJsonl(text) {
|
|
1061
|
+
if (Buffer.byteLength(text) > 1e7) throw new Error("JSONL payload exceeds 10 MB limit");
|
|
1062
|
+
return text.split("\n").filter(Boolean).map((line, index) => {
|
|
1063
|
+
if (Buffer.byteLength(line) > 512e3) throw new Error(`JSONL line ${index + 1} exceeds 512 KB limit`);
|
|
1064
|
+
return runEventSchema.parse(JSON.parse(line));
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
function normalizeCodexJsonl(text, importedRunId = createId("run")) {
|
|
1068
|
+
if (Buffer.byteLength(text) > 1e7) throw new Error("Codex JSONL payload exceeds 10 MB limit");
|
|
1069
|
+
let sequence = 0;
|
|
1070
|
+
const events = [];
|
|
1071
|
+
for (const [index, line] of text.split("\n").filter(Boolean).entries()) {
|
|
1072
|
+
if (Buffer.byteLength(line) > 512e3) throw new Error(`Codex JSONL line ${index + 1} exceeds 512 KB limit`);
|
|
1073
|
+
const raw = JSON.parse(line);
|
|
1074
|
+
const type = String(raw.type ?? raw.event ?? "");
|
|
1075
|
+
const payload = raw.payload ?? raw.item ?? raw;
|
|
1076
|
+
let normalized = "tool.completed";
|
|
1077
|
+
if (/session_meta|thread\.started|turn\.started|run\.started/.test(type)) normalized = "run.started";
|
|
1078
|
+
else if (/turn\.completed|thread\.completed|run\.finished/.test(type)) normalized = "run.finished";
|
|
1079
|
+
else if (/error|failed/.test(type)) normalized = "error";
|
|
1080
|
+
else if (/file_change|file\.changed/.test(type) || payload.type === "file_change") normalized = "file.changed";
|
|
1081
|
+
else if (/command.*requested|command_execution.*started/.test(type)) normalized = "command.requested";
|
|
1082
|
+
else if (/tool.*requested|item\.started/.test(type)) normalized = "tool.requested";
|
|
1083
|
+
events.push(createEvent({ runId: String(raw.runId ?? raw.thread_id ?? importedRunId), sequence: sequence++, type: normalized, payload: raw, adapter: "codex-jsonl", agent: "codex" }));
|
|
1084
|
+
}
|
|
1085
|
+
return events;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ../packages/run-capture/src/index.ts
|
|
1089
|
+
async function captureCommand(input) {
|
|
1090
|
+
if (!input.command.length || !input.command[0]) throw new Error("usage: scriptonia run -- <command> [args...]");
|
|
1091
|
+
const store = new BrainStore(input.root);
|
|
1092
|
+
const runId = createId("run");
|
|
1093
|
+
const before = buildSnapshot({ cwd: input.root, base: "HEAD" });
|
|
1094
|
+
const beforeHashes = snapshotFileHashes(input.root, before.files);
|
|
1095
|
+
let sequence = 0;
|
|
1096
|
+
const persist = (type, payload) => {
|
|
1097
|
+
const event = createEvent({ runId, sequence: sequence++, type, payload, adapter: input.adapter, agent: input.agent, redaction: input.redaction });
|
|
1098
|
+
store.appendEvent({ eventId: event.eventId, runId, sequence: event.sequence, occurredAt: event.occurredAt, type: event.type, payload: event.payload, redacted: event.redaction.applied });
|
|
1099
|
+
};
|
|
1100
|
+
store.createRun({ id: runId, adapter: input.adapter ?? "explicit-wrapper", command: input.command, startedAt: nowIso(), baseSha: before.headSha });
|
|
1101
|
+
persist("run.started", { baseSha: before.headSha, dirty: before.dirty });
|
|
1102
|
+
persist("command.requested", { argv: input.command, cwd: ".", inheritedAuthority: true });
|
|
1103
|
+
let exitCode = 1;
|
|
1104
|
+
try {
|
|
1105
|
+
exitCode = await new Promise((resolve, reject) => {
|
|
1106
|
+
const child = spawn(input.command[0], input.command.slice(1), { cwd: input.root, stdio: "inherit", shell: false, env: process.env });
|
|
1107
|
+
child.once("error", reject);
|
|
1108
|
+
child.once("exit", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
|
|
1109
|
+
});
|
|
1110
|
+
const after = buildSnapshot({ cwd: input.root, base: before.headSha });
|
|
1111
|
+
const afterHashes = snapshotFileHashes(input.root, after.files);
|
|
1112
|
+
const changedByCommand = after.files.filter((file) => beforeHashes.get(file.path) !== afterHashes.get(file.path));
|
|
1113
|
+
for (const file of changedByCommand) persist("file.changed", { path: file.path, status: file.status, binary: file.binary });
|
|
1114
|
+
persist("run.finished", { exitCode, headSha: after.headSha, diffHash: after.diffHash, changedFiles: changedByCommand.length });
|
|
1115
|
+
store.finishRun({ id: runId, finishedAt: nowIso(), headSha: after.headSha, diffHash: after.diffHash, exitCode, status: exitCode === 0 ? "completed" : "failed" });
|
|
1116
|
+
return { runId, exitCode };
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
persist("error", { message: error instanceof Error ? error.message : String(error) });
|
|
1119
|
+
store.finishRun({ id: runId, finishedAt: nowIso(), exitCode, status: "error" });
|
|
1120
|
+
throw error;
|
|
1121
|
+
} finally {
|
|
1122
|
+
store.close();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function snapshotFileHashes(root, files) {
|
|
1126
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
1127
|
+
for (const file of files) {
|
|
1128
|
+
if (file.status === "deleted") {
|
|
1129
|
+
hashes.set(file.path, "deleted");
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
hashes.set(file.path, sha256(fs6.readFileSync(path6.join(root, file.path))));
|
|
1134
|
+
} catch {
|
|
1135
|
+
hashes.set(file.path, "unreadable");
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return hashes;
|
|
1139
|
+
}
|
|
1140
|
+
function ingestJsonl(root, text, adapter = "normalized") {
|
|
1141
|
+
const events = adapter === "codex" ? normalizeCodexJsonl(text) : parseJsonl(text);
|
|
1142
|
+
if (!events.length) return 0;
|
|
1143
|
+
const store = new BrainStore(root);
|
|
1144
|
+
try {
|
|
1145
|
+
const runIds = new Set(events.map((event) => event.runId));
|
|
1146
|
+
for (const runId of runIds) if (!store.getRun(runId)) store.createRun({ id: runId, adapter: events.find((event) => event.runId === runId)?.source.adapter ?? adapter, command: ["imported-events"], startedAt: events.find((event) => event.runId === runId)?.occurredAt ?? nowIso() });
|
|
1147
|
+
for (const event of events) store.appendEvent({ eventId: event.eventId, runId: event.runId, sequence: event.sequence, occurredAt: event.occurredAt, type: event.type, payload: event.payload, redacted: event.redaction.applied });
|
|
1148
|
+
for (const runId of runIds) store.finishRun({ id: runId, finishedAt: nowIso(), exitCode: 0, status: "imported" });
|
|
1149
|
+
return events.length;
|
|
1150
|
+
} finally {
|
|
1151
|
+
store.close();
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
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
|
+
|
|
2153
|
+
// src/index.ts
|
|
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"]);
|
|
2155
|
+
var requested = process.argv[2];
|
|
2156
|
+
if (!requested || !V2_COMMANDS.has(requested)) {
|
|
2157
|
+
await import(new URL("../bin/scriptonia.mjs", import.meta.url).href);
|
|
2158
|
+
} else {
|
|
2159
|
+
await runV2();
|
|
2160
|
+
}
|
|
2161
|
+
async function runV2() {
|
|
2162
|
+
await initializeTelemetry();
|
|
2163
|
+
const program = new Command();
|
|
2164
|
+
program.name("scriptonia").description("Evidence-based product memory and verification for AI coding agents").version("0.9.0-rc.2");
|
|
2165
|
+
registerBrainCommands(program);
|
|
2166
|
+
program.command("v2-init").description("Create inspectable V2 configuration directories").action(() => {
|
|
2167
|
+
const root = findRepositoryRoot();
|
|
2168
|
+
const filename = ensureConfig(root);
|
|
2169
|
+
console.log(`${pc2.green("\u2713")} V2 brain configured at ${path9.relative(root, filename)}`);
|
|
2170
|
+
});
|
|
2171
|
+
program.command("diff-check").description("Run only sub-second deterministic changed-file policies").option("--base <ref>", "base Git reference", "HEAD").action((options) => {
|
|
2172
|
+
const started = performance.now();
|
|
2173
|
+
const root = findRepositoryRoot();
|
|
2174
|
+
const snapshot = buildSnapshot({ cwd: root, base: options.base });
|
|
2175
|
+
const definitions = loadDecisions(root);
|
|
2176
|
+
const context = buildScopeContext(root, snapshot.files.map((file) => file.path));
|
|
2177
|
+
const results = evaluateDecisions(definitions.decisions, context).filter((result) => result.state !== "SKIPPED");
|
|
2178
|
+
const plan = path9.join(root, "PLAN.md");
|
|
2179
|
+
const unresolved = fs9.existsSync(plan) ? fs9.readFileSync(plan, "utf8").split("\n").filter((line) => /(?:⚠\s*)?UNRESOLVED/i.test(line)) : [];
|
|
2180
|
+
console.log(`
|
|
2181
|
+
${pc2.bold("SCRIPTONIA DIFF CHECK")} \xB7 ${snapshot.files.length} changed files
|
|
2182
|
+
`);
|
|
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).`);
|
|
2186
|
+
const blocked = unresolved.length > 0 || results.some((result) => result.state === "FAIL" && result.enforcement === "block");
|
|
2187
|
+
const errored = definitions.errors.length > 0 || results.some((result) => result.state === "ERROR");
|
|
2188
|
+
console.log(`
|
|
2189
|
+
${errored ? "ERROR" : blocked ? "BLOCKED" : "PASS"} \xB7 ${Math.round(performance.now() - started)}ms
|
|
2190
|
+
`);
|
|
2191
|
+
process.exitCode = errored ? 4 : blocked ? 3 : 0;
|
|
2192
|
+
});
|
|
2193
|
+
program.command("decide <title>").description("Create a reviewable structured product or architecture decision").requiredOption("--kind <predicate>", "deterministic predicate kind").option("--names <values>", "comma-separated dependency, symbol, or path values", "").option("--include <globs>", "comma-separated scope globs", "**").option("--exclude <globs>", "comma-separated excluded globs", "").option("--owner <owner>", "responsible owner").option("--enforcement <level>", "observe, warn, action_required, or block", "observe").option("--confirm <actor>", "activate and record the confirming human").action((title, options) => {
|
|
2194
|
+
const root = findRepositoryRoot();
|
|
2195
|
+
ensureConfig(root);
|
|
2196
|
+
const id = `decision_${slug(title)}_${Date.now().toString(36)}`;
|
|
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 });
|
|
2198
|
+
const filename = path9.join(root, ".scriptonia", "decisions", `${id}.yaml`);
|
|
2199
|
+
fs9.writeFileSync(filename, YAML3.stringify(decision));
|
|
2200
|
+
console.log(`${pc2.green("\u2713")} ${decision.state} decision written to ${path9.relative(root, filename)}`);
|
|
2201
|
+
if (decision.state === "draft") console.log(" Review its scope and predicate, then confirm it before enforcement.");
|
|
2202
|
+
});
|
|
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) => {
|
|
2204
|
+
const root = findRepositoryRoot();
|
|
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
|
+
}
|
|
2221
|
+
console.log(`
|
|
2222
|
+
${pc2.bold("WHY")} ${target}${relative && relative !== target ? ` \u2192 ${relative}` : ""}
|
|
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
|
+
}
|
|
2248
|
+
for (const decision of decisions) console.log(` decision ${decision.id}@${decision.version} \xB7 ${decision.title} \xB7 ${decision.enforcement}`);
|
|
2249
|
+
for (const evaluation2 of evals) console.log(` eval ${evaluation2.id}@${evaluation2.version} \xB7 ${evaluation2.title} \xB7 ${evaluation2.enforcement}`);
|
|
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.");
|
|
2251
|
+
console.log("");
|
|
2252
|
+
});
|
|
2253
|
+
program.command("recall <query>").description("Search inspectable local decisions and evals").option("--limit <number>", "maximum matches", "10").action((query, options) => {
|
|
2254
|
+
const root = findRepositoryRoot();
|
|
2255
|
+
const entries = [
|
|
2256
|
+
...loadDecisions(root).decisions.map((value) => ({ kind: "decision", id: value.id, title: value.title, body: JSON.stringify(value) })),
|
|
2257
|
+
...loadEvals(root).evals.map((value) => ({ kind: "eval", id: value.id, title: value.title, body: JSON.stringify(value) }))
|
|
2258
|
+
];
|
|
2259
|
+
const tokens = query.toLowerCase().split(/\W+/).filter(Boolean);
|
|
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));
|
|
2261
|
+
console.log(`
|
|
2262
|
+
${pc2.bold("RECALL")} \u201C${query}\u201D
|
|
2263
|
+
`);
|
|
2264
|
+
for (const entry of matches) console.log(` ${entry.kind.padEnd(9)} ${entry.id} \xB7 ${entry.title}`);
|
|
2265
|
+
if (!matches.length) console.log(" No matching local memory.");
|
|
2266
|
+
console.log("");
|
|
2267
|
+
});
|
|
2268
|
+
program.command("pulse").description("Show local brain and repository health").action(() => {
|
|
2269
|
+
const root = findRepositoryRoot();
|
|
2270
|
+
ensureConfig(root);
|
|
2271
|
+
const snapshot = buildSnapshot({ cwd: root, base: "HEAD" });
|
|
2272
|
+
const decisions = loadDecisions(root);
|
|
2273
|
+
const evals = loadEvals(root);
|
|
2274
|
+
const health = evalDoctor(root);
|
|
2275
|
+
const store = new BrainStore(root);
|
|
2276
|
+
try {
|
|
2277
|
+
const runs = store.listRuns(5);
|
|
2278
|
+
console.log(`
|
|
2279
|
+
${pc2.bold("SCRIPTONIA PULSE")}
|
|
2280
|
+
`);
|
|
2281
|
+
console.log(` repository ${snapshot.dirty ? pc2.yellow("dirty") : pc2.green("clean")} \xB7 ${snapshot.files.length} changed files`);
|
|
2282
|
+
console.log(` decisions ${decisions.decisions.filter((entry) => entry.state === "active").length} active \xB7 ${decisions.errors.length} invalid`);
|
|
2283
|
+
console.log(` evals ${evals.evals.filter((entry) => entry.state === "active").length} active \xB7 ${health.issues.length} health issues`);
|
|
2284
|
+
console.log(` runs ${runs.length} recent captured`);
|
|
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")}`);
|
|
2286
|
+
console.log("");
|
|
2287
|
+
} finally {
|
|
2288
|
+
store.close();
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
program.command("run [argv...]").description("Capture exactly the agent command supplied after --").option("--adapter <name>", "adapter identity", "explicit-wrapper").option("--agent <name>", "agent identity").allowUnknownOption(true).action(async (argv, options) => {
|
|
2292
|
+
const root = findRepositoryRoot();
|
|
2293
|
+
if (argv[0] === "show") {
|
|
2294
|
+
if (!argv[1]) throw new Error("usage: scriptonia run show <run-id>");
|
|
2295
|
+
const store = new BrainStore(root);
|
|
2296
|
+
try {
|
|
2297
|
+
const run = store.getRun(argv[1]);
|
|
2298
|
+
if (!run) throw new Error(`run not found: ${argv[1]}`);
|
|
2299
|
+
console.log(JSON.stringify(run, null, 2));
|
|
2300
|
+
} finally {
|
|
2301
|
+
store.close();
|
|
2302
|
+
}
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
const separator = process.argv.indexOf("--");
|
|
2306
|
+
const exact = separator >= 0 ? process.argv.slice(separator + 1) : argv;
|
|
2307
|
+
const result = await captureCommand({ root, command: exact, adapter: options.adapter, agent: options.agent });
|
|
2308
|
+
console.log(`
|
|
2309
|
+
${pc2.green("\u2713")} captured ${result.runId} \xB7 child exit ${result.exitCode}`);
|
|
2310
|
+
process.exitCode = result.exitCode;
|
|
2311
|
+
});
|
|
2312
|
+
program.command("runs").description("List recent captured agent runs").option("--json", "print JSON").option("--limit <number>", "maximum rows", "20").action((options) => {
|
|
2313
|
+
const root = findRepositoryRoot();
|
|
2314
|
+
const store = new BrainStore(root);
|
|
2315
|
+
try {
|
|
2316
|
+
const runs = store.listRuns(Math.max(1, Math.min(200, Number(options.limit) || 20)));
|
|
2317
|
+
if (options.json) console.log(JSON.stringify(runs, null, 2));
|
|
2318
|
+
else {
|
|
2319
|
+
console.log(`
|
|
2320
|
+
${pc2.bold("RECENT AGENT RUNS")}
|
|
2321
|
+
`);
|
|
2322
|
+
if (!runs.length) console.log(" No captured runs yet. Use: scriptonia run -- <agent-command>");
|
|
2323
|
+
for (const run of runs) console.log(` ${String(run.status).padEnd(10)} ${run.id} ${JSON.stringify(run.command)} exit=${run.exit_code ?? "-"}`);
|
|
2324
|
+
console.log("");
|
|
2325
|
+
}
|
|
2326
|
+
} finally {
|
|
2327
|
+
store.close();
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
for (const name of ["verify", "gate"]) {
|
|
2331
|
+
program.command(name).description(name === "gate" ? "Aggregate evidence into a CI-safe exit status" : "Produce a complete local evidence report").option("--base <ref>", "base Git reference").option("--plan <file>", "plan file", "PLAN.md").option("--refresh", "create a new verification instead of using the immutable cache").option("--semantic", "enable advisory semantic evaluation").option("--ci", "consume CI evidence from SCRIPTONIA_NEEDS_JSON").option("--json [file]", "print JSON or write it to a file").option("--sarif <file>", "write SARIF output").action(async (options) => {
|
|
2332
|
+
const root = findRepositoryRoot();
|
|
2333
|
+
if (options.semantic) configureGatewayFromLegacyLogin();
|
|
2334
|
+
const result = await verifyRepository({ root, base: options.base, plan: options.plan, refresh: options.refresh, semantic: options.semantic, trustedBase: Boolean(options.ci) });
|
|
2335
|
+
emitVerification(root, result, options);
|
|
2336
|
+
if (name === "gate") process.exitCode = result.exitCode;
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
program.command("learn <finding-id>").description("Convert a confirmed finding into a reviewable draft eval").action((findingId) => {
|
|
2340
|
+
const root = findRepositoryRoot();
|
|
2341
|
+
const learned = learnFromFinding(root, findingId);
|
|
2342
|
+
console.log(`${pc2.green("\u2713")} draft eval ${learned.evalCase.id} written to ${path9.relative(root, learned.path)}`);
|
|
2343
|
+
console.log(" Review its scope and evidence, add an owner, then change state to active.");
|
|
2344
|
+
});
|
|
2345
|
+
const evaluation = program.command("eval").description("Run and maintain versioned regression cases");
|
|
2346
|
+
evaluation.command("run [id-or-scope]").option("--base <ref>", "base Git reference").option("--refresh", "bypass immutable cache").option("--json [file]", "print JSON or write it to a file").action(async (filter, options) => {
|
|
2347
|
+
const root = findRepositoryRoot();
|
|
2348
|
+
const result = await verifyRepository({ root, base: options.base, refresh: options.refresh, evalFilter: filter });
|
|
2349
|
+
emitVerification(root, result, options);
|
|
2350
|
+
process.exitCode = result.exitCode;
|
|
2351
|
+
});
|
|
2352
|
+
evaluation.command("doctor").action(() => {
|
|
2353
|
+
const root = findRepositoryRoot();
|
|
2354
|
+
const report = evalDoctor(root);
|
|
2355
|
+
console.log(`
|
|
2356
|
+
${pc2.bold("EVAL DOCTOR")} \xB7 ${report.healthy ? pc2.green("HEALTHY") : pc2.yellow("NEEDS ATTENTION")}
|
|
2357
|
+
`);
|
|
2358
|
+
for (const issue of report.issues) console.log(` ${issue.kind.padEnd(16)} ${issue.id} \xB7 ${issue.message}`);
|
|
2359
|
+
if (!report.issues.length) console.log(" No duplicate, stale, ownerless, invalid, or expired evals.");
|
|
2360
|
+
console.log("");
|
|
2361
|
+
process.exitCode = report.healthy ? 0 : 1;
|
|
2362
|
+
});
|
|
2363
|
+
program.command("acknowledge <finding-id>").requiredOption("--reason <reason>", "accepted_risk, false_positive, not_applicable, or fix_later").requiredOption("--actor <identity>", "human identity").option("--comment <text>", "reason details").action((findingId, options) => {
|
|
2364
|
+
const allowed = ["accepted_risk", "false_positive", "not_applicable", "fix_later"];
|
|
2365
|
+
if (!allowed.includes(options.reason)) throw new Error(`invalid reason: ${options.reason}`);
|
|
2366
|
+
const root = findRepositoryRoot();
|
|
2367
|
+
const id = acknowledgeFinding(root, findingId, options.reason, options.actor, options.comment);
|
|
2368
|
+
console.log(`${pc2.green("\u2713")} acknowledgement recorded: ${id}`);
|
|
2369
|
+
});
|
|
2370
|
+
const events = program.command("events").description("Ingest normalized adapter events");
|
|
2371
|
+
events.command("ingest <file>").option("--adapter <name>", "normalized or codex", "normalized").action((filename, options) => {
|
|
2372
|
+
const root = findRepositoryRoot();
|
|
2373
|
+
const full = assertInside(root, path9.resolve(root, filename));
|
|
2374
|
+
if (options.adapter !== "normalized" && options.adapter !== "codex") throw new Error(`unsupported event adapter: ${options.adapter}`);
|
|
2375
|
+
const count = ingestJsonl(root, fs9.readFileSync(full, "utf8"), options.adapter);
|
|
2376
|
+
console.log(`${pc2.green("\u2713")} ingested ${count} normalized run events`);
|
|
2377
|
+
});
|
|
2378
|
+
program.command("report-summary <file>").action((filename) => {
|
|
2379
|
+
const root = findRepositoryRoot();
|
|
2380
|
+
const full = assertInside(root, path9.resolve(root, filename));
|
|
2381
|
+
const result = JSON.parse(fs9.readFileSync(full, "utf8"));
|
|
2382
|
+
const markdown = githubStepSummary(result);
|
|
2383
|
+
if (process.env.GITHUB_STEP_SUMMARY) fs9.appendFileSync(process.env.GITHUB_STEP_SUMMARY, markdown);
|
|
2384
|
+
else console.log(markdown);
|
|
2385
|
+
});
|
|
2386
|
+
await program.parseAsync(process.argv);
|
|
2387
|
+
}
|
|
2388
|
+
function emitVerification(root, result, options) {
|
|
2389
|
+
if (options.json === true) console.log(JSON.stringify(result, null, 2));
|
|
2390
|
+
else {
|
|
2391
|
+
const color = result.gate === "PASS" ? pc2.green : result.gate === "PASS_WITH_WARNINGS" ? pc2.yellow : pc2.red;
|
|
2392
|
+
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}`));
|
|
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}
|
|
2396
|
+
`);
|
|
2397
|
+
for (const check of result.checks) {
|
|
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);
|
|
2399
|
+
console.log(` ${state.padEnd(18)} ${check.title}`);
|
|
2400
|
+
if (check.state !== "PASS" && check.state !== "SKIPPED") console.log(` ${pc2.dim(check.reason)} \xB7 ${check.findingId ?? check.id}`);
|
|
2401
|
+
}
|
|
2402
|
+
console.log(`
|
|
2403
|
+
exit ${result.exitCode}
|
|
2404
|
+
`);
|
|
2405
|
+
}
|
|
2406
|
+
if (typeof options.json === "string") writeOutput(root, options.json, `${JSON.stringify(result, null, 2)}
|
|
2407
|
+
`);
|
|
2408
|
+
if (options.sarif) writeOutput(root, options.sarif, `${JSON.stringify(toSarif(result), null, 2)}
|
|
2409
|
+
`);
|
|
2410
|
+
}
|
|
2411
|
+
function writeOutput(root, filename, content) {
|
|
2412
|
+
const full = assertInside(root, path9.resolve(root, filename));
|
|
2413
|
+
fs9.mkdirSync(path9.dirname(full), { recursive: true });
|
|
2414
|
+
fs9.writeFileSync(full, content);
|
|
2415
|
+
}
|
|
2416
|
+
function csv(value) {
|
|
2417
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
2418
|
+
}
|
|
2419
|
+
function slug(value) {
|
|
2420
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "").slice(0, 48) || "decision";
|
|
2421
|
+
}
|
|
2422
|
+
function configureGatewayFromLegacyLogin() {
|
|
2423
|
+
if (process.env.SCRIPTONIA_MODEL_GATEWAY_URL && process.env.SCRIPTONIA_API_TOKEN) return;
|
|
2424
|
+
try {
|
|
2425
|
+
const config = JSON.parse(fs9.readFileSync(path9.join(os2.homedir(), ".scriptonia", "config.json"), "utf8"));
|
|
2426
|
+
if (config.url && !process.env.SCRIPTONIA_MODEL_GATEWAY_URL) process.env.SCRIPTONIA_MODEL_GATEWAY_URL = config.url;
|
|
2427
|
+
if (config.token && !process.env.SCRIPTONIA_API_TOKEN) process.env.SCRIPTONIA_API_TOKEN = config.token;
|
|
2428
|
+
} catch {
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
//# sourceMappingURL=index.js.map
|