tickmarkr 1.66.0 → 1.67.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.
@@ -1,7 +1,8 @@
1
1
  import { channelsFromConfig } from "../../adapters/types.js";
2
2
  import { candidateRow, shapeCandidates } from "../../cli/commands/fleet-picker.js";
3
- import { INTEGRITY_FLOOR_SHAPES, loadConfigWithMode, } from "../../config/config.js";
3
+ import { DEFAULT_CONFIG, fleetEditableFromConfig, INTEGRITY_FLOOR_SHAPES, loadConfigWithMode, ROUTING_MODES, TIER_RANK, } from "../../config/config.js";
4
4
  import { SHAPES } from "../../graph/schema.js";
5
+ import { FleetStaging } from "../staging.js";
5
6
  // Same preview task fleet.ts's picker ranks (one per shape, complexity 3): the inspector must
6
7
  // rank the exact inputs the fleet editor ranks, or the two surfaces could disagree.
7
8
  export function routingPreviewTask(shape) {
@@ -22,6 +23,43 @@ export function routingPreviewTask(shape) {
22
23
  };
23
24
  }
24
25
  const isIntegrity = (shape) => INTEGRITY_FLOOR_SHAPES.includes(shape);
26
+ // Staged-cell glyph — the same ● the shell status bar counts staged changes with (app.ts).
27
+ const STAGED_MARK = "●";
28
+ const lowerTier = (t) => (t === "frontier" ? "mid" : "cheap");
29
+ const maxTier = (a, b) => (TIER_RANK[a] >= TIER_RANK[b] ? a : b);
30
+ // config.ts presetFloor mirrored (module-private there): a mode compiles into floors at load,
31
+ // so staging a mode stages exactly this table.
32
+ function presetFloor(mode, shape) {
33
+ const dflt = DEFAULT_CONFIG.routing.floors[shape] ?? "cheap";
34
+ if (mode === "partner-led")
35
+ return "frontier";
36
+ if (mode === "staff-led")
37
+ return maxTier(lowerTier(dflt), isIntegrity(shape) ? "frontier" : "cheap");
38
+ return dflt;
39
+ }
40
+ function floorsEqual(a, b) {
41
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
42
+ for (const k of keys)
43
+ if (a[k] !== b[k])
44
+ return false;
45
+ return true;
46
+ }
47
+ /** The header mode is DERIVED from the buffer, never stored: clean floors show the loaded mode;
48
+ * floors matching a preset show that mode (staged); anything else reads "custom". */
49
+ function deriveMode(loadedMode, loaded, buffer) {
50
+ if (floorsEqual(loaded.floors, buffer.floors))
51
+ return { mode: loadedMode, staged: false };
52
+ for (const mode of ROUTING_MODES) {
53
+ if (SHAPES.every((s) => buffer.floors[s] === presetFloor(mode, s)))
54
+ return { mode, staged: true };
55
+ }
56
+ return { mode: "custom", staged: true };
57
+ }
58
+ /** The config the router would see with the staged policy applied: map + floors ride the buffer
59
+ * (deny/tier edits are the fleet view's lane, T3). The buffer snapshot is already detached. */
60
+ function effectiveConfig(cfg, buffer) {
61
+ return { ...cfg, routing: { ...cfg.routing, map: buffer.map, floors: buffer.floors } };
62
+ }
25
63
  // No-arg shell path (app.ts constructs views without deps): load the operator's resolved config
26
64
  // and derive channels from the configured tier table. Best-effort — a load failure degrades the
27
65
  // view to an explanatory note instead of breaking the studio.
@@ -37,60 +75,158 @@ function loadDefaultData(repoRoot) {
37
75
  }
