tickmarkr 1.72.0 → 1.74.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 (52) hide show
  1. package/dist/cli/commands/approve.js +21 -3
  2. package/dist/cli/commands/resume.js +10 -2
  3. package/dist/cli/commands/status.d.ts +2 -0
  4. package/dist/cli/commands/status.js +260 -41
  5. package/dist/cli/commands/ui.d.ts +6 -2
  6. package/dist/cli/commands/ui.js +2 -4
  7. package/dist/config/config.d.ts +4 -0
  8. package/dist/config/config.js +6 -1
  9. package/dist/drivers/herdr.d.ts +5 -0
  10. package/dist/drivers/herdr.js +100 -10
  11. package/dist/drivers/subprocess.d.ts +3 -3
  12. package/dist/drivers/subprocess.js +10 -5
  13. package/dist/gates/acceptance.js +19 -1
  14. package/dist/gates/llm.d.ts +4 -0
  15. package/dist/gates/llm.js +16 -3
  16. package/dist/gates/run-gates.d.ts +1 -1
  17. package/dist/gates/run-gates.js +25 -3
  18. package/dist/route/preference.d.ts +2 -1
  19. package/dist/route/preference.js +8 -2
  20. package/dist/run/daemon.d.ts +1 -0
  21. package/dist/run/daemon.js +139 -4
  22. package/dist/run/git.d.ts +2 -0
  23. package/dist/run/git.js +55 -0
  24. package/dist/run/journal.d.ts +21 -1
  25. package/dist/run/journal.js +83 -5
  26. package/dist/tui/ink/components.d.ts +18 -0
  27. package/dist/tui/ink/components.js +30 -2
  28. package/dist/tui/ink/studio-app.d.ts +59 -0
  29. package/dist/tui/ink/studio-app.js +320 -0
  30. package/package.json +1 -1
  31. package/dist/tui/app.d.ts +0 -72
  32. package/dist/tui/app.js +0 -334
  33. package/dist/tui/engine.d.ts +0 -52
  34. package/dist/tui/engine.js +0 -92
  35. package/dist/tui/frame.d.ts +0 -22
  36. package/dist/tui/frame.js +0 -56
  37. package/dist/tui/input.d.ts +0 -41
  38. package/dist/tui/input.js +0 -100
  39. package/dist/tui/views/consult-dossier.d.ts +0 -49
  40. package/dist/tui/views/consult-dossier.js +0 -169
  41. package/dist/tui/views/diff-modal.d.ts +0 -9
  42. package/dist/tui/views/diff-modal.js +0 -19
  43. package/dist/tui/views/fleet-view.d.ts +0 -91
  44. package/dist/tui/views/fleet-view.js +0 -337
  45. package/dist/tui/views/preview-view.d.ts +0 -7
  46. package/dist/tui/views/preview-view.js +0 -80
  47. package/dist/tui/views/profile-view.d.ts +0 -15
  48. package/dist/tui/views/profile-view.js +0 -93
  49. package/dist/tui/views/routing-view.d.ts +0 -48
  50. package/dist/tui/views/routing-view.js +0 -234
  51. package/dist/tui/views/runs-view.d.ts +0 -70
  52. package/dist/tui/views/runs-view.js +0 -387
