@linzumi/cli 1.0.112 → 1.0.114
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/README.md +1 -1
- package/dist/crossHarnessFailover.mjs +6 -0
- package/dist/index.js +431 -431
- package/package.json +1 -1
- package/scripts/build-core-fate-ladder-parity-oracle.mjs +66 -0
- package/scripts/build-cross-harness-failover-oracle.mjs +349 -0
- package/scripts/build-differential-entry.mjs +21 -1
- package/scripts/build-tui-demo-evidence.mjs +107 -0
- package/scripts/build-tui-parity-oracle.mjs +577 -0
- package/scripts/build.mjs +40 -1
- package/scripts/f5_live_cross_harness_failover_e2e.mjs +239 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 11, the TUI).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the commander
|
|
4
|
+
// console/TUI port - the deterministic core of the console-reporter module
|
|
5
|
+
// (dashboard state fold, selection machinery, key/chunk parsers, every
|
|
6
|
+
// render surface incl. the TUI table/header-layout models) plus the
|
|
7
|
+
// blessed hardening transforms (terminfo conditional repair, the ACS
|
|
8
|
+
// charset fix, the #2955 screen-registry sweep) and the reporter's
|
|
9
|
+
// teardown/degrade/fail-soft latches. ONE bundle imports the REAL TS
|
|
10
|
+
// module exports behind the one-shot batch driver: a JSON array of cases
|
|
11
|
+
// on stdin, a JSON array of results on stdout. The ReScript package's
|
|
12
|
+
// ConsoleTuiParityConformance drives the SAME inputs through
|
|
13
|
+
// src/tui/CommanderConsole* / CommanderTuiGuards and asserts byte-equal
|
|
14
|
+
// rendered strings, deep-equal state projections, and identical event
|
|
15
|
+
// traces.
|
|
16
|
+
//
|
|
17
|
+
// Determinism: every op is pure given its inputs - clocks arrive as
|
|
18
|
+
// explicit nowMs values, and the reporter-latch ops reset the module
|
|
19
|
+
// latches per case via the exported *ForTest hooks. The teardown/degrade
|
|
20
|
+
// ops route observability through explicit sinks (so the TS default audit
|
|
21
|
+
// sink never fires), and scripted dashboards/streams throw
|
|
22
|
+
// deterministically shaped errors.
|
|
23
|
+
//
|
|
24
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
25
|
+
// payloads; nothing here logs case contents.
|
|
26
|
+
//
|
|
27
|
+
// The output is a local test artifact (.differential/ is gitignored, never
|
|
28
|
+
// published) and stays unminified for debuggability.
|
|
29
|
+
import { mkdir } from 'node:fs/promises';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { build } from 'esbuild';
|
|
33
|
+
|
|
34
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
35
|
+
|
|
36
|
+
const driverSource = `
|
|
37
|
+
import {
|
|
38
|
+
arrowKeysInRawChunk,
|
|
39
|
+
createRunnerConsoleDashboard,
|
|
40
|
+
dashboardJobKeyAtTableRow,
|
|
41
|
+
dashboardNoticeLines,
|
|
42
|
+
droppedArrowKeysInRawChunk,
|
|
43
|
+
formatRunnerConsoleEvent,
|
|
44
|
+
keypressIsEscape,
|
|
45
|
+
rawInputChunkContainsMouseReport,
|
|
46
|
+
rawInputChunkSignalsEscape,
|
|
47
|
+
renderRunnerConsoleDashboard,
|
|
48
|
+
resolveSelectionAfterRefresh,
|
|
49
|
+
runnerConsoleDashboardTableModel,
|
|
50
|
+
runnerConsoleTuiEscapePressed,
|
|
51
|
+
runnerConsoleTuiHeaderLayout,
|
|
52
|
+
selectDashboardJobAtVisibleIndex,
|
|
53
|
+
selectDashboardJobByKey,
|
|
54
|
+
tableSelectionPageStep,
|
|
55
|
+
tableSelectionRowIndex,
|
|
56
|
+
updateRunnerConsoleDashboard,
|
|
57
|
+
updateRunnerConsoleDashboardMode,
|
|
58
|
+
createRunnerConsoleDashboardTuiFailSoft,
|
|
59
|
+
resetRunnerConsoleDashboardTuiConstructionForTest,
|
|
60
|
+
teardownRunnerConsoleDashboardTuiSafely,
|
|
61
|
+
degradeRunnerConsoleTuiToHeadless,
|
|
62
|
+
resetRunnerConsoleTuiDegradeForTest,
|
|
63
|
+
runnerConsoleTuiDegradedToHeadless,
|
|
64
|
+
setRunnerConsoleDashboardTuiForTest,
|
|
65
|
+
runnerConsoleDashboardTuiActiveForTest,
|
|
66
|
+
} from './src/runnerConsoleReporter';
|
|
67
|
+
import { balanceTerminfoConditionals } from './src/blessedTputSetulcShim';
|
|
68
|
+
import {
|
|
69
|
+
applyBlessedAcsCharsetFix,
|
|
70
|
+
blessedWouldUseAcsCharset,
|
|
71
|
+
forceUnicodeBoxDrawingOnTput,
|
|
72
|
+
} from './src/blessedAcsCharsetShim';
|
|
73
|
+
import {
|
|
74
|
+
constructBlessedScreenGuarded,
|
|
75
|
+
detachBlessedScreenFromRegistry,
|
|
76
|
+
sweepStrandedBlessedScreens,
|
|
77
|
+
} from './src/blessedScreenLifecycleGuard';
|
|
78
|
+
import { linzumiCliVersion } from './src/version';
|
|
79
|
+
|
|
80
|
+
// --- Projections ------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
const orNull = (value) => (value === undefined ? null : value);
|
|
83
|
+
|
|
84
|
+
function projectJob(job) {
|
|
85
|
+
return {
|
|
86
|
+
key: job.key,
|
|
87
|
+
createdAtMs: job.createdAtMs,
|
|
88
|
+
createdOrdinal: job.createdOrdinal,
|
|
89
|
+
agentProvider: orNull(job.agentProvider),
|
|
90
|
+
linzumiThreadId: orNull(job.linzumiThreadId),
|
|
91
|
+
linzumiThreadTitle: orNull(job.linzumiThreadTitle),
|
|
92
|
+
// The shared agent-session slot: the TS field carries the deprecated
|
|
93
|
+
// name; the projection uses the canonical key (D44) so both impls
|
|
94
|
+
// compare on neutral vocabulary.
|
|
95
|
+
harnessSessionId: orNull(job.codexThreadId),
|
|
96
|
+
tokenUsage: orNull(job.tokenUsage),
|
|
97
|
+
lastIncomingPreview: orNull(job.lastIncomingPreview),
|
|
98
|
+
lastIncomingAtMs: orNull(job.lastIncomingAtMs),
|
|
99
|
+
lastAssistantPreview: orNull(job.lastAssistantPreview),
|
|
100
|
+
lastAssistantAtMs: orNull(job.lastAssistantAtMs),
|
|
101
|
+
latestHarnessEventId: orNull(job.latestCodexEventId),
|
|
102
|
+
eventType: orNull(job.eventType),
|
|
103
|
+
creditExhaustion: orNull(job.creditExhaustion),
|
|
104
|
+
queueDepth: orNull(job.queueDepth),
|
|
105
|
+
turnId: orNull(job.turnId),
|
|
106
|
+
updatedAtMs: job.updatedAtMs,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function projectState(state) {
|
|
111
|
+
return {
|
|
112
|
+
jobs: [...state.jobs.entries()].map(([key, job]) => [key, projectJob(job)]),
|
|
113
|
+
discovery: [...state.discovery.entries()].map(([key, status]) => [
|
|
114
|
+
key,
|
|
115
|
+
{
|
|
116
|
+
key: status.key,
|
|
117
|
+
phase: status.phase,
|
|
118
|
+
status: status.status,
|
|
119
|
+
placesSearched: status.placesSearched,
|
|
120
|
+
message: orNull(status.message),
|
|
121
|
+
currentPlace: orNull(status.currentPlace),
|
|
122
|
+
currentSource: orNull(status.currentSource),
|
|
123
|
+
lastError: orNull(status.lastError),
|
|
124
|
+
metadata: orNull(status.metadata),
|
|
125
|
+
updatedAtMs: status.updatedAtMs,
|
|
126
|
+
},
|
|
127
|
+
]),
|
|
128
|
+
notices: [...state.notices.entries()].map(([key, notice]) => [
|
|
129
|
+
key,
|
|
130
|
+
{
|
|
131
|
+
key: notice.key,
|
|
132
|
+
severity: notice.severity,
|
|
133
|
+
message: notice.message,
|
|
134
|
+
count: notice.count,
|
|
135
|
+
updatedAtMs: notice.updatedAtMs,
|
|
136
|
+
},
|
|
137
|
+
]),
|
|
138
|
+
rawLines: state.rawLines.map((line) => ({
|
|
139
|
+
jobKey: orNull(line.jobKey),
|
|
140
|
+
line: line.line,
|
|
141
|
+
})),
|
|
142
|
+
nextJobOrdinal: state.nextJobOrdinal,
|
|
143
|
+
mode:
|
|
144
|
+
state.mode.type === 'raw_job'
|
|
145
|
+
? { type: 'raw_job', jobKey: state.mode.jobKey }
|
|
146
|
+
: { type: state.mode.type },
|
|
147
|
+
selectedJobKey: orNull(state.selectedJobKey),
|
|
148
|
+
selectedJobIndexHint: orNull(state.selectedJobIndexHint),
|
|
149
|
+
tokenUsage: orNull(state.tokenUsage),
|
|
150
|
+
rateLimit: orNull(state.rateLimit),
|
|
151
|
+
creditExhaustion: orNull(state.creditExhaustion),
|
|
152
|
+
lastUpdateAtMs: orNull(state.lastUpdateAtMs),
|
|
153
|
+
browserUrl: orNull(state.browserUrl),
|
|
154
|
+
workspace: orNull(state.workspace),
|
|
155
|
+
runnerId: orNull(state.runnerId),
|
|
156
|
+
connectionStale: state.connectionStale,
|
|
157
|
+
rosterTruncated: state.rosterTruncated,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function projectTableModel(model) {
|
|
162
|
+
return {
|
|
163
|
+
rows: model.rows,
|
|
164
|
+
rowJobKeys: model.rowJobKeys.map((key) => orNull(key)),
|
|
165
|
+
selectedJobKey: orNull(model.selectedJobKey),
|
|
166
|
+
jobKeys: model.jobs.map((job) => job.key),
|
|
167
|
+
selectionRowIndex: tableSelectionRowIndex(model),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- scenario: the state fold + render surfaces ------------------------------------------
|
|
172
|
+
|
|
173
|
+
function handleScenario(testCase) {
|
|
174
|
+
const state = createRunnerConsoleDashboard();
|
|
175
|
+
const outputs = [];
|
|
176
|
+
for (const step of testCase.steps) {
|
|
177
|
+
switch (step.op) {
|
|
178
|
+
case 'event':
|
|
179
|
+
updateRunnerConsoleDashboard(state, step.event, step.payload, step.nowMs);
|
|
180
|
+
outputs.push(null);
|
|
181
|
+
break;
|
|
182
|
+
case 'mode_key':
|
|
183
|
+
updateRunnerConsoleDashboardMode(state, step.key);
|
|
184
|
+
outputs.push(null);
|
|
185
|
+
break;
|
|
186
|
+
case 'select_index':
|
|
187
|
+
selectDashboardJobAtVisibleIndex(state, step.index);
|
|
188
|
+
outputs.push(null);
|
|
189
|
+
break;
|
|
190
|
+
case 'select_key':
|
|
191
|
+
selectDashboardJobByKey(state, step.key ?? undefined);
|
|
192
|
+
outputs.push(null);
|
|
193
|
+
break;
|
|
194
|
+
case 'escape_pressed':
|
|
195
|
+
outputs.push(runnerConsoleTuiEscapePressed(state));
|
|
196
|
+
break;
|
|
197
|
+
case 'escape_chunk': {
|
|
198
|
+
const signals = rawInputChunkSignalsEscape(step.chunk);
|
|
199
|
+
const changed = signals ? runnerConsoleTuiEscapePressed(state) : false;
|
|
200
|
+
outputs.push({ signals, changed });
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'render':
|
|
204
|
+
outputs.push(orNull(renderRunnerConsoleDashboard(state, step.nowMs)));
|
|
205
|
+
break;
|
|
206
|
+
case 'table_model':
|
|
207
|
+
outputs.push(projectTableModel(runnerConsoleDashboardTableModel(state, step.nowMs)));
|
|
208
|
+
break;
|
|
209
|
+
case 'header_layout':
|
|
210
|
+
outputs.push(
|
|
211
|
+
runnerConsoleTuiHeaderLayout(
|
|
212
|
+
state,
|
|
213
|
+
step.jobCount,
|
|
214
|
+
step.nowMs,
|
|
215
|
+
step.terminalRows
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
break;
|
|
219
|
+
case 'notice_lines':
|
|
220
|
+
outputs.push(dashboardNoticeLines(state, step.mode));
|
|
221
|
+
break;
|
|
222
|
+
case 'state':
|
|
223
|
+
outputs.push(projectState(state));
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
outputs.push({ threw: true, message: 'unknown scenario step ' + String(step.op) });
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return outputs;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Reporter-latch ops -------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
function makeSinkCollector() {
|
|
236
|
+
const events = [];
|
|
237
|
+
return {
|
|
238
|
+
events,
|
|
239
|
+
sinks: {
|
|
240
|
+
audit: (event, fields) => events.push({ sink: 'audit', event, fields }),
|
|
241
|
+
log: (event, fields) => events.push({ sink: 'log', event, fields }),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function scriptedError(spec) {
|
|
247
|
+
const error = new Error(spec.message);
|
|
248
|
+
if (spec.code !== undefined) {
|
|
249
|
+
error.code = spec.code;
|
|
250
|
+
}
|
|
251
|
+
return error;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function handleTeardownScenario(testCase) {
|
|
255
|
+
resetRunnerConsoleTuiDegradeForTest();
|
|
256
|
+
setRunnerConsoleDashboardTuiForTest(undefined);
|
|
257
|
+
const collector = makeSinkCollector();
|
|
258
|
+
const outputs = [];
|
|
259
|
+
let stopCalls = 0;
|
|
260
|
+
let destroyCalls = 0;
|
|
261
|
+
for (const step of testCase.steps) {
|
|
262
|
+
switch (step.op) {
|
|
263
|
+
case 'install': {
|
|
264
|
+
destroyCalls = 0;
|
|
265
|
+
stopCalls = 0;
|
|
266
|
+
const dashboard = {
|
|
267
|
+
render: () => {},
|
|
268
|
+
destroy: () => {
|
|
269
|
+
destroyCalls += 1;
|
|
270
|
+
if (step.destroyError !== undefined) {
|
|
271
|
+
throw scriptedError(step.destroyError);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
setRunnerConsoleDashboardTuiForTest(
|
|
276
|
+
dashboard,
|
|
277
|
+
{ stdin: step.stdinTty ?? true, stdout: step.stdoutTty ?? true },
|
|
278
|
+
() => {
|
|
279
|
+
stopCalls += 1;
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
outputs.push(null);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case 'teardown': {
|
|
286
|
+
const outcome = teardownRunnerConsoleDashboardTuiSafely(
|
|
287
|
+
{ trigger: step.trigger },
|
|
288
|
+
collector.sinks
|
|
289
|
+
);
|
|
290
|
+
outputs.push({
|
|
291
|
+
hadTui: outcome.hadTui,
|
|
292
|
+
ok: outcome.ok,
|
|
293
|
+
swallowedErrorCode: orNull(outcome.swallowedErrorCode),
|
|
294
|
+
active: runnerConsoleDashboardTuiActiveForTest(),
|
|
295
|
+
stopCalls,
|
|
296
|
+
destroyCalls,
|
|
297
|
+
});
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case 'degrade': {
|
|
301
|
+
degradeRunnerConsoleTuiToHeadless(
|
|
302
|
+
{ code: step.code ?? undefined, stage: step.stage },
|
|
303
|
+
collector.sinks
|
|
304
|
+
);
|
|
305
|
+
outputs.push({
|
|
306
|
+
degraded: runnerConsoleTuiDegradedToHeadless(),
|
|
307
|
+
active: runnerConsoleDashboardTuiActiveForTest(),
|
|
308
|
+
stopCalls,
|
|
309
|
+
destroyCalls,
|
|
310
|
+
});
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case 'reset_degrade':
|
|
314
|
+
resetRunnerConsoleTuiDegradeForTest();
|
|
315
|
+
outputs.push(null);
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
outputs.push({ threw: true, message: 'unknown teardown step ' + String(step.op) });
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { outputs, events: collector.events };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function handleFailSoftScenario(testCase) {
|
|
326
|
+
resetRunnerConsoleDashboardTuiConstructionForTest();
|
|
327
|
+
const collector = makeSinkCollector();
|
|
328
|
+
const outputs = [];
|
|
329
|
+
let createCalls = 0;
|
|
330
|
+
for (const step of testCase.steps) {
|
|
331
|
+
switch (step.op) {
|
|
332
|
+
case 'create': {
|
|
333
|
+
const result = createRunnerConsoleDashboardTuiFailSoft(() => {
|
|
334
|
+
createCalls += 1;
|
|
335
|
+
if (step.createError !== undefined) {
|
|
336
|
+
throw scriptedError(step.createError);
|
|
337
|
+
}
|
|
338
|
+
return { render: () => {}, destroy: () => {} };
|
|
339
|
+
}, collector.sinks);
|
|
340
|
+
outputs.push({ defined: result !== undefined, createCalls });
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case 'reset_construction':
|
|
344
|
+
resetRunnerConsoleDashboardTuiConstructionForTest();
|
|
345
|
+
outputs.push(null);
|
|
346
|
+
break;
|
|
347
|
+
default:
|
|
348
|
+
outputs.push({ threw: true, message: 'unknown failsoft step ' + String(step.op) });
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
outputs,
|
|
354
|
+
// The guarded construction wraps the REAL blessed registry; only the
|
|
355
|
+
// event vocabulary is compared (stack contents are impl-specific), so
|
|
356
|
+
// project events to sink+event+swept_screens.
|
|
357
|
+
events: collector.events.map((entry) => ({
|
|
358
|
+
sink: entry.sink,
|
|
359
|
+
event: entry.event,
|
|
360
|
+
swept_screens: orNull(entry.fields.swept_screens),
|
|
361
|
+
})),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- Blessed-guard ops -----------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
function makeFakeScreen(spec, sweepTrace) {
|
|
368
|
+
const screen = {};
|
|
369
|
+
if (spec.hasProgram) {
|
|
370
|
+
screen.program = {};
|
|
371
|
+
}
|
|
372
|
+
screen.destroy = () => {
|
|
373
|
+
sweepTrace.push('destroy:' + spec.name);
|
|
374
|
+
if (spec.destroyError !== undefined) {
|
|
375
|
+
throw scriptedError(spec.destroyError);
|
|
376
|
+
}
|
|
377
|
+
// A real complete screen's destroy() detaches itself from the
|
|
378
|
+
// registry; the scripted double mirrors that so the sweep's
|
|
379
|
+
// idempotent re-detach is exercised the same way on both impls.
|
|
380
|
+
if (spec.selfDetach !== false) {
|
|
381
|
+
const index = spec.registry.instances.indexOf(screen);
|
|
382
|
+
if (index !== -1) {
|
|
383
|
+
spec.registry.instances.splice(index, 1);
|
|
384
|
+
spec.registry.total -= 1;
|
|
385
|
+
spec.registry.global = spec.registry.instances[0];
|
|
386
|
+
if (spec.registry.total === 0) {
|
|
387
|
+
spec.registry.global = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
return screen;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function projectRegistry(registry, names) {
|
|
396
|
+
return {
|
|
397
|
+
instanceNames: registry.instances.map((instance) => names.get(instance) ?? '?'),
|
|
398
|
+
total: registry.total,
|
|
399
|
+
global:
|
|
400
|
+
registry.global === null
|
|
401
|
+
? null
|
|
402
|
+
: registry.global === undefined
|
|
403
|
+
? '<undefined>'
|
|
404
|
+
: (names.get(registry.global) ?? '?'),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function handleRegistryGuard(testCase) {
|
|
409
|
+
const collector = makeSinkCollector();
|
|
410
|
+
const sweepTrace = [];
|
|
411
|
+
const registry = { instances: [], total: 0, global: null };
|
|
412
|
+
const names = new Map();
|
|
413
|
+
const preexisting = [];
|
|
414
|
+
for (const spec of testCase.preexisting ?? []) {
|
|
415
|
+
const screen = makeFakeScreen({ ...spec, registry }, sweepTrace);
|
|
416
|
+
names.set(screen, spec.name);
|
|
417
|
+
registry.instances.push(screen);
|
|
418
|
+
registry.total += 1;
|
|
419
|
+
preexisting.push(screen);
|
|
420
|
+
}
|
|
421
|
+
registry.global = registry.instances[0] ?? null;
|
|
422
|
+
let result;
|
|
423
|
+
try {
|
|
424
|
+
result = {
|
|
425
|
+
returned: constructBlessedScreenGuarded(
|
|
426
|
+
registry,
|
|
427
|
+
() => {
|
|
428
|
+
for (const spec of testCase.constructed ?? []) {
|
|
429
|
+
const screen = makeFakeScreen({ ...spec, registry }, sweepTrace);
|
|
430
|
+
names.set(screen, spec.name);
|
|
431
|
+
registry.instances.push(screen);
|
|
432
|
+
registry.total += 1;
|
|
433
|
+
}
|
|
434
|
+
if (testCase.constructError !== undefined) {
|
|
435
|
+
throw scriptedError(testCase.constructError);
|
|
436
|
+
}
|
|
437
|
+
return 'constructed';
|
|
438
|
+
},
|
|
439
|
+
{ sinks: collector.sinks, context: { surface: 'parity_case' } }
|
|
440
|
+
),
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
result = { threw: true, message: error instanceof Error ? error.message : String(error) };
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
result,
|
|
447
|
+
registry: projectRegistry(registry, names),
|
|
448
|
+
sweepTrace,
|
|
449
|
+
events: collector.events,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function handleRegistryDetach(testCase) {
|
|
454
|
+
const registry = { instances: [], total: testCase.total ?? 0, global: null };
|
|
455
|
+
const names = new Map();
|
|
456
|
+
const screens = [];
|
|
457
|
+
for (const name of testCase.instances) {
|
|
458
|
+
const screen = { name };
|
|
459
|
+
names.set(screen, name);
|
|
460
|
+
registry.instances.push(screen);
|
|
461
|
+
screens.push(screen);
|
|
462
|
+
}
|
|
463
|
+
registry.global = registry.instances[0] ?? null;
|
|
464
|
+
const target = screens[testCase.detachIndex];
|
|
465
|
+
const detached = detachBlessedScreenFromRegistry(
|
|
466
|
+
registry,
|
|
467
|
+
target ?? { name: 'unregistered' }
|
|
468
|
+
);
|
|
469
|
+
return { detached, registry: projectRegistry(registry, names) };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// --- Dispatch ----------------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
function handle(testCase) {
|
|
475
|
+
switch (testCase.op) {
|
|
476
|
+
case 'version':
|
|
477
|
+
return linzumiCliVersion;
|
|
478
|
+
case 'format_line':
|
|
479
|
+
return orNull(formatRunnerConsoleEvent(testCase.event, testCase.payload));
|
|
480
|
+
case 'keys':
|
|
481
|
+
return {
|
|
482
|
+
escape: rawInputChunkSignalsEscape(testCase.chunk),
|
|
483
|
+
mouse: rawInputChunkContainsMouseReport(testCase.chunk),
|
|
484
|
+
arrows: arrowKeysInRawChunk(testCase.chunk),
|
|
485
|
+
dropped: droppedArrowKeysInRawChunk(testCase.chunk),
|
|
486
|
+
};
|
|
487
|
+
case 'keypress_escape':
|
|
488
|
+
return keypressIsEscape(testCase.key);
|
|
489
|
+
case 'selection_resolve': {
|
|
490
|
+
const resolved = resolveSelectionAfterRefresh(
|
|
491
|
+
testCase.previousKey ?? undefined,
|
|
492
|
+
testCase.previousIndexHint ?? undefined,
|
|
493
|
+
testCase.jobKeys
|
|
494
|
+
);
|
|
495
|
+
return { key: orNull(resolved.key), index: orNull(resolved.index) };
|
|
496
|
+
}
|
|
497
|
+
case 'page_step':
|
|
498
|
+
return tableSelectionPageStep(testCase.tableHeight);
|
|
499
|
+
case 'job_key_at_row': {
|
|
500
|
+
const state = createRunnerConsoleDashboard();
|
|
501
|
+
for (const step of testCase.events) {
|
|
502
|
+
updateRunnerConsoleDashboard(state, step.event, step.payload, step.nowMs);
|
|
503
|
+
}
|
|
504
|
+
const model = runnerConsoleDashboardTableModel(state, testCase.nowMs);
|
|
505
|
+
return testCase.rows.map((row) => orNull(dashboardJobKeyAtTableRow(model, row)));
|
|
506
|
+
}
|
|
507
|
+
case 'terminfo_balance':
|
|
508
|
+
return balanceTerminfoConditionals(testCase.cap);
|
|
509
|
+
case 'acs_would_use':
|
|
510
|
+
return blessedWouldUseAcsCharset(
|
|
511
|
+
testCase.tput ?? undefined,
|
|
512
|
+
testCase.ch,
|
|
513
|
+
testCase.alreadyInAcsRun ?? false
|
|
514
|
+
);
|
|
515
|
+
case 'acs_force': {
|
|
516
|
+
const tput = testCase.tput ?? undefined;
|
|
517
|
+
const mutated = forceUnicodeBoxDrawingOnTput(tput);
|
|
518
|
+
return { mutated, tput: orNull(tput) };
|
|
519
|
+
}
|
|
520
|
+
case 'acs_fix': {
|
|
521
|
+
const screen = testCase.screen ?? undefined;
|
|
522
|
+
if (screen !== undefined && testCase.shareProgramTput === true) {
|
|
523
|
+
screen.program = { tput: screen.tput };
|
|
524
|
+
}
|
|
525
|
+
applyBlessedAcsCharsetFix(screen);
|
|
526
|
+
return orNull(screen);
|
|
527
|
+
}
|
|
528
|
+
case 'scenario':
|
|
529
|
+
return handleScenario(testCase);
|
|
530
|
+
case 'teardown_scenario':
|
|
531
|
+
return handleTeardownScenario(testCase);
|
|
532
|
+
case 'failsoft_scenario':
|
|
533
|
+
return handleFailSoftScenario(testCase);
|
|
534
|
+
case 'registry_guard':
|
|
535
|
+
return handleRegistryGuard(testCase);
|
|
536
|
+
case 'registry_detach':
|
|
537
|
+
return handleRegistryDetach(testCase);
|
|
538
|
+
default:
|
|
539
|
+
return { threw: true, message: 'unknown op ' + String(testCase.op) };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let stdin = '';
|
|
544
|
+
process.stdin.setEncoding('utf8');
|
|
545
|
+
for await (const chunk of process.stdin) {
|
|
546
|
+
stdin += chunk;
|
|
547
|
+
}
|
|
548
|
+
const cases = JSON.parse(stdin);
|
|
549
|
+
const results = [];
|
|
550
|
+
for (const testCase of cases) {
|
|
551
|
+
results.push(handle(testCase));
|
|
552
|
+
}
|
|
553
|
+
process.stdout.write(JSON.stringify(results));
|
|
554
|
+
`;
|
|
555
|
+
|
|
556
|
+
await mkdir(join(packageRoot, '.differential'), { recursive: true });
|
|
557
|
+
|
|
558
|
+
await build({
|
|
559
|
+
stdin: {
|
|
560
|
+
contents: driverSource,
|
|
561
|
+
resolveDir: packageRoot,
|
|
562
|
+
sourcefile: 'tui-parity-driver.ts',
|
|
563
|
+
loader: 'ts',
|
|
564
|
+
},
|
|
565
|
+
bundle: true,
|
|
566
|
+
platform: 'node',
|
|
567
|
+
target: 'node20',
|
|
568
|
+
format: 'esm',
|
|
569
|
+
outfile: join(packageRoot, '.differential/tui-parity-oracle.mjs'),
|
|
570
|
+
minify: false,
|
|
571
|
+
sourcemap: false,
|
|
572
|
+
legalComments: 'none',
|
|
573
|
+
// The console-reporter module imports blessed (and the crash-handler /
|
|
574
|
+
// logger slice pulls the runtime graph); the runtime externals stay
|
|
575
|
+
// aligned with the other cluster oracles.
|
|
576
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
577
|
+
});
|
package/scripts/build.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Spec: plans/2026-05-17-linzumi-cli-node-toolchain-note.md
|
|
2
2
|
// Relationship: Builds the published CLI package with Node-hosted tooling.
|
|
3
|
-
import { copyFile, mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { copyFile, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { build } from 'esbuild';
|
|
@@ -84,6 +84,45 @@ await build({
|
|
|
84
84
|
banner,
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
// F5 cross-harness failover bridge (Workstream F task F5): the runner
|
|
88
|
+
// dynamic-imports ./crossHarnessFailover.mjs relative to its own bundle, so
|
|
89
|
+
// dist ships a self-contained sibling bundle with the compiled ReScript
|
|
90
|
+
// transpiler artifacts (packages/linzumi-cli-rescript, built by `rescript`)
|
|
91
|
+
// resolved at build time. When the artifacts are not built in this checkout
|
|
92
|
+
// the publish must not silently ship a broken rung: emit an HONEST stub
|
|
93
|
+
// whose import succeeds but whose executor refuses loudly - the ladder then
|
|
94
|
+
// terminates red-with-receipts exactly as F2 (the flag's rollback posture) -
|
|
95
|
+
// and warn in the build log.
|
|
96
|
+
try {
|
|
97
|
+
await build({
|
|
98
|
+
entryPoints: [join(packageRoot, 'src/crossHarnessFailover.mjs')],
|
|
99
|
+
bundle: true,
|
|
100
|
+
platform: 'node',
|
|
101
|
+
target: 'node20',
|
|
102
|
+
format: 'esm',
|
|
103
|
+
outfile: join(packageRoot, 'dist/crossHarnessFailover.mjs'),
|
|
104
|
+
minify: true,
|
|
105
|
+
sourcemap: false,
|
|
106
|
+
legalComments: 'none',
|
|
107
|
+
banner,
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.warn(
|
|
111
|
+
'[build] cross-harness failover bridge could not be bundled (are the ' +
|
|
112
|
+
'linzumi-cli-rescript artifacts built?); shipping the honest refusal ' +
|
|
113
|
+
`stub instead: ${error instanceof Error ? error.message : String(error)}`
|
|
114
|
+
);
|
|
115
|
+
await writeFile(
|
|
116
|
+
join(packageRoot, 'dist/crossHarnessFailover.mjs'),
|
|
117
|
+
'export async function transpileAndSeedClaudeSession() {\n' +
|
|
118
|
+
' return {\n' +
|
|
119
|
+
' ok: false,\n' +
|
|
120
|
+
" failure: 'the cross-harness failover transpiler bridge was not bundled into this build',\n" +
|
|
121
|
+
' };\n' +
|
|
122
|
+
'}\n'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
87
126
|
await mkdir(join(packageRoot, 'dist/assets'), { recursive: true });
|
|
88
127
|
await copyFile(
|
|
89
128
|
join(packageRoot, 'src/assets/linzumi-logo.svg'),
|