clearotron 0.2.1 → 0.3.0-beta.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/INSTALL.md +5 -4
- package/bin/example.mjs +9 -5
- package/bin/onboard.mjs +19 -19
- package/bin/stop.mjs +65 -3
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +21 -0
- package/driver/declination-call.mjs +32 -0
- package/driver/driver.config.mjs +20 -0
- package/driver/engine/mcp/recording-server.mjs +4 -0
- package/driver/gateway.mjs +8 -3
- package/driver/knockout-assess-record.mjs +5 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +83 -2
- package/driver/predelivery-lint.mjs +22 -4
- package/driver/publish/knockout.mjs +12 -6
- package/driver/publish/render-knockout.mjs +133 -23
- package/driver/publish/report-data.mjs +13 -3
- package/driver/record-carry.mjs +2 -2
- package/driver/reference-score.mjs +1 -1
- package/driver/result-noun-fields.mjs +7 -0
- package/driver/skills/knockout-assess/SKILL.md +10 -4
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +66 -18
- package/driver/unit-inventory.mjs +47 -0
- package/driver/unit-state-verdict.mjs +8 -8
- package/driver/verify-knockout.mjs +9 -1
- package/driver/whatif-memo-run.mjs +45 -4
- package/mcp-server/CHANGELOG.md +2 -0
- package/mcp-server/lib/brief.mjs +15 -0
- package/mcp-server/lib/driver.mjs +6 -0
- package/mcp-server/lib/knockout.mjs +435 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +69 -4
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/drain-preflight.mjs +2 -2
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/headless-page.mjs +51 -2
- package/scripts/live-surface-check.mjs +86 -17
- package/scripts/render-check.mjs +61 -2
- package/scripts/deploy-test.sh +0 -309
package/driver/suite-census.json
CHANGED
|
@@ -213,6 +213,12 @@
|
|
|
213
213
|
"skips": 0,
|
|
214
214
|
"todos": 0
|
|
215
215
|
},
|
|
216
|
+
"a-dynamic-import-of-a-path-is-a-file-url.test.mjs": {
|
|
217
|
+
"tests": 3,
|
|
218
|
+
"asserts": 6,
|
|
219
|
+
"skips": 1,
|
|
220
|
+
"todos": 0
|
|
221
|
+
},
|
|
216
222
|
"a-fact-about-a-named-party.test.mjs": {
|
|
217
223
|
"tests": 30,
|
|
218
224
|
"asserts": 80,
|
|
@@ -298,8 +304,8 @@
|
|
|
298
304
|
"todos": 0
|
|
299
305
|
},
|
|
300
306
|
"a-memo-over-a-delivered-report-leaves-it-alone.test.mjs": {
|
|
301
|
-
"tests":
|
|
302
|
-
"asserts":
|
|
307
|
+
"tests": 13,
|
|
308
|
+
"asserts": 52,
|
|
303
309
|
"skips": 0,
|
|
304
310
|
"todos": 0
|
|
305
311
|
},
|
|
@@ -387,6 +393,12 @@
|
|
|
387
393
|
"skips": 0,
|
|
388
394
|
"todos": 0
|
|
389
395
|
},
|
|
396
|
+
"a-queued-what-if-is-answered-before-its-run-archives.test.mjs": {
|
|
397
|
+
"tests": 3,
|
|
398
|
+
"asserts": 12,
|
|
399
|
+
"skips": 0,
|
|
400
|
+
"todos": 0
|
|
401
|
+
},
|
|
390
402
|
"a-real-install-starts-with-an-empty-archive.test.mjs": {
|
|
391
403
|
"tests": 6,
|
|
392
404
|
"asserts": 16,
|
|
@@ -496,8 +508,8 @@
|
|
|
496
508
|
"todos": 0
|
|
497
509
|
},
|
|
498
510
|
"a-screenshot-of-an-error-page-is-not-a-report.test.mjs": {
|
|
499
|
-
"tests":
|
|
500
|
-
"asserts":
|
|
511
|
+
"tests": 21,
|
|
512
|
+
"asserts": 55,
|
|
501
513
|
"skips": 4,
|
|
502
514
|
"todos": 0
|
|
503
515
|
},
|
|
@@ -567,6 +579,12 @@
|
|
|
567
579
|
"skips": 0,
|
|
568
580
|
"todos": 0
|
|
569
581
|
},
|
|
582
|
+
"a-unit-is-attributed-by-what-it-runs-not-only-by-what-it-declares.test.mjs": {
|
|
583
|
+
"tests": 10,
|
|
584
|
+
"asserts": 27,
|
|
585
|
+
"skips": 0,
|
|
586
|
+
"todos": 0
|
|
587
|
+
},
|
|
570
588
|
"a-verb-is-a-module-until-it-is-the-command.test.mjs": {
|
|
571
589
|
"tests": 3,
|
|
572
590
|
"asserts": 8,
|
|
@@ -700,8 +718,8 @@
|
|
|
700
718
|
"todos": 0
|
|
701
719
|
},
|
|
702
720
|
"an-experiment-receipt-names-its-engine.test.mjs": {
|
|
703
|
-
"tests":
|
|
704
|
-
"asserts":
|
|
721
|
+
"tests": 8,
|
|
722
|
+
"asserts": 27,
|
|
705
723
|
"skips": 0,
|
|
706
724
|
"todos": 0
|
|
707
725
|
},
|
|
@@ -711,12 +729,24 @@
|
|
|
711
729
|
"skips": 1,
|
|
712
730
|
"todos": 0
|
|
713
731
|
},
|
|
732
|
+
"an-observed-model-id-says-whether-it-is-pinned.test.mjs": {
|
|
733
|
+
"tests": 4,
|
|
734
|
+
"asserts": 10,
|
|
735
|
+
"skips": 0,
|
|
736
|
+
"todos": 0
|
|
737
|
+
},
|
|
714
738
|
"an-ordered-field-is-a-field-the-tool-can-express.test.mjs": {
|
|
715
739
|
"tests": 1,
|
|
716
740
|
"asserts": 6,
|
|
717
741
|
"skips": 0,
|
|
718
742
|
"todos": 0
|
|
719
743
|
},
|
|
744
|
+
"an-unread-probe-is-not-a-failed-measurement.test.mjs": {
|
|
745
|
+
"tests": 5,
|
|
746
|
+
"asserts": 11,
|
|
747
|
+
"skips": 0,
|
|
748
|
+
"todos": 0
|
|
749
|
+
},
|
|
720
750
|
"anchor-and-script-aware-quote.test.mjs": {
|
|
721
751
|
"tests": 12,
|
|
722
752
|
"asserts": 40,
|
|
@@ -1348,8 +1378,8 @@
|
|
|
1348
1378
|
"todos": 0
|
|
1349
1379
|
},
|
|
1350
1380
|
"declination-is-stated-not-inferred.test.mjs": {
|
|
1351
|
-
"tests":
|
|
1352
|
-
"asserts":
|
|
1381
|
+
"tests": 24,
|
|
1382
|
+
"asserts": 69,
|
|
1353
1383
|
"skips": 0,
|
|
1354
1384
|
"todos": 0
|
|
1355
1385
|
},
|
|
@@ -1397,7 +1427,7 @@
|
|
|
1397
1427
|
},
|
|
1398
1428
|
"deployment-hostnames.test.mjs": {
|
|
1399
1429
|
"tests": 13,
|
|
1400
|
-
"asserts":
|
|
1430
|
+
"asserts": 36,
|
|
1401
1431
|
"skips": 0,
|
|
1402
1432
|
"todos": 0
|
|
1403
1433
|
},
|
|
@@ -2457,6 +2487,12 @@
|
|
|
2457
2487
|
"skips": 2,
|
|
2458
2488
|
"todos": 0
|
|
2459
2489
|
},
|
|
2490
|
+
"no-run-codenames-in-tracked-files.test.mjs": {
|
|
2491
|
+
"tests": 6,
|
|
2492
|
+
"asserts": 13,
|
|
2493
|
+
"skips": 4,
|
|
2494
|
+
"todos": 0
|
|
2495
|
+
},
|
|
2460
2496
|
"no-test-writes-inside-the-checkout.test.mjs": {
|
|
2461
2497
|
"tests": 16,
|
|
2462
2498
|
"asserts": 35,
|
|
@@ -3161,7 +3197,7 @@
|
|
|
3161
3197
|
},
|
|
3162
3198
|
"register-count.test.mjs": {
|
|
3163
3199
|
"tests": 20,
|
|
3164
|
-
"asserts":
|
|
3200
|
+
"asserts": 148,
|
|
3165
3201
|
"skips": 0,
|
|
3166
3202
|
"todos": 0
|
|
3167
3203
|
},
|
|
@@ -3262,8 +3298,8 @@
|
|
|
3262
3298
|
"todos": 0
|
|
3263
3299
|
},
|
|
3264
3300
|
"release-pipeline.test.mjs": {
|
|
3265
|
-
"tests":
|
|
3266
|
-
"asserts":
|
|
3301
|
+
"tests": 87,
|
|
3302
|
+
"asserts": 334,
|
|
3267
3303
|
"skips": 0,
|
|
3268
3304
|
"todos": 0
|
|
3269
3305
|
},
|
|
@@ -3880,8 +3916,8 @@
|
|
|
3880
3916
|
"todos": 0
|
|
3881
3917
|
},
|
|
3882
3918
|
"start-background-units.test.mjs": {
|
|
3883
|
-
"tests":
|
|
3884
|
-
"asserts":
|
|
3919
|
+
"tests": 9,
|
|
3920
|
+
"asserts": 26,
|
|
3885
3921
|
"skips": 0,
|
|
3886
3922
|
"todos": 0
|
|
3887
3923
|
},
|
|
@@ -4276,8 +4312,8 @@
|
|
|
4276
4312
|
"todos": 0
|
|
4277
4313
|
},
|
|
4278
4314
|
"the-narrative-is-checked-against-its-depth-rules.test.mjs": {
|
|
4279
|
-
"tests":
|
|
4280
|
-
"asserts":
|
|
4315
|
+
"tests": 19,
|
|
4316
|
+
"asserts": 70,
|
|
4281
4317
|
"skips": 0,
|
|
4282
4318
|
"todos": 0
|
|
4283
4319
|
},
|
|
@@ -4377,6 +4413,12 @@
|
|
|
4377
4413
|
"skips": 1,
|
|
4378
4414
|
"todos": 0
|
|
4379
4415
|
},
|
|
4416
|
+
"the-report-carries-what-the-assessment-wrote.test.mjs": {
|
|
4417
|
+
"tests": 10,
|
|
4418
|
+
"asserts": 41,
|
|
4419
|
+
"skips": 0,
|
|
4420
|
+
"todos": 0
|
|
4421
|
+
},
|
|
4380
4422
|
"the-report-shell-keeps-what-a-repair-does-not-resend.test.mjs": {
|
|
4381
4423
|
"tests": 6,
|
|
4382
4424
|
"asserts": 28,
|
|
@@ -4720,8 +4762,8 @@
|
|
|
4720
4762
|
"todos": 0
|
|
4721
4763
|
},
|
|
4722
4764
|
"verify-knockout.test.mjs": {
|
|
4723
|
-
"tests":
|
|
4724
|
-
"asserts":
|
|
4765
|
+
"tests": 2,
|
|
4766
|
+
"asserts": 11,
|
|
4725
4767
|
"skips": 0,
|
|
4726
4768
|
"todos": 0
|
|
4727
4769
|
},
|
|
@@ -4952,6 +4994,12 @@
|
|
|
4952
4994
|
"skips": 0,
|
|
4953
4995
|
"todos": 0
|
|
4954
4996
|
},
|
|
4997
|
+
"knockout-audit-tools.test.mjs": {
|
|
4998
|
+
"tests": 16,
|
|
4999
|
+
"asserts": 73,
|
|
5000
|
+
"skips": 0,
|
|
5001
|
+
"todos": 0
|
|
5002
|
+
},
|
|
4955
5003
|
"lexsearch.test.mjs": {
|
|
4956
5004
|
"tests": 5,
|
|
4957
5005
|
"asserts": 11,
|
|
@@ -641,6 +641,53 @@ export function unitInventoryVerdict({
|
|
|
641
641
|
* @param {string|null} raw the value `systemctl show -p WorkingDirectory` printed
|
|
642
642
|
* @returns {{path: string|null, prefixes: string, why: string|null}}
|
|
643
643
|
*/
|
|
644
|
+
/**
|
|
645
|
+
* Which checkout is a unit actually serving — and which piece of evidence said so.
|
|
646
|
+
*
|
|
647
|
+
* A unit can be attributed two ways and they are not equally good.
|
|
648
|
+
*
|
|
649
|
+
* `WorkingDirectory` is a DECLARATION. It is what the unit file asks for, and it is frequently not a
|
|
650
|
+
* checkout at all: a unit written as `WorkingDirectory=~` reports `!/home/<user>`, which resolves to a
|
|
651
|
+
* home directory that is not a git tree, and the unit then drops out of the comparison entirely. That is
|
|
652
|
+
* not a rare shape — it is what a unit looks like when its author put the checkout in `ExecStart=`
|
|
653
|
+
* instead, which is the normal thing to do, because systemd cannot expand a variable in
|
|
654
|
+
* `WorkingDirectory=` and can in `ExecStart=`.
|
|
655
|
+
*
|
|
656
|
+
* The RUNNING COMMAND LINE is an OBSERVATION: the absolute path the live process was actually started
|
|
657
|
+
* with. So it wins, and that order is the point rather than a preference. The incident this comparison
|
|
658
|
+
* exists for is a deployment that served its portal from one clone while running its runner from
|
|
659
|
+
* another — and in that state the declaration and the process disagree, with the process telling the
|
|
660
|
+
* truth. Preferring the declaration would take the wrong side of exactly the case the check is for.
|
|
661
|
+
*
|
|
662
|
+
* WHEN BOTH RESOLVE AND DISAGREE, that IS the finding, and it is returned rather than quietly resolved:
|
|
663
|
+
* a unit whose declared tree and running tree differ has been repointed since it started, and it is
|
|
664
|
+
* serving the older one until something restarts it.
|
|
665
|
+
*
|
|
666
|
+
* PURE — the caller does the reading (systemd, the unit file, the process table) and this decides.
|
|
667
|
+
*
|
|
668
|
+
* @param {object} a
|
|
669
|
+
* @param {string|null} a.declaredTree a git tree resolved from `WorkingDirectory`, or null
|
|
670
|
+
* @param {string|null} a.runningTree a git tree resolved from the live process's argv, or null
|
|
671
|
+
* @param {string|null} a.declaredWhy why the declaration gave nothing, if it gave nothing
|
|
672
|
+
* @param {string|null} a.runningWhy why the process gave nothing, if it gave nothing
|
|
673
|
+
* @returns {{clone: string|null, source: string|null, disagreement: string|null, why: string|null}}
|
|
674
|
+
*/
|
|
675
|
+
export function unitClone({ declaredTree = null, runningTree = null, declaredWhy = null, runningWhy = null } = {}) {
|
|
676
|
+
const norm = (p) => String(p ?? "").replace(/\/+$/, "") || null;
|
|
677
|
+
const declared = norm(declaredTree);
|
|
678
|
+
const running = norm(runningTree);
|
|
679
|
+
const disagreement = declared && running && declared !== running
|
|
680
|
+
? `the unit declares ${declared} and its running process was started from ${running} — it is serving the second until something restarts it`
|
|
681
|
+
: null;
|
|
682
|
+
if (running) return { clone: running, source: "the running command line", disagreement, why: null };
|
|
683
|
+
if (declared) return { clone: declared, source: "WorkingDirectory", disagreement, why: null };
|
|
684
|
+
// BOTH REASONS, never the first one that came to hand. A caller that reports only "no WorkingDirectory"
|
|
685
|
+
// sends the next reader to fix a unit file when the process table was the half that could not be read.
|
|
686
|
+
const parts = [declaredWhy, runningWhy].filter(Boolean);
|
|
687
|
+
return { clone: null, source: null, disagreement: null,
|
|
688
|
+
why: parts.length ? parts.join("; and ") : "neither the unit's WorkingDirectory nor its running command line named a checkout" };
|
|
689
|
+
}
|
|
690
|
+
|
|
644
691
|
export function unitWorkingDirectory(raw) {
|
|
645
692
|
const s = String(raw ?? "").trim();
|
|
646
693
|
if (!s) return { path: null, prefixes: "", why: "the unit reported no WorkingDirectory" };
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Anything that is neither `active` nor `inactive` is a fault. `prelim-driver` is a `Type=oneshot` fired
|
|
16
16
|
// by a 90-second timer, so EVERY drain passes through `activating` — and `activating` landed in that
|
|
17
|
-
// list. The filter tolerated the oneshot at rest and failed it for working.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// list. The filter tolerated the oneshot at rest and failed it for working. The hourly test-instance
|
|
18
|
+
// deploy gates on this script's exit code, so a deploy that happened to land inside a drain window
|
|
19
|
+
// reported the instance unhealthy and exited 1 on a deployment that was fine.
|
|
20
20
|
//
|
|
21
21
|
// The bug is not the enumeration — was right to give this three outcomes instead of two. The bug is
|
|
22
22
|
// INVERSE ENUMERATION over an incomplete vocabulary: listing the two states you have seen and calling
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// The ruling on says "starting and stopping are both tolerated for a one-shot". That is descriptive
|
|
40
40
|
// of the case that was observed, not a restriction: the issue's own Scope section carries no unit-type
|
|
41
41
|
// qualifier, the arm has never been unit-type-aware, and it already tolerates `inactive` for everything.
|
|
42
|
-
// It is also the wrong place to draw the line — deploy
|
|
42
|
+
// It is also the wrong place to draw the line — the hourly deploy RESTARTS the long-running services
|
|
43
43
|
// immediately before running this check, so `trademark-portal=activating` is the same race with a
|
|
44
44
|
// different unit, and a oneshot-only fix would leave it standing. The unit's Type is still carried into
|
|
45
45
|
// the message so a reader can tell a oneshot mid-fire from a service mid-restart.
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
// This is not caution for its own sake. An earlier draft of this fix also widened the count to treat
|
|
64
64
|
// transitional units as up, on the reasoning that a oneshot mid-fire is "not nothing". The consequence:
|
|
65
65
|
// {5 services activating, 3 inactive} returned `pass` with a message that began "0 active" — a green
|
|
66
|
-
// tick on the deploy's final gate having confirmed that ZERO services were running. deploy
|
|
66
|
+
// tick on the deploy's final gate having confirmed that ZERO services were running. The hourly deploy
|
|
67
67
|
// restarts the long-running services immediately before running this check, so that is the ordinary
|
|
68
|
-
// shape of the
|
|
68
|
+
// shape of the instance, not a corner. Widening a count to make a message read better is how a guard dies.
|
|
69
69
|
|
|
70
70
|
/** The six documented ActiveState values of systemd 255, mapped to what the check must do about them. */
|
|
71
71
|
export const ACTIVE_STATE_MEANING = Object.freeze({
|
|
@@ -134,8 +134,8 @@ export function unitsActiveVerdict({ units, probe }) {
|
|
|
134
134
|
// CHANGES NOTHING HERE. The count below is origin/main's, predicate and message unchanged:
|
|
135
135
|
// `active`, literally, and nothing else. An earlier draft of this fix widened it to count
|
|
136
136
|
// transitional units as up, which defeated this guard outright — {5 activating, 3 inactive} then
|
|
137
|
-
// returned `pass` with a message that began "0 active", on the deploy's FINAL gate, on
|
|
138
|
-
// deploy
|
|
137
|
+
// returned `pass` with a message that began "0 active", on the deploy's FINAL gate, on an instance
|
|
138
|
+
// where the hourly deploy has just restarted every long-running service. Scope names one thing, the
|
|
139
139
|
// state vocabulary of the FAULT branch above. Transitional units are named in the messages here so
|
|
140
140
|
// they are never swallowed; they are counted as up nowhere.
|
|
141
141
|
const activeCount = (units ?? []).filter((u) => u.active === "active").length;
|
|
@@ -442,7 +442,7 @@ export const validators = {
|
|
|
442
442
|
// — the register reads and the per-finding weighed lists, both joined to the store.
|
|
443
443
|
if (m.registerReads !== undefined && m.registerReads !== null) {
|
|
444
444
|
if (!Array.isArray(m.registerReads))
|
|
445
|
-
return { ok: false, reason: `mark "${m.name}": registerReads must be an ARRAY of { recordId, read } rows, or omitted entirely` };
|
|
445
|
+
return { ok: false, reason: `mark "${m.name}": registerReads must be an ARRAY of { recordId, read, band? } rows, or omitted entirely` };
|
|
446
446
|
const held = registerRecordIdsFor(runDir, m.name);
|
|
447
447
|
for (const row of m.registerReads) {
|
|
448
448
|
const id = String(row?.recordId ?? "").trim();
|
|
@@ -451,6 +451,14 @@ export const validators = {
|
|
|
451
451
|
if (!read) return { ok: false, reason: `mark "${m.name}": registerReads row "${id}" has an empty read. Omit the row rather than sending an empty one: a filing with no read keeps the card's neutral line, which is true` };
|
|
452
452
|
if (!held.has(id))
|
|
453
453
|
return { ok: false, reason: `mark "${m.name}": registerReads cites "${id}", which is not a record this run holds for that mark. The id must be copied from the filings you were handed — the driver joins it against the store and never takes your word for it` };
|
|
454
|
+
// The band is OPTIONAL and, when present, is the same closed vocabulary the mark's own rating
|
|
455
|
+
// uses — the card prints it as this filing's rating, so a word outside the frozen ladder would
|
|
456
|
+
// put a chip on the page in a dialect the framework does not define. Absence stays free: a read
|
|
457
|
+
// with no band prints the read and no chip, which is what every run before this one produced.
|
|
458
|
+
if (row?.band != null && String(row.band).trim()) {
|
|
459
|
+
if (ladder.length && bandIdx(ladder, row.band) < 0)
|
|
460
|
+
return { ok: false, reason: `knockout_band_unknown:${m.name}: registerReads row "${id}" carries band "${row.band}", which is not in the frozen ladder (${ladder.join(" / ")}) — rate the filing in the framework's own vocabulary, or omit the band and let the read stand alone` };
|
|
461
|
+
}
|
|
454
462
|
}
|
|
455
463
|
}
|
|
456
464
|
for (const f of (Array.isArray(m.findings) ? m.findings : [])) {
|
|
@@ -31,7 +31,7 @@ import { createHash } from "node:crypto";
|
|
|
31
31
|
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync, statSync } from "node:fs";
|
|
32
32
|
import { join, dirname } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
|
-
import { driverDir } from "../shared/driver-dir.mjs";
|
|
34
|
+
import { driverDir, DRIVER_DIR } from "../shared/driver-dir.mjs"; // the subtree name from its one definition, never a literal
|
|
35
35
|
import { composeMemo } from "./whatif-memo.mjs";
|
|
36
36
|
|
|
37
37
|
export const SKILL_ROOT = join(dirname(fileURLToPath(import.meta.url)), "skills");
|
|
@@ -85,6 +85,43 @@ export function movedArtifacts(before = {}, after = {}) {
|
|
|
85
85
|
.map((p) => ({ path: p, before: before[p] ?? null, after: after[p] ?? null }));
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Split what moved into the memo's OWN RECORD and everything else.
|
|
90
|
+
*
|
|
91
|
+
* A memo legitimately leaves two kinds of trace on its parent. Its documents go under `_memos/`, which
|
|
92
|
+
* `digestRunDir` already skips. Its DISPATCH leaves driver telemetry — `_driver/whatif-memo.*` and an
|
|
93
|
+
* appended line in `_driver/run.jsonl` — because the memo rides the same gateway ladder as any seat and
|
|
94
|
+
* the gateway records what it dispatched. That is the parent gaining a RECORD of the memo, which is
|
|
95
|
+
* correct behaviour and the same rule the experiment path already lives by; it is not the archive being
|
|
96
|
+
* rewritten.
|
|
97
|
+
*
|
|
98
|
+
* ✕ EVERYTHING ELSE IS THE SEAT WRITING WHERE IT WAS NOT INVITED, and it is refused by name. The seat
|
|
99
|
+
* has a real file-write grant on the run directory, and on a delivered run it used it: an archived
|
|
100
|
+
* client run carries `memo-osler-coexistence.json` at its root, which is the seat's reply written to a
|
|
101
|
+
* path of its own choosing instead of to the `expectFile` it was given. That also explains the retry
|
|
102
|
+
* that preceded it — the harness looked for the reply where it had asked for it and did not find it.
|
|
103
|
+
*
|
|
104
|
+
* `run.jsonl` may only GROW. A shrink or a rewrite is not an append, and an append is the only thing a
|
|
105
|
+
* dispatch does to it. The check is on size because the digest carries `sha:size` and not the bytes; a
|
|
106
|
+
* rewrite that happened to keep the same length would pass, and that is written down rather than
|
|
107
|
+
* claimed against. PURE.
|
|
108
|
+
*/
|
|
109
|
+
export function splitMemoFootprint(moved = []) {
|
|
110
|
+
const own = [];
|
|
111
|
+
const foreign = [];
|
|
112
|
+
const sizeOf = (v) => { const n = Number(String(v ?? "").split(":")[1]); return Number.isFinite(n) ? n : null; };
|
|
113
|
+
for (const m of moved) {
|
|
114
|
+
const p = String(m?.path ?? "");
|
|
115
|
+
if (p.startsWith(`${DRIVER_DIR}/whatif-memo.`)) { own.push(m); continue; }
|
|
116
|
+
if (p === `${DRIVER_DIR}/run.jsonl`) {
|
|
117
|
+
const b = sizeOf(m.before); const a = sizeOf(m.after);
|
|
118
|
+
if (b !== null && a !== null && a > b) { own.push(m); continue; }
|
|
119
|
+
}
|
|
120
|
+
foreign.push(m);
|
|
121
|
+
}
|
|
122
|
+
return { own, foreign };
|
|
123
|
+
}
|
|
124
|
+
|
|
88
125
|
/** The profile key the PARENT rated under — read from its frozen sidecar, never re-resolved. */
|
|
89
126
|
export function parentRatedUnder(runDir, { read = readFileSync } = {}) {
|
|
90
127
|
try { return JSON.parse(read(driverDir(runDir, "profile.json"), "utf8"))?.profileKey ?? null; }
|
|
@@ -299,10 +336,14 @@ export async function askArchivedRun({ runId, question, requestedBy = null } = {
|
|
|
299
336
|
|
|
300
337
|
// AFTER the write, deliberately: the check has to cover this call's own behaviour, not merely the
|
|
301
338
|
// reasoning pass's. If writing the memo touched the parent, that is exactly what must be caught.
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
339
|
+
const { own, foreign } = splitMemoFootprint(movedArtifacts(before, digestRunDir(run.runDir)));
|
|
340
|
+
if (foreign.length)
|
|
304
341
|
return { ok: false, fail: MEMO_FAILS.PARENT_MOVED,
|
|
305
|
-
|
|
342
|
+
// NAMES WHAT IT FOUND AND WHY IT IS WRONG. "the parent's artifacts moved" was true of a correct
|
|
343
|
+
// memo too — the dispatch telemetry tripped it — so the sentence could not tell a reader whether
|
|
344
|
+
// the archive had been rewritten or the memo had merely run.
|
|
345
|
+
detail: `a memo may write only to ${MEMO_DIR}/ and its own reply file; these were written into the parent run instead: ${foreign.slice(0, 5).map((m) => m.path).join(", ")}`,
|
|
346
|
+
moved: foreign, memoFootprint: own };
|
|
306
347
|
|
|
307
348
|
return {
|
|
308
349
|
ok: true,
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/lib/brief.mjs
CHANGED
|
@@ -135,6 +135,21 @@ export function buildBrief(run) {
|
|
|
135
135
|
lines.push(`- **${m.name}** — ${band}.${d.url ? ` Report: ${d.url}` : ""}`);
|
|
136
136
|
for (const f of (m.findings ?? [])) {
|
|
137
137
|
const who = [f.name, f.owner].filter(Boolean).join(" — ");
|
|
138
|
+
// A PROMOTED REGISTER FILING SHOWS THE RATING AND THE READ THE SEARCH ACTUALLY MADE (tracker
|
|
139
|
+
// issue 274). This line used to print `net` alone, and for a register card `net` carried the
|
|
140
|
+
// stated "no rating of its own" — so the one hard legal right on a page was described here as
|
|
141
|
+
// unrated even on runs where the assessment had written a full read of that exact filing and
|
|
142
|
+
// the report was already printing it. The page and this briefing disagreed.
|
|
143
|
+
//
|
|
144
|
+
// SCOPED TO `shape === 'register'` DELIBERATELY. Every other finding line is left exactly as it
|
|
145
|
+
// was: a typed conflict already leads with its own band on the report, and widening this to all
|
|
146
|
+
// findings would change what this briefing says about runs that have no register layer at all.
|
|
147
|
+
if (f.shape === "register") {
|
|
148
|
+
const rating = f.band ? ` — ${titleCase(String(f.band))} risk.` : "";
|
|
149
|
+
const read = f.basis && f.basis !== f.net ? ` ${f.basis}` : "";
|
|
150
|
+
lines.push(` - ${who}${rating}${f.net ? ` ${f.net}` : ""}${read}`.trimEnd());
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
138
153
|
lines.push(` - ${who}${f.net ? `: ${f.net}` : ""}`.trimEnd());
|
|
139
154
|
}
|
|
140
155
|
}
|
|
@@ -14,6 +14,12 @@ export {
|
|
|
14
14
|
REGISTER_AXES, decideAxes, axisTier,
|
|
15
15
|
} from "../../driver/stages.mjs";
|
|
16
16
|
|
|
17
|
+
// The KNOCKOUT lane's own run-dir table. `paths` above is the CLEARANCE table and has no entry for any
|
|
18
|
+
// file this lane writes, which is why every audit projection read a delivered knockout as a run with
|
|
19
|
+
// nothing on disk (tracker issue 275). Re-exported rather than re-derived for the reason this whole file
|
|
20
|
+
// exists: a second copy of a path table drifts, and the drift shows up as an artifact reported missing.
|
|
21
|
+
export { koPaths } from "../../driver/stages-knockout.mjs";
|
|
22
|
+
|
|
17
23
|
// stripInternal/stripEngineInternals/stripTelemetry are the driver's OWN client-safety transforms — the
|
|
18
24
|
// same ones publish/render.mjs applies to the client HTML export. lib/scrub.mjs composes them so the MCP
|
|
19
25
|
// client surface and the delivered report answer "what may a client see?" from ONE definition ( R1:
|