@kodr/aiwrangler 0.1.1 → 0.1.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/CHANGELOG.md +27 -0
- package/README.md +34 -12
- package/dist/{chunk-NQFO47CV.js → chunk-4D4NYPL3.js} +5 -5
- package/dist/{chunk-EAQASAXL.js → chunk-F3YFQ4NW.js} +18 -403
- package/dist/chunk-F3YFQ4NW.js.map +1 -0
- package/dist/{chunk-EDVWHCQV.js → chunk-HT3QDS3C.js} +2 -2
- package/dist/{chunk-2CIV4KGU.js → chunk-RZWCNEZS.js} +408 -21
- package/dist/chunk-RZWCNEZS.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +10 -1
- package/dist/index.js +22 -16
- package/dist/mcp.js +4 -4
- package/dist/plugin.js +184 -5
- package/dist/plugin.js.map +1 -1
- package/dist/tools.js +4 -4
- package/package.json +11 -5
- package/dist/chunk-2CIV4KGU.js.map +0 -1
- package/dist/chunk-EAQASAXL.js.map +0 -1
- /package/dist/{chunk-NQFO47CV.js.map → chunk-4D4NYPL3.js.map} +0 -0
- /package/dist/{chunk-EDVWHCQV.js.map → chunk-HT3QDS3C.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 0.1.2 - 2026-10-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- OpenCode V2 plugin entrypoint. `@kodr/aiwrangler/plugin` now default-exports
|
|
13
|
+
a V2 plugin (id `aiwrangler.nudge`) alongside the unchanged V1 named export
|
|
14
|
+
`WranglerWatchPlugin`: add `@kodr/aiwrangler` to the `plugins` array in
|
|
15
|
+
`opencode.json`. On session completion (`session.execution.succeeded`, with
|
|
16
|
+
V1's `session.idle` kept as a deduped fallback) it runs the deterministic
|
|
17
|
+
gates against the dirty diff and nudges the session to fix blocking
|
|
18
|
+
findings (fingerprint-deduped, capped at `gates.session.maxNudges`, one
|
|
19
|
+
nudge per completion even with multiple opencode locations loaded
|
|
20
|
+
(single-actor lease on plugin storage). Session directories come from the
|
|
21
|
+
event's location, and diagnostics go to `<repo>/.aiwrangler/plugin.log`.
|
|
22
|
+
The V2 half activates only when `@opencode/plugin` resolves; V1 installs
|
|
23
|
+
keep working unchanged.
|
|
24
|
+
- `runDeterministic`, `formatFindings`, and `diffFingerprint` are now exported
|
|
25
|
+
from the package root (previously internal, forcing external consumers to
|
|
26
|
+
import from `src/` paths).
|
|
27
|
+
- `verify:plugin` (part of `npm test`) asserts the built dual entrypoint:
|
|
28
|
+
V1 named export, V2 `id`/`setup`, and the `server()` bridge.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- `@opencode/plugin` (>=2.1.1) is an optional peer dependency, like
|
|
33
|
+
`@opencode-ai/plugin`: neither host SDK is required at install time.
|
|
34
|
+
|
|
8
35
|
## 0.1.1 - 2026-10-19
|
|
9
36
|
|
|
10
37
|
### Added
|
package/README.md
CHANGED
|
@@ -222,27 +222,46 @@ npm link
|
|
|
222
222
|
|
|
223
223
|
### opencode (native)
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
**OpenCode 2.x**: add the plugin package to `opencode.json` (global
|
|
226
|
+
`~/.config/opencode/opencode.json`, or the project's own):
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
```jsonc
|
|
229
|
+
{
|
|
230
|
+
"$schema": "https://opencode.ai/config.json",
|
|
231
|
+
"plugins": [{ "package": "@kodr/aiwrangler" }]
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The plugin is inert in repos without an `aiwrangler.config.json` (or
|
|
236
|
+
`AIWRANGLER_CONFIG`), and when `gates.session.nudge` is off. On session
|
|
237
|
+
completion it runs the deterministic gates against the dirty diff and
|
|
238
|
+
injects a fix-nudge into the session (fingerprint-deduped, capped at
|
|
239
|
+
`gates.session.maxNudges`, exactly one nudge per completion even when
|
|
240
|
+
several opencode locations are loaded. Its diagnostics land in
|
|
241
|
+
`<repo>/.aiwrangler/plugin.log`.
|
|
242
|
+
|
|
243
|
+
**OpenCode 1.x (legacy)**: make the package resolvable from the opencode
|
|
244
|
+
config dir (the `npm link` above covers it), then add two small files.
|
|
245
|
+
`~/.config/opencode/plugins/wrangler-watch.ts` re-exports the named export:
|
|
229
246
|
|
|
230
247
|
```ts
|
|
231
|
-
export {
|
|
248
|
+
export { WranglerWatchPlugin } from "@kodr/aiwrangler/plugin"
|
|
232
249
|
```
|
|
233
250
|
|
|
234
|
-
`~/.config/opencode/
|
|
251
|
+
`~/.config/opencode/tools/wrangler.ts` re-exports the native tools:
|
|
235
252
|
|
|
236
253
|
```ts
|
|
237
|
-
export {
|
|
254
|
+
export { wrangler_config, wrangler_plan, wrangler_check, wrangler_ack, wrangler_override, wrangler_loop_reset } from "@kodr/aiwrangler/tools"
|
|
238
255
|
```
|
|
239
256
|
|
|
240
|
-
Restart opencode. The
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
`
|
|
257
|
+
Restart opencode. The MCP server and the plugin compose and do not overlap:
|
|
258
|
+
MCP gives the tools on demand (`wrangler_config`, `wrangler_check`,
|
|
259
|
+
acknowledgements), and the plugin adds the automatic nudge loop at session
|
|
260
|
+
completion. `@opencode-ai/plugin` and `@opencode/plugin` are both optional
|
|
261
|
+
peer dependencies: the host provides the one it runs, and the MCP server
|
|
262
|
+
and the `aiwrangler` CLI load neither. Prefer the MCP server
|
|
244
263
|
inside opencode for approvals: it can show an elicitation form in the
|
|
245
|
-
editor. The native tools
|
|
264
|
+
editor. The 1.x native tools have no MCP session, so a write falls back
|
|
246
265
|
to a desktop dialog. Neither path treats the agent's arguments as a yes.
|
|
247
266
|
|
|
248
267
|
### Any MCP client (Cursor, Claude Code, Windsurf, ...)
|
|
@@ -410,16 +429,19 @@ AIWRANGLER_MODEL=jev-1.13.0
|
|
|
410
429
|
| `diffs/<hash>.txt` | capped snapshot of the exact diff each check judged |
|
|
411
430
|
| `cache/` | content-addressed semantic answers (replay without re-billing) |
|
|
412
431
|
| `runtime-overrides.json` | active runtime overrides, if any (auto-expire on PASS) |
|
|
432
|
+
| `watch-state.json` | session-nudge loop state: last diff fingerprint, nudge count, green announcement |
|
|
433
|
+
| `plugin.log` | tail-truncated diagnostics from the opencode nudge plugin |
|
|
413
434
|
|
|
414
435
|
## Development
|
|
415
436
|
|
|
416
437
|
```
|
|
417
438
|
npm run build # library, owner CLI, and MCP server bundles
|
|
418
439
|
npm run typecheck
|
|
419
|
-
npm test # verify:p0–p4, verify:mcp, and verify:
|
|
440
|
+
npm test # verify:p0–p4, verify:mcp, verify:cli, and verify:plugin (what CI gates on)
|
|
420
441
|
npm run verify:p0 # config, corpus parsing, hash stability
|
|
421
442
|
npm run verify:mcp # MCP handshake; mock client accepts and declines elicitation
|
|
422
443
|
npm run verify:cli # TTY confirmation, --yes refusal, injected desktop dialog, diff-hash binding
|
|
444
|
+
npm run verify:plugin # built dual entrypoint: V1 named export + V2 default export
|
|
423
445
|
```
|
|
424
446
|
|
|
425
447
|
`verify:p1` through `verify:p4` cover rule detection, the plan gate, the
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
effectiveTruth,
|
|
3
3
|
formatReport,
|
|
4
|
+
loadCorpus,
|
|
4
5
|
runCheck,
|
|
5
6
|
runPlanGate,
|
|
6
7
|
sectionsOfKind
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RZWCNEZS.js";
|
|
8
9
|
import {
|
|
9
10
|
Jev
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-HT3QDS3C.js";
|
|
11
12
|
import {
|
|
12
13
|
Ledger,
|
|
13
14
|
activeThresholds,
|
|
@@ -17,10 +18,9 @@ import {
|
|
|
17
18
|
findConfigFile,
|
|
18
19
|
flattenGates,
|
|
19
20
|
loadConfig,
|
|
20
|
-
loadCorpus,
|
|
21
21
|
readRuntimeOverridesFile,
|
|
22
22
|
writeRuntimeOverridesFile
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-F3YFQ4NW.js";
|
|
24
24
|
|
|
25
25
|
// src/osdialog.ts
|
|
26
26
|
import { spawn } from "child_process";
|
|
@@ -519,4 +519,4 @@ export {
|
|
|
519
519
|
requestHumanOverride,
|
|
520
520
|
loopResetGate
|
|
521
521
|
};
|
|
522
|
-
//# sourceMappingURL=chunk-
|
|
522
|
+
//# sourceMappingURL=chunk-4D4NYPL3.js.map
|
|
@@ -1,13 +1,3 @@
|
|
|
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
1
|
// src/jsonc.ts
|
|
12
2
|
function stripJsonc(text) {
|
|
13
3
|
let out = "";
|
|
@@ -322,374 +312,12 @@ function shortHash(text) {
|
|
|
322
312
|
return sha256Hex(text).slice(0, 12);
|
|
323
313
|
}
|
|
324
314
|
|
|
325
|
-
// src/
|
|
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
|
|
315
|
+
// src/git/diff.ts
|
|
614
316
|
import { spawnSync } from "child_process";
|
|
615
|
-
import { existsSync as existsSync3,
|
|
317
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
616
318
|
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
319
|
function git(repoDir, args, timeoutMs = 3e4) {
|
|
692
|
-
const res =
|
|
320
|
+
const res = spawnSync("git", ["-C", repoDir, ...args], { encoding: "utf8", timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
|
|
693
321
|
if (res.error) return { code: null, stdout: "", stderr: String(res.error) };
|
|
694
322
|
return { code: res.status, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
695
323
|
}
|
|
@@ -757,8 +385,8 @@ function repoDirs(config, root) {
|
|
|
757
385
|
const superPath = expandPath(config.repos.super, root);
|
|
758
386
|
const dirs = [{ repo: "(super)", dir: superPath }];
|
|
759
387
|
for (const member of config.repos.members) {
|
|
760
|
-
const dir =
|
|
761
|
-
if (
|
|
388
|
+
const dir = join3(superPath, member);
|
|
389
|
+
if (existsSync3(join3(dir, ".git"))) dirs.push({ repo: member, dir });
|
|
762
390
|
}
|
|
763
391
|
return dirs;
|
|
764
392
|
}
|
|
@@ -808,7 +436,7 @@ function dirtySet(config, root) {
|
|
|
808
436
|
seen.add(path);
|
|
809
437
|
if (untracked.includes(path)) {
|
|
810
438
|
try {
|
|
811
|
-
const content =
|
|
439
|
+
const content = readFileSync3(join3(dir, path), "utf8");
|
|
812
440
|
const lines = content.split("\n");
|
|
813
441
|
files.push({
|
|
814
442
|
path,
|
|
@@ -1102,7 +730,7 @@ function formatFindings(findings) {
|
|
|
1102
730
|
}
|
|
1103
731
|
|
|
1104
732
|
// src/loop.ts
|
|
1105
|
-
import { existsSync as
|
|
733
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1106
734
|
import { dirname as dirname3 } from "path";
|
|
1107
735
|
function diffFingerprint(diffText) {
|
|
1108
736
|
return sha256Hex(diffText);
|
|
@@ -1113,9 +741,9 @@ var LoopTracker = class {
|
|
|
1113
741
|
constructor(statePath) {
|
|
1114
742
|
this.path = statePath;
|
|
1115
743
|
this.state = { cycles: [] };
|
|
1116
|
-
if (
|
|
744
|
+
if (existsSync4(statePath)) {
|
|
1117
745
|
try {
|
|
1118
|
-
this.state = JSON.parse(
|
|
746
|
+
this.state = JSON.parse(readFileSync4(statePath, "utf8"));
|
|
1119
747
|
} catch {
|
|
1120
748
|
this.state = { cycles: [] };
|
|
1121
749
|
}
|
|
@@ -1126,13 +754,13 @@ var LoopTracker = class {
|
|
|
1126
754
|
}
|
|
1127
755
|
record(cycle) {
|
|
1128
756
|
this.state.cycles = [...this.state.cycles.slice(-9), cycle];
|
|
1129
|
-
|
|
1130
|
-
|
|
757
|
+
mkdirSync2(dirname3(this.path), { recursive: true });
|
|
758
|
+
writeFileSync2(this.path, JSON.stringify(this.state), "utf8");
|
|
1131
759
|
}
|
|
1132
760
|
reset() {
|
|
1133
761
|
this.state = { cycles: [] };
|
|
1134
|
-
if (
|
|
1135
|
-
|
|
762
|
+
if (existsSync4(this.path)) mkdirSync2(dirname3(this.path), { recursive: true });
|
|
763
|
+
writeFileSync2(this.path, JSON.stringify(this.state), "utf8");
|
|
1136
764
|
}
|
|
1137
765
|
evaluate(diffHash, reasons, maxCycles) {
|
|
1138
766
|
const last = this.state.cycles[this.state.cycles.length - 1];
|
|
@@ -1173,7 +801,7 @@ function reasonsOverlap(a, b) {
|
|
|
1173
801
|
}
|
|
1174
802
|
|
|
1175
803
|
// src/ledger.ts
|
|
1176
|
-
import { appendFileSync, existsSync as
|
|
804
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
1177
805
|
import { dirname as dirname4 } from "path";
|
|
1178
806
|
var Ledger = class {
|
|
1179
807
|
path;
|
|
@@ -1184,12 +812,12 @@ var Ledger = class {
|
|
|
1184
812
|
return this.path;
|
|
1185
813
|
}
|
|
1186
814
|
append(entry) {
|
|
1187
|
-
|
|
815
|
+
mkdirSync3(dirname4(this.path), { recursive: true });
|
|
1188
816
|
appendFileSync(this.path, JSON.stringify(entry) + "\n", "utf8");
|
|
1189
817
|
}
|
|
1190
818
|
read() {
|
|
1191
|
-
if (!
|
|
1192
|
-
return
|
|
819
|
+
if (!existsSync5(this.path)) return [];
|
|
820
|
+
return readFileSync5(this.path, "utf8").split("\n").filter(Boolean).map((line) => {
|
|
1193
821
|
try {
|
|
1194
822
|
return JSON.parse(line);
|
|
1195
823
|
} catch {
|
|
@@ -1210,7 +838,6 @@ var Ledger = class {
|
|
|
1210
838
|
};
|
|
1211
839
|
|
|
1212
840
|
export {
|
|
1213
|
-
PRECEDENCE,
|
|
1214
841
|
stripJsonc,
|
|
1215
842
|
parseJsonc,
|
|
1216
843
|
RuntimeOverridesSchema,
|
|
@@ -1228,18 +855,6 @@ export {
|
|
|
1228
855
|
activeThresholds,
|
|
1229
856
|
sha256Hex,
|
|
1230
857
|
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
858
|
parseCargoJson,
|
|
1244
859
|
checkRepoDiff,
|
|
1245
860
|
checkCommitMessage,
|
|
@@ -1249,4 +864,4 @@ export {
|
|
|
1249
864
|
LoopTracker,
|
|
1250
865
|
Ledger
|
|
1251
866
|
};
|
|
1252
|
-
//# sourceMappingURL=chunk-
|
|
867
|
+
//# sourceMappingURL=chunk-F3YFQ4NW.js.map
|