38
76
  export function createRoutingView(deps = {}) {
39
77
  const data = deps.data ?? loadDefaultData(deps.repoRoot ?? process.cwd());
78
+ // The one edit authority: every mutating method below is a staging.apply()/revert() over this
79
+ // buffer, so a staged change can never hide in private view state.
80
+ const staging = deps.staging ??
81
+ new FleetStaging(data ? fleetEditableFromConfig(data.cfg) : { denyAdapters: [], denyModels: [], tiers: {}, map: {}, floors: {} });
40
82
  let cursor = 0;
41
83
  let inspector = null;
84
+ const rankInspector = (shape) => {
85
+ if (!data)
86
+ return [];
87
+ // THE parity seam: rank through the same shared picker glue the fleet editor uses — no
88
+ // comparator lives here. Clean buffer ⇒ the injected config by reference (the delegation
89
+ // tests pin that identity); staged edits ⇒ the effective config, what the router would
90
+ // choose given the staged inputs.
91
+ const cfgNow = staging.isDirty ? effectiveConfig(data.cfg, staging.current) : data.cfg;
92
+ return shapeCandidates(routingPreviewTask(shape), cfgNow, data.channels, data.profile);
93
+ };
94
+ const rowOf = (c) => candidateRow(c, data?.cfg.pricing ?? {});
95
+ // Re-rank the open inspector after a staging mutation changed the ranking inputs.
96
+ const refreshInspector = () => {
97
+ if (!inspector)
98
+ return;
99
+ inspector.ranked = rankInspector(inspector.shape);
100
+ inspector.cursor = Math.min(inspector.cursor, Math.max(inspector.ranked.length - 1, 0));
101
+ };
42
102
  return {
43
103
  id: "routing",
44
104
  label: "Routing",
45
105
  get selectedShape() {
46
106
  return SHAPES[cursor];
47
107
  },
108
+ get selectedMode() {
109
+ const loadedMode = data?.mode ?? data?.cfg.routing.mode ?? "risk-based";
110
+ return deriveMode(loadedMode, staging.loadedState, staging.current).mode;
111
+ },
112
+ get staging() {
113
+ return staging;
114
+ },
48
115
  moveCursor(delta) {
49
116
  cursor = (cursor + delta + SHAPES.length) % SHAPES.length;
50
117
  },
118
+ moveInspectorCursor(delta) {
119
+ if (!inspector || inspector.ranked.length === 0)
120
+ return;
121
+ const n = inspector.ranked.length;
122
+ inspector.cursor = (inspector.cursor + delta + n) % n;
123
+ },
51
124
  openInspector(shape) {
52
125
  if (!data)
53
126
  return;
54
127
  const target = shape ?? SHAPES[cursor];
55
- // THE parity seam: rank through the same shared picker glue the fleet editor uses — no
56
- // comparator lives here — so rows, cost signals, and provenance match the router's choice.
57
- const ranked = shapeCandidates(routingPreviewTask(target), data.cfg, data.channels, data.profile);
58
- inspector = { shape: target, rows: ranked.map((c) => candidateRow(c, data.cfg.pricing)) };
128
+ inspector = { shape: target, cursor: 0, ranked: rankInspector(target) };
59
129
  },
60
130
  closeInspector() {
61
131
  inspector = null;
62
132
  },
133
+ stageInspectorPin() {
134
+ if (!data || !inspector)
135
+ return;
136
+ const target = inspector.shape;
137
+ const chosen = inspector.ranked[inspector.cursor];
138
+ if (!chosen)
139
+ return;
140
+ const { adapter, model } = chosen.assignment;
141
+ staging.apply((buffer) => {
142
+ buffer.map[target] = { ...buffer.map[target], pin: { via: adapter, model } };
143
+ });
144
+ // No re-rank: shapeCandidates strips the shape's own pin from the ranking inputs.
145
+ },
146
+ stageFloor(tier) {
147
+ if (!data)
148
+ return;
149
+ const target = SHAPES[cursor];
150
+ staging.apply((buffer) => {
151
+ buffer.floors[target] = tier;
152
+ });
153
+ refreshInspector();
154
+ },
155
+ stagePrefer(chain) {
156
+ if (!data)
157
+ return;
158
+ const target = SHAPES[cursor];
159
+ staging.apply((buffer) => {
160
+ const entry = { ...buffer.map[target] };
161
+ if (chain.length)
162
+ entry.prefer = [...chain];
163
+ else
164
+ delete entry.prefer;
165
+ // an emptied entry leaves no phantom delta behind
166
+ if (Object.keys(entry).length)
167
+ buffer.map[target] = entry;
168
+ else
169
+ delete buffer.map[target];
170
+ });
171
+ refreshInspector();
172
+ },
173
+ stageMode(mode) {
174
+ if (!data)
175
+ return;
176
+ staging.apply((buffer) => {
177
+ for (const shape of SHAPES)
178
+ buffer.floors[shape] = presetFloor(mode, shape);
179
+ });
180
+ refreshInspector();
181
+ },
182
+ revert() {
183
+ staging.revert();
184
+ refreshInspector();
185
+ },
63
186
  inspectorRows() {
64
- return inspector ? [...inspector.rows] : null;
187
+ return inspector ? inspector.ranked.map(rowOf) : null;
65
188
  },
66
189
  render: () => {
67
- const lines = ["Routing view — per-shape policy grid (read-only)"];
190
+ const lines = ["Routing view — per-shape policy grid"];
68
191
  if (!data) {
69
192
  lines.push("routing data unavailable — see `tickmarkr fleet --print` for the line-mode surface");
70
193
  return lines;
71
194
  }
72
195
  const { cfg } = data;
73
- const mode = data.mode ?? cfg.routing.mode ?? "risk-based";
74
- lines.push(`mode: ${mode}`);
196
+ const buffer = staging.current;
197
+ const loaded = staging.loadedState;
198
+ const loadedMode = data.mode ?? cfg.routing.mode ?? "risk-based";
199
+ const { mode, staged: modeStaged } = deriveMode(loadedMode, loaded, buffer);
200
+ lines.push(`mode: ${mode}${modeStaged ? ` ${STAGED_MARK} (staged)` : ""}`);
75
201
  lines.push(` ${"shape".padEnd(10)}${"floor".padEnd(10)}${"pin".padEnd(20)}prefer`);
76
202
  for (const [i, shape] of SHAPES.entries()) {
77
- const entry = cfg.routing.map[shape];
78
- const floor = cfg.routing.floors[shape] ?? "—";
203
+ const entry = buffer.map[shape];
204
+ const loadedEntry = loaded.map[shape];
205
+ const floor = buffer.floors[shape] ?? "—";
206
+ const floorStaged = loaded.floors[shape] !== buffer.floors[shape];
79
207
  const marker = isIntegrity(shape) ? "*" : "";
80
208
  const pin = entry?.pin ? `${entry.pin.via}:${entry.pin.model}` : "—";
81
209
  const prefer = entry?.prefer?.length ? entry.prefer.join(", ") : "—";
210
+ const pinStaged = JSON.stringify(loadedEntry?.pin ?? null) !== JSON.stringify(entry?.pin ?? null);
211
+ const preferStaged = JSON.stringify(loadedEntry?.prefer ?? null) !== JSON.stringify(entry?.prefer ?? null);
82
212
  const sel = i === cursor ? "❯" : " ";
83
- lines.push(`${sel} ${shape.padEnd(10)}${`${floor}${marker}`.padEnd(10)}${pin.padEnd(20)}${prefer}`);
213
+ const floorCell = `${floor}${marker}${floorStaged ? STAGED_MARK : ""}`.padEnd(10);
214
+ const pinCell = `${pin}${pinStaged ? STAGED_MARK : ""}`.padEnd(20);
215
+ const preferCell = `${prefer}${preferStaged ? STAGED_MARK : ""}`;
216
+ lines.push(`${sel} ${shape.padEnd(10)}${floorCell}${pinCell}${preferCell}`);
84
217
  }
85
218
  lines.push("* integrity set: never below frontier");
86
- lines.push(`pick: ${SHAPES[cursor]} — enter opens the candidate inspector (read-only)`);
219
+ if (staging.isDirty)
220
+ lines.push(`${STAGED_MARK} staged — not saved`);
221
+ lines.push(`pick: ${SHAPES[cursor]} — enter opens the candidate inspector`);
87
222
  if (inspector) {
88
223
  lines.push(`─ candidates: ${inspector.shape} — the fleet picker ranking for the same inputs ──`);
89
- if (inspector.rows.length === 0)
224
+ if (inspector.ranked.length === 0)
90
225
  lines.push(" (no routable candidates)");
91
- for (const [j, row] of inspector.rows.entries()) {
92
- lines.push(`${j === 0 ? "❯ " : " "}${row}`);
226
+ for (const [j, c] of inspector.ranked.entries()) {
227
+ lines.push(`${j === inspector.cursor ? "❯ " : " "}${rowOf(c)}`);
93
228
  }
229
+ lines.push("select stages the highlighted candidate as this shape's pin");
94
230
  }
95
231
  return lines;
96
232
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.66.0",
3
+ "version": "1.67.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",