tickmarkr 1.78.0 → 1.80.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.
Files changed (39) hide show
  1. package/dist/adapters/claude-code.d.ts +11 -0
  2. package/dist/adapters/claude-code.js +105 -16
  3. package/dist/adapters/kimi.d.ts +6 -0
  4. package/dist/adapters/kimi.js +88 -26
  5. package/dist/adapters/registry.js +6 -1
  6. package/dist/brand.d.ts +4 -0
  7. package/dist/brand.js +4 -0
  8. package/dist/cli/commands/doctor.d.ts +2 -0
  9. package/dist/cli/commands/doctor.js +23 -0
  10. package/dist/cli/commands/status.d.ts +4 -0
  11. package/dist/cli/commands/status.js +237 -18
  12. package/dist/cli/commands/ui.d.ts +1 -1
  13. package/dist/cli/commands/ui.js +11 -1
  14. package/dist/drivers/herdr.js +36 -7
  15. package/dist/drivers/types.d.ts +22 -1
  16. package/dist/gates/acceptance.d.ts +5 -0
  17. package/dist/gates/acceptance.js +5 -3
  18. package/dist/gates/llm.d.ts +9 -0
  19. package/dist/gates/llm.js +74 -0
  20. package/dist/gates/review.d.ts +5 -0
  21. package/dist/gates/review.js +5 -3
  22. package/dist/run/journal.d.ts +1 -1
  23. package/dist/tui/cockpit/capture.d.ts +117 -0
  24. package/dist/tui/cockpit/capture.js +329 -0
  25. package/dist/tui/cockpit/components.d.ts +179 -0
  26. package/dist/tui/cockpit/components.js +280 -0
  27. package/dist/tui/cockpit/demo.d.ts +41 -0
  28. package/dist/tui/cockpit/demo.js +144 -0
  29. package/dist/tui/cockpit/derive.d.ts +39 -0
  30. package/dist/tui/cockpit/derive.js +390 -0
  31. package/dist/tui/cockpit/layout.d.ts +76 -0
  32. package/dist/tui/cockpit/layout.js +84 -0
  33. package/dist/tui/cockpit/run-cockpit.d.ts +19 -0
  34. package/dist/tui/cockpit/run-cockpit.js +194 -0
  35. package/dist/tui/cockpit/setup-cockpit.d.ts +41 -0
  36. package/dist/tui/cockpit/setup-cockpit.js +488 -0
  37. package/dist/tui/cockpit/theme.d.ts +161 -0
  38. package/dist/tui/cockpit/theme.js +170 -0
  39. package/package.json +2 -1
