@gajae-code/coding-agent 0.12.5 → 0.12.7
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 +34 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +43 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +430 -46
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/modes/acp/acp-agent.ts +27 -13
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +30 -6
- package/src/sdk/bus/index.ts +23 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +752 -339
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +1 -0
- package/src/session/session-manager.ts +7 -0
- package/src/session/session-storage.ts +146 -24
- package/src/task/index.ts +230 -19
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +2 -0
- package/src/task/types.ts +10 -0
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SEMANTIC_ANCHOR_DOMAIN,
|
|
5
|
+
STICKY_VIEWPORT_FRAME_TEXT_WITNESS,
|
|
6
|
+
type StickyViewportShowcaseKey,
|
|
7
|
+
semanticAnchorDigest,
|
|
8
|
+
semanticAnchorNamespace,
|
|
9
|
+
} from "../test/fixtures/tui/sticky-viewport-showcase";
|
|
10
|
+
import {
|
|
11
|
+
ansiToHtml,
|
|
12
|
+
captureProvenance,
|
|
13
|
+
committedBlobSha256,
|
|
14
|
+
gitObjectType,
|
|
15
|
+
PROVENANCE_DIFF_SCOPE,
|
|
16
|
+
resolveRepositoryPath,
|
|
17
|
+
xterm256Color,
|
|
18
|
+
} from "./capture-sticky-viewport-showcase";
|
|
4
19
|
|
|
5
20
|
const KEYS = [
|
|
6
21
|
"live-overflow/80x24/unicode-color",
|
|
@@ -64,6 +79,48 @@ const INDEPENDENT_REVIEW_KEYS = [
|
|
|
64
79
|
const INDEPENDENT_REVIEW_RESULT_KEYS = ["key", "result", "notes", "artifact_checks"] as const;
|
|
65
80
|
const INDEPENDENT_REVIEW_DEFECT_KEYS = ["description", "accepted"] as const;
|
|
66
81
|
const ARTIFACT_CHECK_KEYS = ["terminal_txt", "terminal_ansi_txt", "terminal_html", "metadata_json"] as const;
|
|
82
|
+
const SEMANTIC_ANCHOR_KEYS = [
|
|
83
|
+
"domain",
|
|
84
|
+
"id",
|
|
85
|
+
"namespace",
|
|
86
|
+
"grapheme_start",
|
|
87
|
+
"grapheme_end",
|
|
88
|
+
"cell_start",
|
|
89
|
+
"cell_end",
|
|
90
|
+
"frame_start_row",
|
|
91
|
+
"row_text_sha256",
|
|
92
|
+
"frame_sha256",
|
|
93
|
+
"frame_text_sha256",
|
|
94
|
+
] as const;
|
|
95
|
+
const SEMANTIC_ROOT_IDS = [
|
|
96
|
+
"irc-split",
|
|
97
|
+
"pending-messages",
|
|
98
|
+
"status-container",
|
|
99
|
+
"todos",
|
|
100
|
+
"btw",
|
|
101
|
+
"status-line",
|
|
102
|
+
"hooks-above",
|
|
103
|
+
"editor-container",
|
|
104
|
+
"pet-floor",
|
|
105
|
+
"hooks-below",
|
|
106
|
+
] as const;
|
|
107
|
+
const STATE_KEYS = [
|
|
108
|
+
"manual",
|
|
109
|
+
"notice",
|
|
110
|
+
"observed_output_revision",
|
|
111
|
+
"transcript_capacity",
|
|
112
|
+
"composer_visible",
|
|
113
|
+
"resize_probes",
|
|
114
|
+
"visible_empty_irc_frame",
|
|
115
|
+
"root_order",
|
|
116
|
+
"pin_boundary",
|
|
117
|
+
"focused_component",
|
|
118
|
+
"cursor",
|
|
119
|
+
"selection",
|
|
120
|
+
"semantic_anchor",
|
|
121
|
+
"cjk_contiguous_semantics",
|
|
122
|
+
"coverage",
|
|
123
|
+
] as const;
|
|
67
124
|
type Style = {
|
|
68
125
|
foreground: string;
|
|
69
126
|
background: string;
|
|
@@ -78,7 +135,52 @@ type Style = {
|
|
|
78
135
|
overline: boolean;
|
|
79
136
|
};
|
|
80
137
|
type Run = { text: string; style: Style };
|
|
81
|
-
const hash = (value: string) => new Bun.CryptoHasher("sha256").update(value).digest("hex");
|
|
138
|
+
const hash = (value: string | Uint8Array) => new Bun.CryptoHasher("sha256").update(value).digest("hex");
|
|
139
|
+
const cellWidth = (grapheme: string) => {
|
|
140
|
+
const scalar = grapheme.codePointAt(0)!;
|
|
141
|
+
if (scalar === 0x200d || (scalar >= 0x300 && scalar <= 0x36f) || (scalar >= 0xfe00 && scalar <= 0xfe0f)) return 0;
|
|
142
|
+
return (scalar >= 0x1100 && scalar <= 0x115f) ||
|
|
143
|
+
(scalar >= 0x2e80 && scalar <= 0xa4cf) ||
|
|
144
|
+
(scalar >= 0xac00 && scalar <= 0xd7a3) ||
|
|
145
|
+
(scalar >= 0xf900 && scalar <= 0xfaff) ||
|
|
146
|
+
(scalar >= 0xff01 && scalar <= 0xff60) ||
|
|
147
|
+
(scalar >= 0xffe0 && scalar <= 0xffe6)
|
|
148
|
+
? 2
|
|
149
|
+
: 1;
|
|
150
|
+
};
|
|
151
|
+
const terminalRows = (text: string, columns: number) =>
|
|
152
|
+
text
|
|
153
|
+
.slice(0, -1)
|
|
154
|
+
.split("\n")
|
|
155
|
+
.map(text => {
|
|
156
|
+
const cells = [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)].map(item => ({
|
|
157
|
+
grapheme: item.segment,
|
|
158
|
+
width: cellWidth(item.segment),
|
|
159
|
+
}));
|
|
160
|
+
const width = cells.reduce((total, cell) => total + cell.width, 0);
|
|
161
|
+
if (width > columns) fail(`terminal row exceeds ${columns} cells`);
|
|
162
|
+
return { text, cells, width };
|
|
163
|
+
});
|
|
164
|
+
const verifyCjkCellOracle = (text: string, columns: number, pinRow: unknown, cursorRow: unknown) => {
|
|
165
|
+
if (!Number.isInteger(pinRow) || !Number.isInteger(cursorRow)) fail("narrow CJK lane geometry missing");
|
|
166
|
+
const pinnedRow = pinRow as number;
|
|
167
|
+
const editorRow = cursorRow as number;
|
|
168
|
+
const rows = terminalRows(text, columns);
|
|
169
|
+
const phrase = CJK[1];
|
|
170
|
+
const row = rows.findIndex(candidate => candidate.text.includes(phrase));
|
|
171
|
+
if (row < 0) fail("narrow CJK cell oracle missing canonical phrase");
|
|
172
|
+
const candidate = rows[row]!;
|
|
173
|
+
const phraseIndex = candidate.text.indexOf(phrase);
|
|
174
|
+
const prefix = candidate.text.slice(0, phraseIndex);
|
|
175
|
+
const phraseCells = [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(phrase)];
|
|
176
|
+
const phraseStart = [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(prefix)].reduce(
|
|
177
|
+
(total, item) => total + cellWidth(item.segment),
|
|
178
|
+
0,
|
|
179
|
+
);
|
|
180
|
+
const phraseWidth = phraseCells.reduce((total, item) => total + cellWidth(item.segment), 0);
|
|
181
|
+
if (phraseStart + phraseWidth > columns || row >= pinnedRow || row === editorRow)
|
|
182
|
+
fail("narrow CJK cell oracle lane overlap");
|
|
183
|
+
};
|
|
82
184
|
const fail = (message: string): never => {
|
|
83
185
|
throw new Error(`Sticky viewport evidence invalid: ${message}`);
|
|
84
186
|
};
|
|
@@ -281,79 +383,328 @@ const normalized = (runs: Run[]) => {
|
|
|
281
383
|
return merged;
|
|
282
384
|
};
|
|
283
385
|
const equalRuns = (left: Run[], right: Run[]) => JSON.stringify(normalized(left)) === JSON.stringify(normalized(right));
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
386
|
+
const hasColorSgr = (ansi: string) =>
|
|
387
|
+
[...ansi.matchAll(/\x1b\[([0-9;]*)m/g)].some(match => {
|
|
388
|
+
const codes = (match[1] || "0").split(";").map(Number);
|
|
389
|
+
return codes.some(
|
|
390
|
+
code =>
|
|
391
|
+
(code >= 30 && code <= 37) ||
|
|
392
|
+
(code >= 40 && code <= 47) ||
|
|
393
|
+
(code >= 90 && code <= 107) ||
|
|
394
|
+
code === 38 ||
|
|
395
|
+
code === 48,
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
// Independent, frame-derived transcript geometry. The renderer assigns
|
|
399
|
+
// `transcriptCapacity` and `pinBoundary.row` from one local (tui.ts), so
|
|
400
|
+
// comparing those two to each other can never fail. These derivations read the
|
|
401
|
+
// committed paint instead, so a renderer reporting stale geometry is rejected.
|
|
402
|
+
const STATUS_ROW_MARKER = "⬢";
|
|
403
|
+
const NOTICE_MARKER = "New output — type to follow";
|
|
404
|
+
const frameGeometry = (key: string, text: string) => {
|
|
405
|
+
const rows = text.split("\n").slice(0, -1);
|
|
406
|
+
const statusRows = rows.filter(row => row.includes(STATUS_ROW_MARKER));
|
|
407
|
+
if (statusRows.length !== 1) fail(`entry ${key} status row cardinality is not exactly one`);
|
|
408
|
+
const statusRow = rows.findIndex(row => row.includes(STATUS_ROW_MARKER));
|
|
409
|
+
const noticeRows = text.split(NOTICE_MARKER).length - 1;
|
|
410
|
+
if (noticeRows > 1) fail(`entry ${key} notice row cardinality exceeds one`);
|
|
411
|
+
// The notice, when present, occupies the first row below the transcript, so
|
|
412
|
+
// the painted status row sits one lower than the transcript capacity.
|
|
413
|
+
return { capacity: statusRow - noticeRows, statusRow, noticeRows };
|
|
414
|
+
};
|
|
415
|
+
// Immutable per-entry anchor expectation. A recomputed digest only proves internal
|
|
416
|
+
// consistency: the producer chooses `frame_start_row` and the geometry, so it can
|
|
417
|
+
// mint a cryptographically valid id for a RELOCATED or TRANSPLANTED anchor. This
|
|
418
|
+
// table is the independent witness. It lives in verifier source, not in the bundle,
|
|
419
|
+
// and this file's own sha256 is inside `source_sha256`, so a bundle author cannot
|
|
420
|
+
// restate it. Geometry was measured identical across indexed-color and truecolor
|
|
421
|
+
// hosts, so pinning it costs no host portability.
|
|
422
|
+
// Files that OWN the verification contract itself: the expectation table, the
|
|
423
|
+
// anchor digest function, and every guard below. `source_sha256` cannot protect
|
|
424
|
+
// these, because it is produced by `captureProvenance()` from the worktree -- an
|
|
425
|
+
// author who edits an oracle and restamps provenance gets a self-consistent
|
|
426
|
+
// bundle. So these two are pinned to their committed blobs at the bundle's own
|
|
427
|
+
// `git_head` instead. Product-source diffs stay permitted for staged current-dev
|
|
428
|
+
// integration; oracle diffs do not.
|
|
429
|
+
const ORACLE_SOURCES = [
|
|
430
|
+
"packages/coding-agent/scripts/verify-sticky-viewport-showcase.ts",
|
|
431
|
+
"packages/coding-agent/test/fixtures/tui/sticky-viewport-showcase.ts",
|
|
432
|
+
] as const;
|
|
433
|
+
// The reviewed PR commit, supplied out-of-band by the verifying operator.
|
|
434
|
+
// `provenance.git_head` is `git rev-parse HEAD`, which is NOT the reviewed commit
|
|
435
|
+
// during an uncommitted synthetic merge: there HEAD is the integration base while
|
|
436
|
+
// the running oracle is the staged PR version. Comparing against HEAD alone
|
|
437
|
+
// therefore rejects an honest bundle before any evidence guard runs. This value is
|
|
438
|
+
// environment-supplied rather than bundle-supplied, so a bundle author cannot
|
|
439
|
+
// choose which commit vouches for the oracle.
|
|
440
|
+
const ORACLE_COMMIT_ENV = "GJC_STICKY_VIEWPORT_ORACLE_COMMIT";
|
|
441
|
+
const verifyOracleIntegrity = async (gitHead: string, declaredProvenanceCommit: unknown) => {
|
|
442
|
+
// Authority is exactly one commit. Reachability is NOT authority: bytes
|
|
443
|
+
// committed on an unrelated local or remote-tracking ref are reachable via
|
|
444
|
+
// `--all` yet were never reviewed, so enumerating refs would let any pushed
|
|
445
|
+
// or fetched branch authorize the oracle. The reviewed commit is therefore
|
|
446
|
+
// either declared out-of-band by the review harness or it is `git_head`.
|
|
447
|
+
const declared = process.env[ORACLE_COMMIT_ENV]?.trim();
|
|
448
|
+
if (declared !== undefined && !/^[0-9a-f]{40}$/.test(declared))
|
|
449
|
+
fail(`oracle integrity: ${ORACLE_COMMIT_ENV} must be a full 40-hex commit id`);
|
|
450
|
+
const authority = declared ?? gitHead;
|
|
451
|
+
// Fail closed when the authority is not a readable commit object.
|
|
452
|
+
if ((await gitObjectType(authority)) !== "commit")
|
|
453
|
+
fail(`oracle integrity: authority ${authority} is not a readable commit object`);
|
|
454
|
+
// The bundle records which commit it claimed; a mismatch means the bundle was
|
|
455
|
+
// captured against a different authority than the one being verified.
|
|
456
|
+
if (declaredProvenanceCommit !== authority)
|
|
457
|
+
fail(
|
|
458
|
+
`oracle integrity: provenance oracle_commit ${String(declaredProvenanceCommit)} is not the authority ${authority}`,
|
|
459
|
+
);
|
|
460
|
+
// Both oracle files must resolve at that same commit; per-file provenance
|
|
461
|
+
// from different refs is not accepted.
|
|
462
|
+
for (const source of ORACLE_SOURCES) {
|
|
463
|
+
const committed = await committedBlobSha256(authority, source);
|
|
464
|
+
if (committed === null) fail(`oracle integrity: ${source} has no committed blob at ${authority}`);
|
|
465
|
+
const running = hash(new Uint8Array(await fs.readFile(resolveRepositoryPath(source))));
|
|
466
|
+
if (running !== committed) fail(`oracle integrity: ${source} differs from its committed blob at ${authority}`);
|
|
467
|
+
}
|
|
287
468
|
};
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
469
|
+
const SEMANTIC_ANCHOR_EXPECTATION: Readonly<
|
|
470
|
+
Record<string, { frameRow: number; graphemeStart: number; graphemeEnd: number; cellStart: number; cellEnd: number }>
|
|
471
|
+
> = Object.freeze({
|
|
472
|
+
"live-overflow/80x24/unicode-color": {
|
|
473
|
+
frameRow: 2,
|
|
474
|
+
graphemeStart: 0,
|
|
475
|
+
graphemeEnd: 1638400,
|
|
476
|
+
cellStart: 0,
|
|
477
|
+
cellEnd: 1638400,
|
|
478
|
+
},
|
|
479
|
+
"live-overflow/120x36/unicode-color": {
|
|
480
|
+
frameRow: 2,
|
|
481
|
+
graphemeStart: 0,
|
|
482
|
+
graphemeEnd: 3276800,
|
|
483
|
+
cellStart: 0,
|
|
484
|
+
cellEnd: 3276800,
|
|
485
|
+
},
|
|
486
|
+
"manual-history/80x24/unicode-color": {
|
|
487
|
+
frameRow: 0,
|
|
488
|
+
graphemeStart: 1638400,
|
|
489
|
+
graphemeEnd: 3276800,
|
|
490
|
+
cellStart: 1638400,
|
|
491
|
+
cellEnd: 3276800,
|
|
492
|
+
},
|
|
493
|
+
"manual-history/120x36/unicode-color": {
|
|
494
|
+
frameRow: 0,
|
|
495
|
+
graphemeStart: 0,
|
|
496
|
+
graphemeEnd: 3276800,
|
|
497
|
+
cellStart: 0,
|
|
498
|
+
cellEnd: 3276800,
|
|
499
|
+
},
|
|
500
|
+
"manual-new-output/80x24/unicode-color": {
|
|
501
|
+
frameRow: 0,
|
|
502
|
+
graphemeStart: 1638400,
|
|
503
|
+
graphemeEnd: 3276800,
|
|
504
|
+
cellStart: 1638400,
|
|
505
|
+
cellEnd: 3276800,
|
|
506
|
+
},
|
|
507
|
+
"manual-new-output/120x36/unicode-color": {
|
|
508
|
+
frameRow: 0,
|
|
509
|
+
graphemeStart: 0,
|
|
510
|
+
graphemeEnd: 3276800,
|
|
511
|
+
cellStart: 0,
|
|
512
|
+
cellEnd: 3276800,
|
|
513
|
+
},
|
|
514
|
+
"multiline-editor-hooks-pet/80x24/unicode-color": {
|
|
515
|
+
frameRow: 3,
|
|
516
|
+
graphemeStart: 0,
|
|
517
|
+
graphemeEnd: 1638400,
|
|
518
|
+
cellStart: 0,
|
|
519
|
+
cellEnd: 1638400,
|
|
520
|
+
},
|
|
521
|
+
"multiline-editor-hooks-pet/120x36/unicode-color": {
|
|
522
|
+
frameRow: 3,
|
|
523
|
+
graphemeStart: 0,
|
|
524
|
+
graphemeEnd: 3276800,
|
|
525
|
+
cellStart: 0,
|
|
526
|
+
cellEnd: 3276800,
|
|
527
|
+
},
|
|
528
|
+
"capacity-many/80x24/unicode-color": {
|
|
529
|
+
frameRow: 0,
|
|
530
|
+
graphemeStart: 1638400,
|
|
531
|
+
graphemeEnd: 3276800,
|
|
532
|
+
cellStart: 1638400,
|
|
533
|
+
cellEnd: 3276800,
|
|
534
|
+
},
|
|
535
|
+
"capacity-many/120x36/unicode-color": {
|
|
536
|
+
frameRow: 0,
|
|
537
|
+
graphemeStart: 0,
|
|
538
|
+
graphemeEnd: 3276800,
|
|
539
|
+
cellStart: 0,
|
|
540
|
+
cellEnd: 3276800,
|
|
541
|
+
},
|
|
542
|
+
"capacity-one/80x24/unicode-color": {
|
|
543
|
+
frameRow: 0,
|
|
544
|
+
graphemeStart: 0,
|
|
545
|
+
graphemeEnd: 1605632,
|
|
546
|
+
cellStart: 0,
|
|
547
|
+
cellEnd: 1605632,
|
|
548
|
+
},
|
|
549
|
+
"capacity-one/120x36/unicode-color": {
|
|
550
|
+
frameRow: 0,
|
|
551
|
+
graphemeStart: 0,
|
|
552
|
+
graphemeEnd: 3211264,
|
|
553
|
+
cellStart: 0,
|
|
554
|
+
cellEnd: 3211264,
|
|
555
|
+
},
|
|
556
|
+
"selection-boundary/80x24/unicode-color": {
|
|
557
|
+
frameRow: 0,
|
|
558
|
+
graphemeStart: 1638400,
|
|
559
|
+
graphemeEnd: 3276800,
|
|
560
|
+
cellStart: 1638400,
|
|
561
|
+
cellEnd: 3276800,
|
|
562
|
+
},
|
|
563
|
+
"selection-boundary/120x36/unicode-color": {
|
|
564
|
+
frameRow: 0,
|
|
565
|
+
graphemeStart: 0,
|
|
566
|
+
graphemeEnd: 3276800,
|
|
567
|
+
cellStart: 0,
|
|
568
|
+
cellEnd: 3276800,
|
|
569
|
+
},
|
|
570
|
+
"manual-new-output/80x24/ascii-no-color": {
|
|
571
|
+
frameRow: 0,
|
|
572
|
+
graphemeStart: 1638400,
|
|
573
|
+
graphemeEnd: 3276800,
|
|
574
|
+
cellStart: 1638400,
|
|
575
|
+
cellEnd: 3276800,
|
|
576
|
+
},
|
|
577
|
+
"multiline-editor-hooks-pet/48x10/unicode-color": {
|
|
578
|
+
frameRow: 0,
|
|
579
|
+
graphemeStart: 1638400,
|
|
580
|
+
graphemeEnd: 3276800,
|
|
581
|
+
cellStart: 1638400,
|
|
582
|
+
cellEnd: 3276800,
|
|
583
|
+
},
|
|
584
|
+
"narrow-cjk/48x10/unicode-color": {
|
|
585
|
+
frameRow: 3,
|
|
586
|
+
graphemeStart: 0,
|
|
587
|
+
graphemeEnd: 589824,
|
|
588
|
+
cellStart: 0,
|
|
589
|
+
cellEnd: 589824,
|
|
590
|
+
},
|
|
293
591
|
});
|
|
294
|
-
|
|
592
|
+
// Semantic anchor identity guard. The persisted `semantic_anchor.id` is the only
|
|
593
|
+
// durable handle on WHICH painted row the evidence anchors, so it must be
|
|
594
|
+
// unforgeable rather than merely well-formed. A geometry-only digest failed both
|
|
595
|
+
// ways: distinct entries painting different content at equal offsets collapsed
|
|
596
|
+
// onto one id, and an 8-hex truncation is brute-forceable. This recomputes the
|
|
597
|
+
// full domain-separated digest from the persisted inputs plus the committed paint
|
|
598
|
+
// and rejects arbitrary, transplanted, aliased, malformed, and truncated ids. It
|
|
599
|
+
// runs BEFORE every downstream metadata check so a forged id can never ride
|
|
600
|
+
// through on an earlier-passing path.
|
|
601
|
+
const verifySemanticAnchor = (
|
|
295
602
|
key: string,
|
|
603
|
+
state: string,
|
|
604
|
+
value: unknown,
|
|
296
605
|
text: string,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (stateEvidence.manual !== expected.manual || stateEvidence.notice !== expected.notice)
|
|
304
|
-
fail(`immutable state oracle mismatch for ${key}`);
|
|
305
|
-
if (lines.filter(line => line.includes(expected.status)).length !== 1)
|
|
306
|
-
fail(`exact status oracle mismatch for ${key}`);
|
|
307
|
-
const markers = [
|
|
308
|
-
expected.status,
|
|
309
|
-
...(expected.multiline
|
|
310
|
-
? ["hook: ready", "completed: visual proof", "pet: ◕‿◕", "> ", "first composer line", "second composer line"]
|
|
311
|
-
: ["> "]),
|
|
312
|
-
];
|
|
313
|
-
let position = -1;
|
|
314
|
-
for (const marker of markers) {
|
|
315
|
-
const next = text.indexOf(marker, position + 1);
|
|
316
|
-
if (next < 0) fail(`ordered suffix oracle missing ${marker} for ${key}`);
|
|
317
|
-
position = next;
|
|
606
|
+
ansiSha256: string,
|
|
607
|
+
seen: Map<string, string>,
|
|
608
|
+
) => {
|
|
609
|
+
if (state === "capacity-zero") {
|
|
610
|
+
if (value !== null) fail(`semantic anchor guard: ${key} must not carry an anchor at zero capacity`);
|
|
611
|
+
return;
|
|
318
612
|
}
|
|
319
|
-
|
|
320
|
-
|
|
613
|
+
const anchor = object(value, `semantic anchor ${key}`);
|
|
614
|
+
exactKeys(anchor, SEMANTIC_ANCHOR_KEYS, `semantic anchor ${key}`);
|
|
615
|
+
if (anchor.domain !== SEMANTIC_ANCHOR_DOMAIN)
|
|
616
|
+
fail(`semantic anchor guard: ${key} domain separation literal mismatch`);
|
|
617
|
+
const id = anchor.id;
|
|
618
|
+
const namespace = anchor.namespace;
|
|
619
|
+
if (typeof id !== "string" || typeof namespace !== "string" || !namespace)
|
|
620
|
+
fail(`semantic anchor guard: ${key} id or namespace is not a nonempty string`);
|
|
621
|
+
const geometry = ["grapheme_start", "grapheme_end", "cell_start", "cell_end", "frame_start_row"] as const;
|
|
622
|
+
for (const field of geometry)
|
|
623
|
+
if (!Number.isInteger(anchor[field]) || (anchor[field] as number) < 0)
|
|
624
|
+
fail(`semantic anchor guard: ${key} ${field} is not a nonnegative integer`);
|
|
625
|
+
const graphemeStart = anchor.grapheme_start as number;
|
|
626
|
+
const graphemeEnd = anchor.grapheme_end as number;
|
|
627
|
+
const cellStart = anchor.cell_start as number;
|
|
628
|
+
const cellEnd = anchor.cell_end as number;
|
|
629
|
+
const frameRow = anchor.frame_start_row as number;
|
|
630
|
+
if (graphemeEnd <= graphemeStart || cellEnd <= cellStart)
|
|
631
|
+
fail(`semantic anchor guard: ${key} geometry span is empty or inverted`);
|
|
632
|
+
// Independent witness: reject relocation/transplant even when every digest and
|
|
633
|
+
// provenance field is internally consistent.
|
|
634
|
+
const expectation = SEMANTIC_ANCHOR_EXPECTATION[key];
|
|
635
|
+
if (!expectation) fail(`semantic anchor guard: ${key} has no immutable anchor expectation`);
|
|
321
636
|
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
637
|
+
frameRow !== expectation!.frameRow ||
|
|
638
|
+
graphemeStart !== expectation!.graphemeStart ||
|
|
639
|
+
graphemeEnd !== expectation!.graphemeEnd ||
|
|
640
|
+
cellStart !== expectation!.cellStart ||
|
|
641
|
+
cellEnd !== expectation!.cellEnd
|
|
327
642
|
)
|
|
328
|
-
fail(`
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
fail(`
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
643
|
+
fail(`semantic anchor guard: ${key} anchor row or geometry does not match its immutable expectation`);
|
|
644
|
+
// The row text is read from the committed paint, never from metadata, so the id
|
|
645
|
+
// stays bound to what the artifact actually renders at the recorded row.
|
|
646
|
+
const rows = text.split("\n");
|
|
647
|
+
const rowText = rows[frameRow];
|
|
648
|
+
if (rowText === undefined) fail(`semantic anchor guard: ${key} frame_start_row is outside the committed paint`);
|
|
649
|
+
if (anchor.row_text_sha256 !== hash(rowText as string))
|
|
650
|
+
fail(`semantic anchor guard: ${key} row content digest does not match the painted anchor row`);
|
|
651
|
+
if (anchor.frame_sha256 !== ansiSha256)
|
|
652
|
+
fail(`semantic anchor guard: ${key} frame digest does not match the committed frame`);
|
|
653
|
+
const frameTextSha256 = hash(Bun.stripANSI(text));
|
|
654
|
+
if (anchor.frame_text_sha256 !== frameTextSha256)
|
|
655
|
+
fail(`semantic anchor guard: ${key} frame text digest does not match the committed paint`);
|
|
656
|
+
if (semanticAnchorNamespace(id as string) !== namespace)
|
|
657
|
+
fail(`semantic anchor guard: ${key} id namespace does not match the persisted namespace`);
|
|
658
|
+
const expected = `${namespace}:${semanticAnchorDigest({
|
|
659
|
+
entryKey: key,
|
|
660
|
+
namespace: namespace as string,
|
|
661
|
+
rowText: rowText as string,
|
|
662
|
+
graphemeStart,
|
|
663
|
+
graphemeEnd,
|
|
664
|
+
cellStart,
|
|
665
|
+
cellEnd,
|
|
666
|
+
frameRow,
|
|
667
|
+
frameTextSha256,
|
|
668
|
+
})}`;
|
|
669
|
+
if (id !== expected) fail(`semantic anchor guard: ${key} id is not the digest of its own persisted inputs`);
|
|
670
|
+
// No silent aliasing: distinct evidence entries are distinct anchors by
|
|
671
|
+
// construction, because the entry key is inside the preimage. A repeat here can
|
|
672
|
+
// only mean a transplanted id, so it is a hard failure rather than an
|
|
673
|
+
// equivalence to be tolerated.
|
|
674
|
+
const owner = seen.get(id as string);
|
|
675
|
+
if (owner !== undefined) fail(`semantic anchor guard: ${key} id aliases the anchor already claimed by ${owner}`);
|
|
676
|
+
seen.set(id as string, key);
|
|
677
|
+
};
|
|
678
|
+
// Whole-frame content witness. The semantic anchor guard above pins exactly ONE
|
|
679
|
+
// painted row per entry, and `anchor.frame_sha256` is recomputed from the
|
|
680
|
+
// bundle's OWN artifact, so it only proves internal consistency: a producer who
|
|
681
|
+
// rewrites a non-anchor row and coordinately rehashes every dependent digest --
|
|
682
|
+
// artifacts, metadata, manifest entry, provenance blocks, review-input binding --
|
|
683
|
+
// stayed self-consistent and was ACCEPTED. In an 80x24 frame 13 rows carry
|
|
684
|
+
// content while one is anchored, so the transcript rows carrying the bundle's own
|
|
685
|
+
// evidence text were rewritable at will.
|
|
686
|
+
//
|
|
687
|
+
// This compares the painted frame against a value committed in oracle source,
|
|
688
|
+
// which no bundle can reach. It is invoked LAST in the per-entry loop, after the
|
|
689
|
+
// anchor guard and after every geometry, marker, and CJK oracle, so a mutation
|
|
690
|
+
// those already attribute keeps its narrower message and only the rows no other
|
|
691
|
+
// oracle inspects surface here.
|
|
692
|
+
//
|
|
693
|
+
// The digest is over the ANSI-STRIPPED paint. `frame_sha256` stays untouched as
|
|
694
|
+
// the artifact digest and legitimately diverges between an indexed-color and a
|
|
695
|
+
// truecolor host; the stripped paint does not, so it is the only frame-wide
|
|
696
|
+
// surface that a single committed value can pin on both hosts.
|
|
697
|
+
export const stickyViewportFrameTextDigest = (frame: string): string => hash(Bun.stripANSI(frame));
|
|
698
|
+
const verifyFrameTextWitness = (key: string, text: string) => {
|
|
699
|
+
// Fail closed: an unwitnessed key is an unpinned frame, not a pass.
|
|
700
|
+
const expected: string | undefined = STICKY_VIEWPORT_FRAME_TEXT_WITNESS[key as StickyViewportShowcaseKey];
|
|
701
|
+
if (expected === undefined) fail(`frame content guard: ${key} has no committed frame text witness`);
|
|
702
|
+
const observed = stickyViewportFrameTextDigest(text);
|
|
703
|
+
if (observed !== expected)
|
|
704
|
+
fail(
|
|
705
|
+
`frame content guard: ${key} painted frame digest is ${observed} but the committed witness pins ${expected}`,
|
|
706
|
+
);
|
|
707
|
+
};
|
|
357
708
|
export async function verifyStickyViewportShowcase(rootInput: string, requireIndependentReview = false): Promise<void> {
|
|
358
709
|
const root = path.resolve(rootInput);
|
|
359
710
|
const manifestText = await fs.readFile(path.join(root, "manifest.json"), "utf8");
|
|
@@ -370,6 +721,7 @@ export async function verifyStickyViewportShowcase(rootInput: string, requireInd
|
|
|
370
721
|
fail("manifest schema or provenance literals mismatch");
|
|
371
722
|
strings(manifest.ordered_keys, KEYS, "manifest ordered_keys");
|
|
372
723
|
const provenance = object(manifest.provenance, "manifest provenance");
|
|
724
|
+
const expectedProvenance = await captureProvenance();
|
|
373
725
|
if (
|
|
374
726
|
provenance.capture_mode !== "production-tui-virtual-terminal" ||
|
|
375
727
|
provenance.live_pty !== false ||
|
|
@@ -381,8 +733,24 @@ export async function verifyStickyViewportShowcase(rootInput: string, requireInd
|
|
|
381
733
|
!provenance.executor_identity.trim()
|
|
382
734
|
)
|
|
383
735
|
fail("manifest provenance mismatch");
|
|
736
|
+
// `git_diff_scope` is compared against the verifier's own constant, so a bundle
|
|
737
|
+
// cannot narrow the covered surface to dodge the digest: claiming a smaller
|
|
738
|
+
// scope than the verifier requires is itself a staleness rejection.
|
|
739
|
+
if (
|
|
740
|
+
provenance.git_head !== expectedProvenance.git_head ||
|
|
741
|
+
JSON.stringify(provenance.git_diff_scope) !== JSON.stringify(PROVENANCE_DIFF_SCOPE) ||
|
|
742
|
+
provenance.git_diff_binary_sha256 !== expectedProvenance.git_diff_binary_sha256 ||
|
|
743
|
+
JSON.stringify(provenance.source_sha256) !== JSON.stringify(expectedProvenance.source_sha256)
|
|
744
|
+
)
|
|
745
|
+
fail("manifest capture provenance is stale");
|
|
746
|
+
// Runs before every entry check: a rewritten oracle would otherwise decide
|
|
747
|
+
// what "valid" means for all of them.
|
|
748
|
+
await verifyOracleIntegrity(provenance.git_head as string, provenance.oracle_commit);
|
|
384
749
|
const entries = array(manifest.entries, "manifest entries");
|
|
385
750
|
if (entries.length !== KEYS.length) fail("manifest entries must contain exactly 20 entries");
|
|
751
|
+
// Cross-entry anchor identity ledger. Uniqueness is a hard contract, not a
|
|
752
|
+
// tolerance: every distinct evidence entry must own a distinct anchor id.
|
|
753
|
+
const anchorIds = new Map<string, string>();
|
|
386
754
|
for (let index = 0; index < KEYS.length; index += 1) {
|
|
387
755
|
const key = KEYS[index]!;
|
|
388
756
|
const entry = object(entries[index], `entry ${index}`);
|
|
@@ -422,21 +790,12 @@ export async function verifyStickyViewportShowcase(rootInput: string, requireInd
|
|
|
422
790
|
!equalRuns(ansiStyleRuns, htmlStyleRuns)
|
|
423
791
|
)
|
|
424
792
|
fail(`entry ${key} ANSI/HTML style-run mismatch`);
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
text
|
|
433
|
-
.split("\n")
|
|
434
|
-
.slice(0, -1)
|
|
435
|
-
.some(row => Bun.stringWidth(row) !== columns) ||
|
|
436
|
-
text.split("\n").length - 1 !== rows
|
|
437
|
-
)
|
|
438
|
-
fail(`entry ${key} terminal dimensions mismatch`);
|
|
439
|
-
if (mode === "ascii-no-color" ? /\x1b\[/.test(ansi) : !/\x1b\[[0-9;]*(?:3[0-9]|38;)/.test(ansi))
|
|
793
|
+
if (text.split("\n").length - 1 !== rows) fail(`entry ${key} terminal row count mismatch`);
|
|
794
|
+
// `ascii-no-color` artifacts must contain no escape sequence at all, which is
|
|
795
|
+
// host-independent. The color branch keeps the widened check below because the
|
|
796
|
+
// prior `3[0-9]|38;` regex missed background (40-47/100-107) and bright (90-97)
|
|
797
|
+
// color, so a frame carrying only those would have passed as "no color".
|
|
798
|
+
if (mode === "ascii-no-color" ? /\x1b\[/.test(ansi) : !hasColorSgr(ansi))
|
|
440
799
|
fail(`entry ${key} ANSI mode/color mismatch`);
|
|
441
800
|
const metadata = await readJson(path.join(root, key, "metadata.json"), `metadata ${key}`);
|
|
442
801
|
exactKeys(
|
|
@@ -476,7 +835,7 @@ export async function verifyStickyViewportShowcase(rootInput: string, requireInd
|
|
|
476
835
|
metadata.fixture_source !== FIXTURE ||
|
|
477
836
|
metadata.render_mode !== mode ||
|
|
478
837
|
metadata.ansi_mode !== (mode === "unicode-color") ||
|
|
479
|
-
metadata.source_revision !== "production-tui-virtual-terminal-
|
|
838
|
+
metadata.source_revision !== "production-tui-virtual-terminal-v3" ||
|
|
480
839
|
terminal.id !== id ||
|
|
481
840
|
terminal.columns !== columns ||
|
|
482
841
|
terminal.rows !== rows ||
|
|
@@ -488,22 +847,182 @@ export async function verifyStickyViewportShowcase(rootInput: string, requireInd
|
|
|
488
847
|
metaProvenance.fixed_clock !== true ||
|
|
489
848
|
metaProvenance.author_identity !== provenance.author_identity ||
|
|
490
849
|
metaProvenance.executor_identity !== provenance.executor_identity ||
|
|
850
|
+
metaProvenance.git_head !== expectedProvenance.git_head ||
|
|
851
|
+
JSON.stringify(metaProvenance.git_diff_scope) !== JSON.stringify(PROVENANCE_DIFF_SCOPE) ||
|
|
852
|
+
metaProvenance.git_diff_binary_sha256 !== expectedProvenance.git_diff_binary_sha256 ||
|
|
853
|
+
JSON.stringify(metaProvenance.source_sha256) !== JSON.stringify(expectedProvenance.source_sha256) ||
|
|
491
854
|
stateEvidence.composer_visible !== true
|
|
492
855
|
)
|
|
493
856
|
fail(`metadata schema mismatch for ${key}`);
|
|
494
|
-
|
|
857
|
+
// Anchor identity is validated here, before any downstream metadata check, so
|
|
858
|
+
// a forged, transplanted, or aliased id cannot ride through on an
|
|
859
|
+
// earlier-passing path.
|
|
860
|
+
verifySemanticAnchor(key, state!, stateEvidence.semantic_anchor, text, hash(ansi), anchorIds);
|
|
861
|
+
// Frame-derived geometry must AGREE with the renderer's self-report, which is
|
|
862
|
+
// strictly stronger than either alone. `transcript_capacity` and
|
|
863
|
+
// `pin_boundary.row` both come from one renderer local, so they can only be
|
|
864
|
+
// falsified against the committed paint: a renderer reporting stale capacity
|
|
865
|
+
// or pin geometry now fails here instead of passing self-consistently.
|
|
866
|
+
const frameGeom = frameGeometry(key, text);
|
|
867
|
+
if (
|
|
868
|
+
!Number.isInteger(stateEvidence.transcript_capacity) ||
|
|
869
|
+
stateEvidence.transcript_capacity !== frameGeom.capacity ||
|
|
870
|
+
frameGeom.noticeRows !== (state === "manual-new-output" ? 1 : 0)
|
|
871
|
+
)
|
|
495
872
|
fail(`capacity metadata/frame mismatch for ${key}`);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
873
|
+
// Ordered suffix markers follow the production root order: status-line (5),
|
|
874
|
+
// hooks-above (6), editor-container (7). Painted order, not reported order.
|
|
875
|
+
let markerPosition = -1;
|
|
876
|
+
for (const marker of [STATUS_ROW_MARKER, "hook: ready", "> "]) {
|
|
877
|
+
const next = text.indexOf(marker, markerPosition + 1);
|
|
878
|
+
if (next < 0) fail(`entry ${key} ordered suffix marker missing: ${marker}`);
|
|
879
|
+
markerPosition = next;
|
|
880
|
+
}
|
|
881
|
+
const observations = array(stateEvidence.resize_probes, `metadata ${key} resize observations`);
|
|
882
|
+
const probeWidths = [64, 65, 80, 120, 160, 120, 80, 65, 64];
|
|
883
|
+
const rootOrder = array(stateEvidence.root_order, `metadata ${key} root order`);
|
|
884
|
+
const pinBoundary = object(stateEvidence.pin_boundary, `metadata ${key} pin boundary`);
|
|
885
|
+
const cursor = object(stateEvidence.cursor, `metadata ${key} cursor`);
|
|
886
|
+
const selection = stateEvidence.selection;
|
|
887
|
+
const expectedManual = state !== "live-overflow" && state !== "capacity-zero";
|
|
888
|
+
const expectedNotice = state === "manual-new-output";
|
|
889
|
+
const expectedRevision = expectedNotice ? "1" : "0";
|
|
890
|
+
if (
|
|
891
|
+
stateEvidence.manual !== expectedManual ||
|
|
892
|
+
stateEvidence.notice !== expectedNotice ||
|
|
893
|
+
stateEvidence.observed_output_revision !== expectedRevision ||
|
|
894
|
+
metadata.output_revision !== stateEvidence.observed_output_revision
|
|
895
|
+
)
|
|
896
|
+
fail(`renderer-owned viewport state mismatch for ${key}`);
|
|
897
|
+
const visibleEmpty = object(stateEvidence.visible_empty_irc_frame, `metadata ${key} visible empty IRC frame`);
|
|
898
|
+
exactKeys(stateEvidence, STATE_KEYS, `metadata ${key} state`);
|
|
899
|
+
strings(rootOrder, SEMANTIC_ROOT_IDS, `metadata ${key} semantic root IDs`);
|
|
900
|
+
const coverage = object(stateEvidence.coverage, `metadata ${key} coverage`);
|
|
901
|
+
exactKeys(
|
|
902
|
+
coverage,
|
|
903
|
+
["irc", "todo", "widths", "heights", "viewport", "chrome", "evidence"],
|
|
904
|
+
`metadata ${key} coverage`,
|
|
905
|
+
);
|
|
906
|
+
strings(coverage.irc, ["empty", "streaming", "long"], `metadata ${key} IRC coverage`);
|
|
907
|
+
strings(
|
|
908
|
+
coverage.todo,
|
|
909
|
+
["empty", "populated", "long", "multi-phase", "collapsed", "expanded"],
|
|
910
|
+
`metadata ${key} todo coverage`,
|
|
911
|
+
);
|
|
912
|
+
const emptyText = visibleEmpty.text as string;
|
|
913
|
+
const capacityConstrained = state === "capacity-one" || state === "capacity-zero";
|
|
914
|
+
if (
|
|
915
|
+
JSON.stringify(coverage.widths) !== JSON.stringify(probeWidths) ||
|
|
916
|
+
emptyText.includes("worker → you") ||
|
|
917
|
+
emptyText.includes("long IRC observation") ||
|
|
918
|
+
observations.length !== probeWidths.length ||
|
|
919
|
+
observations.some((value, index) => {
|
|
920
|
+
const probe = object(value, `metadata ${key} resize observation`);
|
|
921
|
+
const frame = object(probe.frame, `metadata ${key} resize frame`);
|
|
922
|
+
const split = probeWidths[index]! >= 65;
|
|
923
|
+
return (
|
|
924
|
+
probe.columns !== probeWidths[index] ||
|
|
925
|
+
probe.effective_lane !== (split ? "split" : "transcript") ||
|
|
926
|
+
probe.separator_width !== (split ? 3 : 0) ||
|
|
927
|
+
(probe.left_width as number) + (probe.separator_width as number) + (probe.right_width as number) !==
|
|
928
|
+
probeWidths[index] ||
|
|
929
|
+
probe.irc_records !== (split ? 1 : 0) ||
|
|
930
|
+
probe.todo_rows !== (split ? 1 : 0) ||
|
|
931
|
+
probe.todo_expanded !== (probe.columns as number) >= 80 ||
|
|
932
|
+
typeof frame.ansi !== "string" ||
|
|
933
|
+
frame.text !== Bun.stripANSI(frame.ansi) ||
|
|
934
|
+
frame.sha256 !== hash(frame.ansi) ||
|
|
935
|
+
// Required ASCII metadata frames must be escape-free too, or the bundle
|
|
936
|
+
// digest stays host-dependent even when the top-level payload is canonical.
|
|
937
|
+
(mode === "ascii-no-color" && /\x1b\[/.test(frame.ansi as string)) ||
|
|
938
|
+
(!capacityConstrained && split && !frame.text.includes("│")) ||
|
|
939
|
+
(!capacityConstrained &&
|
|
940
|
+
!split &&
|
|
941
|
+
(frame.text.includes("worker → you") || frame.text.includes("Todos"))) ||
|
|
942
|
+
(!capacityConstrained &&
|
|
943
|
+
(probe.columns as number) >= 80 &&
|
|
944
|
+
(!frame.text.includes("long IRC observation") ||
|
|
945
|
+
!frame.text.includes("☑ verify production todo") ||
|
|
946
|
+
!frame.text.includes("☐ expanded production todo"))) ||
|
|
947
|
+
(!capacityConstrained && (probe.columns as number) >= 120 && !frame.text.includes("worker → you"))
|
|
948
|
+
);
|
|
949
|
+
}) ||
|
|
950
|
+
typeof visibleEmpty.ansi !== "string" ||
|
|
951
|
+
visibleEmpty.text !== Bun.stripANSI(visibleEmpty.ansi) ||
|
|
952
|
+
visibleEmpty.sha256 !== hash(visibleEmpty.ansi) ||
|
|
953
|
+
(mode === "ascii-no-color" && /\x1b\[/.test(visibleEmpty.ansi as string)) ||
|
|
954
|
+
JSON.stringify(rootOrder) !==
|
|
955
|
+
JSON.stringify([
|
|
956
|
+
"irc-split",
|
|
957
|
+
"pending-messages",
|
|
958
|
+
"status-container",
|
|
959
|
+
"todos",
|
|
960
|
+
"btw",
|
|
961
|
+
"status-line",
|
|
962
|
+
"hooks-above",
|
|
963
|
+
"editor-container",
|
|
964
|
+
"pet-floor",
|
|
965
|
+
"hooks-below",
|
|
966
|
+
]) ||
|
|
967
|
+
pinBoundary.component !== "status-line" ||
|
|
968
|
+
pinBoundary.index !== 5 ||
|
|
969
|
+
pinBoundary.row !== frameGeom.capacity ||
|
|
970
|
+
pinBoundary.pinned !== true ||
|
|
971
|
+
stateEvidence.focused_component !== "editor" ||
|
|
972
|
+
!Number.isInteger(cursor.row) ||
|
|
973
|
+
(cursor.row as number) < 0 ||
|
|
974
|
+
(cursor.row as number) >= rows ||
|
|
975
|
+
!Number.isInteger(cursor.col) ||
|
|
976
|
+
(cursor.col as number) < 0 ||
|
|
977
|
+
(cursor.col as number) >= columns ||
|
|
978
|
+
cursor.frame_sha256 !== hash(ansi) ||
|
|
979
|
+
cursor.blink !== true
|
|
980
|
+
)
|
|
981
|
+
fail(`runtime observation mismatch for ${key}`);
|
|
982
|
+
if (
|
|
983
|
+
(state === "capacity-many" && (stateEvidence.transcript_capacity as number) <= 1) ||
|
|
984
|
+
(state === "capacity-one" && stateEvidence.transcript_capacity !== 1) ||
|
|
985
|
+
(state === "capacity-zero" && stateEvidence.transcript_capacity !== 0)
|
|
986
|
+
)
|
|
987
|
+
fail(`capacity scenario mismatch for ${key}`);
|
|
988
|
+
if (state === "selection-boundary") {
|
|
989
|
+
const selected = object(selection, `metadata ${key} selection`);
|
|
990
|
+
const start = object(selected.start, `metadata ${key} selection start`);
|
|
991
|
+
const end = object(selected.end, `metadata ${key} selection end`);
|
|
992
|
+
if (
|
|
993
|
+
!Number.isInteger(start.row) ||
|
|
994
|
+
!Number.isInteger(start.col) ||
|
|
995
|
+
!Number.isInteger(end.row) ||
|
|
996
|
+
!Number.isInteger(end.col) ||
|
|
997
|
+
(start.row as number) < 0 ||
|
|
998
|
+
(end.row as number) >= (stateEvidence.transcript_capacity as number) ||
|
|
999
|
+
((start.row as number) === (end.row as number) && (start.col as number) >= (end.col as number))
|
|
1000
|
+
)
|
|
1001
|
+
fail(`selection boundary evidence missing for ${key}`);
|
|
1002
|
+
} else if (selection !== null) fail(`unexpected selection evidence for ${key}`);
|
|
503
1003
|
if (state === "narrow-cjk") {
|
|
504
1004
|
strings(metadata.cjk_phrase_boundaries, CJK, "narrow CJK boundaries");
|
|
505
|
-
|
|
1005
|
+
const probeTexts = observations.map(value => {
|
|
1006
|
+
const probe = object(value, `metadata ${key} resize observation`);
|
|
1007
|
+
return object(probe.frame, `metadata ${key} resize frame`).text;
|
|
1008
|
+
});
|
|
1009
|
+
if (
|
|
1010
|
+
CJK.some(
|
|
1011
|
+
boundary => ![text, ...probeTexts].some(frame => typeof frame === "string" && frame.includes(boundary)),
|
|
1012
|
+
)
|
|
1013
|
+
)
|
|
1014
|
+
fail("narrow CJK visible terminal evidence missing");
|
|
1015
|
+
verifyCjkCellOracle(text, columns, pinBoundary.row, cursor.row);
|
|
506
1016
|
} else strings(metadata.cjk_phrase_boundaries, [], `non-narrow CJK boundaries for ${key}`);
|
|
1017
|
+
// Whole-frame pin, LAST in the per-entry loop. It runs after the semantic
|
|
1018
|
+
// anchor guard so an anchor-row mutation keeps its narrower anchor
|
|
1019
|
+
// attribution, and after the geometry, marker, and CJK oracles so a case that
|
|
1020
|
+
// mutates the paint to reach one of those keeps its own attribution too.
|
|
1021
|
+
// Ordering costs nothing here: every check above reads the same painted frame
|
|
1022
|
+
// this pins, so none of them can be subverted by a paint this guard would
|
|
1023
|
+
// reject. What lands here is the residue -- rows no other oracle looks at,
|
|
1024
|
+
// which is precisely the unpinned surface this guard exists to close.
|
|
1025
|
+
verifyFrameTextWitness(key, text);
|
|
507
1026
|
}
|
|
508
1027
|
const required = new Set([
|
|
509
1028
|
"manifest.json",
|
|
@@ -513,6 +1032,14 @@ export async function verifyStickyViewportShowcase(rootInput: string, requireInd
|
|
|
513
1032
|
]);
|
|
514
1033
|
for (const file of await allFiles(root)) if (!required.has(file)) fail(`unexpected file ${file}`);
|
|
515
1034
|
const reviewInput = await readJson(path.join(root, "review-input.json"), "review input");
|
|
1035
|
+
const reviewProvenance = object(reviewInput.provenance, "review input provenance");
|
|
1036
|
+
if (
|
|
1037
|
+
reviewProvenance.git_head !== expectedProvenance.git_head ||
|
|
1038
|
+
JSON.stringify(reviewProvenance.git_diff_scope) !== JSON.stringify(PROVENANCE_DIFF_SCOPE) ||
|
|
1039
|
+
reviewProvenance.git_diff_binary_sha256 !== expectedProvenance.git_diff_binary_sha256 ||
|
|
1040
|
+
JSON.stringify(reviewProvenance.source_sha256) !== JSON.stringify(expectedProvenance.source_sha256)
|
|
1041
|
+
)
|
|
1042
|
+
fail("review input capture provenance is stale");
|
|
516
1043
|
if (
|
|
517
1044
|
reviewInput.schema_version !== 2 ||
|
|
518
1045
|
reviewInput.manifest_sha256 !== hash(manifestText) ||
|