tickmarkr 1.80.0 → 1.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/brand.d.ts +4 -1
- package/dist/brand.js +46 -7
- package/dist/cli/commands/approve.js +53 -8
- package/dist/cli/commands/plan.js +17 -2
- package/dist/cli/commands/report.js +15 -6
- package/dist/cli/commands/ui.d.ts +1 -1
- package/dist/cli/commands/ui.js +47 -4
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/collateral.d.ts +19 -0
- package/dist/compile/collateral.js +90 -0
- package/dist/compile/index.js +18 -4
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +19 -0
- package/dist/drivers/types.d.ts +1 -0
- package/dist/gates/acceptance.js +50 -8
- package/dist/gates/llm.js +34 -19
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +173 -7
- package/dist/gates/run-gates.d.ts +1 -0
- package/dist/gates/run-gates.js +18 -1
- package/dist/report/bundle.d.ts +6 -1
- package/dist/report/bundle.js +13 -3
- package/dist/run/daemon.d.ts +5 -0
- package/dist/run/daemon.js +188 -27
- package/dist/run/journal.d.ts +5 -0
- package/dist/run/journal.js +71 -1
- package/dist/tui/cockpit/capture.d.ts +107 -1
- package/dist/tui/cockpit/capture.js +287 -10
- package/dist/tui/cockpit/components.d.ts +15 -86
- package/dist/tui/cockpit/components.js +49 -55
- package/dist/tui/cockpit/derive.d.ts +73 -1
- package/dist/tui/cockpit/derive.js +211 -15
- package/dist/tui/cockpit/keys.d.ts +244 -0
- package/dist/tui/cockpit/keys.js +634 -0
- package/dist/tui/cockpit/layout.d.ts +162 -0
- package/dist/tui/cockpit/layout.js +229 -0
- package/dist/tui/cockpit/live.d.ts +80 -0
- package/dist/tui/cockpit/live.js +311 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +65 -2
- package/dist/tui/cockpit/run-cockpit.js +705 -128
- package/dist/tui/cockpit/setup-cockpit.d.ts +149 -1
- package/dist/tui/cockpit/setup-cockpit.js +432 -56
- package/dist/tui/cockpit/views.d.ts +72 -0
- package/dist/tui/cockpit/views.js +56 -0
- package/dist/tui/cockpit/width.d.ts +105 -0
- package/dist/tui/cockpit/width.js +276 -0
- package/fixtures/speckit-sample/tasks.md +2 -2
- package/package.json +3 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type DemoCaptures } from "./demo.js";
|
|
3
|
+
import { type RunInteractionState, type RunKeyProjection } from "./keys.js";
|
|
3
4
|
export type CockpitName = "run" | "setup";
|
|
4
5
|
export type CockpitRenderer = "frame" | "plain";
|
|
5
6
|
export type RendererCaptureOptions = {
|
|
@@ -21,6 +22,8 @@ export type CaptureCockpitOptions = {
|
|
|
21
22
|
readonly interactive?: boolean;
|
|
22
23
|
readonly colour?: boolean;
|
|
23
24
|
readonly ci?: boolean;
|
|
25
|
+
readonly runInteraction?: RunInteractionState;
|
|
26
|
+
readonly runKeyProjection?: RunKeyProjection;
|
|
24
27
|
readonly captures?: DemoCaptures;
|
|
25
28
|
readonly plainRenderer?: (cockpit: CockpitName, binaryVersion: string, captures: DemoCaptures) => string;
|
|
26
29
|
readonly frameRenderer?: FrameRenderer;
|
|
@@ -40,6 +43,98 @@ export declare const WIDTH_BAND_CASES: readonly [{
|
|
|
40
43
|
readonly columns: 140;
|
|
41
44
|
}];
|
|
42
45
|
export declare const HEIGHT_TIER_BOUNDARIES: readonly [14, 18, 24, 40];
|
|
46
|
+
export declare const COCKPIT_CONTRACT_DOMAIN: {
|
|
47
|
+
readonly columns: {
|
|
48
|
+
readonly minimum: 40;
|
|
49
|
+
readonly maximum: 220;
|
|
50
|
+
};
|
|
51
|
+
readonly rows: {
|
|
52
|
+
readonly minimum: 14;
|
|
53
|
+
readonly maximum: 50;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* What the contract's artwork draws for the rail: a 15-cell content band, then
|
|
58
|
+
* one blank gutter cell, then the divider at cell 16. Both numbers are measured
|
|
59
|
+
* from the approved frames in the design contract, and the rail span the plan
|
|
60
|
+
* allocates is the same 15.
|
|
61
|
+
*/
|
|
62
|
+
export declare const COCKPIT_SIDEBAR_CONTENT_COLUMNS = 15;
|
|
63
|
+
export declare const COCKPIT_SIDEBAR_GUTTER_COLUMNS = 1;
|
|
64
|
+
/** Content plus gutter — the cells before the divider. */
|
|
65
|
+
export declare const COCKPIT_SIDEBAR_COLUMNS: number;
|
|
66
|
+
/**
|
|
67
|
+
* Where the paint puts the frame's own region boundaries, read off the bytes.
|
|
68
|
+
* The contract's artwork draws a region edge as a glyph — a full-width rule
|
|
69
|
+
* between row bands, a vertical between the rail and the body — so a region
|
|
70
|
+
* that moved or that overlaps its neighbour is visible here as a boundary in
|
|
71
|
+
* the wrong place, at unchanged outer dimensions.
|
|
72
|
+
*/
|
|
73
|
+
export type CockpitFrameBoundaries = {
|
|
74
|
+
/** The paint is exactly `rows` lines of exactly `columns` cells. */
|
|
75
|
+
readonly grid: boolean;
|
|
76
|
+
/** Rows drawn as a full-width horizontal rule, in order. */
|
|
77
|
+
readonly ruleRows: readonly number[];
|
|
78
|
+
/** Columns carrying a vertical boundary inside the body band, in order. */
|
|
79
|
+
readonly dividerColumns: readonly number[];
|
|
80
|
+
/** Every contiguous vertical boundary span the paint draws in the body. */
|
|
81
|
+
readonly dividerSpans: readonly CockpitBoundarySpan[];
|
|
82
|
+
/** Rows carrying an advertised key label, in order. */
|
|
83
|
+
readonly keyRows: readonly number[];
|
|
84
|
+
};
|
|
85
|
+
export type CockpitBoundarySpan = {
|
|
86
|
+
readonly column: number;
|
|
87
|
+
readonly row: number;
|
|
88
|
+
readonly rows: number;
|
|
89
|
+
};
|
|
90
|
+
export type CockpitFrameContractInspection = {
|
|
91
|
+
readonly tiles: boolean;
|
|
92
|
+
/** The boundary geometry `tiles` is decided from. */
|
|
93
|
+
readonly boundaries: CockpitFrameBoundaries;
|
|
94
|
+
readonly withinWidth: boolean;
|
|
95
|
+
readonly headerCount: number;
|
|
96
|
+
readonly markGlyphCount: number;
|
|
97
|
+
readonly keybarContained: boolean;
|
|
98
|
+
/** Cells the rail's content band occupies; 0 where the plan draws no rail. */
|
|
99
|
+
readonly sidebarContentColumns: number;
|
|
100
|
+
/** Blank cells between that band and the divider; 0 where there is no rail. */
|
|
101
|
+
readonly sidebarGutterColumns: number;
|
|
102
|
+
/** Focus markers inside the navigation zone alone — never the whole frame. */
|
|
103
|
+
readonly sidebarFocusMarkers: number;
|
|
104
|
+
/** Selection markers inside the active list zone alone. */
|
|
105
|
+
readonly listSelectionMarkers: number;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The mark's alphabet: the block range minus the glyphs the approved artwork
|
|
109
|
+
* itself draws — the sparkline's eighth-block ramp (▁▂▃▄▅▆▇█) and the meter's
|
|
110
|
+
* shades (░▒▓). Subtracting them is what lets the scan run over the whole frame
|
|
111
|
+
* rather than over a header band a renderer could redefine, which is the point:
|
|
112
|
+
* a mark reintroduced anywhere is found.
|
|
113
|
+
*/
|
|
114
|
+
export declare const COCKPIT_MARK_ALPHABET = "[\u2580\u2589-\u2590\u2594-\u259F]";
|
|
115
|
+
/** Where the paint drew the header signature, in cells rather than in bytes. */
|
|
116
|
+
export type HeaderSignatureHit = {
|
|
117
|
+
readonly row: number;
|
|
118
|
+
readonly column: number;
|
|
119
|
+
readonly text: string;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Every header signature in a painted frame, at whatever indentation it was
|
|
123
|
+
* drawn. The inspector counts these and the falsification lever plants one, so
|
|
124
|
+
* neither can hold a private idea of what a header looks like.
|
|
125
|
+
*/
|
|
126
|
+
export declare function findHeaderSignatures(lines: readonly string[]): HeaderSignatureHit[];
|
|
127
|
+
/**
|
|
128
|
+
* Inspect emitted terminal bytes without re-planning their geometry. This is
|
|
129
|
+
* deliberately renderer-agnostic: the sweep and the production capture path
|
|
130
|
+
* can judge the same paint, and a future renderer cannot move this target by
|
|
131
|
+
* changing its own plan. The one thing it does take from the plan is where the
|
|
132
|
+
* zones are, because "one zone holds focus" is a statement about zones and
|
|
133
|
+
* cannot be counted as a frame-wide glyph total.
|
|
134
|
+
*/
|
|
135
|
+
export declare function inspectCockpitFrameContract(output: string, columns: number, rows: number): CockpitFrameContractInspection;
|
|
136
|
+
/** What the contract's artwork draws at one size — the sweep's expectation. */
|
|
137
|
+
export declare function cockpitFrameContractExpectation(columns: number, rows: number): Omit<CockpitFrameContractInspection, "listSelectionMarkers" | "boundaries">;
|
|
43
138
|
export type GoldenFrameCase = {
|
|
44
139
|
readonly id: string;
|
|
45
140
|
readonly fixture: string;
|
|
@@ -51,6 +146,14 @@ export type GoldenFrameCase = {
|
|
|
51
146
|
readonly colour: boolean;
|
|
52
147
|
readonly ci: boolean;
|
|
53
148
|
};
|
|
149
|
+
/** The surfaces whose rendered appearance this redesign moved. */
|
|
150
|
+
export declare const RETIRED_RENDERED_COCKPITS: readonly CockpitName[];
|
|
151
|
+
/**
|
|
152
|
+
* A golden case is RETIRED when its cockpit's rendered appearance moved and
|
|
153
|
+
* the case is a rendered one. Retirement keeps the superseded bytes committed
|
|
154
|
+
* and the case in the shipped manifest — only regeneration skips it (OBS-207).
|
|
155
|
+
*/
|
|
156
|
+
export declare const isRetiredGoldenFrame: (golden: GoldenFrameCase) => boolean;
|
|
54
157
|
export declare const GOLDEN_FRAME_CASES: readonly GoldenFrameCase[];
|
|
55
158
|
export type ColourFrameCase = {
|
|
56
159
|
readonly id: "healthy-colour" | "healthy-no-colour" | "warning-failure-colour";
|
|
@@ -62,7 +165,9 @@ export type ColourFrameCase = {
|
|
|
62
165
|
};
|
|
63
166
|
/**
|
|
64
167
|
* The colour corpus has a deliberately separate manifest from the layout
|
|
65
|
-
* corpus. Layout cases stay colourless; these cases alone exercise real ink
|
|
168
|
+
* corpus. Layout cases stay colourless; these cases alone exercise real ink,
|
|
169
|
+
* and their committed bytes are RETIRED superseded evidence: the ink oracles
|
|
170
|
+
* render each case fresh and judge it.
|
|
66
171
|
*/
|
|
67
172
|
export declare const COLOUR_FRAME_CASES: readonly [{
|
|
68
173
|
readonly id: "healthy-colour";
|
|
@@ -86,6 +191,7 @@ export declare const COLOUR_FRAME_CASES: readonly [{
|
|
|
86
191
|
readonly rows: 24;
|
|
87
192
|
readonly colour: true;
|
|
88
193
|
}];
|
|
194
|
+
export declare function defaultPlainRenderer(cockpit: CockpitName, binaryVersion: string, captures: DemoCaptures, columns?: number, runInteraction?: RunInteractionState, runKeyProjection?: RunKeyProjection): string;
|
|
89
195
|
/**
|
|
90
196
|
* Capture exactly the bytes Ink writes for one static paint. Debug mode is
|
|
91
197
|
* deliberate: it exposes the complete frame rather than terminal patch
|
|
@@ -9,17 +9,259 @@ import { GLYPHS } from "../../brand.js";
|
|
|
9
9
|
import { version } from "../../cli/commands/version.js";
|
|
10
10
|
import { KEYBAR_KEYS } from "./components.js";
|
|
11
11
|
import { loadDemoCaptures } from "./demo.js";
|
|
12
|
-
import { resolveCockpitLayout, } from "./layout.js";
|
|
13
|
-
import { deriveRunCockpitData, RunCockpitFrame, } from "./run-cockpit.js";
|
|
12
|
+
import { planFrame, RAIL_COLUMNS, resolveCockpitLayout, } from "./layout.js";
|
|
13
|
+
import { deriveRunCockpitData, deriveRunViewRows, RunCockpitFrame, } from "./run-cockpit.js";
|
|
14
14
|
import { deriveSetupCockpitData, SetupCockpitFrame, } from "./setup-cockpit.js";
|
|
15
|
+
import { formatKeybarEntries, initialRunInteractionState, projectRunKeyEntries, } from "./keys.js";
|
|
16
|
+
import { cellWidth } from "./width.js";
|
|
15
17
|
export const WIDTH_BAND_CASES = [
|
|
16
18
|
{ arrangement: "stacked", columns: 80 },
|
|
17
19
|
{ arrangement: "folded-keys", columns: 100 },
|
|
18
20
|
{ arrangement: "three-column", columns: 140 },
|
|
19
21
|
];
|
|
20
22
|
export const HEIGHT_TIER_BOUNDARIES = [14, 18, 24, 40];
|
|
23
|
+
export const COCKPIT_CONTRACT_DOMAIN = {
|
|
24
|
+
columns: { minimum: 40, maximum: 220 },
|
|
25
|
+
rows: { minimum: 14, maximum: 50 },
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* What the contract's artwork draws for the rail: a 15-cell content band, then
|
|
29
|
+
* one blank gutter cell, then the divider at cell 16. Both numbers are measured
|
|
30
|
+
* from the approved frames in the design contract, and the rail span the plan
|
|
31
|
+
* allocates is the same 15.
|
|
32
|
+
*/
|
|
33
|
+
export const COCKPIT_SIDEBAR_CONTENT_COLUMNS = RAIL_COLUMNS;
|
|
34
|
+
export const COCKPIT_SIDEBAR_GUTTER_COLUMNS = 1;
|
|
35
|
+
/** Content plus gutter — the cells before the divider. */
|
|
36
|
+
export const COCKPIT_SIDEBAR_COLUMNS = COCKPIT_SIDEBAR_CONTENT_COLUMNS + COCKPIT_SIDEBAR_GUTTER_COLUMNS;
|
|
37
|
+
const CAPTURE_ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
38
|
+
/**
|
|
39
|
+
* The mark's alphabet: the block range minus the glyphs the approved artwork
|
|
40
|
+
* itself draws — the sparkline's eighth-block ramp (▁▂▃▄▅▆▇█) and the meter's
|
|
41
|
+
* shades (░▒▓). Subtracting them is what lets the scan run over the whole frame
|
|
42
|
+
* rather than over a header band a renderer could redefine, which is the point:
|
|
43
|
+
* a mark reintroduced anywhere is found.
|
|
44
|
+
*/
|
|
45
|
+
export const COCKPIT_MARK_ALPHABET = "[▀▉-▐▔-▟]";
|
|
46
|
+
const COCKPIT_MARK_GLYPHS = new RegExp(COCKPIT_MARK_ALPHABET, "gu");
|
|
47
|
+
const KEYBAR_LABEL = /(?:↑↓ Move|⏎ Open|← Back|Tab (?:Setup|Watch)|1–5 View|f Follow|\? Help|q Quit)/u;
|
|
48
|
+
/**
|
|
49
|
+
* The header signature: the brand word standing on its own, wherever the paint
|
|
50
|
+
* puts it. Indentation is not part of the signature — the artwork insets the
|
|
51
|
+
* word on wide frames, so anchoring at column zero would read a valid header as
|
|
52
|
+
* absent and an indented duplicate as absent too. A path or branch reference
|
|
53
|
+
* that merely contains the word (`tickmarkr/run-…`) is not a header, so both
|
|
54
|
+
* sides are bounded against the characters such a reference joins with.
|
|
55
|
+
*/
|
|
56
|
+
const HEADER_SIGNATURE = /(?<![\w/-])tickmarkr(?![\w/-])/gu;
|
|
57
|
+
const FOCUS_MARKER = "❯";
|
|
58
|
+
/** The glyphs the contract's artwork draws a region edge with. */
|
|
59
|
+
const RULE_GLYPH = "─";
|
|
60
|
+
const DIVIDER_GLYPH = "│";
|
|
61
|
+
const sameSequence = (observed, planned) => observed.length === planned.length
|
|
62
|
+
&& observed.every((value, index) => value === planned[index]);
|
|
63
|
+
const sameBoundarySpans = (observed, planned) => observed.length === planned.length
|
|
64
|
+
&& observed.every((span, index) => {
|
|
65
|
+
const expected = planned[index];
|
|
66
|
+
return expected !== undefined
|
|
67
|
+
&& span.column === expected.column
|
|
68
|
+
&& span.row === expected.row
|
|
69
|
+
&& span.rows === expected.rows;
|
|
70
|
+
});
|
|
71
|
+
/** Display-cell offsets carrying `glyph`, never code-point offsets. */
|
|
72
|
+
function glyphColumns(line, glyph) {
|
|
73
|
+
return [...line.matchAll(new RegExp(glyph, "gu"))].map((match) => cellWidth(line.slice(0, match.index)));
|
|
74
|
+
}
|
|
75
|
+
/** Contiguous row spans for every column carrying the vertical boundary. */
|
|
76
|
+
function verticalBoundarySpans(lines, region) {
|
|
77
|
+
if (!region)
|
|
78
|
+
return [];
|
|
79
|
+
const rowsByColumn = new Map();
|
|
80
|
+
for (let row = region.row; row < region.row + region.rows; row += 1) {
|
|
81
|
+
for (const column of glyphColumns(lines[row] ?? "", DIVIDER_GLYPH)) {
|
|
82
|
+
const observedRows = rowsByColumn.get(column) ?? [];
|
|
83
|
+
observedRows.push(row);
|
|
84
|
+
rowsByColumn.set(column, observedRows);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...rowsByColumn.entries()]
|
|
88
|
+
.sort(([left], [right]) => left - right)
|
|
89
|
+
.flatMap(([column, observedRows]) => {
|
|
90
|
+
const spans = [];
|
|
91
|
+
let start = observedRows[0];
|
|
92
|
+
let previous = start;
|
|
93
|
+
for (const row of observedRows.slice(1)) {
|
|
94
|
+
if (previous !== undefined && row === previous + 1) {
|
|
95
|
+
previous = row;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (start !== undefined && previous !== undefined) {
|
|
99
|
+
spans.push({ column, row: start, rows: previous - start + 1 });
|
|
100
|
+
}
|
|
101
|
+
start = row;
|
|
102
|
+
previous = row;
|
|
103
|
+
}
|
|
104
|
+
if (start !== undefined && previous !== undefined) {
|
|
105
|
+
spans.push({ column, row: start, rows: previous - start + 1 });
|
|
106
|
+
}
|
|
107
|
+
return spans;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Every header signature in a painted frame, at whatever indentation it was
|
|
112
|
+
* drawn. The inspector counts these and the falsification lever plants one, so
|
|
113
|
+
* neither can hold a private idea of what a header looks like.
|
|
114
|
+
*/
|
|
115
|
+
export function findHeaderSignatures(lines) {
|
|
116
|
+
return lines.flatMap((line, row) => [...line.matchAll(HEADER_SIGNATURE)].map((match) => ({
|
|
117
|
+
row,
|
|
118
|
+
// Cells, not code units — the same count the frame's cells are read in.
|
|
119
|
+
column: cellWidth(line.slice(0, match.index)),
|
|
120
|
+
text: match[0],
|
|
121
|
+
})));
|
|
122
|
+
}
|
|
123
|
+
/** The cells of one row inside a planned region's column span. */
|
|
124
|
+
function regionSlice(line, region) {
|
|
125
|
+
const cells = [...line];
|
|
126
|
+
return cells.slice(region.column, region.column + region.columns).join("");
|
|
127
|
+
}
|
|
128
|
+
function markersIn(lines, region) {
|
|
129
|
+
if (!region)
|
|
130
|
+
return 0;
|
|
131
|
+
let markers = 0;
|
|
132
|
+
for (let row = region.row; row < region.row + region.rows; row += 1) {
|
|
133
|
+
const line = lines[row];
|
|
134
|
+
if (line === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
markers += [...regionSlice(line, region)]
|
|
137
|
+
.filter((glyph) => glyph === FOCUS_MARKER).length;
|
|
138
|
+
}
|
|
139
|
+
return markers;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Inspect emitted terminal bytes without re-planning their geometry. This is
|
|
143
|
+
* deliberately renderer-agnostic: the sweep and the production capture path
|
|
144
|
+
* can judge the same paint, and a future renderer cannot move this target by
|
|
145
|
+
* changing its own plan. The one thing it does take from the plan is where the
|
|
146
|
+
* zones are, because "one zone holds focus" is a statement about zones and
|
|
147
|
+
* cannot be counted as a frame-wide glyph total.
|
|
148
|
+
*/
|
|
149
|
+
export function inspectCockpitFrameContract(output, columns, rows) {
|
|
150
|
+
const visible = output.replace(CAPTURE_ANSI, "").replace(/\n$/u, "");
|
|
151
|
+
const lines = visible.split("\n");
|
|
152
|
+
const widths = lines.map((line) => cellWidth(line));
|
|
153
|
+
const headerCount = findHeaderSignatures(lines).length;
|
|
154
|
+
const markGlyphCount = lines.reduce((count, line) => count + [...line.matchAll(COCKPIT_MARK_GLYPHS)].length, 0);
|
|
155
|
+
const keyRows = lines.flatMap((line, row) => KEYBAR_LABEL.test(line) ? [row] : []);
|
|
156
|
+
const plan = planFrame({ columns, rows });
|
|
157
|
+
const regions = plan.kind === "frame" ? plan.regions : [];
|
|
158
|
+
const region = (id) => regions.find((candidate) => candidate.id === id);
|
|
159
|
+
const body = region("body");
|
|
160
|
+
const rail = region("rail");
|
|
161
|
+
const navigation = rail ?? region("strip");
|
|
162
|
+
const keybar = region("keybar");
|
|
163
|
+
// The boundaries the paint actually draws. A full-width rule is a row band's
|
|
164
|
+
// edge; a vertical inside the body band is the rail's edge. Both are read
|
|
165
|
+
// from the bytes and neither is inferred from whichever row happens to be
|
|
166
|
+
// painted last.
|
|
167
|
+
const ruleRows = lines.flatMap((line, row) => {
|
|
168
|
+
const cells = [...line];
|
|
169
|
+
return cells.length > 0 && cellWidth(line) === columns
|
|
170
|
+
&& cells.every((glyph) => glyph === RULE_GLYPH)
|
|
171
|
+
? [row]
|
|
172
|
+
: [];
|
|
173
|
+
});
|
|
174
|
+
const dividerSpans = verticalBoundarySpans(lines, body);
|
|
175
|
+
const dividerColumns = [...new Set(dividerSpans.map(({ column }) => column))];
|
|
176
|
+
const boundaries = {
|
|
177
|
+
grid: lines.length === rows
|
|
178
|
+
&& lines.every((line) => cellWidth(line) === columns),
|
|
179
|
+
ruleRows,
|
|
180
|
+
dividerColumns,
|
|
181
|
+
dividerSpans,
|
|
182
|
+
keyRows,
|
|
183
|
+
};
|
|
184
|
+
// What the plan says those boundaries must be: a rule at each rule band, and
|
|
185
|
+
// — only where the artwork draws a rail — one vertical, at the divider cell
|
|
186
|
+
// the rail's content band and its gutter end at.
|
|
187
|
+
const plannedRuleRows = regions
|
|
188
|
+
.filter((candidate) => candidate.id === "rule" || candidate.id === "rule2")
|
|
189
|
+
.map((candidate) => candidate.row)
|
|
190
|
+
.sort((left, right) => left - right);
|
|
191
|
+
const plannedDividerColumns = rail ? [COCKPIT_SIDEBAR_COLUMNS] : [];
|
|
192
|
+
const plannedDividerSpans = rail && body
|
|
193
|
+
? [{
|
|
194
|
+
column: COCKPIT_SIDEBAR_COLUMNS,
|
|
195
|
+
row: body.row,
|
|
196
|
+
rows: body.rows,
|
|
197
|
+
}]
|
|
198
|
+
: [];
|
|
199
|
+
// The rail's content band and its gutter, read off the paint: the divider
|
|
200
|
+
// column the body rows agree on, less the blank cells every one of them
|
|
201
|
+
// leaves in front of it.
|
|
202
|
+
let sidebarContentColumns = 0;
|
|
203
|
+
let sidebarGutterColumns = 0;
|
|
204
|
+
if (rail && body) {
|
|
205
|
+
const railRows = lines
|
|
206
|
+
.slice(body.row, body.row + body.rows)
|
|
207
|
+
.filter((line) => line.includes("│"));
|
|
208
|
+
const dividers = new Set(railRows.map((line) => cellWidth(line.slice(0, line.indexOf("│")))));
|
|
209
|
+
if (railRows.length === body.rows && dividers.size === 1) {
|
|
210
|
+
const divider = [...dividers][0];
|
|
211
|
+
let gutter = 0;
|
|
212
|
+
while (gutter < divider
|
|
213
|
+
&& railRows.every((line) => ([...line][divider - gutter - 1] ?? " ") === " ")) {
|
|
214
|
+
gutter += 1;
|
|
215
|
+
}
|
|
216
|
+
sidebarContentColumns = divider - gutter;
|
|
217
|
+
sidebarGutterColumns = gutter;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
// Tiling is region ownership, not a filled rectangle: the paint covers the
|
|
222
|
+
// whole grid AND every boundary sits at the offset the plan gives it, so a
|
|
223
|
+
// region moved or overlapped at unchanged outer dimensions is a violation.
|
|
224
|
+
tiles: boundaries.grid
|
|
225
|
+
&& sameSequence(ruleRows, plannedRuleRows)
|
|
226
|
+
&& sameSequence(dividerColumns, plannedDividerColumns)
|
|
227
|
+
&& sameBoundarySpans(dividerSpans, plannedDividerSpans),
|
|
228
|
+
boundaries,
|
|
229
|
+
withinWidth: widths.every((width) => width <= columns),
|
|
230
|
+
headerCount,
|
|
231
|
+
markGlyphCount,
|
|
232
|
+
// Contained means inside the keybar band the plan allocated — never
|
|
233
|
+
// "wherever the last painted row turned out to be", which accepts a keybar
|
|
234
|
+
// one row above its own blank band.
|
|
235
|
+
keybarContained: keybar !== undefined
|
|
236
|
+
&& keyRows.length > 0
|
|
237
|
+
&& keyRows.every((row) => row === keybar.row),
|
|
238
|
+
sidebarContentColumns,
|
|
239
|
+
sidebarGutterColumns,
|
|
240
|
+
sidebarFocusMarkers: markersIn(lines, navigation),
|
|
241
|
+
listSelectionMarkers: markersIn(lines, body),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** What the contract's artwork draws at one size — the sweep's expectation. */
|
|
245
|
+
export function cockpitFrameContractExpectation(columns, rows) {
|
|
246
|
+
const plan = planFrame({ columns, rows });
|
|
247
|
+
const sidebar = plan.kind === "frame" && plan.band === "sidebar";
|
|
248
|
+
return {
|
|
249
|
+
tiles: true,
|
|
250
|
+
withinWidth: true,
|
|
251
|
+
headerCount: 1,
|
|
252
|
+
markGlyphCount: 0,
|
|
253
|
+
keybarContained: true,
|
|
254
|
+
sidebarContentColumns: sidebar ? COCKPIT_SIDEBAR_CONTENT_COLUMNS : 0,
|
|
255
|
+
sidebarGutterColumns: sidebar ? COCKPIT_SIDEBAR_GUTTER_COLUMNS : 0,
|
|
256
|
+
sidebarFocusMarkers: 1,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
21
259
|
const fixtureName = (cockpit, id, columns, rows) => `${cockpit}.${id}.${columns}x${rows}.txt`;
|
|
22
|
-
|
|
260
|
+
/**
|
|
261
|
+
* The rendered corpus of one surface: a frame at each contracted width band and
|
|
262
|
+
* each height tier boundary, plus the colourless variant.
|
|
263
|
+
*/
|
|
264
|
+
const renderedCasesFor = (cockpit) => [
|
|
23
265
|
...WIDTH_BAND_CASES.map(({ arrangement, columns }) => ({
|
|
24
266
|
id: `${cockpit}-width-${arrangement}`,
|
|
25
267
|
fixture: fixtureName(cockpit, `width-${arrangement}`, columns, 24),
|
|
@@ -53,6 +295,9 @@ const goldenCasesFor = (cockpit) => [
|
|
|
53
295
|
colour: false,
|
|
54
296
|
ci: false,
|
|
55
297
|
},
|
|
298
|
+
];
|
|
299
|
+
/** The two machine surfaces: output this redesign did not move. */
|
|
300
|
+
const machineCasesFor = (cockpit) => [
|
|
56
301
|
{
|
|
57
302
|
id: `${cockpit}-non-tty`,
|
|
58
303
|
fixture: fixtureName(cockpit, "non-tty", 140, 24),
|
|
@@ -76,13 +321,29 @@ const goldenCasesFor = (cockpit) => [
|
|
|
76
321
|
ci: true,
|
|
77
322
|
},
|
|
78
323
|
];
|
|
324
|
+
/** The surfaces whose rendered appearance this redesign moved. */
|
|
325
|
+
export const RETIRED_RENDERED_COCKPITS = ["run"];
|
|
326
|
+
/**
|
|
327
|
+
* A golden case is RETIRED when its cockpit's rendered appearance moved and
|
|
328
|
+
* the case is a rendered one. Retirement keeps the superseded bytes committed
|
|
329
|
+
* and the case in the shipped manifest — only regeneration skips it (OBS-207).
|
|
330
|
+
*/
|
|
331
|
+
export const isRetiredGoldenFrame = (golden) => RETIRED_RENDERED_COCKPITS.includes(golden.cockpit)
|
|
332
|
+
&& golden.interactive
|
|
333
|
+
&& !golden.ci;
|
|
334
|
+
const goldenCasesFor = (cockpit) => [
|
|
335
|
+
...renderedCasesFor(cockpit),
|
|
336
|
+
...machineCasesFor(cockpit),
|
|
337
|
+
];
|
|
79
338
|
export const GOLDEN_FRAME_CASES = [
|
|
80
339
|
...goldenCasesFor("run"),
|
|
81
340
|
...goldenCasesFor("setup"),
|
|
82
341
|
];
|
|
83
342
|
/**
|
|
84
343
|
* The colour corpus has a deliberately separate manifest from the layout
|
|
85
|
-
* corpus. Layout cases stay colourless; these cases alone exercise real ink
|
|
344
|
+
* corpus. Layout cases stay colourless; these cases alone exercise real ink,
|
|
345
|
+
* and their committed bytes are RETIRED superseded evidence: the ink oracles
|
|
346
|
+
* render each case fresh and judge it.
|
|
86
347
|
*/
|
|
87
348
|
export const COLOUR_FRAME_CASES = [
|
|
88
349
|
{
|
|
@@ -137,15 +398,20 @@ function stateGlyph(state) {
|
|
|
137
398
|
return GLYPHS.attention;
|
|
138
399
|
return GLYPHS.neutral;
|
|
139
400
|
}
|
|
140
|
-
function defaultPlainRenderer(cockpit, binaryVersion, captures) {
|
|
401
|
+
export function defaultPlainRenderer(cockpit, binaryVersion, captures, columns = 140, runInteraction = initialRunInteractionState(), runKeyProjection = projectRunKeyEntries) {
|
|
141
402
|
const derived = derivedData(cockpit, binaryVersion, captures);
|
|
142
403
|
if (derived.cockpit === "run") {
|
|
143
404
|
const data = derived.data;
|
|
405
|
+
const rowIds = deriveRunViewRows(data, runInteraction.activeView, runInteraction.filterQuery).map((row) => row.id);
|
|
144
406
|
return [
|
|
145
407
|
`tickmarkr v${binaryVersion}`,
|
|
146
408
|
`${data.runId} ${data.status} ${data.elapsed}`,
|
|
147
409
|
...data.statusItems.map((item) => `${stateGlyph(item.state)} ${item.text}`),
|
|
148
|
-
`keys ${
|
|
410
|
+
`keys ${formatKeybarEntries(runKeyProjection({
|
|
411
|
+
interaction: runInteraction,
|
|
412
|
+
columns,
|
|
413
|
+
rowIds,
|
|
414
|
+
}))}`,
|
|
149
415
|
"",
|
|
150
416
|
].join("\n");
|
|
151
417
|
}
|
|
@@ -161,12 +427,14 @@ function defaultPlainRenderer(cockpit, binaryVersion, captures) {
|
|
|
161
427
|
"",
|
|
162
428
|
].join("\n");
|
|
163
429
|
}
|
|
164
|
-
function frameNode(derived, columns, rows) {
|
|
430
|
+
function frameNode(derived, columns, rows, runInteraction = initialRunInteractionState(), runKeyProjection = projectRunKeyEntries) {
|
|
165
431
|
if (derived.cockpit === "run") {
|
|
166
432
|
return createElement(RunCockpitFrame, {
|
|
167
433
|
data: derived.data,
|
|
168
434
|
columns,
|
|
169
435
|
rows,
|
|
436
|
+
interaction: runInteraction,
|
|
437
|
+
keyProjection: runKeyProjection,
|
|
170
438
|
});
|
|
171
439
|
}
|
|
172
440
|
return createElement(SetupCockpitFrame, {
|
|
@@ -239,7 +507,9 @@ export async function captureCockpitOutput(options) {
|
|
|
239
507
|
?? process.env.NO_COLOR === undefined;
|
|
240
508
|
const ci = options.ci ?? process.env.CI === "true";
|
|
241
509
|
const captures = options.captures ?? loadDemoCaptures();
|
|
242
|
-
const plain = () =>
|
|
510
|
+
const plain = () => options.plainRenderer === undefined
|
|
511
|
+
? defaultPlainRenderer(options.cockpit, options.binaryVersion, captures, columns, options.runInteraction, options.runKeyProjection)
|
|
512
|
+
: options.plainRenderer(options.cockpit, options.binaryVersion, captures);
|
|
243
513
|
const layout = resolveCockpitLayout(columns, rows);
|
|
244
514
|
if (!interactive || options.output.isTTY !== true || ci || layout.renderer === "plain") {
|
|
245
515
|
const plainOutput = plain();
|
|
@@ -247,7 +517,7 @@ export async function captureCockpitOutput(options) {
|
|
|
247
517
|
return { renderer: "plain", output: plainOutput };
|
|
248
518
|
}
|
|
249
519
|
const derived = derivedData(options.cockpit, options.binaryVersion, captures);
|
|
250
|
-
const frameOutput = await (options.frameRenderer ?? captureRendererOutput)(frameNode(derived, columns, rows), { columns, rows, colour });
|
|
520
|
+
const frameOutput = await (options.frameRenderer ?? captureRendererOutput)(frameNode(derived, columns, rows, options.runInteraction, options.runKeyProjection), { columns, rows, colour });
|
|
251
521
|
if (frameOutput.length === 0) {
|
|
252
522
|
const plainOutput = plain();
|
|
253
523
|
options.output.write(plainOutput);
|
|
@@ -261,6 +531,10 @@ export async function regenerateGoldenFrames(binaryVersion) {
|
|
|
261
531
|
const captures = loadDemoCaptures();
|
|
262
532
|
const regenerated = [];
|
|
263
533
|
for (const golden of GOLDEN_FRAME_CASES) {
|
|
534
|
+
// Regenerating a retired case would produce the new appearance, and
|
|
535
|
+
// byte-comparing would force the re-stamp the contract forbids.
|
|
536
|
+
if (isRetiredGoldenFrame(golden))
|
|
537
|
+
continue;
|
|
264
538
|
const output = new PassThrough();
|
|
265
539
|
output.isTTY = golden.interactive;
|
|
266
540
|
output.columns = golden.columns;
|
|
@@ -291,7 +565,10 @@ export async function regenerateGoldenFrames(binaryVersion) {
|
|
|
291
565
|
return regenerated;
|
|
292
566
|
}
|
|
293
567
|
export function findGoldenFrameMismatches(regenerated, committed) {
|
|
294
|
-
|
|
568
|
+
// Membership is judged against the whole shipped manifest, retired cases
|
|
569
|
+
// included: a retired fixture is claimed, so only its bytes are exempt.
|
|
570
|
+
// Anything committed that no case claims is still named.
|
|
571
|
+
const expected = new Set(GOLDEN_FRAME_CASES.map((frame) => frame.fixture));
|
|
295
572
|
return [
|
|
296
573
|
...regenerated.flatMap((frame) => committed.get(frame.fixture) === frame.output ? [] : [frame.fixture]),
|
|
297
574
|
...[...committed.keys()].filter((fixture) => !expected.has(fixture)),
|
|
@@ -13,96 +13,16 @@ export type BandAllocationOptions = {
|
|
|
13
13
|
};
|
|
14
14
|
export type TextEmphasis = keyof typeof TEXT_EMPHASIS_TOKENS;
|
|
15
15
|
export type ComponentState = "active" | "inactive" | "pass" | "fail" | "warn" | "neutral";
|
|
16
|
-
export type KeybarSurface = "run" | "setup";
|
|
17
16
|
export type KeybarItem = {
|
|
18
17
|
readonly key: string;
|
|
19
18
|
readonly label: string;
|
|
20
19
|
};
|
|
21
20
|
export declare const KEYBAR_KEYS: {
|
|
22
|
-
readonly
|
|
23
|
-
|
|
24
|
-
readonly label: "Move";
|
|
25
|
-
}, {
|
|
26
|
-
readonly key: "⏎";
|
|
27
|
-
readonly label: "Open";
|
|
28
|
-
}, {
|
|
29
|
-
readonly key: "←";
|
|
30
|
-
readonly label: "Back";
|
|
31
|
-
}, {
|
|
32
|
-
readonly key: "Tab";
|
|
33
|
-
readonly label: "Panel";
|
|
34
|
-
}, {
|
|
35
|
-
readonly key: "?";
|
|
36
|
-
readonly label: "Help";
|
|
37
|
-
}, {
|
|
38
|
-
readonly key: "q";
|
|
39
|
-
readonly label: "Quit";
|
|
40
|
-
}, {
|
|
41
|
-
readonly key: "f";
|
|
42
|
-
readonly label: "Follow";
|
|
43
|
-
}, {
|
|
44
|
-
readonly key: "/";
|
|
45
|
-
readonly label: "Filter";
|
|
46
|
-
}];
|
|
47
|
-
readonly setup: readonly [{
|
|
48
|
-
readonly key: "↑↓";
|
|
49
|
-
readonly label: "Move";
|
|
50
|
-
}, {
|
|
51
|
-
readonly key: "⏎";
|
|
52
|
-
readonly label: "Next";
|
|
53
|
-
}, {
|
|
54
|
-
readonly key: "←";
|
|
55
|
-
readonly label: "Back";
|
|
56
|
-
}, {
|
|
57
|
-
readonly key: "Tab";
|
|
58
|
-
readonly label: "Panel";
|
|
59
|
-
}, {
|
|
60
|
-
readonly key: "?";
|
|
61
|
-
readonly label: "Help";
|
|
62
|
-
}, {
|
|
63
|
-
readonly key: "q";
|
|
64
|
-
readonly label: "Quit";
|
|
65
|
-
}, {
|
|
66
|
-
readonly key: "␣";
|
|
67
|
-
readonly label: "Toggle";
|
|
68
|
-
}, {
|
|
69
|
-
readonly key: "a";
|
|
70
|
-
readonly label: "All";
|
|
71
|
-
}, {
|
|
72
|
-
readonly key: "r";
|
|
73
|
-
readonly label: "Re-probe";
|
|
74
|
-
}, {
|
|
75
|
-
readonly key: "s";
|
|
76
|
-
readonly label: "Save";
|
|
77
|
-
}, {
|
|
78
|
-
readonly key: "n";
|
|
79
|
-
readonly label: "Next";
|
|
80
|
-
}, {
|
|
81
|
-
readonly key: "p";
|
|
82
|
-
readonly label: "Prev";
|
|
83
|
-
}];
|
|
84
|
-
readonly setupAdditional: readonly [{
|
|
85
|
-
readonly key: "␣";
|
|
86
|
-
readonly label: "Toggle";
|
|
87
|
-
}, {
|
|
88
|
-
readonly key: "a";
|
|
89
|
-
readonly label: "All";
|
|
90
|
-
}, {
|
|
91
|
-
readonly key: "r";
|
|
92
|
-
readonly label: "Re-probe";
|
|
93
|
-
}, {
|
|
94
|
-
readonly key: "s";
|
|
95
|
-
readonly label: "Save";
|
|
96
|
-
}, {
|
|
97
|
-
readonly key: "n";
|
|
98
|
-
readonly label: "Next";
|
|
99
|
-
}, {
|
|
100
|
-
readonly key: "p";
|
|
101
|
-
readonly label: "Prev";
|
|
102
|
-
}];
|
|
21
|
+
readonly setup: readonly import("./keys.js").KeybarEntry[];
|
|
22
|
+
readonly setupAdditional: import("./keys.js").KeybarEntry[];
|
|
103
23
|
};
|
|
104
24
|
export type JournalRow = {
|
|
105
|
-
readonly id
|
|
25
|
+
readonly id: string;
|
|
106
26
|
readonly time: string;
|
|
107
27
|
readonly state: ComponentState;
|
|
108
28
|
readonly text: string;
|
|
@@ -159,8 +79,15 @@ export declare function ProgressMeter({ value, width, }: {
|
|
|
159
79
|
value: number;
|
|
160
80
|
width?: number;
|
|
161
81
|
}): ReactElement;
|
|
162
|
-
|
|
163
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Wrap a projected roster without losing a byte. Unlike prose wrapping, these
|
|
84
|
+
* rows are exact consecutive slices: stripping continuation markers and
|
|
85
|
+
* joining the slices reconstructs the registry projection byte for byte.
|
|
86
|
+
*/
|
|
87
|
+
export declare function keyRosterLines(entries: readonly KeybarItem[], columns: number): readonly string[];
|
|
88
|
+
/** The lossless, width-safe rendering of one projected key roster. */
|
|
89
|
+
export declare function KeyRoster({ entries, width, }: {
|
|
90
|
+
entries: readonly KeybarItem[];
|
|
164
91
|
width: number;
|
|
165
92
|
}): ReactElement;
|
|
166
93
|
export type StatusStripItem = {
|
|
@@ -171,9 +98,11 @@ export declare function StatusStrip({ items, width, }: {
|
|
|
171
98
|
items: readonly StatusStripItem[];
|
|
172
99
|
width: number;
|
|
173
100
|
}): ReactElement;
|
|
174
|
-
export declare function JournalRowPanel({ rows, width, focused, title, }: {
|
|
101
|
+
export declare function JournalRowPanel({ rows, width, focused, title, selection, }: {
|
|
175
102
|
rows: readonly JournalRow[];
|
|
176
103
|
width?: number | string;
|
|
177
104
|
focused?: boolean;
|
|
178
105
|
title?: string;
|
|
106
|
+
/** Index of the row the pointer marks; undefined draws no pointer. */
|
|
107
|
+
selection?: number;
|
|
179
108
|
}): ReactElement;
|