@kodr/aiwrangler 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +434 -2
- package/aiwrangler.config.example.json +62 -0
- package/dist/chunk-2CIV4KGU.js +1276 -0
- package/dist/chunk-2CIV4KGU.js.map +1 -0
- package/dist/chunk-EAQASAXL.js +1252 -0
- package/dist/chunk-EAQASAXL.js.map +1 -0
- package/dist/chunk-EDVWHCQV.js +145 -0
- package/dist/chunk-EDVWHCQV.js.map +1 -0
- package/dist/chunk-NQFO47CV.js +522 -0
- package/dist/chunk-NQFO47CV.js.map +1 -0
- package/dist/cli.js +262 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +697 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.js +214 -0
- package/dist/mcp.js.map +1 -0
- package/dist/plugin.js +112 -0
- package/dist/plugin.js.map +1 -0
- package/dist/tools.js +127 -0
- package/dist/tools.js.map +1 -0
- package/package.json +87 -3
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var PRECEDENCE = {
|
|
3
|
+
ruling: 0,
|
|
4
|
+
paper: 1,
|
|
5
|
+
door: 1,
|
|
6
|
+
spec: 2,
|
|
7
|
+
doc: 3,
|
|
8
|
+
nonRuling: 4
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/jsonc.ts
|
|
12
|
+
function stripJsonc(text) {
|
|
13
|
+
let out = "";
|
|
14
|
+
let inString = false;
|
|
15
|
+
let inLine = false;
|
|
16
|
+
let inBlock = false;
|
|
17
|
+
for (let i = 0; i < text.length; i++) {
|
|
18
|
+
const c = text[i] ?? "";
|
|
19
|
+
const next = text[i + 1] ?? "";
|
|
20
|
+
if (inLine) {
|
|
21
|
+
if (c === "\n") {
|
|
22
|
+
inLine = false;
|
|
23
|
+
out += c;
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (inBlock) {
|
|
28
|
+
if (c === "*" && next === "/") {
|
|
29
|
+
inBlock = false;
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (inString) {
|
|
35
|
+
out += c;
|
|
36
|
+
if (c === "\\" && next !== "") {
|
|
37
|
+
out += next;
|
|
38
|
+
i++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (c === '"') inString = false;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (c === '"') {
|
|
45
|
+
inString = true;
|
|
46
|
+
out += c;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (c === "/" && next === "/") {
|
|
50
|
+
inLine = true;
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (c === "/" && next === "*") {
|
|
55
|
+
inBlock = true;
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
out += c;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function parseJsonc(text) {
|
|
64
|
+
const stripped = stripJsonc(text).replace(/,(\s*[}\]])/g, "$1");
|
|
65
|
+
return JSON.parse(stripped);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/overrides.ts
|
|
69
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, unlinkSync, renameSync, mkdirSync } from "fs";
|
|
70
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
71
|
+
import { z as z2 } from "zod";
|
|
72
|
+
|
|
73
|
+
// src/config.ts
|
|
74
|
+
import { existsSync, readFileSync } from "fs";
|
|
75
|
+
import { homedir } from "os";
|
|
76
|
+
import { isAbsolute, join, dirname, resolve } from "path";
|
|
77
|
+
import { z } from "zod";
|
|
78
|
+
var bool = (def) => z.boolean().default(def);
|
|
79
|
+
var GatesSchema = z.object({
|
|
80
|
+
plan: z.object({ inSpec: bool(true), ambiguity: bool(true), openQuestions: bool(true), oneWayDoors: bool(true) }).default({}),
|
|
81
|
+
retrieval: z.object({ rerank: bool(true), topK: z.number().int().min(1).max(20).default(6) }).default({}),
|
|
82
|
+
check: z.object({
|
|
83
|
+
deterministic: bool(true),
|
|
84
|
+
battery: bool(true),
|
|
85
|
+
selfReport: bool(true),
|
|
86
|
+
testQuality: bool(true),
|
|
87
|
+
docDrift: bool(true),
|
|
88
|
+
injectionDefense: bool(true)
|
|
89
|
+
}).default({}),
|
|
90
|
+
security: z.object({ trustBoundary: bool(true), severity: bool(true), alwaysHuman: z.array(z.string()).default(["keys", "seals", "digests"]) }).default({}),
|
|
91
|
+
loop: z.object({ retryProgress: bool(true), errorSalience: bool(true), maxCycles: z.number().int().min(1).default(3) }).default({}),
|
|
92
|
+
session: z.object({ endAudit: bool(true), nudge: bool(true), maxNudges: z.number().int().min(0).default(3) }).default({})
|
|
93
|
+
});
|
|
94
|
+
var ThresholdsSchema = z.object({
|
|
95
|
+
actionThreshold: z.number().min(0).max(1).default(0.8),
|
|
96
|
+
reviewThreshold: z.number().min(0).max(1).default(0.5)
|
|
97
|
+
});
|
|
98
|
+
var ConfigSchema = z.object({
|
|
99
|
+
model: z.string().default("jev-1.13.0"),
|
|
100
|
+
activePolicy: z.string().default("strict"),
|
|
101
|
+
egress: z.object({ semantic: bool(true), warnOncePerSession: bool(true) }).default({}),
|
|
102
|
+
corpus: z.object({
|
|
103
|
+
spec: z.string().default("corpus/spec.md"),
|
|
104
|
+
rules: z.string().default("corpus/rules.md"),
|
|
105
|
+
divergences: z.string().default("corpus/divergences.md"),
|
|
106
|
+
questions: z.string().default("corpus/questions.md"),
|
|
107
|
+
plan: z.string().default("corpus/plan.md"),
|
|
108
|
+
docs: z.array(z.string()).default([]),
|
|
109
|
+
whitepaper: z.string().default("corpus/whitepaper.pdf")
|
|
110
|
+
}).default({}),
|
|
111
|
+
gates: GatesSchema.default({}),
|
|
112
|
+
policies: z.record(z.string(), ThresholdsSchema).default({ strict: {}, permissive: { actionThreshold: 0.9, reviewThreshold: 0.35 } }),
|
|
113
|
+
cache: z.object({ dir: z.string().default(".aiwrangler") }).default({}),
|
|
114
|
+
ledger: z.object({ path: z.string().default(".aiwrangler/ledger.jsonl") }).default({}),
|
|
115
|
+
repos: z.object({
|
|
116
|
+
super: z.string().default("."),
|
|
117
|
+
members: z.array(z.string()).default([]),
|
|
118
|
+
reviewed: z.array(z.string()).default([])
|
|
119
|
+
}).default({})
|
|
120
|
+
});
|
|
121
|
+
function expandPath(p, root) {
|
|
122
|
+
if (p.startsWith("~")) return join(homedir(), p.slice(1));
|
|
123
|
+
if (isAbsolute(p)) return p;
|
|
124
|
+
return resolve(root, p);
|
|
125
|
+
}
|
|
126
|
+
function findConfigFile(startDir) {
|
|
127
|
+
let dir = resolve(startDir);
|
|
128
|
+
for (; ; ) {
|
|
129
|
+
const candidate = join(dir, "aiwrangler.config.json");
|
|
130
|
+
if (existsSync(candidate)) return { path: candidate, source: "discovered" };
|
|
131
|
+
const parent = dirname(dir);
|
|
132
|
+
if (parent === dir) break;
|
|
133
|
+
dir = parent;
|
|
134
|
+
}
|
|
135
|
+
const user = join(homedir(), ".config", "aiwrangler", "aiwrangler.config.json");
|
|
136
|
+
if (existsSync(user)) return { path: user, source: "user" };
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function flattenGates(config) {
|
|
140
|
+
const flat = /* @__PURE__ */ new Map();
|
|
141
|
+
const walk = (obj, prefix) => {
|
|
142
|
+
if (obj === null || typeof obj !== "object") return;
|
|
143
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
144
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
145
|
+
if (typeof value === "boolean") flat.set(path, value);
|
|
146
|
+
else if (typeof value === "object" && !Array.isArray(value)) walk(value, path);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
walk(config.gates, "");
|
|
150
|
+
return flat;
|
|
151
|
+
}
|
|
152
|
+
function applyGateFlag(config, name, value) {
|
|
153
|
+
const flat = flattenGates(config);
|
|
154
|
+
const applied = [];
|
|
155
|
+
for (const path of flat.keys()) {
|
|
156
|
+
const leaf = path.split(".").pop() ?? path;
|
|
157
|
+
if (path === name || leaf === name) {
|
|
158
|
+
const parts = path.split(".");
|
|
159
|
+
let node = config.gates;
|
|
160
|
+
for (const part of parts.slice(0, -1)) node = node[part];
|
|
161
|
+
node[parts[parts.length - 1]] = value;
|
|
162
|
+
applied.push(path);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (applied.length === 0) {
|
|
166
|
+
throw new Error(`unknown gate '${name}'. Known gates: ${[...flat.keys()].join(", ")}`);
|
|
167
|
+
}
|
|
168
|
+
return applied;
|
|
169
|
+
}
|
|
170
|
+
function applyFlagList(config, list, value) {
|
|
171
|
+
const applied = [];
|
|
172
|
+
const names = list.split(",").map((s) => s.trim()).filter(Boolean);
|
|
173
|
+
for (const name of names) {
|
|
174
|
+
applied.push(...applyGateFlag(config, name, value));
|
|
175
|
+
}
|
|
176
|
+
return applied;
|
|
177
|
+
}
|
|
178
|
+
function applyEnvOverrides(config, env = process.env) {
|
|
179
|
+
const next = structuredClone(config);
|
|
180
|
+
if (env.AIWRANGLER_MODEL) next.model = env.AIWRANGLER_MODEL;
|
|
181
|
+
if (env.AIWRANGLER_POLICY) {
|
|
182
|
+
if (!(env.AIWRANGLER_POLICY in next.policies)) {
|
|
183
|
+
throw new Error(`AIWRANGLER_POLICY: unknown policy '${env.AIWRANGLER_POLICY}'. Known: ${Object.keys(next.policies).join(", ")}`);
|
|
184
|
+
}
|
|
185
|
+
next.activePolicy = env.AIWRANGLER_POLICY;
|
|
186
|
+
}
|
|
187
|
+
if (env.AIWRANGLER_EGRESS) {
|
|
188
|
+
const on = ["1", "true", "on", "yes"].includes(env.AIWRANGLER_EGRESS.toLowerCase());
|
|
189
|
+
next.egress.semantic = on;
|
|
190
|
+
}
|
|
191
|
+
if (env.AIWRANGLER_DISABLE) applyFlagList(next, env.AIWRANGLER_DISABLE, false);
|
|
192
|
+
if (env.AIWRANGLER_ENABLE) applyFlagList(next, env.AIWRANGLER_ENABLE, true);
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
function loadConfig(startDir, env = process.env) {
|
|
196
|
+
const explicit = env.AIWRANGLER_CONFIG;
|
|
197
|
+
let raw;
|
|
198
|
+
let root;
|
|
199
|
+
let source;
|
|
200
|
+
let path;
|
|
201
|
+
if (explicit) {
|
|
202
|
+
path = expandPath(explicit, resolve(startDir));
|
|
203
|
+
if (!existsSync(path)) throw new Error(`AIWRANGLER_CONFIG: file not found: ${path}`);
|
|
204
|
+
raw = parseJsonc(readFileSync(path, "utf8"));
|
|
205
|
+
root = dirname(path);
|
|
206
|
+
source = "explicit";
|
|
207
|
+
} else {
|
|
208
|
+
const found = findConfigFile(startDir);
|
|
209
|
+
if (found) {
|
|
210
|
+
path = found.path;
|
|
211
|
+
raw = parseJsonc(readFileSync(found.path, "utf8"));
|
|
212
|
+
root = dirname(found.path);
|
|
213
|
+
source = found.source;
|
|
214
|
+
} else {
|
|
215
|
+
raw = {};
|
|
216
|
+
root = resolve(startDir);
|
|
217
|
+
source = "defaults";
|
|
218
|
+
path = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const parsed = ConfigSchema.safeParse(raw);
|
|
222
|
+
if (!parsed.success) {
|
|
223
|
+
const issues = parsed.error.issues.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
224
|
+
throw new Error(`invalid aiwrangler config at ${path ?? "defaults"}:
|
|
225
|
+
${issues}`);
|
|
226
|
+
}
|
|
227
|
+
const { config, overrides } = applyRuntimeOverridesFromDisk(parsed.data, root);
|
|
228
|
+
return { config: applyEnvOverrides(config, env), root, source, path, overrides };
|
|
229
|
+
}
|
|
230
|
+
function activeThresholds(config) {
|
|
231
|
+
const policy = config.policies[config.activePolicy];
|
|
232
|
+
if (!policy) throw new Error(`activePolicy '${config.activePolicy}' not defined in policies`);
|
|
233
|
+
return policy;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/overrides.ts
|
|
237
|
+
var OVERRIDES_FILENAME = "runtime-overrides.json";
|
|
238
|
+
var RuntimeOverridesSchema = z2.object({
|
|
239
|
+
activePolicy: z2.string().optional(),
|
|
240
|
+
egressSemantic: z2.boolean().optional(),
|
|
241
|
+
actionThreshold: z2.number().min(0).max(1).optional(),
|
|
242
|
+
reviewThreshold: z2.number().min(0).max(1).optional(),
|
|
243
|
+
gates: z2.record(z2.string(), z2.boolean()).optional()
|
|
244
|
+
});
|
|
245
|
+
var OverridesFileSchema = z2.object({
|
|
246
|
+
version: z2.literal(1),
|
|
247
|
+
createdAt: z2.string(),
|
|
248
|
+
note: z2.string().optional(),
|
|
249
|
+
expiresOn: z2.enum(["pass", "manual"]),
|
|
250
|
+
overrides: RuntimeOverridesSchema
|
|
251
|
+
});
|
|
252
|
+
function overridesFilePath(config, root) {
|
|
253
|
+
return join2(expandPath(config.cache.dir, root), OVERRIDES_FILENAME);
|
|
254
|
+
}
|
|
255
|
+
function readRuntimeOverridesFile(config, root) {
|
|
256
|
+
const path = overridesFilePath(config, root);
|
|
257
|
+
if (!existsSync2(path)) return null;
|
|
258
|
+
try {
|
|
259
|
+
const parsed = OverridesFileSchema.safeParse(JSON.parse(readFileSync2(path, "utf8")));
|
|
260
|
+
if (!parsed.success) return null;
|
|
261
|
+
return parsed.data;
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function writeRuntimeOverridesFile(config, root, file) {
|
|
267
|
+
const path = overridesFilePath(config, root);
|
|
268
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
269
|
+
const tmp = path + ".tmp";
|
|
270
|
+
writeFileSync(tmp, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
271
|
+
renameSync(tmp, path);
|
|
272
|
+
}
|
|
273
|
+
function clearRuntimeOverrides(config, root) {
|
|
274
|
+
const path = overridesFilePath(config, root);
|
|
275
|
+
if (!existsSync2(path)) return false;
|
|
276
|
+
try {
|
|
277
|
+
unlinkSync(path);
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function applyRuntimeOverrides(config, overrides) {
|
|
284
|
+
const next = structuredClone(config);
|
|
285
|
+
const appliedGates = [];
|
|
286
|
+
if (overrides.activePolicy !== void 0) {
|
|
287
|
+
if (!(overrides.activePolicy in next.policies)) {
|
|
288
|
+
throw new Error(`runtime override activePolicy '${overrides.activePolicy}' is not a defined policy`);
|
|
289
|
+
}
|
|
290
|
+
next.activePolicy = overrides.activePolicy;
|
|
291
|
+
}
|
|
292
|
+
if (overrides.egressSemantic !== void 0) next.egress.semantic = overrides.egressSemantic;
|
|
293
|
+
if (overrides.actionThreshold !== void 0 || overrides.reviewThreshold !== void 0) {
|
|
294
|
+
const policy = next.policies[next.activePolicy];
|
|
295
|
+
if (!policy) throw new Error(`activePolicy '${next.activePolicy}' not defined in policies`);
|
|
296
|
+
next.policies[next.activePolicy] = {
|
|
297
|
+
actionThreshold: overrides.actionThreshold ?? policy.actionThreshold,
|
|
298
|
+
reviewThreshold: overrides.reviewThreshold ?? policy.reviewThreshold
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
for (const [name, value] of Object.entries(overrides.gates ?? {})) {
|
|
302
|
+
appliedGates.push(...applyGateFlag(next, name, value));
|
|
303
|
+
}
|
|
304
|
+
return { config: next, appliedGates };
|
|
305
|
+
}
|
|
306
|
+
function applyRuntimeOverridesFromDisk(config, root) {
|
|
307
|
+
const file = readRuntimeOverridesFile(config, root);
|
|
308
|
+
if (!file) return { config, overrides: null };
|
|
309
|
+
try {
|
|
310
|
+
return { config: applyRuntimeOverrides(config, file.overrides).config, overrides: file.overrides };
|
|
311
|
+
} catch {
|
|
312
|
+
return { config, overrides: null };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/hash.ts
|
|
317
|
+
import { createHash } from "crypto";
|
|
318
|
+
function sha256Hex(text) {
|
|
319
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
320
|
+
}
|
|
321
|
+
function shortHash(text) {
|
|
322
|
+
return sha256Hex(text).slice(0, 12);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/groundtruth/sectionize.ts
|
|
326
|
+
function splitMarkdown(md) {
|
|
327
|
+
const lines = md.split("\n");
|
|
328
|
+
const sections = [];
|
|
329
|
+
let current = null;
|
|
330
|
+
for (let i = 0; i < lines.length; i++) {
|
|
331
|
+
const line = lines[i] ?? "";
|
|
332
|
+
const m = line.match(/^(#{1,6})\s+(.*)$/);
|
|
333
|
+
if (m) {
|
|
334
|
+
if (current) sections.push(current);
|
|
335
|
+
current = { level: m[1].length, title: m[2].trim(), text: "", line: i + 1 };
|
|
336
|
+
} else if (current) {
|
|
337
|
+
current.text += line + "\n";
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (current) sections.push(current);
|
|
341
|
+
return sections.map((s) => ({ ...s, text: s.text.trim() }));
|
|
342
|
+
}
|
|
343
|
+
function slug(title) {
|
|
344
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/groundtruth/parsers.ts
|
|
348
|
+
function section(partial) {
|
|
349
|
+
return { precedence: PRECEDENCE.doc, ...partial };
|
|
350
|
+
}
|
|
351
|
+
var ROMAN_RE = /^[IVX]+(?:-[A-Z])?$/;
|
|
352
|
+
function parseSpec(md, source) {
|
|
353
|
+
const out = [];
|
|
354
|
+
for (const sec of splitMarkdown(md)) {
|
|
355
|
+
const m = sec.title.match(/^(\d+)\.\s+(.*)$/);
|
|
356
|
+
if (!m) continue;
|
|
357
|
+
const title = m[2];
|
|
358
|
+
const ref = title.match(/\(([^)]+)\)\s*$/);
|
|
359
|
+
const paperRef = ref && ROMAN_RE.test(ref[1]) ? ref[1] : void 0;
|
|
360
|
+
out.push(
|
|
361
|
+
section({
|
|
362
|
+
id: `SPEC-${m[1]}`,
|
|
363
|
+
kind: "spec",
|
|
364
|
+
title,
|
|
365
|
+
text: sec.text,
|
|
366
|
+
source,
|
|
367
|
+
status: "info",
|
|
368
|
+
...paperRef ? { paperRef } : {},
|
|
369
|
+
precedence: PRECEDENCE.spec
|
|
370
|
+
})
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
function parseRules(md, source) {
|
|
376
|
+
const lines = md.split("\n");
|
|
377
|
+
const out = [];
|
|
378
|
+
let current = null;
|
|
379
|
+
const flush = () => {
|
|
380
|
+
if (!current) return;
|
|
381
|
+
out.push(
|
|
382
|
+
section({
|
|
383
|
+
id: `RULE-${current.n}`,
|
|
384
|
+
kind: "rule",
|
|
385
|
+
title: current.lines[0].trim(),
|
|
386
|
+
text: current.lines.join("\n").trim(),
|
|
387
|
+
source,
|
|
388
|
+
status: "info",
|
|
389
|
+
precedence: PRECEDENCE.spec
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
current = null;
|
|
393
|
+
};
|
|
394
|
+
for (const line of lines) {
|
|
395
|
+
const m = line.match(/^(\d+)\.\s+(.*)$/);
|
|
396
|
+
if (m) {
|
|
397
|
+
flush();
|
|
398
|
+
current = { n: m[1], lines: [m[2]] };
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (current && (/^\s{2,}/.test(line) || line.trim() === "")) {
|
|
402
|
+
if (line.trim() === "" && current.lines[current.lines.length - 1] === "") continue;
|
|
403
|
+
current.lines.push(line.trim() === "" ? "" : line.trim());
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
flush();
|
|
407
|
+
}
|
|
408
|
+
flush();
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
function parseDivergences(md, source) {
|
|
412
|
+
const lines = md.split("\n");
|
|
413
|
+
const out = [];
|
|
414
|
+
let topic = "";
|
|
415
|
+
let current = null;
|
|
416
|
+
const flush = () => {
|
|
417
|
+
if (!current) return;
|
|
418
|
+
const body = current.lines.join("\n").trim();
|
|
419
|
+
const marker = body.indexOf("**RULED");
|
|
420
|
+
const ruled = marker >= 0;
|
|
421
|
+
const ruling = ruled ? body.slice(marker).replace(/\*\*/g, "").replace(/\s+/g, " ").trim().slice(0, 800) : void 0;
|
|
422
|
+
out.push(
|
|
423
|
+
section({
|
|
424
|
+
id: `DIV-D-${current.id}`,
|
|
425
|
+
kind: "divergence",
|
|
426
|
+
title: `${current.topic ? current.topic + ": " : ""}${current.lines[0].trim()}`,
|
|
427
|
+
text: body,
|
|
428
|
+
source,
|
|
429
|
+
status: ruled ? "ruled" : "open",
|
|
430
|
+
...ruled && ruling ? { ruling } : {},
|
|
431
|
+
precedence: ruled ? PRECEDENCE.ruling : PRECEDENCE.nonRuling
|
|
432
|
+
})
|
|
433
|
+
);
|
|
434
|
+
current = null;
|
|
435
|
+
};
|
|
436
|
+
for (const line of lines) {
|
|
437
|
+
const h = line.match(/^##\s+(.*)$/);
|
|
438
|
+
if (h) {
|
|
439
|
+
flush();
|
|
440
|
+
topic = h[1].trim();
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const m = line.match(/^D-(\d+)\s+(.*)$/);
|
|
444
|
+
if (m) {
|
|
445
|
+
flush();
|
|
446
|
+
current = { id: m[1], lines: [m[2]], topic };
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (current) current.lines.push(line);
|
|
450
|
+
}
|
|
451
|
+
flush();
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
function parseQuestions(md, source) {
|
|
455
|
+
const lines = md.split("\n");
|
|
456
|
+
const out = [];
|
|
457
|
+
let heading = "";
|
|
458
|
+
let current = null;
|
|
459
|
+
const flush = () => {
|
|
460
|
+
if (!current) return;
|
|
461
|
+
const body = current.lines.join("\n").trim();
|
|
462
|
+
const marker = body.indexOf("**ANSWERED");
|
|
463
|
+
const answered = marker >= 0;
|
|
464
|
+
const ruling = answered ? body.slice(marker).replace(/\*\*/g, "").replace(/\s+/g, " ").trim().slice(0, 800) : void 0;
|
|
465
|
+
out.push(
|
|
466
|
+
section({
|
|
467
|
+
id: `Q-${current.n}`,
|
|
468
|
+
kind: "question",
|
|
469
|
+
title: current.lines[0].trim(),
|
|
470
|
+
text: body,
|
|
471
|
+
source,
|
|
472
|
+
status: answered ? "answered" : "open",
|
|
473
|
+
...answered && ruling ? { ruling } : {},
|
|
474
|
+
precedence: answered ? PRECEDENCE.ruling : PRECEDENCE.nonRuling
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
current = null;
|
|
478
|
+
};
|
|
479
|
+
for (const line of lines) {
|
|
480
|
+
const h = line.match(/^##\s+(.*)$/);
|
|
481
|
+
if (h) {
|
|
482
|
+
flush();
|
|
483
|
+
heading = h[1].trim();
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const m = line.match(/^(\d+)\.\s+(.*)$/);
|
|
487
|
+
if (m) {
|
|
488
|
+
flush();
|
|
489
|
+
current = { n: m[1], lines: [m[2]], heading };
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (current) current.lines.push(line);
|
|
493
|
+
}
|
|
494
|
+
flush();
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
function parsePlan(md, source) {
|
|
498
|
+
const out = [];
|
|
499
|
+
for (const sec of splitMarkdown(md)) {
|
|
500
|
+
if (!/one-way door|launch gate/i.test(sec.title)) continue;
|
|
501
|
+
out.push(
|
|
502
|
+
section({
|
|
503
|
+
id: `DOOR-${slug(sec.title) || out.length + 1}`,
|
|
504
|
+
kind: "door",
|
|
505
|
+
title: sec.title,
|
|
506
|
+
text: sec.text,
|
|
507
|
+
source,
|
|
508
|
+
status: "info",
|
|
509
|
+
precedence: PRECEDENCE.door
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
function parseDoc(md, source, label) {
|
|
516
|
+
return splitMarkdown(md).filter((s) => s.text.length > 0).map(
|
|
517
|
+
(sec, i) => section({
|
|
518
|
+
id: `DOC-${label}-${String(i + 1).padStart(2, "0")}`,
|
|
519
|
+
kind: "doc",
|
|
520
|
+
title: sec.title || "(intro)",
|
|
521
|
+
text: sec.text,
|
|
522
|
+
source,
|
|
523
|
+
status: "info",
|
|
524
|
+
precedence: PRECEDENCE.doc
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
var PAPER_HEADING_RE = /^([IVX]+(?:-[A-Z])?)?\.?\s{1,4}(.+)$/;
|
|
529
|
+
var ROMAN_ONLY_RE = /^[IVX]+$/;
|
|
530
|
+
function mergeSpacedCaps(title) {
|
|
531
|
+
const tokens = title.split(/\s+/).filter(Boolean);
|
|
532
|
+
const merged = [];
|
|
533
|
+
let i = 0;
|
|
534
|
+
while (i < tokens.length) {
|
|
535
|
+
const tok = tokens[i];
|
|
536
|
+
const next = tokens[i + 1];
|
|
537
|
+
if (next !== void 0 && /^-?[A-Z]$/.test(tok) && /^[A-Z]/.test(next) && next.length > 1) {
|
|
538
|
+
merged.push((tok.startsWith("-") ? tok : "") + next);
|
|
539
|
+
i += 2;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (tok.length === 1 && /^[A-Z]$/.test(tok) && next !== void 0 && /^[A-Z]/.test(next)) {
|
|
543
|
+
merged.push(tok + next);
|
|
544
|
+
i += 2;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
merged.push(tok);
|
|
548
|
+
i++;
|
|
549
|
+
}
|
|
550
|
+
return merged.join(" ").replace(/ -/g, "-").replace(/\s+,/g, ",");
|
|
551
|
+
}
|
|
552
|
+
function capsRatio(s) {
|
|
553
|
+
const letters = s.replace(/[^A-Za-z]/g, "");
|
|
554
|
+
if (letters.length === 0) return 0;
|
|
555
|
+
const upper = letters.replace(/[^A-Z]/g, "");
|
|
556
|
+
return upper.length / letters.length;
|
|
557
|
+
}
|
|
558
|
+
function parsePaper(text, source) {
|
|
559
|
+
const lines = text.split("\n").map((l) => l.replace(/^\f/, ""));
|
|
560
|
+
const byId = /* @__PURE__ */ new Map();
|
|
561
|
+
let current = null;
|
|
562
|
+
const looksLikeCapsFragment = (line) => {
|
|
563
|
+
const t = line.trim();
|
|
564
|
+
return t.length >= 3 && t.length <= 40 && !/[.:;]$/.test(t) && !/\.\s*\.\s*\./.test(t) && capsRatio(t) >= 0.6;
|
|
565
|
+
};
|
|
566
|
+
const flush = () => {
|
|
567
|
+
if (!current) return;
|
|
568
|
+
const body = current.lines.join("\n").trim();
|
|
569
|
+
const existing = byId.get(current.id);
|
|
570
|
+
if (existing) {
|
|
571
|
+
existing.text = (existing.text + "\n" + body).trim();
|
|
572
|
+
} else {
|
|
573
|
+
byId.set(
|
|
574
|
+
current.id,
|
|
575
|
+
section({
|
|
576
|
+
id: `PAPER-${current.id}`,
|
|
577
|
+
kind: "paper",
|
|
578
|
+
title: current.title,
|
|
579
|
+
text: body,
|
|
580
|
+
source,
|
|
581
|
+
status: "info",
|
|
582
|
+
precedence: PRECEDENCE.paper
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
current = null;
|
|
587
|
+
};
|
|
588
|
+
for (let li = 0; li < lines.length; li++) {
|
|
589
|
+
const line = lines[li] ?? "";
|
|
590
|
+
if (/\.\s*\.\s*\./.test(line)) continue;
|
|
591
|
+
const m = line.match(PAPER_HEADING_RE);
|
|
592
|
+
if (m && m[1] && !ROMAN_ONLY_RE.test(m[2] ?? "") && !/^[ivx]+$/.test(m[1])) {
|
|
593
|
+
let title = mergeSpacedCaps(m[2] ?? "");
|
|
594
|
+
let consumed = 1;
|
|
595
|
+
while (li + consumed < lines.length && looksLikeCapsFragment(lines[li + consumed] ?? "") && title.split(/\s+/).length < 10) {
|
|
596
|
+
title = title + " " + mergeSpacedCaps((lines[li + consumed] ?? "").trim());
|
|
597
|
+
consumed++;
|
|
598
|
+
}
|
|
599
|
+
const letters = title.replace(/[^A-Za-z]/g, "");
|
|
600
|
+
if (letters.length >= 4 && capsRatio(title) >= 0.7) {
|
|
601
|
+
flush();
|
|
602
|
+
current = { id: m[1], title: title.replace(/\s{2,}/g, " ").trim(), lines: [] };
|
|
603
|
+
li += consumed - 1;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (current) current.lines.push(line);
|
|
608
|
+
}
|
|
609
|
+
flush();
|
|
610
|
+
return [...byId.values()];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/groundtruth/whitepaper.ts
|
|
614
|
+
import { spawnSync } from "child_process";
|
|
615
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
616
|
+
import { join as join3 } from "path";
|
|
617
|
+
function ensurePaperText(config, root) {
|
|
618
|
+
const pdf = expandPath(config.corpus.whitepaper, root);
|
|
619
|
+
const cacheDir = expandPath(config.cache.dir, root);
|
|
620
|
+
const cached = join3(cacheDir, "whitepaper.txt");
|
|
621
|
+
const pdfMtime = existsSync3(pdf) ? statSync(pdf).mtimeMs : 0;
|
|
622
|
+
if (existsSync3(cached) && statSync(cached).mtimeMs >= pdfMtime) {
|
|
623
|
+
return { text: readFileSync3(cached, "utf8"), extracted: false };
|
|
624
|
+
}
|
|
625
|
+
if (!existsSync3(pdf)) return { text: null, extracted: false };
|
|
626
|
+
const res = spawnSync("pdftotext", [pdf, "-"], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
627
|
+
if (res.error || res.status !== 0) {
|
|
628
|
+
const reason = res.error ? res.error.message : `pdftotext exit ${res.status}`;
|
|
629
|
+
throw new Error(`whitepaper extraction failed (${reason}); is poppler-utils installed?`);
|
|
630
|
+
}
|
|
631
|
+
const text = res.stdout;
|
|
632
|
+
if (text.split(/\s+/).length < 1e3) throw new Error("whitepaper extraction produced suspiciously little text; refusing to cache");
|
|
633
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
634
|
+
writeFileSync2(cached, text, "utf8");
|
|
635
|
+
return { text, extracted: true };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/groundtruth/corpus.ts
|
|
639
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
640
|
+
function slugFromPath(rel) {
|
|
641
|
+
return rel.toLowerCase().replace(/\.[a-z0-9]+$/i, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
642
|
+
}
|
|
643
|
+
function readOr(path, fallback) {
|
|
644
|
+
try {
|
|
645
|
+
return { text: readFileSync4(path, "utf8"), sections: [] };
|
|
646
|
+
} catch {
|
|
647
|
+
return { text: null, sections: fallback };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function loadCorpus(config, root) {
|
|
651
|
+
const sections = [];
|
|
652
|
+
const sources = [];
|
|
653
|
+
const addFile = (path, parse) => {
|
|
654
|
+
const resolved = expandPath(path, root);
|
|
655
|
+
const { text } = readOr(resolved, []);
|
|
656
|
+
if (text === null) return;
|
|
657
|
+
sources.push({ path: resolved, hash: sha256Hex(text) });
|
|
658
|
+
sections.push(...parse(text, resolved));
|
|
659
|
+
};
|
|
660
|
+
addFile(config.corpus.spec, parseSpec);
|
|
661
|
+
addFile(config.corpus.rules, parseRules);
|
|
662
|
+
addFile(config.corpus.divergences, parseDivergences);
|
|
663
|
+
addFile(config.corpus.questions, parseQuestions);
|
|
664
|
+
addFile(config.corpus.plan, parsePlan);
|
|
665
|
+
for (const doc of config.corpus.docs) {
|
|
666
|
+
const resolved = expandPath(doc, root);
|
|
667
|
+
let label = doc.split("/").pop()?.replace(/\.[a-z]+$/i, "") ?? "doc";
|
|
668
|
+
const rel = resolved.startsWith(root + "/") ? resolved.slice(root.length + 1) : null;
|
|
669
|
+
if (rel && rel.includes("/")) label = slugFromPath(rel);
|
|
670
|
+
addFile(doc, (md, source) => parseDoc(md, source, label));
|
|
671
|
+
}
|
|
672
|
+
const paper = ensurePaperText(config, root);
|
|
673
|
+
if (paper.text !== null) {
|
|
674
|
+
const paperPath = expandPath(config.corpus.whitepaper, root);
|
|
675
|
+
sources.push({ path: paperPath, hash: sha256Hex(paper.text) });
|
|
676
|
+
sections.push(...parsePaper(paper.text, paperPath));
|
|
677
|
+
}
|
|
678
|
+
const hash = sha256Hex(JSON.stringify({ sources: sources.map((s) => [s.path, s.hash]).sort((a, b) => (a[0] ?? "").localeCompare(b[0] ?? "")), ids: sections.map((s) => s.id).sort() }));
|
|
679
|
+
return { sections, sources, hash, paperText: paper.text ?? "" };
|
|
680
|
+
}
|
|
681
|
+
function loadCorpusFrom(startDir) {
|
|
682
|
+
const loaded = loadConfig(startDir);
|
|
683
|
+
const corpus = loadCorpus(loaded.config, loaded.root);
|
|
684
|
+
return { loaded, corpus };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/git/diff.ts
|
|
688
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
689
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
690
|
+
import { join as join4 } from "path";
|
|
691
|
+
function git(repoDir, args, timeoutMs = 3e4) {
|
|
692
|
+
const res = spawnSync2("git", ["-C", repoDir, ...args], { encoding: "utf8", timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
|
|
693
|
+
if (res.error) return { code: null, stdout: "", stderr: String(res.error) };
|
|
694
|
+
return { code: res.status, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
695
|
+
}
|
|
696
|
+
function parseUnifiedDiff(text) {
|
|
697
|
+
const files = [];
|
|
698
|
+
const lines = text.split("\n");
|
|
699
|
+
let file = null;
|
|
700
|
+
let hunk = null;
|
|
701
|
+
let newLine = 0;
|
|
702
|
+
let oldLine = 0;
|
|
703
|
+
for (const line of lines) {
|
|
704
|
+
const header = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
705
|
+
if (header) {
|
|
706
|
+
file = {
|
|
707
|
+
path: header[2],
|
|
708
|
+
...header[1] !== header[2] ? { oldPath: header[1] } : {},
|
|
709
|
+
binary: false,
|
|
710
|
+
hunks: []
|
|
711
|
+
};
|
|
712
|
+
files.push(file);
|
|
713
|
+
hunk = null;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (!file) continue;
|
|
717
|
+
if (line.startsWith("Binary files") || line.startsWith("GIT binary patch")) {
|
|
718
|
+
file.binary = true;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (line.startsWith("new file mode") || line.startsWith("deleted file mode") || line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("rename from") || line.startsWith("rename to") || line.startsWith("similarity index") || line.startsWith("old mode") || line.startsWith("new mode")) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
725
|
+
if (hunkMatch) {
|
|
726
|
+
hunk = { header: line, lines: [] };
|
|
727
|
+
file.hunks.push(hunk);
|
|
728
|
+
oldLine = Number(hunkMatch[1]);
|
|
729
|
+
newLine = Number(hunkMatch[2]);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (!hunk) continue;
|
|
733
|
+
if (line.startsWith("+")) {
|
|
734
|
+
hunk.lines.push({ kind: "add", text: line.slice(1), newLine });
|
|
735
|
+
newLine++;
|
|
736
|
+
} else if (line.startsWith("-")) {
|
|
737
|
+
hunk.lines.push({ kind: "del", text: line.slice(1), oldLine });
|
|
738
|
+
oldLine++;
|
|
739
|
+
} else if (line.startsWith(" ")) {
|
|
740
|
+
hunk.lines.push({ kind: "ctx", text: line.slice(1), newLine, oldLine });
|
|
741
|
+
newLine++;
|
|
742
|
+
oldLine++;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return files;
|
|
746
|
+
}
|
|
747
|
+
function addedLines(file) {
|
|
748
|
+
const out = [];
|
|
749
|
+
for (const hunk of file.hunks) {
|
|
750
|
+
for (const l of hunk.lines) {
|
|
751
|
+
if (l.kind === "add" && l.newLine !== void 0) out.push({ text: l.text, line: l.newLine });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
function repoDirs(config, root) {
|
|
757
|
+
const superPath = expandPath(config.repos.super, root);
|
|
758
|
+
const dirs = [{ repo: "(super)", dir: superPath }];
|
|
759
|
+
for (const member of config.repos.members) {
|
|
760
|
+
const dir = join4(superPath, member);
|
|
761
|
+
if (existsSync4(join4(dir, ".git"))) dirs.push({ repo: member, dir });
|
|
762
|
+
}
|
|
763
|
+
return dirs;
|
|
764
|
+
}
|
|
765
|
+
function dirtyFiles(repoDir) {
|
|
766
|
+
const modified = [];
|
|
767
|
+
const untracked = [];
|
|
768
|
+
const res = git(repoDir, ["status", "--porcelain=v1"]);
|
|
769
|
+
if (res.code !== 0) return { modified, untracked };
|
|
770
|
+
for (const line of res.stdout.split("\n")) {
|
|
771
|
+
if (!line.trim()) continue;
|
|
772
|
+
const xy = line.slice(0, 2);
|
|
773
|
+
const path = line.slice(3).trim().replace(/"(.*)"/, "$1");
|
|
774
|
+
if (!path) continue;
|
|
775
|
+
if (xy.includes("?")) untracked.push(path);
|
|
776
|
+
else modified.push(path);
|
|
777
|
+
}
|
|
778
|
+
if (untracked.length > 0) {
|
|
779
|
+
const ls = git(repoDir, ["ls-files", "--others", "--exclude-standard"]);
|
|
780
|
+
const dirs = untracked.filter((p) => p.endsWith("/"));
|
|
781
|
+
if (ls.code === 0 && dirs.length > 0) {
|
|
782
|
+
const exact = ls.stdout.split("\n").filter(Boolean);
|
|
783
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
784
|
+
for (const p of untracked) {
|
|
785
|
+
if (!p.endsWith("/")) {
|
|
786
|
+
expanded.add(p);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const prefix = p.slice(0, -1);
|
|
790
|
+
for (const f of exact) {
|
|
791
|
+
if (f === prefix || f.startsWith(prefix + "/")) expanded.add(f);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
untracked.length = 0;
|
|
795
|
+
untracked.push(...expanded);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return { modified, untracked };
|
|
799
|
+
}
|
|
800
|
+
function dirtySet(config, root) {
|
|
801
|
+
const out = [];
|
|
802
|
+
for (const { repo, dir } of repoDirs(config, root)) {
|
|
803
|
+
const { modified, untracked } = dirtyFiles(dir);
|
|
804
|
+
const files = [];
|
|
805
|
+
const seen = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const path of [...modified, ...untracked]) {
|
|
807
|
+
if (seen.has(path)) continue;
|
|
808
|
+
seen.add(path);
|
|
809
|
+
if (untracked.includes(path)) {
|
|
810
|
+
try {
|
|
811
|
+
const content = readFileSync5(join4(dir, path), "utf8");
|
|
812
|
+
const lines = content.split("\n");
|
|
813
|
+
files.push({
|
|
814
|
+
path,
|
|
815
|
+
binary: false,
|
|
816
|
+
hunks: [{ header: "@@ new file @@", lines: lines.map((text, i) => ({ kind: "add", text, newLine: i + 1 })) }]
|
|
817
|
+
});
|
|
818
|
+
} catch {
|
|
819
|
+
files.push({ path, binary: true, hunks: [] });
|
|
820
|
+
}
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const combined = git(dir, ["diff", "HEAD", "--", path]).stdout + git(dir, ["diff", "--cached", "--", path]).stdout;
|
|
824
|
+
const parsed = parseUnifiedDiff(combined);
|
|
825
|
+
files.push(...parsed);
|
|
826
|
+
}
|
|
827
|
+
if (files.length > 0) out.push({ repo, repoDir: dir, files });
|
|
828
|
+
}
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/gates/deterministic/rules.ts
|
|
833
|
+
var SILENT_DEFAULT_PATTERNS = [
|
|
834
|
+
{ re: /\.unwrap_or_default\(\)/, note: "unwrap_or_default() silently substitutes a default; per RULE-1 malformed/missing values must be errors" },
|
|
835
|
+
{ re: /\.unwrap_or\(\s*(0|false|true|None)\s*\)/, note: "unwrap_or(literal) silently substitutes a value; per RULE-1 malformed/missing values must be errors" },
|
|
836
|
+
{ re: /\.unwrap_or\(\s*(String::new\(\)|Vec::new\(\)|vec!\[\]|Default::default\(\))\s*\)/, note: "unwrap_or(empty container) silently substitutes; per RULE-1 malformed/missing values must be errors" },
|
|
837
|
+
{ re: /Ok\(\s*_\s*\)\s*=>\s*(0|false|true|None|Default::default\(\))/, note: "Ok(_) arm returns a literal default; per RULE-1 this maps a parsed value to a silent default" },
|
|
838
|
+
{ re: /Err\(\s*_?\s*\)\s*=>\s*(0|false|true|None|Default::default\(\)|vec!\[\])/, note: "Err(_) arm returns a literal instead of propagating; per RULE-1 errors must not become values" },
|
|
839
|
+
{ re: /\bcatch\s*\{\s*\}/, note: "empty catch block swallows failures; per RULE-1 unreadable/malformed input must be an error" }
|
|
840
|
+
];
|
|
841
|
+
var TODO_BARE = /TODO(?!\s*\([a-z0-9-]+\))/;
|
|
842
|
+
var TODO_TAGGED = /TODO\([a-z0-9-]+\)/;
|
|
843
|
+
var EM_DASH = /\u2014/;
|
|
844
|
+
var AI_TRAILER = /^\s*(Co-Authored-By:.*\b(Claude|Copilot|GPT|ChatGPT|Gemini|Cursor)\b|Generated with)/i;
|
|
845
|
+
var SENSITIVE_AREA = /(?<![A-Za-z])(sealed?|sealing|seals?|digests?|disclosure|keygen|keys?)(?![A-Za-z])/i;
|
|
846
|
+
var FUEL_DECL = /((fn|const|static)\s+[A-Za-z_]*(fuel|budget|limit)[A-Za-z_]*|let\s+mut\s+[A-Za-z_]*(fuel|budget|limit)[A-Za-z_]*|:\s*(Fuel|Budget|Limit)[<\s>])/i;
|
|
847
|
+
function checkFile(repo, file, config) {
|
|
848
|
+
const findings = [];
|
|
849
|
+
const adds = addedLines(file);
|
|
850
|
+
if (adds.length === 0) return findings;
|
|
851
|
+
const pathSensitive = SENSITIVE_AREA.test(file.path);
|
|
852
|
+
for (const { text, line } of adds) {
|
|
853
|
+
for (const { re, note } of SILENT_DEFAULT_PATTERNS) {
|
|
854
|
+
if (re.test(text)) {
|
|
855
|
+
findings.push({ rule: "RULE-1", severity: "review", repo, file: file.path, line, evidence: text.trim().slice(0, 120), note });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (/TODO/.test(text) && TODO_BARE.test(text) && !TODO_TAGGED.test(text)) {
|
|
859
|
+
findings.push({
|
|
860
|
+
rule: "RULE-7",
|
|
861
|
+
severity: "review",
|
|
862
|
+
repo,
|
|
863
|
+
file: file.path,
|
|
864
|
+
line,
|
|
865
|
+
evidence: text.trim().slice(0, 120),
|
|
866
|
+
note: "TODO without a backend tag; every TODO must name the backend it waits on, e.g. TODO(node)"
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (EM_DASH.test(text)) {
|
|
870
|
+
findings.push({ rule: "RULE-6", severity: "block", repo, file: file.path, line, evidence: text.trim().slice(0, 120), note: "em dash in added line; house style bans em dashes" });
|
|
871
|
+
}
|
|
872
|
+
if (config.repos.members.some((m) => text.includes(`../${m}/`))) {
|
|
873
|
+
findings.push({
|
|
874
|
+
rule: "RULE-6",
|
|
875
|
+
severity: "review",
|
|
876
|
+
repo,
|
|
877
|
+
file: file.path,
|
|
878
|
+
line,
|
|
879
|
+
evidence: text.trim().slice(0, 120),
|
|
880
|
+
note: "relative path into a sibling repo; name the sibling instead of a relative path"
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
if (FUEL_DECL.test(text)) {
|
|
884
|
+
findings.push({
|
|
885
|
+
rule: "RULE-9",
|
|
886
|
+
severity: "review",
|
|
887
|
+
repo,
|
|
888
|
+
file: file.path,
|
|
889
|
+
line,
|
|
890
|
+
evidence: text.trim().slice(0, 120),
|
|
891
|
+
note: "fuel/budget/limit declaration; must state units and who pays"
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
if (pathSensitive || SENSITIVE_AREA.test(text) && /^(pub\s+)?(async\s+)?(fn|struct|enum|impl|trait|mod)\s/.test(text.trim())) {
|
|
895
|
+
findings.push({
|
|
896
|
+
rule: "RULE-10",
|
|
897
|
+
severity: "human",
|
|
898
|
+
repo,
|
|
899
|
+
file: file.path,
|
|
900
|
+
line,
|
|
901
|
+
evidence: text.trim().slice(0, 120),
|
|
902
|
+
note: "touches keys/seals/digests/disclosure; requires human eyes even if correct"
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
if (/^\s*pub\s+(async\s+)?(fn|struct|enum|trait|type|mod|const|static)\s/.test(text) && config.repos.reviewed.includes(repo)) {
|
|
906
|
+
findings.push({
|
|
907
|
+
rule: "RULE-2",
|
|
908
|
+
severity: "review",
|
|
909
|
+
repo,
|
|
910
|
+
file: file.path,
|
|
911
|
+
line,
|
|
912
|
+
evidence: text.trim().slice(0, 120),
|
|
913
|
+
note: `new public API in reviewed repo '${repo}'; requires an enumerated-surface argument listing existing public items that are missing`
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return findings;
|
|
918
|
+
}
|
|
919
|
+
function checkRepoDiff(diff, config) {
|
|
920
|
+
const out = [];
|
|
921
|
+
for (const file of diff.files) {
|
|
922
|
+
if (file.binary) continue;
|
|
923
|
+
out.push(...checkFile(diff.repo, file, config));
|
|
924
|
+
}
|
|
925
|
+
const testsInDiff = diff.files.some(
|
|
926
|
+
(f) => f.path.includes("test") || f.path.startsWith("tests/") || f.path.endsWith("tests.rs")
|
|
927
|
+
);
|
|
928
|
+
if (!testsInDiff) {
|
|
929
|
+
for (const file of diff.files) {
|
|
930
|
+
if (!file.path.endsWith(".rs") || !/^src\//.test(file.path) || file.path.includes("test")) continue;
|
|
931
|
+
const adds = addedLines(file);
|
|
932
|
+
const behavioral = adds.filter((a) => /^(pub\s+)?(async\s+)?(fn|impl|struct|enum|trait)\s|^\s+(pub\s+)?fn\s/.test(a.text.trim()));
|
|
933
|
+
if (behavioral.length > 0) {
|
|
934
|
+
out.push({
|
|
935
|
+
rule: "RULE-8",
|
|
936
|
+
severity: "review",
|
|
937
|
+
repo: diff.repo,
|
|
938
|
+
file: file.path,
|
|
939
|
+
line: behavioral[0]?.line ?? null,
|
|
940
|
+
evidence: `${behavioral.length} behavioral line(s) added in ${file.path}; no test file anywhere in this diff`,
|
|
941
|
+
note: "new behavior ships with a test that fails without it; this repo-diff touches no tests"
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return dedupe(out);
|
|
947
|
+
}
|
|
948
|
+
function checkCommitMessage(message, repo) {
|
|
949
|
+
const out = [];
|
|
950
|
+
for (const line of message.split("\n")) {
|
|
951
|
+
if (AI_TRAILER.test(line)) {
|
|
952
|
+
out.push({ rule: "RULE-6", severity: "block", repo, file: "(commit message)", line: null, evidence: line.trim().slice(0, 120), note: "AI-identifying trailer in commit message; house style bans them" });
|
|
953
|
+
}
|
|
954
|
+
if (EM_DASH.test(line)) {
|
|
955
|
+
out.push({ rule: "RULE-6", severity: "block", repo, file: "(commit message)", line: null, evidence: line.trim().slice(0, 120), note: "em dash in commit message; house style bans em dashes" });
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return out;
|
|
959
|
+
}
|
|
960
|
+
function dedupe(findings) {
|
|
961
|
+
const seen = /* @__PURE__ */ new Set();
|
|
962
|
+
const out = [];
|
|
963
|
+
for (const f of findings) {
|
|
964
|
+
const key = `${f.rule}|${f.repo}|${f.file}|${f.line}|${f.evidence}`;
|
|
965
|
+
if (seen.has(key)) continue;
|
|
966
|
+
seen.add(key);
|
|
967
|
+
out.push(f);
|
|
968
|
+
}
|
|
969
|
+
return out;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/gates/deterministic/cargo.ts
|
|
973
|
+
import { spawn } from "child_process";
|
|
974
|
+
function run(cmd, opts) {
|
|
975
|
+
return new Promise((resolve2) => {
|
|
976
|
+
let proc;
|
|
977
|
+
try {
|
|
978
|
+
proc = spawn(cmd[0], cmd.slice(1), { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"], signal: opts.signal });
|
|
979
|
+
} catch (err) {
|
|
980
|
+
resolve2({ code: null, stdout: "", stderr: String(err), timedOut: false });
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
let stdout = "";
|
|
984
|
+
let stderr = "";
|
|
985
|
+
let timedOut = false;
|
|
986
|
+
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
987
|
+
timedOut = true;
|
|
988
|
+
proc.kill("SIGKILL");
|
|
989
|
+
}, opts.timeoutMs) : void 0;
|
|
990
|
+
proc.stdout?.on("data", (d) => stdout += d.toString());
|
|
991
|
+
proc.stderr?.on("data", (d) => stderr += d.toString());
|
|
992
|
+
const done = (code, extra = "") => {
|
|
993
|
+
if (timer) clearTimeout(timer);
|
|
994
|
+
resolve2({ code, stdout, stderr: stderr + extra, timedOut });
|
|
995
|
+
};
|
|
996
|
+
proc.on("error", (err) => done(null, String(err)));
|
|
997
|
+
proc.on("close", (code) => done(code));
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
function summarizeTests(output) {
|
|
1001
|
+
const passed = [...output.matchAll(/test result: \w+\. (\d+) passed/g)].reduce((n, m) => n + Number(m[1]), 0);
|
|
1002
|
+
const failed = [...output.matchAll(/test result: \w+\. \d+ passed; (\d+) failed/g)].reduce((n, m) => n + Number(m[1]), 0);
|
|
1003
|
+
const suites = (output.match(/test result:/g) ?? []).length;
|
|
1004
|
+
return { passed, failed, suites };
|
|
1005
|
+
}
|
|
1006
|
+
async function cargoCheck(repoDir, opts = {}) {
|
|
1007
|
+
const args = ["--color", "never"];
|
|
1008
|
+
if (opts.package) args.push("-p", opts.package);
|
|
1009
|
+
const steps = [];
|
|
1010
|
+
const checkArgs = ["check", ...args];
|
|
1011
|
+
const check = await run(["cargo", ...checkArgs], { cwd: repoDir, timeoutMs: opts.timeoutMs ?? 10 * 6e4, ...opts.signal ? { signal: opts.signal } : {} });
|
|
1012
|
+
const checkOut = check.stdout + "\n" + check.stderr;
|
|
1013
|
+
steps.push({
|
|
1014
|
+
label: "cargo check",
|
|
1015
|
+
ok: !check.timedOut && check.code === 0,
|
|
1016
|
+
summary: check.timedOut ? "TIMED OUT" : check.code === 0 ? "OK" : `FAILED (exit ${check.code})`,
|
|
1017
|
+
errors: errorLines(checkOut),
|
|
1018
|
+
output: checkOut
|
|
1019
|
+
});
|
|
1020
|
+
if (!steps[0].ok) return steps;
|
|
1021
|
+
if (opts.tests) {
|
|
1022
|
+
const test = await run(["cargo", "test", ...args], { cwd: repoDir, timeoutMs: opts.timeoutMs ?? 10 * 6e4, ...opts.signal ? { signal: opts.signal } : {} });
|
|
1023
|
+
const testOut = test.stdout + "\n" + test.stderr;
|
|
1024
|
+
const t = summarizeTests(testOut);
|
|
1025
|
+
steps.push({
|
|
1026
|
+
label: "cargo test",
|
|
1027
|
+
ok: !test.timedOut && test.code === 0,
|
|
1028
|
+
summary: test.timedOut ? "TIMED OUT" : test.code === 0 ? `OK: ${t.suites} suite(s), ${t.passed} passed, ${t.failed} failed` : `FAILED (exit ${test.code})`,
|
|
1029
|
+
errors: errorLines(testOut),
|
|
1030
|
+
output: testOut
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
return steps;
|
|
1034
|
+
}
|
|
1035
|
+
function errorLines(output) {
|
|
1036
|
+
return output.split("\n").filter((l) => /\berror(\[|:)/i.test(l) || /^failures:$/m.test(l) || /test result: FAILED/i.test(l)).slice(0, 30);
|
|
1037
|
+
}
|
|
1038
|
+
function parseCargoJson(output) {
|
|
1039
|
+
const diags = [];
|
|
1040
|
+
for (const line of output.split("\n")) {
|
|
1041
|
+
const trimmed = line.trim();
|
|
1042
|
+
if (!trimmed.startsWith("{")) continue;
|
|
1043
|
+
let msg;
|
|
1044
|
+
try {
|
|
1045
|
+
msg = JSON.parse(trimmed);
|
|
1046
|
+
} catch {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (msg?.reason !== "compiler-message" || !msg.message) continue;
|
|
1050
|
+
const m = msg.message;
|
|
1051
|
+
const spans = Array.isArray(m.spans) ? m.spans : [];
|
|
1052
|
+
const primary = spans.find((s) => s.is_primary) ?? spans[0];
|
|
1053
|
+
diags.push({
|
|
1054
|
+
file: primary?.file_name ?? "",
|
|
1055
|
+
line: primary?.line_start ?? null,
|
|
1056
|
+
col: primary?.column_start ?? null,
|
|
1057
|
+
severity: m.level ?? "unknown",
|
|
1058
|
+
message: m.message ?? "",
|
|
1059
|
+
code: m.code?.code ?? null,
|
|
1060
|
+
isRootCauseCandidate: m.level === "error" && (!m.code || !CASCADING_CODES.includes(m.code.code)) || m.level === "error: aborting"
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
return diags;
|
|
1064
|
+
}
|
|
1065
|
+
var CASCADING_CODES = ["E0432", "E0433", "E0603", "E0560"];
|
|
1066
|
+
|
|
1067
|
+
// src/gates/deterministic/index.ts
|
|
1068
|
+
async function runDeterministic(config, root, opts = {}) {
|
|
1069
|
+
const repoDiffs = opts.diffs ?? dirtySet(config, root);
|
|
1070
|
+
const findings = [];
|
|
1071
|
+
for (const diff of repoDiffs) {
|
|
1072
|
+
if (opts.repos && !opts.repos.includes(diff.repo)) continue;
|
|
1073
|
+
findings.push(...checkRepoDiff(diff, config));
|
|
1074
|
+
}
|
|
1075
|
+
const cargo = [];
|
|
1076
|
+
if (opts.cargo !== false) {
|
|
1077
|
+
for (const diff of repoDiffs) {
|
|
1078
|
+
if (opts.repos && !opts.repos.includes(diff.repo)) continue;
|
|
1079
|
+
const rustTouched = diff.files.some((f) => f.path.endsWith(".rs") || f.path.endsWith("Cargo.toml"));
|
|
1080
|
+
if (!rustTouched || diff.repo === "(super)") continue;
|
|
1081
|
+
const steps = await cargoCheck(diff.repoDir, { tests: opts.tests ?? false });
|
|
1082
|
+
cargo.push({ repo: diff.repo, steps });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return { repoDiffs, findings, cargo };
|
|
1086
|
+
}
|
|
1087
|
+
var SEVERITY_ORDER = { block: 0, human: 1, review: 2 };
|
|
1088
|
+
function formatFindings(findings) {
|
|
1089
|
+
if (findings.length === 0) return "no deterministic findings";
|
|
1090
|
+
const sorted = [...findings].sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
1091
|
+
const bySeverity = /* @__PURE__ */ new Map();
|
|
1092
|
+
for (const f of sorted) bySeverity.set(f.severity, [...bySeverity.get(f.severity) ?? [], f]);
|
|
1093
|
+
const out = [];
|
|
1094
|
+
for (const [sev, fs] of bySeverity) {
|
|
1095
|
+
out.push(`${sev.toUpperCase()} (${fs.length}):`);
|
|
1096
|
+
for (const f of fs) {
|
|
1097
|
+
out.push(` ${f.rule} ${f.repo}/${f.file}:${f.line ?? "-"}: ${f.note}`);
|
|
1098
|
+
out.push(` | ${f.evidence}`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return out.join("\n");
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/loop.ts
|
|
1105
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
1106
|
+
import { dirname as dirname3 } from "path";
|
|
1107
|
+
function diffFingerprint(diffText) {
|
|
1108
|
+
return sha256Hex(diffText);
|
|
1109
|
+
}
|
|
1110
|
+
var LoopTracker = class {
|
|
1111
|
+
path;
|
|
1112
|
+
state;
|
|
1113
|
+
constructor(statePath) {
|
|
1114
|
+
this.path = statePath;
|
|
1115
|
+
this.state = { cycles: [] };
|
|
1116
|
+
if (existsSync5(statePath)) {
|
|
1117
|
+
try {
|
|
1118
|
+
this.state = JSON.parse(readFileSync6(statePath, "utf8"));
|
|
1119
|
+
} catch {
|
|
1120
|
+
this.state = { cycles: [] };
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
get history() {
|
|
1125
|
+
return this.state.cycles;
|
|
1126
|
+
}
|
|
1127
|
+
record(cycle) {
|
|
1128
|
+
this.state.cycles = [...this.state.cycles.slice(-9), cycle];
|
|
1129
|
+
mkdirSync3(dirname3(this.path), { recursive: true });
|
|
1130
|
+
writeFileSync3(this.path, JSON.stringify(this.state), "utf8");
|
|
1131
|
+
}
|
|
1132
|
+
reset() {
|
|
1133
|
+
this.state = { cycles: [] };
|
|
1134
|
+
if (existsSync5(this.path)) mkdirSync3(dirname3(this.path), { recursive: true });
|
|
1135
|
+
writeFileSync3(this.path, JSON.stringify(this.state), "utf8");
|
|
1136
|
+
}
|
|
1137
|
+
evaluate(diffHash, reasons, maxCycles) {
|
|
1138
|
+
const last = this.state.cycles[this.state.cycles.length - 1];
|
|
1139
|
+
if (!last) return { cycle: 1, repeatedDiff: false, escalate: false };
|
|
1140
|
+
const repeatedDiff = last.diffHash === diffHash;
|
|
1141
|
+
const reasonsOverlap2 = reasons.filter((r) => last.topReasons.some((prev) => overlapTokens(prev, r) > 0.5)).length >= Math.min(2, reasons.length);
|
|
1142
|
+
const cycle = repeatedDiff || reasonsOverlap2 ? this.consecutiveCount(diffHash, reasons) + 1 : 1;
|
|
1143
|
+
if (cycle >= maxCycles) {
|
|
1144
|
+
return {
|
|
1145
|
+
cycle,
|
|
1146
|
+
repeatedDiff,
|
|
1147
|
+
escalate: true,
|
|
1148
|
+
reason: `circuit breaker: ${cycle} consecutive wrangler cycles without progress (repeated diff: ${repeatedDiff}, same findings: ${reasonsOverlap2}); stop retrying and surface to the user`
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
return { cycle, repeatedDiff, escalate: false };
|
|
1152
|
+
}
|
|
1153
|
+
consecutiveCount(diffHash, reasons) {
|
|
1154
|
+
let n = 0;
|
|
1155
|
+
for (let i = this.state.cycles.length - 1; i >= 0; i--) {
|
|
1156
|
+
const c = this.state.cycles[i];
|
|
1157
|
+
if (c.diffHash === diffHash || reasonsOverlap(c.topReasons, reasons)) n++;
|
|
1158
|
+
else break;
|
|
1159
|
+
}
|
|
1160
|
+
return n;
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
function overlapTokens(a, b) {
|
|
1164
|
+
const ta = new Set(a.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 3));
|
|
1165
|
+
const tb = new Set(b.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 3));
|
|
1166
|
+
if (ta.size === 0 || tb.size === 0) return 0;
|
|
1167
|
+
let shared = 0;
|
|
1168
|
+
for (const t of tb) if (ta.has(t)) shared++;
|
|
1169
|
+
return shared / Math.max(ta.size, tb.size);
|
|
1170
|
+
}
|
|
1171
|
+
function reasonsOverlap(a, b) {
|
|
1172
|
+
return b.some((r) => a.some((prev) => overlapTokens(prev, r) > 0.5));
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/ledger.ts
|
|
1176
|
+
import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7 } from "fs";
|
|
1177
|
+
import { dirname as dirname4 } from "path";
|
|
1178
|
+
var Ledger = class {
|
|
1179
|
+
path;
|
|
1180
|
+
constructor(config, root) {
|
|
1181
|
+
this.path = expandPath(config.ledger.path, root);
|
|
1182
|
+
}
|
|
1183
|
+
get filePath() {
|
|
1184
|
+
return this.path;
|
|
1185
|
+
}
|
|
1186
|
+
append(entry) {
|
|
1187
|
+
mkdirSync4(dirname4(this.path), { recursive: true });
|
|
1188
|
+
appendFileSync(this.path, JSON.stringify(entry) + "\n", "utf8");
|
|
1189
|
+
}
|
|
1190
|
+
read() {
|
|
1191
|
+
if (!existsSync6(this.path)) return [];
|
|
1192
|
+
return readFileSync7(this.path, "utf8").split("\n").filter(Boolean).map((line) => {
|
|
1193
|
+
try {
|
|
1194
|
+
return JSON.parse(line);
|
|
1195
|
+
} catch {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
}).filter((e) => e !== null);
|
|
1199
|
+
}
|
|
1200
|
+
recent(kind, n = 20) {
|
|
1201
|
+
return this.read().filter((e) => kind ? e.kind === kind : true).slice(-n);
|
|
1202
|
+
}
|
|
1203
|
+
acksFor(diffHash) {
|
|
1204
|
+
return this.read().filter((e) => e.kind === "ack" && e.diffHash === diffHash);
|
|
1205
|
+
}
|
|
1206
|
+
latestAckFor(diffHash) {
|
|
1207
|
+
const acks = this.acksFor(diffHash);
|
|
1208
|
+
return acks.length > 0 ? acks[acks.length - 1] : null;
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
export {
|
|
1213
|
+
PRECEDENCE,
|
|
1214
|
+
stripJsonc,
|
|
1215
|
+
parseJsonc,
|
|
1216
|
+
RuntimeOverridesSchema,
|
|
1217
|
+
readRuntimeOverridesFile,
|
|
1218
|
+
writeRuntimeOverridesFile,
|
|
1219
|
+
clearRuntimeOverrides,
|
|
1220
|
+
applyRuntimeOverrides,
|
|
1221
|
+
ConfigSchema,
|
|
1222
|
+
expandPath,
|
|
1223
|
+
findConfigFile,
|
|
1224
|
+
flattenGates,
|
|
1225
|
+
applyGateFlag,
|
|
1226
|
+
applyEnvOverrides,
|
|
1227
|
+
loadConfig,
|
|
1228
|
+
activeThresholds,
|
|
1229
|
+
sha256Hex,
|
|
1230
|
+
shortHash,
|
|
1231
|
+
splitMarkdown,
|
|
1232
|
+
slug,
|
|
1233
|
+
parseSpec,
|
|
1234
|
+
parseRules,
|
|
1235
|
+
parseDivergences,
|
|
1236
|
+
parseQuestions,
|
|
1237
|
+
parsePlan,
|
|
1238
|
+
parseDoc,
|
|
1239
|
+
parsePaper,
|
|
1240
|
+
ensurePaperText,
|
|
1241
|
+
loadCorpus,
|
|
1242
|
+
loadCorpusFrom,
|
|
1243
|
+
parseCargoJson,
|
|
1244
|
+
checkRepoDiff,
|
|
1245
|
+
checkCommitMessage,
|
|
1246
|
+
runDeterministic,
|
|
1247
|
+
formatFindings,
|
|
1248
|
+
diffFingerprint,
|
|
1249
|
+
LoopTracker,
|
|
1250
|
+
Ledger
|
|
1251
|
+
};
|
|
1252
|
+
//# sourceMappingURL=chunk-EAQASAXL.js.map
|