@@ -0,0 +1,488 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box } from "ink";
3
+ import { cloneElement, } from "react";
4
+ import { parse } from "yaml";
5
+ import { BANNER, GLYPHS, PLAIN_BANNER, } from "../../brand.js";
6
+ import { DEFAULT_CONFIG, INTEGRITY_FLOOR_SHAPES, TIER_RANK, TickmarkrConfigSchema, unifiedYamlDiff, } from "../../config/config.js";
7
+ import { SHAPES } from "../../graph/schema.js";
8
+ import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, KEYBAR_KEYS, Keybar, measureBandRows, Panel, StateGlyph, StatTile, StatusStrip, } from "./components.js";
9
+ import { FULL_JOURNAL_ROWS, resolveCockpitLayout, } from "./layout.js";
10
+ import { deriveRunCockpitData, PANEL_CHROME_ROWS, panelRows, wrappedRows, } from "./run-cockpit.js";
11
+ const APPROVED_SIDE_RAIL_COLUMN_FLOOR = 80;
12
+ function deepMerge(base, overlay) {
13
+ if (overlay === undefined || overlay === null)
14
+ return base;
15
+ if (Array.isArray(base)
16
+ || Array.isArray(overlay)
17
+ || typeof base !== "object"
18
+ || typeof overlay !== "object"
19
+ || base === null) {
20
+ return overlay;
21
+ }
22
+ const merged = {
23
+ ...base,
24
+ };
25
+ for (const [key, value] of Object.entries(overlay)) {
26
+ if (value === null) {
27
+ delete merged[key];
28
+ continue;
29
+ }
30
+ merged[key] = key in merged
31
+ ? deepMerge(merged[key], value)
32
+ : deepMerge({}, value);
33
+ }
34
+ return merged;
35
+ }
36
+ function floorEdits(layers) {
37
+ const explicit = new Set();
38
+ const tombstoned = new Set();
39
+ for (const layer of layers) {
40
+ const floors = layer?.routing?.floors;
41
+ if (!floors || Array.isArray(floors))
42
+ continue;
43
+ for (const [shape, value] of Object.entries(floors)) {
44
+ if (value === null) {
45
+ explicit.delete(shape);
46
+ tombstoned.add(shape);
47
+ }
48
+ else {
49
+ tombstoned.delete(shape);
50
+ explicit.add(shape);
51
+ }
52
+ }
53
+ }
54
+ return { explicit, tombstoned };
55
+ }
56
+ function lowerTier(tier) {
57
+ return tier === "frontier" ? "mid" : "cheap";
58
+ }
59
+ function integrityMinimum(shape) {
60
+ return INTEGRITY_FLOOR_SHAPES.includes(shape)
61
+ ? "frontier"
62
+ : "cheap";
63
+ }
64
+ function presetFloor(mode, shape, base) {
65
+ if (mode === "partner-led")
66
+ return "frontier";
67
+ if (mode === "risk-based")
68
+ return base;
69
+ const lowered = lowerTier(base);
70
+ const minimum = integrityMinimum(shape);
71
+ return TIER_RANK[lowered] >= TIER_RANK[minimum] ? lowered : minimum;
72
+ }
73
+ function resolveCapturedConfig(captures) {
74
+ const globalLayer = parse(captures.config.global.raw);
75
+ const repoLayer = parse(captures.config.repo.raw);
76
+ const parsed = TickmarkrConfigSchema.parse(deepMerge(deepMerge(structuredClone(DEFAULT_CONFIG), globalLayer), repoLayer));
77
+ const mode = parsed.routing.mode ?? "risk-based";
78
+ const edits = floorEdits([globalLayer, repoLayer]);
79
+ for (const shape of SHAPES) {
80
+ if (edits.tombstoned.has(shape) || edits.explicit.has(shape))
81
+ continue;
82
+ parsed.routing.floors[shape] = presetFloor(mode, shape, DEFAULT_CONFIG.routing.floors[shape]);
83
+ }
84
+ if (mode === "partner-led") {
85
+ parsed.routing.explore = { ...parsed.routing.explore, mode: "off" };
86
+ }
87
+ return parsed;
88
+ }
89
+ function stagedPaths(value, path = []) {
90
+ if (value === null
91
+ || Array.isArray(value)
92
+ || typeof value !== "object") {
93
+ return path.length > 0 ? [path.join(".")] : [];
94
+ }
95
+ return Object.entries(value).flatMap(([key, child]) => stagedPaths(child, [...path, key]));
96
+ }
97
+ function deniedReason(reason) {
98
+ if (!reason)
99
+ return "channel denied access";
100
+ return reason.match(/Invalid params: "[^"]+"/)?.[0]
101
+ ?? reason.split(/\.\s+Run\b/, 1)[0]
102
+ ?? reason;
103
+ }
104
+ export function deriveSetupCockpitData(captures, binaryVersion) {
105
+ const harnessHealth = Object.entries(captures.doctor.data)
106
+ .filter(([id]) => id !== "autoPrefer")
107
+ .map(([id, raw]) => [id, raw]);
108
+ const harnesses = harnessHealth.map(([id, health]) => {
109
+ const models = new Set([
110
+ ...(health.models ?? []),
111
+ ...Object.keys(health.modelAuth ?? {}),
112
+ ]);
113
+ const state = health.installed !== true
114
+ ? "inactive"
115
+ : health.authed === true
116
+ ? "pass"
117
+ : "fail";
118
+ return {
119
+ id,
120
+ version: health.version ?? "not installed",
121
+ modelCount: models.size,
122
+ state,
123
+ stateWord: state === "pass" ? "authed"
124
+ : state === "inactive" ? "missing"
125
+ : "unauthed",
126
+ };
127
+ });
128
+ const deniedChannels = harnessHealth.flatMap(([id, health]) => Object.entries(health.modelAuth ?? {}).flatMap(([model, verdict]) => verdict.authed === false
129
+ ? [{
130
+ channel: `${id}:${model}`,
131
+ reason: deniedReason(verdict.reason),
132
+ }]
133
+ : []));
134
+ const authenticated = harnessHealth.filter(([, health]) => health.installed === true && health.authed === true).length;
135
+ const routable = harnessHealth.filter(([, health]) => health.installed === true
136
+ && health.authed === true
137
+ && Object.values(health.modelAuth ?? {}).some((model) => model.authed === true)).length;
138
+ const repoLayer = parse(captures.config.repo.raw);
139
+ const reviewSource = captures.journals.find((capture) => capture.fileName === "run-20260724-231138.journal.jsonl");
140
+ if (!reviewSource)
141
+ throw new Error("healthy cockpit capture is missing");
142
+ return {
143
+ binaryVersion,
144
+ harnesses,
145
+ deniedChannels,
146
+ counts: {
147
+ found: harnesses.length,
148
+ authenticated,
149
+ routable,
150
+ },
151
+ config: resolveCapturedConfig(captures),
152
+ overlayDiff: unifiedYamlDiff(captures.config.global.raw, captures.config.repo.raw, captures.config.repo.fileName),
153
+ stagedChanges: stagedPaths(repoLayer),
154
+ reviewRows: deriveRunCockpitData(reviewSource, binaryVersion).journalRows,
155
+ overlayTarget: captures.config.repo.fileName,
156
+ };
157
+ }
158
+ function identityBannerLines() {
159
+ return (process.env.NO_COLOR === undefined ? BANNER : PLAIN_BANNER).trimEnd().split("\n");
160
+ }
161
+ function identityVersionLines(data) {
162
+ return [`v${data.binaryVersion} · binary ${GLYPHS.pass}`, "setup · step 2/6"];
163
+ }
164
+ function IdentityHeader({ data, secondary = true, }) {
165
+ const mark = identityBannerLines();
166
+ const [version, step] = identityVersionLines(data);
167
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexDirection: "column", children: (secondary ? mark : mark.slice(0, 1)).map((line, index) => (_jsx(BodyText, { emphasis: "strong", children: line }, `${index}:${line}`))) }), _jsxs(Box, { flexDirection: "column", alignItems: "flex-end", children: [_jsx(BodyText, { emphasis: "strong", children: version }), secondary && _jsx(BodyText, { children: step })] })] }));
168
+ }
169
+ function NavigationPanel({ width = 16, compact = false, }) {
170
+ if (!compact) {
171
+ return (_jsxs(Panel, { title: "SETUP", width: width, children: [_jsx(BodyText, { children: " Detect" }), _jsxs(BodyText, { emphasis: "strong", children: [GLYPHS.pointer, " Harnesses"] }), _jsx(BodyText, { children: " Fleet" }), _jsx(BodyText, { children: " Gates" }), _jsx(BodyText, { children: " Consults" }), _jsx(BodyText, { children: " Reviewers" }), _jsx(BodyText, { children: " Review" })] }));
172
+ }
173
+ return (_jsxs(Panel, { title: "SETUP", width: width, children: [_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: " Detect" }) }), _jsx(Box, { height: 1, overflow: "hidden", children: _jsxs(BodyText, { emphasis: "strong", children: [GLYPHS.pointer, " Harnesses"] }) }), _jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: " Fleet" }) }), _jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: " Gates" }) }), _jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: " Consults" }) }), _jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: " Reviewers" }) }), _jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: " Review" }) })] }));
174
+ }
175
+ function KeysPanel({ width = 16 }) {
176
+ return (_jsx(Panel, { title: "KEYS", width: width, children: KEYBAR_KEYS.setup.map((item) => (_jsxs(Box, { children: [_jsx(BodyText, { emphasis: "strong", children: item.key }), _jsxs(BodyText, { children: [" ", item.label] })] }, item.key))) }));
177
+ }
178
+ function CompactKeysPanel({ width = 16 }) {
179
+ const contentColumns = Math.max(1, width - 4);
180
+ return (_jsx(Panel, { title: "KEYS", width: width, children: KEYBAR_KEYS.setupAdditional.map((item) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "strong", children: item.key }), _jsxs(BodyText, { children: [" ", item.label.slice(0, Math.max(0, contentColumns - [...item.key].length - 1))] })] }, item.key))) }));
181
+ }
182
+ function fullKeysFit(rows, layout, data, columns) {
183
+ if (!layout.elements.secondaryHeader)
184
+ return false;
185
+ const wrappedKeyRows = {
186
+ "three-column": 0,
187
+ "folded-keys": 2,
188
+ stacked: 4,
189
+ }[layout.arrangement];
190
+ const panelRows = KEYBAR_KEYS.setup.length
191
+ + wrappedKeyRows
192
+ + 3; // panel borders and title row
193
+ return headerRowsFor(layout, data, columns) + 2 + panelRows <= Math.floor(rows);
194
+ }
195
+ function sidePanelWidth(layout) {
196
+ if (layout.arrangement === "three-column")
197
+ return 16;
198
+ if (layout.arrangement === "folded-keys")
199
+ return 12;
200
+ return 11;
201
+ }
202
+ /**
203
+ * Every detail panel publishes the body lines it draws, so the height ladder
204
+ * can charge it the rows those very lines occupy rather than a constant that
205
+ * goes stale the moment the panel changes.
206
+ */
207
+ function detectedRows(data) {
208
+ return [
209
+ ...data.harnesses.map((harness) => ({
210
+ key: harness.id,
211
+ state: harness.state,
212
+ text: ` ${harness.stateWord} · ${harness.id} · ${harness.version}`
213
+ + ` · ${harness.modelCount} models`,
214
+ })),
215
+ ...data.deniedChannels.map((denied) => ({
216
+ key: denied.channel,
217
+ state: "fail",
218
+ text: ` denied · ${denied.channel} — ${denied.reason}`,
219
+ })),
220
+ ];
221
+ }
222
+ function fleetLines(data) {
223
+ const routing = data.config.routing;
224
+ return [
225
+ `mode ${routing.mode ?? "risk-based"}`,
226
+ `floors ${Object.entries(routing.floors)
227
+ .map(([shape, tier]) => `${shape}:${tier}`)
228
+ .join(" · ")}`,
229
+ `deny ${(routing.deny?.models ?? []).join(", ") || "none"}`,
230
+ ];
231
+ }
232
+ function gatesLines(data) {
233
+ return [
234
+ `test ${data.config.gates.test ?? "auto-detected"}`,
235
+ `diff cap ${data.config.gates.diffCap}`,
236
+ ];
237
+ }
238
+ function consultsLines(data) {
239
+ const consult = data.config.consult;
240
+ return [
241
+ `seat ${consult.adapter}:${consult.model}`,
242
+ `stall ${consult.stallMinutes} minutes`,
243
+ ...(consult.prefer ?? []).map((seat) => `prefer ${seat}`),
244
+ ];
245
+ }
246
+ function reviewersLines(data) {
247
+ const review = data.config.review;
248
+ return [
249
+ `required ${String(review.required)}`,
250
+ `complexity threshold ${review.complexityThreshold}`,
251
+ ...(review.prefer ?? []).map((reviewer) => `prefer ${reviewer}`),
252
+ ];
253
+ }
254
+ function overlayDiffLines(data) {
255
+ const lines = data.overlayDiff.split("\n");
256
+ return [
257
+ ...lines.slice(0, 2),
258
+ ...lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).slice(0, 2),
259
+ ...lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).slice(0, 4),
260
+ ];
261
+ }
262
+ function HarnessRows({ rows, columns, }) {
263
+ return (_jsx(Panel, { title: "DETECTED", children: rows.flatMap((row) => {
264
+ const [first = "", ...continuations] = composeBandLine(row.text.trimStart(), Math.max(1, columns));
265
+ return [
266
+ _jsxs(Box, { children: [_jsx(StateGlyph, { state: row.state }), _jsxs(BodyText, { children: [" ", first] })] }, row.key),
267
+ ...continuations.map((line, index) => (_jsx(BodyText, { children: line }, `${row.key}:continuation:${index}`))),
268
+ ];
269
+ }) }));
270
+ }
271
+ function LinePanel({ title, lines, columns, emphasis, }) {
272
+ return (_jsx(Panel, { title: title, children: _jsx(BandLines, { lines: lines, columns: columns, emphasis: emphasis }) }));
273
+ }
274
+ function lineBandContent(title, lines) {
275
+ return { title, lines };
276
+ }
277
+ function LineBand({ columns, panels, }) {
278
+ const widths = allocateBandColumns(columns, panels);
279
+ return (_jsx(CockpitGrid, { columns: columns, columnContents: panels, children: panels.map((panel, index) => (_jsx(LinePanel, { title: panel.title, lines: panel.lines, columns: Math.max(1, widths[index] - 4) }, panel.title))) }));
280
+ }
281
+ function statsSummaryLine(data) {
282
+ return `found ${data.counts.found} · authenticated ${data.counts.authenticated}`
283
+ + ` · routable ${data.counts.routable}`;
284
+ }
285
+ function SetupStats({ data, mode, }) {
286
+ if (mode === "summary") {
287
+ return _jsx(BodyText, { children: statsSummaryLine(data) });
288
+ }
289
+ return (_jsxs(CockpitGrid, { children: [_jsx(StatTile, { label: "FOUND", value: data.counts.found, samples: [data.counts.found] }), _jsx(StatTile, { label: "AUTHENTICATED", value: data.counts.authenticated, samples: [data.counts.authenticated] }), _jsx(StatTile, { label: "ROUTABLE", value: data.counts.routable, samples: [data.counts.routable] })] }));
290
+ }
291
+ function SizedReviewPanel({ rows, bodyRows, }) {
292
+ const visible = rows.slice(0, bodyRows);
293
+ const emptyRows = Math.max(0, bodyRows - visible.length);
294
+ const review = JournalRowPanel({ rows: visible, title: "REVIEW" });
295
+ const reviewProps = review.props;
296
+ return cloneElement(review, {}, reviewProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "review-space") : null);
297
+ }
298
+ /**
299
+ * The contracted ladder (§5.4) governs the elements every surface shares. The
300
+ * detail panels below it are the setup body, so they surrender first, from the
301
+ * bottom of this list upward: what a step is *about* outlives what merely
302
+ * describes the fleet.
303
+ */
304
+ export const SETUP_DETAIL_ORDER = [
305
+ "detected",
306
+ "consultsAndReviewers",
307
+ "overlayDiff",
308
+ "fleetAndGates",
309
+ ];
310
+ /** Columns the HARNESSES panel gives its body, after side rails and chrome. */
311
+ function contentInnerWidth(columns, layout) {
312
+ const rails = Math.floor(columns) >= APPROVED_SIDE_RAIL_COLUMN_FLOOR
313
+ ? 2 * sidePanelWidth(layout) + 2 // both rails and the gaps beside them
314
+ : 0;
315
+ return Math.max(1, Math.floor(columns) - rails - 4);
316
+ }
317
+ function gridRows(panels, inner) {
318
+ return measureBandRows(inner, panels);
319
+ }
320
+ function detailRows(detail, data, inner) {
321
+ switch (detail) {
322
+ case "consultsAndReviewers":
323
+ return gridRows([
324
+ lineBandContent("CONSULTS", consultsLines(data)),
325
+ lineBandContent("REVIEWERS", reviewersLines(data)),
326
+ ], inner);
327
+ case "overlayDiff":
328
+ return panelRows(overlayDiffLines(data), inner - 4);
329
+ case "fleetAndGates":
330
+ return gridRows([
331
+ lineBandContent("FLEET", fleetLines(data)),
332
+ lineBandContent("GATES", gatesLines(data)),
333
+ ], inner);
334
+ }
335
+ }
336
+ /** Every detected row spends its first two columns on the state glyph and gap. */
337
+ function detectedPanelRows(rows, inner) {
338
+ const columns = Math.max(1, inner - 6);
339
+ return PANEL_CHROME_ROWS + rows.reduce((total, row) => total + composeBandLine(row.text.trimStart(), columns).length, 0);
340
+ }
341
+ /**
342
+ * DETECTED is the body of the step the surface is showing, so — like the
343
+ * journal body it stands in for (§5.4 rank 5) — it shrinks rather than being
344
+ * replaced: it gives up rows from the bottom, never below one row per state it
345
+ * still has to report, and only disappears when even that will not fit.
346
+ */
347
+ function detectedPlan(data, inner, budget) {
348
+ const all = detectedRows(data);
349
+ if (detectedPanelRows(all, inner) <= budget)
350
+ return all;
351
+ const states = new Set();
352
+ const smallest = all.filter((row) => {
353
+ if (states.has(row.state))
354
+ return false;
355
+ states.add(row.state);
356
+ return true;
357
+ });
358
+ if (detectedPanelRows(smallest, inner) > budget)
359
+ return [];
360
+ // Grow back toward the full panel, keeping the panel's own row order.
361
+ const kept = new Set(smallest);
362
+ for (const row of all) {
363
+ if (kept.has(row))
364
+ continue;
365
+ const grown = all.filter((candidate) => kept.has(candidate) || candidate === row);
366
+ if (detectedPanelRows(grown, inner) > budget)
367
+ break;
368
+ kept.add(row);
369
+ }
370
+ return all.filter((row) => kept.has(row));
371
+ }
372
+ function setupDetailPlan(rows, data, inner, base, journalPreferred) {
373
+ // DETECTED may borrow from the review body down to its one-row minimum; the
374
+ // configuration panels below it may not.
375
+ const detected = detectedPlan(data, inner, Math.max(0, Math.floor(rows) - base - 1));
376
+ const retained = new Set(detected.length > 0 ? ["detected"] : []);
377
+ let retainedRows = detected.length > 0
378
+ ? detectedPanelRows(detected, inner)
379
+ : 0;
380
+ let available = Math.max(0, Math.floor(rows) - base - journalPreferred - retainedRows);
381
+ for (const detail of SETUP_DETAIL_ORDER.slice(1)) {
382
+ const cost = detailRows(detail, data, inner);
383
+ if (available < cost)
384
+ break;
385
+ retained.add(detail);
386
+ retainedRows += cost;
387
+ available -= cost;
388
+ }
389
+ return {
390
+ detected,
391
+ retained,
392
+ retainedRows,
393
+ complete: retained.size === SETUP_DETAIL_ORDER.length
394
+ && detected.length === detectedRows(data).length,
395
+ };
396
+ }
397
+ function versionColumnWidth(data) {
398
+ return Math.max(...identityVersionLines(data).map((line) => [...line].length));
399
+ }
400
+ /**
401
+ * The banner sits beside the version column, so a narrow frame leaves it fewer
402
+ * columns and it wraps. Charged from the banner the header actually draws.
403
+ */
404
+ function headerRowsFor(layout, data, columns) {
405
+ if (!layout.elements.secondaryHeader)
406
+ return 1;
407
+ const banner = Math.max(1, Math.floor(columns) - versionColumnWidth(data));
408
+ return identityBannerLines().reduce((rows, line) => rows + wrappedRows(line, banner), 0);
409
+ }
410
+ function statRowsFor(layout, data, inner) {
411
+ // A tile is its value, its sparkline and its chrome; the stacked band spends
412
+ // one more row because the widest label wraps.
413
+ if (layout.stats.mode === "tiles") {
414
+ return PANEL_CHROME_ROWS + 2 + (layout.arrangement === "stacked" ? 1 : 0);
415
+ }
416
+ return wrappedRows(statsSummaryLine(data), inner);
417
+ }
418
+ /** Every row the frame spends before the detail panels and the review body. */
419
+ function baseRows(layout, data, columns, inner) {
420
+ return headerRowsFor(layout, data, columns)
421
+ + 2 // permanent status strip and keybar
422
+ + 3 // outer HARNESSES panel border and title
423
+ + statRowsFor(layout, data, inner)
424
+ + PANEL_CHROME_ROWS; // review border and title
425
+ }
426
+ function LadderSetupPanel({ data, layout, columns, rows, }) {
427
+ const inner = contentInnerWidth(columns, layout);
428
+ const preferredReviewRows = Math.min(FULL_JOURNAL_ROWS, layout.journalRows);
429
+ const base = baseRows(layout, data, columns, inner);
430
+ const details = setupDetailPlan(rows, data, inner, base, preferredReviewRows);
431
+ const spare = Math.max(1, Math.floor(rows) - base - details.retainedRows);
432
+ // Once the whole arrangement is drawn the review body is the only sink left,
433
+ // so it absorbs the surplus; below that it holds its contracted height.
434
+ const visibleReviewRows = details.complete
435
+ ? spare
436
+ : Math.min(preferredReviewRows, spare);
437
+ return (_jsxs(Panel, { title: "HARNESSES", focused: true, flexGrow: 1, children: [_jsx(SetupStats, { data: data, mode: layout.stats.mode }), details.retained.has("detected") && (_jsx(HarnessRows, { rows: details.detected, columns: Math.max(1, inner - 6) })), details.retained.has("fleetAndGates") && (_jsx(LineBand, { columns: inner, panels: [
438
+ lineBandContent("FLEET", fleetLines(data)),
439
+ lineBandContent("GATES", gatesLines(data)),
440
+ ] })), details.retained.has("consultsAndReviewers") && (_jsx(LineBand, { columns: inner, panels: [
441
+ lineBandContent("CONSULTS", consultsLines(data)),
442
+ lineBandContent("REVIEWERS", reviewersLines(data)),
443
+ ] })), details.retained.has("overlayDiff") && (_jsx(LinePanel, { title: "OVERLAY DIFF", lines: overlayDiffLines(data), columns: Math.max(1, inner - 4), emphasis: "dim" })), _jsx(SizedReviewPanel, { rows: data.reviewRows, bodyRows: visibleReviewRows })] }));
444
+ }
445
+ function SetupPanel({ data, inner, }) {
446
+ return (_jsxs(Panel, { title: "HARNESSES", focused: true, flexGrow: 1, children: [_jsxs(CockpitGrid, { children: [_jsx(StatTile, { label: "FOUND", value: data.counts.found, samples: [data.counts.found] }), _jsx(StatTile, { label: "AUTHENTICATED", value: data.counts.authenticated, samples: [data.counts.authenticated] }), _jsx(StatTile, { label: "ROUTABLE", value: data.counts.routable, samples: [data.counts.routable] })] }), _jsx(HarnessRows, { rows: detectedRows(data), columns: Math.max(1, inner - 6) }), _jsx(LineBand, { columns: inner, panels: [
447
+ lineBandContent("FLEET", fleetLines(data)),
448
+ lineBandContent("GATES", gatesLines(data)),
449
+ ] }), _jsx(LineBand, { columns: inner, panels: [
450
+ lineBandContent("CONSULTS", consultsLines(data)),
451
+ lineBandContent("REVIEWERS", reviewersLines(data)),
452
+ ] }), _jsx(LinePanel, { title: "OVERLAY DIFF", lines: overlayDiffLines(data), columns: Math.max(1, inner - 4), emphasis: "dim" }), _jsx(SizedReviewPanel, { rows: data.reviewRows, bodyRows: data.reviewRows.length })] }));
453
+ }
454
+ export function SetupCockpitFrame({ data, columns, rows, }) {
455
+ if (rows === undefined) {
456
+ const inner = Math.max(1, Math.floor(columns) - (2 * 16) - 2 - 4);
457
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(NavigationPanel, {}), _jsx(SetupPanel, { data: data, inner: inner }), _jsx(KeysPanel, {})] }), _jsx(StatusStrip, { width: columns, items: [
458
+ { state: "neutral", text: `overlay ${data.overlayTarget}` },
459
+ { state: "pass", text: "base untouched" },
460
+ {
461
+ state: "warn",
462
+ text: `${data.stagedChanges.length} changes unsaved`,
463
+ },
464
+ ] }), _jsx(Keybar, { surface: "setup", width: columns })] }));
465
+ }
466
+ const layout = resolveCockpitLayout(columns, rows);
467
+ if (layout.renderer === "plain")
468
+ return _jsx(_Fragment, {});
469
+ const showSideRails = columns >= APPROVED_SIDE_RAIL_COLUMN_FLOOR;
470
+ const contentLayout = showSideRails
471
+ ? layout
472
+ : {
473
+ ...layout,
474
+ stats: { ...layout.stats, mode: "summary", rows: 1 },
475
+ };
476
+ const showFullSidePanels = fullKeysFit(rows, contentLayout, data, columns);
477
+ const resolvedSidePanelWidth = sidePanelWidth(contentLayout);
478
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data, secondary: layout.elements.secondaryHeader }), _jsxs(Box, { flexDirection: "row", gap: showSideRails ? 1 : 0, children: [showSideRails && (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(NavigationPanel, { width: resolvedSidePanelWidth, compact: !contentLayout.elements.secondaryHeader }) })), _jsx(LadderSetupPanel, { data: data, layout: contentLayout, columns: columns, rows: rows }), showSideRails && (showFullSidePanels
479
+ ? (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(KeysPanel, { width: resolvedSidePanelWidth }) }))
480
+ : (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(CompactKeysPanel, { width: resolvedSidePanelWidth }) })))] }), _jsx(StatusStrip, { width: columns, items: [
481
+ { state: "neutral", text: `overlay ${data.overlayTarget}` },
482
+ { state: "pass", text: "base untouched" },
483
+ {
484
+ state: "warn",
485
+ text: `${data.stagedChanges.length} changes unsaved`,
486
+ },
487
+ ] }), _jsx(Keybar, { surface: "setup", width: columns })] }));
488
+ }
@@ -0,0 +1,161 @@
1
+ export type HexColour = `#${string}`;
2
+ export type ColourValue = {
3
+ readonly xterm: number;
4
+ readonly hex: HexColour;
5
+ };
6
+ /** The logo remains governed by src/brand.ts; cockpit data never modifies this ramp. */
7
+ export declare const LOGO_RAMP: readonly [84, 78, 41, 35];
8
+ /** Near-black panel fill against which every data shade must remain legible. */
9
+ export declare const COCKPIT_SURFACE: "#1a1a19";
10
+ /** Bright-to-deep, single-hue data ink. Depth communicates magnitude, never tile identity. */
11
+ export declare const COCKPIT_DATA_RAMP: readonly [{
12
+ readonly xterm: 84;
13
+ readonly hex: "#5fff5f";
14
+ }, {
15
+ readonly xterm: 41;
16
+ readonly hex: "#00d75f";
17
+ }, {
18
+ readonly xterm: 35;
19
+ readonly hex: "#00af5f";
20
+ }, {
21
+ readonly xterm: 29;
22
+ readonly hex: "#00875f";
23
+ }];
24
+ /** Documented near-misses retained as guard fixtures, not special cases in the guard. */
25
+ export declare const EXCLUDED_DATA_SHADES: {
26
+ readonly neighbourCollision: {
27
+ readonly xterm: 78;
28
+ readonly hex: "#5fd787";
29
+ };
30
+ readonly insufficientContrast: {
31
+ readonly xterm: 22;
32
+ readonly hex: "#005f00";
33
+ };
34
+ };
35
+ /**
36
+ * The only coloured semantic inks outside the green ramp. Shape and text make
37
+ * both meanings survive NO_COLOR and reduced-colour terminals.
38
+ */
39
+ export declare const COCKPIT_INKS: {
40
+ readonly dataRamp: readonly [{
41
+ readonly xterm: 84;
42
+ readonly hex: "#5fff5f";
43
+ }, {
44
+ readonly xterm: 41;
45
+ readonly hex: "#00d75f";
46
+ }, {
47
+ readonly xterm: 35;
48
+ readonly hex: "#00af5f";
49
+ }, {
50
+ readonly xterm: 29;
51
+ readonly hex: "#00875f";
52
+ }];
53
+ readonly warning: {
54
+ readonly xterm: 214;
55
+ readonly hex: "#ffaf00";
56
+ readonly glyph: "!";
57
+ readonly label: "warning";
58
+ };
59
+ readonly failure: {
60
+ readonly xterm: 203;
61
+ readonly hex: "#ff5f5f";
62
+ readonly glyph: "✗";
63
+ readonly label: "failure";
64
+ };
65
+ };
66
+ /** Every drawable cockpit ink; the panel surface and text emphasis are not inks. */
67
+ export declare const COCKPIT_INK_VOCABULARY: readonly [{
68
+ readonly xterm: 84;
69
+ readonly hex: "#5fff5f";
70
+ }, {
71
+ readonly xterm: 41;
72
+ readonly hex: "#00d75f";
73
+ }, {
74
+ readonly xterm: 35;
75
+ readonly hex: "#00af5f";
76
+ }, {
77
+ readonly xterm: 29;
78
+ readonly hex: "#00875f";
79
+ }, {
80
+ readonly xterm: 214;
81
+ readonly hex: "#ffaf00";
82
+ readonly glyph: "!";
83
+ readonly label: "warning";
84
+ }, {
85
+ readonly xterm: 203;
86
+ readonly hex: "#ff5f5f";
87
+ readonly glyph: "✗";
88
+ readonly label: "failure";
89
+ }];
90
+ /** Text hierarchy is independent of hue: weight and dimming are its only axes. */
91
+ export declare const TEXT_EMPHASIS_TOKENS: {
92
+ readonly strong: {
93
+ readonly weight: "bold";
94
+ readonly dimmed: false;
95
+ };
96
+ readonly normal: {
97
+ readonly weight: "normal";
98
+ readonly dimmed: false;
99
+ };
100
+ readonly dim: {
101
+ readonly weight: "normal";
102
+ readonly dimmed: true;
103
+ };
104
+ };
105
+ /** Deliberate 16-colour choices. Renderers select this map; they do not infer one. */
106
+ export declare const REDUCED_COLOUR_MAPPING: {
107
+ readonly dataRamp: readonly [{
108
+ readonly from: "#5fff5f";
109
+ readonly to: "bright-green";
110
+ }, {
111
+ readonly from: "#00d75f";
112
+ readonly to: "green";
113
+ }, {
114
+ readonly from: "#00af5f";
115
+ readonly to: "green";
116
+ }, {
117
+ readonly from: "#00875f";
118
+ readonly to: "dim-green";
119
+ }];
120
+ readonly warning: {
121
+ readonly from: "#ffaf00";
122
+ readonly to: "yellow";
123
+ };
124
+ readonly failure: {
125
+ readonly from: "#ff5f5f";
126
+ readonly to: "red";
127
+ };
128
+ };
129
+ export type TileMagnitude = 0 | 1 | 2 | 3;
130
+ export type TileColourInput = {
131
+ readonly magnitude: TileMagnitude;
132
+ };
133
+ /**
134
+ * Resolve reusable tile ink from magnitude alone. Tile names and positions are
135
+ * deliberately absent from the contract, so separate tiles cannot acquire identity hues.
136
+ */
137
+ export declare function resolveTileColour({ magnitude }: TileColourInput): (typeof COCKPIT_DATA_RAMP)[number];
138
+ export declare const DATA_RAMP_GUARD: {
139
+ readonly requiredSteps: 4;
140
+ readonly minimumContrast: 3;
141
+ readonly minimumNeighbourDistance: 0.08;
142
+ readonly maximumLightnessStepSpread: 0.02;
143
+ };
144
+ export type PaletteGuardIssueKind = "step-count" | "invalid-colour" | "lightness-order" | "lightness-spacing" | "neighbour-collision" | "contrast";
145
+ export type PaletteGuardIssue = {
146
+ readonly kind: PaletteGuardIssueKind;
147
+ readonly indexes: readonly number[];
148
+ };
149
+ export type PaletteGuardVerdict = {
150
+ readonly ok: boolean;
151
+ readonly lightness: readonly number[];
152
+ readonly lightnessSteps: readonly number[];
153
+ readonly neighbourDistances: readonly number[];
154
+ readonly contrastRatios: readonly number[];
155
+ readonly issues: readonly PaletteGuardIssue[];
156
+ };
157
+ /**
158
+ * Evaluate any four candidate values. Xterm indexes and declared palette names
159
+ * are intentionally ignored: only parsed colour values can earn a verdict.
160
+ */
161
+ export declare function guardDataRamp(candidate: readonly ColourValue[], surface?: HexColour): PaletteGuardVerdict;