@@ -1,337 +0,0 @@
1
- import { GLYPHS, bold, dim, fail, legend, warn } from "../../brand.js";
2
- const keyOf = (r) => `${r.adapter}:${r.model}`;
3
- const median = (xs) => {
4
- const s = [...xs].sort((a, b) => a - b);
5
- const m = Math.floor(s.length / 2);
6
- return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
7
- };
8
- /** Fold one row list into a single digest. Journals are append-only, so input order is
9
- * chronological and the last row's outcome is the channel's last observed outcome. */
10
- function foldRows(key, rows) {
11
- const d = {
12
- key, tasks: 0, attempts: 0, done: 0, failed: 0, human: 0,
13
- overruns: 0, quotaFailovers: 0, gateFails: null, medianDurationMs: null, lastOutcome: null,
14
- };
15
- for (const r of rows) {
16
- d.tasks += 1;
17
- d.attempts += r.attempts;
18
- if (r.outcome === "done")
19
- d.done += 1;
20
- else if (r.outcome === "failed")
21
- d.failed += 1;
22
- else
23
- d.human += 1;
24
- if (r.overrun === true)
25
- d.overruns += 1;
26
- if (r.quotaFailover === true)
27
- d.quotaFailovers += 1;
28
- // absent = unobserved: an all-legacy channel keeps gateFails null rather than a fake 0
29
- if (r.gateFails !== undefined)
30
- d.gateFails = (d.gateFails ?? 0) + r.gateFails;
31
- d.lastOutcome = r.outcome;
32
- }
33
- if (rows.length)
34
- d.medianDurationMs = median(rows.map((r) => r.durationMs));
35
- return d;
36
- }
37
- /** Fold journal telemetry rows into a per-channel health digest keyed by channelKey
38
- * ("adapter:model"). The digest derives from journal telemetry (TelemetryRow, the rows the
39
- * daemon appends to each run's telemetry.jsonl) rather than any new state file: this module
40
- * reads nothing and writes nothing — the caller folds rows out of the existing run journals
41
- * (readAllTelemetry) and the fold is a pure function of them, same telemetry in, same digest
42
- * out. */
43
- export function foldChannelHealth(rows) {
44
- const byChannel = new Map();
45
- for (const r of rows) {
46
- const key = keyOf(r);
47
- const list = byChannel.get(key);
48
- if (list)
49
- list.push(r);
50
- else
51
- byChannel.set(key, [r]);
52
- }
53
- const out = new Map();
54
- for (const [key, list] of byChannel)
55
- out.set(key, foldRows(key, list));
56
- return out;
57
- }
58
- /** Detect deny-vs-pin conflicts from the shared staging model. The buffer's CURRENT state holds
59
- * both views' staged state — the fleet view's deny/allow + tier edits and the routing view's
60
- * pin/floor edits — so reading it sees the combination before anything is saved. A conflict is
61
- * a pinned channel denied by its own entry OR by its adapter's deny (the adapter deny reaches
62
- * every channel under it, same rule as denyOf). */
63
- export function stagedConflicts(staging) {
64
- const cur = staging.current;
65
- const byChannel = new Map();
66
- for (const [shape, entry] of Object.entries(cur.map)) {
67
- const pin = entry?.pin;
68
- if (!pin)
69
- continue;
70
- const key = `${pin.via}:${pin.model}`;
71
- if (cur.denyModels.includes(key) || cur.denyAdapters.includes(pin.via)) {
72
- byChannel.set(key, [...(byChannel.get(key) ?? []), shape]);
73
- }
74
- }
75
- return [...byChannel.entries()].map(([channelKey, shapes]) => ({ channelKey, shapes }));
76
- }
77
- /** Rows whose staged state differs from the loaded (saved) state: adapter deny flips, channel
78
- * deny flips, and tier overrides. Staged rows render marked so an unsaved edit never reads as
79
- * saved state. Same JSON leaf-comparison semantics as the staging model's own delta count. */
80
- function stagedRowSets(staging) {
81
- const cur = staging.current;
82
- const saved = staging.loadedState;
83
- const flipped = (a, b) => new Set([...a.filter((x) => !b.includes(x)), ...b.filter((x) => !a.includes(x))]);
84
- const channels = flipped(cur.denyModels, saved.denyModels);
85
- const tierAdapters = new Set([...Object.keys(cur.tiers), ...Object.keys(saved.tiers)]);
86
- for (const adapter of tierAdapters) {
87
- const models = new Set([
88
- ...Object.keys(cur.tiers[adapter] ?? {}),
89
- ...Object.keys(saved.tiers[adapter] ?? {}),
90
- ]);
91
- for (const model of models) {
92
- if (JSON.stringify(cur.tiers[adapter]?.[model]) !== JSON.stringify(saved.tiers[adapter]?.[model])) {
93
- channels.add(`${adapter}:${model}`);
94
- }
95
- }
96
- }
97
- return { adapters: flipped(cur.denyAdapters, saved.denyAdapters), channels };
98
- }
99
- /** The roster renders the buffer's CURRENT state when a staging model is attached: deny lists
100
- * and tier badges come from the buffer, everything else (health, telemetry, notes, row shape)
101
- * from the injected facts. Without staging the injected facts render as-is (saved state). */
102
- function overlayStaged(data, staging) {
103
- if (!staging)
104
- return data;
105
- const cur = staging.current;
106
- return {
107
- ...data,
108
- denyAdapters: cur.denyAdapters,
109
- denyModels: cur.denyModels,
110
- adapters: data.adapters.map((a) => ({
111
- ...a,
112
- models: a.models.map((m) => ({ ...m, tier: cur.tiers[a.adapter]?.[m.model]?.tier ?? m.tier })),
113
- })),
114
- };
115
- }
116
- /** Sorted in-place toggle so a stage-then-unstage leaves the list byte-identical to loaded. */
117
- const toggleIn = (list, value) => {
118
- const i = list.indexOf(value);
119
- if (i >= 0)
120
- list.splice(i, 1);
121
- else
122
- list.push(value);
123
- list.sort();
124
- };
125
- // fleet.ts formatAge voice — the operator already reads "12m old" on the classic fleet screen.
126
- const formatCacheAge = (ageMs) => {
127
- if (ageMs === null)
128
- return "no probe data";
129
- const mins = Math.floor(ageMs / 60_000);
130
- if (mins < 60)
131
- return `${mins}m old`;
132
- return `${Math.floor(mins / 60)}h old`;
133
- };
134
- const fmtDuration = (ms) => {
135
- const s = Math.round(ms / 1000);
136
- return s < 60 ? `${s}s` : `${(s / 60).toFixed(1)}m`;
137
- };
138
- const trunc = (s, n) => (s.length <= n ? s : `${s.slice(0, n - 1)}…`);
139
- const note = (n) => (n ? ` — ${n}` : "");
140
- const plural = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
141
- /** doctor.ts model-status voice: authed / unauthed with reason+date / unknown. A non-installed
142
- * adapter is "unknown" whatever stale modelAuth its doctor entry still carries — the roster row
143
- * and the detail panel must never disagree about whether a channel is usable. */
144
- function modelAuthText(health, model) {
145
- const v = health?.installed ? health.modelAuth?.[model] : undefined;
146
- if (!v)
147
- return "unknown";
148
- if (v.authed)
149
- return "authed";
150
- return `unauthed: ${trunc(v.reason ?? "probe failed", 40)} (${v.probedAt.slice(0, 10)})`;
151
- }
152
- /** A channel is denied by its own routing.deny.models entry OR by its adapter's deny — the roster
153
- * row and the detail panel resolve deny through this one seam so an adapter-level deny can never
154
- * read as available on the model row while the panel below it says "denied". Adapter deny wins
155
- * and carries the adapter's note: it is the broader reason. */
156
- function denyOf(data, adapter, model) {
157
- if (data.denyAdapters.includes(adapter))
158
- return { denied: true, note: data.notes?.denyAdapters?.[adapter] };
159
- const key = `${adapter}:${model}`;
160
- if (data.denyModels.includes(key))
161
- return { denied: true, note: data.notes?.denyModels?.[key] };
162
- return { denied: false };
163
- }
164
- function digestLine(d, noun) {
165
- if (d.tasks === 0)
166
- return ` journals: no recent telemetry for this ${noun}`;
167
- const parts = [
168
- `${d.tasks} task${d.tasks === 1 ? "" : "s"}`,
169
- `${d.attempts} attempt${d.attempts === 1 ? "" : "s"}`,
170
- `${d.done} done`,
171
- ];
172
- if (d.failed)
173
- parts.push(`${d.failed} failed`);
174
- if (d.human)
175
- parts.push(`${d.human} human`);
176
- if (d.medianDurationMs !== null)
177
- parts.push(`median ${fmtDuration(d.medianDurationMs)}`);
178
- if (d.gateFails !== null)
179
- parts.push(`${d.gateFails} gate-fails`);
180
- parts.push(`${d.overruns} overrun${d.overruns === 1 ? "" : "s"}`);
181
- parts.push(`${d.quotaFailovers} quota failover${d.quotaFailovers === 1 ? "" : "s"}`);
182
- return ` journals: ${parts.join(" · ")}`;
183
- }
184
- export function createFleetView(data, staging) {
185
- // One expanded bit per adapter, default expanded — the roster shows everything at a glance.
186
- const expandedMap = new Map((data?.adapters ?? []).map((a) => [a.adapter, true]));
187
- let cursor = 0;
188
- const visibleRows = () => {
189
- const rows = [];
190
- for (const a of data?.adapters ?? []) {
191
- rows.push({ kind: "adapter", adapter: a.adapter });
192
- if (expandedMap.get(a.adapter) !== false) {
193
- for (const m of a.models)
194
- rows.push({ kind: "model", adapter: a.adapter, model: m.model });
195
- }
196
- }
197
- return rows;
198
- };
199
- const view = {
200
- id: "fleet",
201
- label: "Fleet",
202
- key(name) {
203
- const rows = visibleRows();
204
- if (!rows.length)
205
- return;
206
- if (name === "up")
207
- cursor = Math.max(cursor - 1, 0);
208
- else if (name === "down")
209
- cursor = Math.min(cursor + 1, rows.length - 1);
210
- else if (name === "left" || name === "right" || name === "space" || name === "return") {
211
- const row = rows[cursor];
212
- if (!row)
213
- return;
214
- const cur = expandedMap.get(row.adapter) !== false;
215
- const next = name === "left" ? false : name === "right" ? true : !cur;
216
- expandedMap.set(row.adapter, next);
217
- if (row.kind === "model" && !next) {
218
- // collapsing from a model row parks the cursor on the adapter header row
219
- cursor = visibleRows().findIndex((r) => r.kind === "adapter" && r.adapter === row.adapter);
220
- }
221
- }
222
- const n = visibleRows().length;
223
- cursor = Math.min(Math.max(cursor, 0), n - 1);
224
- },
225
- expanded(adapter) {
226
- return expandedMap.get(adapter) !== false;
227
- },
228
- selectedChannel() {
229
- const row = visibleRows()[cursor];
230
- return row?.kind === "model" ? `${row.adapter}:${row.model}` : null;
231
- },
232
- toggleDenyAdapter(adapter) {
233
- staging?.apply((buffer) => toggleIn(buffer.denyAdapters, adapter));
234
- },
235
- toggleDenyChannel(channelKey) {
236
- staging?.apply((buffer) => toggleIn(buffer.denyModels, channelKey));
237
- },
238
- stageTierOverride(channelKey, tier) {
239
- const split = channelKey.indexOf(":");
240
- if (split <= 0)
241
- return;
242
- const adapter = channelKey.slice(0, split);
243
- const model = channelKey.slice(split + 1);
244
- staging?.apply((buffer) => {
245
- buffer.tiers[adapter] ??= {};
246
- // spread keeps an existing provenance note riding the staged assignment
247
- buffer.tiers[adapter][model] = { ...buffer.tiers[adapter][model], tier };
248
- });
249
- },
250
- render(_props) {
251
- if (!data) {
252
- // T2 stub surface — the shell registers the view before any data source is wired.
253
- return ["Fleet view", "Harness roster and health (read-only in v1.66)."];
254
- }
255
- const eff = overlayStaged(data, staging);
256
- const staged = staging ? stagedRowSets(staging) : null;
257
- const conflicts = new Map((staging ? stagedConflicts(staging) : []).map((c) => [c.channelKey, c.shapes]));
258
- const digest = foldChannelHealth(eff.telemetry);
259
- const lines = [bold("Fleet view — harness roster")];
260
- lines.push(legend(`doctor cache: ${formatCacheAge(eff.doctorAgeMs)} · ${eff.adapters.length} adapters · ` +
261
- `${eff.telemetry.length} journal rows · ↑↓ select · → expand · ← collapse · space toggle`));
262
- const rows = visibleRows();
263
- rows.forEach((row, i) => {
264
- const pointer = i === cursor ? `${GLYPHS.pointer} ` : " ";
265
- const h = eff.health[row.adapter];
266
- if (row.kind === "adapter") {
267
- const marker = expandedMap.get(row.adapter) !== false ? "▾" : "▸";
268
- const state = !h?.installed
269
- ? `${fail(GLYPHS.fail)} not installed`
270
- : `${h.version ?? "installed"} · ${h.authed ? "authed" : "unauthed"}`;
271
- const counts = h?.installed ? ` · ${plural(entryOf(eff, row.adapter)?.models.length ?? 0, "model")}` : "";
272
- const deny = eff.denyAdapters.includes(row.adapter)
273
- ? ` · ${fail("denied")}${note(eff.notes?.denyAdapters?.[row.adapter])}`
274
- : "";
275
- const stagedBit = staged?.adapters.has(row.adapter) ? ` · ${warn("● staged")}` : "";
276
- lines.push(`${pointer}${marker} ${bold(row.adapter)} ${state}${counts}${deny}${stagedBit}`);
277
- return;
278
- }
279
- const m = eff.adapters.find((a) => a.adapter === row.adapter)?.models.find((mm) => mm.model === row.model);
280
- if (!m)
281
- return;
282
- const key = `${row.adapter}:${row.model}`;
283
- const auth = modelAuthText(h, row.model);
284
- const dn = denyOf(eff, row.adapter, row.model);
285
- const deny = dn.denied ? ` ${fail(GLYPHS.fail)} denied${note(dn.note)}` : "";
286
- const d = digest.get(key);
287
- const healthBit = d && d.tasks > 0 ? ` ${dim(`${d.done}/${d.tasks} done`)}` : "";
288
- const stagedBit = staged?.channels.has(key) ? ` ${warn("● staged")}` : "";
289
- const pinnedFor = conflicts.get(key);
290
- const conflictBit = pinnedFor
291
- ? ` ${warn(`${GLYPHS.attention} conflict: pinned for ${pinnedFor.join(", ")}`)}`
292
- : "";
293
- lines.push(`${pointer} ${row.model.padEnd(28)} [${m.tier}] ${m.channel} ${auth}${deny}${healthBit}${stagedBit}${conflictBit}`);
294
- });
295
- lines.push(...detailPanel(eff, digest, rows[cursor]));
296
- return lines;
297
- },
298
- };
299
- return view;
300
- }
301
- const entryOf = (data, adapter) => data.adapters.find((a) => a.adapter === adapter);
302
- /** The bottom panel: the selected channel's journal-derived health digest (or the adapter fold
303
- * when the cursor sits on an adapter header row). */
304
- function detailPanel(data, digest, row) {
305
- if (!row)
306
- return [];
307
- const h = data.health[row.adapter];
308
- if (row.kind === "model") {
309
- const key = `${row.adapter}:${row.model}`;
310
- const m = data.adapters.find((a) => a.adapter === row.adapter)?.models.find((mm) => mm.model === row.model);
311
- const v = h?.installed ? h.modelAuth?.[row.model] : undefined;
312
- // an unauthed reading already carries its probe date inside modelAuthText — appending it again
313
- // printed the same day twice ("… (2026-07-21) (probed 2026-07-21)")
314
- const probed = v?.authed ? ` (probed ${v.probedAt.slice(0, 10)})` : "";
315
- const dn = denyOf(data, row.adapter, row.model);
316
- const d = digest.get(key);
317
- const lines = [
318
- "",
319
- dim(`── ${key} ──`),
320
- ` tier ${m?.tier ?? "???"} · ${m?.channel ?? "?"} · ${modelAuthText(h, row.model)}${probed} · ${dn.denied ? "denied" : "enabled"}${note(dn.note)}`,
321
- digestLine(d ?? foldRows(key, []), "channel"),
322
- ];
323
- if (d?.lastOutcome)
324
- lines.push(` last outcome: ${d.lastOutcome}`);
325
- return lines;
326
- }
327
- const folded = foldRows(row.adapter, data.telemetry.filter((r) => r.adapter === row.adapter));
328
- const state = !h?.installed ? "not installed" : `${h.version ?? "installed"} · ${h.authed ? "authed" : "unauthed"}`;
329
- const denied = data.denyAdapters.includes(row.adapter);
330
- const enabled = `${denied ? "denied" : "enabled"}${denied ? note(data.notes?.denyAdapters?.[row.adapter]) : ""}`;
331
- return [
332
- "",
333
- dim(`── ${row.adapter} (adapter) ──`),
334
- ` ${state} · ${plural(entryOf(data, row.adapter)?.models.length ?? 0, "model")} · ${enabled}`,
335
- digestLine(folded, "adapter"),
336
- ];
337
- }
@@ -1,7 +0,0 @@
1
- import type { WorkerAdapter } from "../../adapters/types.js";
2
- import type { View } from "../app.js";
3
- export type PreviewViewDeps = {
4
- cwd?: string;
5
- adapters?: WorkerAdapter[];
6
- };
7
- export declare function createPreviewView(deps?: PreviewViewDeps): View;
@@ -1,80 +0,0 @@
1
- import { allAdapters, discoverChannels, readDoctor } from "../../adapters/registry.js";
2
- import { costSignal } from "../../cli/commands/fleet-picker.js";
3
- import { loadGraph } from "../../graph/graph.js";
4
- import { SHAPES } from "../../graph/schema.js";
5
- import { route, RoutingError } from "../../route/router.js";
6
- import { resolveRunMode } from "../../run/daemon.js";
7
- import { loadRoutingProfile } from "../../run/journal.js";
8
- // Same exploration setting as the candidate picker (rankCandidates routes noExplore so repeated
9
- // calls agree): a due probe must never make a preview row disagree with the picker's rank-1 for
10
- // the same graph and channel set. Mirrors fleet.ts's PREVIEW_EXPLORE.
11
- const PREVIEW_EXPLORE = { noExplore: true };
12
- // The no-graph stand-in: one synthetic task per shape so the consequence surface still shows what
13
- // the router WOULD do with this fleet. Same shape as fleet.ts's previewTask.
14
- function syntheticTask(shape) {
15
- return {
16
- id: `preview-${shape}`,
17
- title: `${shape} preview`,
18
- goal: "preview",
19
- shape,
20
- complexity: 3,
21
- acceptance: ["done"],
22
- deps: [],
23
- files: [],
24
- context: [],
25
- gates: ["build", "test", "lint", "evidence", "scope", "acceptance", "review"],
26
- humanGate: false,
27
- status: "pending",
28
- evidence: { commits: [], artifacts: [], gateResults: [] },
29
- };
30
- }
31
- // The consequence surface: the routing table a plan dry-run would produce, one row per task with
32
- // the chosen channel, the route()'s why-provenance verbatim, and the picker's cost signal. Every
33
- // render re-reads graph/config/doctor cache from disk (refresh on demand = repaint); the render
34
- // path is strictly read-only — doctor cache only, never probeAll, so a preview spends no tokens
35
- // and invokes no agent. Unroutable rows render the RoutingError in place, like plan's `!!` rows.
36
- function renderPreview(deps) {
37
- const cwd = deps.cwd ?? process.cwd();
38
- const adapters = deps.adapters ?? allAdapters();
39
- let graph = null;
40
- try {
41
- graph = loadGraph(cwd);
42
- }
43
- catch {
44
- graph = null; // absent or invalid — fall through to the synthetic per-shape set
45
- }
46
- const { cfg, mode, source } = resolveRunMode(cwd, { spec: graph?.mode });
47
- const cached = readDoctor(cwd);
48
- const channels = discoverChannels(cfg, adapters, cached ?? {});
49
- const profile = loadRoutingProfile(cwd, cfg, { preview: true });
50
- const tasks = graph ? graph.tasks : SHAPES.map(syntheticTask);
51
- const lines = [
52
- "Preview view — plan dry run (read-only: no writes, no probes, no token spend)",
53
- graph
54
- ? `source: compiled graph (${tasks.length} task${tasks.length === 1 ? "" : "s"}) · mode: ${mode.mode} (${source}) · explore off (picker parity) · ${channels.length} channels`
55
- : `source: no compiled graph — synthetic per-shape task set (${tasks.length} shapes) · mode: ${mode.mode} (${source}) · explore off (picker parity) · ${channels.length} channels`,
56
- ];
57
- if (!cached)
58
- lines.push("doctor cache missing — rows route against zero channels; run `tickmarkr doctor` (the preview never probes)");
59
- lines.push("");
60
- for (const t of tasks) {
61
- try {
62
- const r = route(t, cfg, channels, profile, undefined, undefined, PREVIEW_EXPLORE);
63
- const a = r.assignment;
64
- lines.push(` ${t.id.padEnd(16)} ${t.shape.padEnd(10)} → ${a.adapter}:${a.model} [${a.channel}/${a.tier}] ${costSignal(a, cfg.pricing)} — ${r.provenance}`);
65
- }
66
- catch (e) {
67
- if (!(e instanceof RoutingError))
68
- throw e;
69
- lines.push(` ${t.id.padEnd(16)} ${t.shape.padEnd(10)} !! ${e.message}`);
70
- }
71
- }
72
- return lines;
73
- }
74
- export function createPreviewView(deps = {}) {
75
- return {
76
- id: "preview",
77
- label: "Preview",
78
- render: () => renderPreview(deps),
79
- };
80
- }
@@ -1,15 +0,0 @@
1
- import { type TickmarkrConfig } from "../../config/config.js";
2
- import { type RoutingProfile } from "../../route/profile.js";
3
- import type { View } from "../app.js";
4
- export type ProfileViewData = {
5
- cfg: TickmarkrConfig;
6
- /** the loaded routing profile; undefined ⇒ cold (no telemetry in the window) */
7
- profile?: RoutingProfile;
8
- };
9
- export type ProfileViewDeps = {
10
- /** injected fixture data — with data present the render path never touches the filesystem */
11
- data?: ProfileViewData;
12
- /** where the no-arg studio shell loads config and the profile from (default: process.cwd()) */
13
- repoRoot?: string;
14
- };
15
- export declare function createProfileView(deps?: ProfileViewDeps): View;
@@ -1,93 +0,0 @@
1
- import { channelKey } from "../../adapters/types.js";
2
- import { loadConfig } from "../../config/config.js";
3
- import { AVAIL_WEIGHT, cellsOf, cellSummary, EXPLORE_CAP, HALF_LIFE_RUNS, learnedScore, MIN_SAMPLES, } from "../../route/profile.js";
4
- import { loadRoutingProfile } from "../../run/journal.js";
5
- const fmtSigned = (n) => (n >= 0 ? `+${n.toFixed(3)}` : n.toFixed(3));
6
- // No-arg shell path (app.ts constructs views without deps): the SAME loader the daemon's router
7
- // calls (daemon.ts:194), with the profile command's inspection-surface preview flag so the tab
8
- // still renders under routing.learned:off — labeled inert in the header, as `tickmarkr profile`
9
- // does. Best-effort: a load failure degrades to an explanatory note instead of breaking the studio.
10
- function loadDefaultData(repoRoot) {
11
- try {
12
- const cfg = loadConfig(repoRoot);
13
- return { cfg, profile: loadRoutingProfile(repoRoot, cfg, { preview: true }) };
14
- }
15
- catch {
16
- return null;
17
- }
18
- }
19
- // A configured pin that beat a higher-scored learned channel, or null. route() consults the map
20
- // pin before any learned sorting, so when another channel's learned score for the shape tops the
21
- // pin's, the pin is overriding learned evidence — the case this inspector exists to name.
22
- function pinOverride(profile, cfg, shape, pin, scoreOpts) {
23
- const pinChKey = channelKey({ adapter: pin.via, model: pin.model });
24
- const pinChannel = cfg.tiers[pin.via]?.channel ?? "sub";
25
- const pinScore = learnedScore(profile, shape, pinChKey, pinChannel, scoreOpts);
26
- let top = null;
27
- for (const { shape: s, chKey, channel } of cellsOf(profile)) {
28
- if (s !== shape || (chKey === pinChKey && channel === pinChannel))
29
- continue;
30
- const score = learnedScore(profile, shape, chKey, channel, scoreOpts);
31
- if (score > pinScore && (!top || score > top.score))
32
- top = { chKey, channel, score };
33
- }
34
- if (!top)
35
- return null;
36
- return `${shape}: pin ${pinChKey} (score ${fmtSigned(pinScore)}) overrides higher-scored ${top.chKey} (${top.channel}, score ${fmtSigned(top.score)})`;
37
- }
38
- export function createProfileView(deps = {}) {
39
- const data = deps.data ?? loadDefaultData(deps.repoRoot ?? process.cwd());
40
- return {
41
- id: "profile",
42
- label: "Profile",
43
- render: () => {
44
- const lines = ["Profile view — learned-routing inspector (read-only)"];
45
- if (!data) {
46
- lines.push("profile data unavailable — see `tickmarkr profile` for the line-mode surface");
47
- return lines;
48
- }
49
- const { cfg, profile } = data;
50
- const halfLife = cfg.routing.learnedTuning?.halfLifeRuns ?? HALF_LIFE_RUNS;
51
- const availWeight = cfg.routing.learnedTuning?.availWeight ?? AVAIL_WEIGHT;
52
- // router.ts:29's exploreCap — the exploration fence in effect, not the bare module default
53
- const cap = cfg.routing.explore?.cap ?? EXPLORE_CAP;
54
- lines.push(`routing.learned: ${cfg.routing.learned}${cfg.routing.learned === "off" ? " (preview — routing is inert)" : ""}`);
55
- lines.push(`half-life: ${halfLife} runs availability weight: ${availWeight} exploration cap: ${cap}`);
56
- if (!profile || profile.cells.size === 0) {
57
- // ROUTE-07's cold start, explained: an empty profile scores every channel exactly NEUTRAL,
58
- // so routing is byte-identical static routing until real samples accumulate.
59
- lines.push("cold start — no learned evidence yet:", ` every channel scores exactly neutral and routing follows the static policy`, ` until a shape×channel cell accumulates ${MIN_SAMPLES} samples.`);
60
- return lines;
61
- }
62
- const scoreOpts = { availWeight: cfg.routing.learnedTuning?.availWeight };
63
- lines.push(` ${"shape".padEnd(10)} ${"channel".padEnd(28)} ${"class".padEnd(5)} ${"score".padEnd(8)} samples decay explore`);
64
- for (const { shape, chKey, channel, cell } of cellsOf(profile)) {
65
- const s = cellSummary(cell);
66
- const score = learnedScore(profile, shape, chKey, channel, scoreOpts);
67
- // decayed ⇔ rank-decay folded older runs below full weight (n_eff < n_raw); fresh ⇔ all
68
- // evidence at weight 1. The two states must never render alike.
69
- const decay = s.nEff < s.nRaw ? "decayed" : "fresh";
70
- const remaining = Math.max(0, cap - cell.dispatches);
71
- const explore = remaining > 0 ? `left ${remaining}` : "spent";
72
- const disc = s.discounted > 0 ? ` disc=${s.discounted}` : "";
73
- const cold = s.cold ? ` cold (n<${MIN_SAMPLES})` : "";
74
- lines.push(` ${shape.padEnd(10)} ${chKey.padEnd(28)} ${channel.padEnd(5)} ${fmtSigned(score).padEnd(8)} ${`n_eff=${s.nEff} n_raw=${s.nRaw}`.padEnd(18)} ${decay.padEnd(9)} ${explore}${disc}${cold}`);
75
- }
76
- const overrides = [];
77
- for (const [shape, entry] of Object.entries(cfg.routing.map).sort(([a], [b]) => a.localeCompare(b))) {
78
- if (!entry.pin)
79
- continue;
80
- const named = pinOverride(profile, cfg, shape, entry.pin, scoreOpts);
81
- if (named)
82
- overrides.push(named);
83
- }
84
- if (overrides.length) {
85
- lines.push("pins overriding higher-scored learned channels:", ...overrides.map((l) => ` ${l}`));
86
- }
87
- else if (Object.values(cfg.routing.map).some((e) => e?.pin)) {
88
- lines.push("pins: none overrides a higher-scored learned channel");
89
- }
90
- return lines;
91
- },
92
- };
93
- }
@@ -1,48 +0,0 @@
1
- import { type BillingChannel } from "../../adapters/types.js";
2
- import { type RoutingMode, type TickmarkrConfig, type Tier } from "../../config/config.js";
3
- import { type Shape, type Task } from "../../graph/schema.js";
4
- import type { RoutingProfile } from "../../route/profile.js";
5
- import { FleetStaging } from "../staging.js";
6
- import type { View } from "../app.js";
7
- export type RoutingViewData = {
8
- cfg: TickmarkrConfig;
9
- channels: BillingChannel[];
10
- /** active routing mode for the header; absent ⇒ cfg.routing.mode ?? "risk-based" */
11
- mode?: RoutingMode;
12
- profile?: RoutingProfile;
13
- };
14
- export type RoutingViewDeps = {
15
- /** injected fixture data — with data present the render path never touches the filesystem */
16
- data?: RoutingViewData;
17
- /** where the no-arg studio shell loads config from (default: process.cwd()) */
18
- repoRoot?: string;
19
- /** shared staged-changes buffer; default derives one from data.cfg — edits never leave it */
20
- staging?: FleetStaging;
21
- };
22
- export type RoutingView = View & {
23
- /** open the candidate inspector for a shape (default: the selected grid row) */
24
- openInspector(shape?: Shape): void;
25
- closeInspector(): void;
26
- moveCursor(delta: number): void;
27
- /** move the inspector's candidate highlight (wraps) */
28
- moveInspectorCursor(delta: number): void;
29
- /** stage the highlighted inspector candidate as that shape's map pin */
30
- stageInspectorPin(): void;
31
- /** stage a floor tier for the selected grid shape */
32
- stageFloor(tier: Tier): void;
33
- /** stage a prefer chain for the selected grid shape ([] clears the chain) */
34
- stagePrefer(chain: string[]): void;
35
- /** stage a routing mode — compiles into the preset's floor table in the buffer */
36
- stageMode(mode: RoutingMode): void;
37
- /** discard every staged edit (delegates to the staging model) */
38
- revert(): void;
39
- readonly selectedShape: Shape;
40
- /** mode derived from the buffer: loaded mode, a staged preset, or "custom" — never stored */
41
- readonly selectedMode: RoutingMode | "custom";
42
- /** the staging model every change flows through */
43
- readonly staging: FleetStaging;
44
- /** the inspector's candidate rows — the picker's candidateRow strings, in rank order */
45
- inspectorRows(): string[] | null;
46
- };
47
- export declare function routingPreviewTask(shape: Shape): Task;
48
- export declare function createRoutingView(deps?: RoutingViewDeps): RoutingView;