@north-light/crouter 0.3.261 → 0.3.263

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 (48) hide show
  1. package/dist/api/dto/health.d.ts +2 -0
  2. package/dist/builtin-memory/internal/memory-loading.md +3 -3
  3. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
  4. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +5 -3
  5. package/dist/clients/attach/input/controller.d.ts +4 -0
  6. package/dist/clients/attach/input/controller.js +8 -0
  7. package/dist/clients/attach/session/bindings.d.ts +3 -0
  8. package/dist/clients/attach/session/bindings.js +7 -0
  9. package/dist/clients/attach/session/keys.d.ts +2 -0
  10. package/dist/clients/attach/session/keys.js +3 -3
  11. package/dist/clients/attach/viewer.js +616 -616
  12. package/dist/commands/memory/read.js +15 -38
  13. package/dist/commands/memory/shared.d.ts +1 -1
  14. package/dist/commands/memory/shared.js +1 -1
  15. package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
  16. package/dist/commands/sys/context/admin/model.d.ts +7 -2
  17. package/dist/commands/sys/context/admin/model.js +7 -1
  18. package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
  19. package/dist/commands/sys/context/admin/read-view.js +9 -4
  20. package/dist/commands/sys/context/resolve.d.ts +32 -3
  21. package/dist/commands/sys/context/resolve.js +74 -25
  22. package/dist/commands/sys/panels/models-panel.d.ts +28 -5
  23. package/dist/commands/sys/panels/models-panel.js +558 -75
  24. package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
  25. package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
  26. package/dist/core/__tests__/model-routes-config.test.js +142 -0
  27. package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
  28. package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
  29. package/dist/core/__tests__/seam/broker-provider-retry.test.js +2 -2
  30. package/dist/core/canvas/render-source.d.ts +1 -1
  31. package/dist/core/canvas/render-source.js +3 -2
  32. package/dist/core/config.d.ts +19 -2
  33. package/dist/core/config.js +162 -47
  34. package/dist/core/keybindings/catalog.js +9 -7
  35. package/dist/core/model-routes.js +10 -3
  36. package/dist/core/substrate/listings.d.ts +1 -1
  37. package/dist/core/substrate/listings.js +6 -2
  38. package/dist/core/substrate/on-read.d.ts +61 -5
  39. package/dist/core/substrate/on-read.js +129 -22
  40. package/dist/core/substrate/render.d.ts +3 -2
  41. package/dist/core/substrate/render.js +133 -38
  42. package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
  43. package/dist/daemon/api/handlers/canvas.js +18 -2
  44. package/dist/daemon/api/handlers/health.js +17 -12
  45. package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
  46. package/dist/types.d.ts +4 -2
  47. package/package.json +1 -1
  48. package/runtime.lock.json +2 -2
@@ -212,7 +212,7 @@ test('C3f — a logical request can reach a non-managed raw-registry fallback wh
212
212
  family: 'custom',
213
213
  credentialSource: 'user',
214
214
  providerId: 'c3prov',
215
- models: { strong: 'c3prov/c3model' },
215
+ models: { ultra: 'c3prov/c3model', strong: 'c3prov/c3model', medium: 'c3prov/c3model', light: 'c3prov/c3model' },
216
216
  },
217
217
  },
218
218
  })}\n`);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,142 @@
1
+ import { after, afterEach, before, beforeEach, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { persistModelRoute, readConfig, readMergedLaunchConfig } from '../config.js';
7
+ import { expandModelCandidates } from '../model-routes.js';
8
+ import { resetScopeCache } from '../scope.js';
9
+ let home;
10
+ let work;
11
+ let previousHome;
12
+ function route(providerId = 'provider-x', family = 'family-x', credentialSource = 'source-x') {
13
+ return {
14
+ family,
15
+ credentialSource,
16
+ providerId,
17
+ models: {
18
+ ultra: `${providerId}/ultra`,
19
+ strong: `${providerId}/strong`,
20
+ medium: `${providerId}/medium`,
21
+ light: `${providerId}/light`,
22
+ },
23
+ };
24
+ }
25
+ function writeConfig(root, config) {
26
+ mkdirSync(root, { recursive: true });
27
+ writeFileSync(join(root, 'config.json'), JSON.stringify(config));
28
+ }
29
+ before(() => {
30
+ previousHome = process.env.HOME;
31
+ });
32
+ beforeEach(() => {
33
+ home = mkdtempSync(join(tmpdir(), 'crtr-model-routes-home-'));
34
+ work = mkdtempSync(join(tmpdir(), 'crtr-model-routes-work-'));
35
+ process.env.HOME = home;
36
+ resetScopeCache();
37
+ });
38
+ afterEach(() => {
39
+ resetScopeCache();
40
+ rmSync(home, { recursive: true, force: true });
41
+ rmSync(work, { recursive: true, force: true });
42
+ });
43
+ after(() => {
44
+ if (previousHome === undefined)
45
+ delete process.env.HOME;
46
+ else
47
+ process.env.HOME = previousHome;
48
+ resetScopeCache();
49
+ });
50
+ test('complete routes preserve arbitrary names and credential-source ordering', () => {
51
+ writeConfig(join(home, '.crouter'), {
52
+ modelRoutes: {
53
+ personal: route('shared-provider', 'family/one', 'personal source'),
54
+ funded: route('shared-provider', 'family/one', 'funded source'),
55
+ },
56
+ modelRouting: {
57
+ credentialSourceOrder: ['funded source', 'personal source'],
58
+ familyOrder: ['family/one'],
59
+ strengthFallback: ['strong'],
60
+ },
61
+ });
62
+ const config = readMergedLaunchConfig(work, null);
63
+ assert.deepEqual(config.modelRoutes?.personal?.models, route('shared-provider', 'family/one', 'personal source').models);
64
+ assert.deepEqual(expandModelCandidates({ kind: 'logical', family: 'family/one', strength: 'strong', spec: 'family/one/strong' }, work, null).map((candidate) => candidate.routeId), ['funded', 'personal'], 'same provider/model refs remain distinct candidates when routes differ');
65
+ });
66
+ test('a sparse upper-scope route patch overlays a complete lower route', () => {
67
+ writeConfig(join(home, '.crouter'), { modelRoutes: { shared: route() } });
68
+ writeConfig(join(work, '.crouter'), { modelRoutes: { shared: { models: { medium: 'provider-x/project-medium' } } } });
69
+ const config = readMergedLaunchConfig(work, null);
70
+ assert.equal(config.modelRoutes?.shared?.models.medium, 'provider-x/project-medium');
71
+ assert.equal(config.modelRoutes?.shared?.models.ultra, 'provider-x/ultra');
72
+ });
73
+ test('legacy sparse routes remain readable with no invented cells', () => {
74
+ writeConfig(join(home, '.crouter'), {
75
+ modelRoutes: {
76
+ legacy: { family: 'legacy', credentialSource: 'old', providerId: 'legacy-provider', models: { medium: 'legacy-provider/medium' } },
77
+ },
78
+ });
79
+ const config = readMergedLaunchConfig(work, null);
80
+ assert.equal(config.modelRoutes, undefined);
81
+ assert.deepEqual(config.legacyModelRoutes?.legacy?.models, { medium: 'legacy-provider/medium' });
82
+ assert.deepEqual(readConfig('user').legacyModelRoutes?.legacy?.models, { medium: 'legacy-provider/medium' });
83
+ assert.deepEqual(expandModelCandidates({ kind: 'logical', family: 'legacy', strength: 'medium', spec: 'legacy/medium' }, work, null).map((candidate) => candidate.modelId), ['medium']);
84
+ });
85
+ test('route records retain __proto__ ids when read and persisted', () => {
86
+ const root = join(home, '.crouter');
87
+ writeConfig(root, JSON.parse(JSON.stringify({ modelRoutes: { ['__proto__']: route() } })));
88
+ assert.deepEqual(readMergedLaunchConfig(work, null).modelRoutes?.['__proto__'], route());
89
+ persistModelRoute(root, '__proto__', route('persisted-provider'));
90
+ const raw = JSON.parse(readFileSync(join(root, 'config.json'), 'utf8'));
91
+ assert.deepEqual(raw.modelRoutes['__proto__'], route('persisted-provider'));
92
+ });
93
+ test('route candidate identity distinguishes delimiter-containing route and provider ids', () => {
94
+ writeConfig(join(home, '.crouter'), {
95
+ modelRoutes: {
96
+ a: route('b:c', 'family', 'first'),
97
+ 'a:b': route('c', 'family', 'second'),
98
+ },
99
+ modelRouting: {
100
+ credentialSourceOrder: ['first', 'second'],
101
+ familyOrder: ['family'],
102
+ strengthFallback: ['strong'],
103
+ },
104
+ });
105
+ assert.deepEqual(expandModelCandidates({ kind: 'logical', family: 'family', strength: 'strong', spec: 'family/strong' }, work, null).map((candidate) => candidate.routeId), ['a', 'a:b']);
106
+ });
107
+ test('route writes reject missing cells and provider-prefix mismatches with actionable diagnostics', () => {
108
+ const root = join(home, '.crouter');
109
+ const incomplete = { ...route(), models: { strong: 'provider-x/strong' } };
110
+ assert.throws(() => persistModelRoute(root, 'needs-all-rungs', incomplete), /model route "needs-all-rungs" is incomplete; missing strengths: ultra, medium, light/);
111
+ assert.throws(() => persistModelRoute(root, 'wrong-provider', { ...route(), models: { ...route().models, light: 'other/light' } }), /model route "wrong-provider" models.light provider prefix must equal providerId/);
112
+ });
113
+ test('invalid route entries do not block valid routes, and no valid routes falls back to local ladders', () => {
114
+ writeConfig(join(home, '.crouter'), {
115
+ modelRoutes: {
116
+ valid: route('valid-provider', 'valid family', 'valid source'),
117
+ invalid: { family: 'bad', credentialSource: 'bad', providerId: 'one', models: { ultra: 'two/u', strong: 'two/s', medium: 'two/m', light: 'two/l' } },
118
+ },
119
+ });
120
+ assert.deepEqual(Object.keys(readMergedLaunchConfig(work, null).modelRoutes ?? {}), ['valid']);
121
+ writeConfig(join(home, '.crouter'), {
122
+ modelRoutes: {
123
+ invalid: { family: 'bad', credentialSource: 'bad', providerId: 'one', models: { ultra: 'two/u', strong: 'two/s', medium: 'two/m', light: 'two/l' } },
124
+ },
125
+ });
126
+ assert.equal(readMergedLaunchConfig(work, null).modelRoutes, undefined);
127
+ assert.equal(expandModelCandidates({ kind: 'logical', family: 'anthropic', strength: 'strong', spec: 'anthropic/strong' }, work, null)[0]?.routeId, 'anthropic');
128
+ });
129
+ test('persistModelRoute touches only its raw route and deletes by restoring inheritance', () => {
130
+ const root = join(home, '.crouter');
131
+ const other = route('other-provider');
132
+ writeConfig(root, { modelRoutes: { other }, max_panes_per_window: 7 });
133
+ persistModelRoute(root, 'edited', route('edited-provider'));
134
+ let raw = JSON.parse(readFileSync(join(root, 'config.json'), 'utf8'));
135
+ assert.deepEqual(raw.modelRoutes.other, other);
136
+ assert.deepEqual(raw.modelRoutes.edited, route('edited-provider'));
137
+ assert.equal(raw.modelLadders, undefined);
138
+ assert.equal(raw.max_panes_per_window, 7);
139
+ persistModelRoute(root, 'edited', undefined);
140
+ raw = JSON.parse(readFileSync(join(root, 'config.json'), 'utf8'));
141
+ assert.deepEqual(raw.modelRoutes, { other });
142
+ });
@@ -69,6 +69,11 @@ function makeWorkTree() {
69
69
  ' - {on: workspace-open, at: content}\n' +
70
70
  ' - {on: read, match: "./**", at: content}\n---\n' +
71
71
  `${NESTED_BODY}\n`);
72
+ // A bootless neighbour in the same discovered store: it wins no delivery of
73
+ // its own, so only a listing can ever disclose it.
74
+ writeFileSync(join(nestedStore, 'constraint.md'), '---\nkind: knowledge\n' +
75
+ 'when-and-why-to-read: When changing pkg internals, this knowledge should be read because it names the constraint.\n---\n' +
76
+ 'NESTED NEIGHBOUR BODY\n');
72
77
  return work;
73
78
  }
74
79
  test('a nested store front door surfaces on a read beneath its owning dir, not outside it', () => {
@@ -79,6 +84,9 @@ test('a nested store front door surfaces on a read beneath its owning dir, not o
79
84
  writeFileSync(inside, 'export const x = 1;\n');
80
85
  const rendered = renderOnReadDocs(node, inside, freshTranscriptTarget());
81
86
  assert.ok(rendered.includes(NESTED_BODY), `read beneath pkg surfaces the nested front door; got: ${rendered}`);
87
+ // A content delivery is a read: the discovered store's own listing comes
88
+ // with it, answered from the corpus the read walk discovered.
89
+ assert.match(rendered, /\[\[work\/pkg\/constraint\]\]: When changing pkg internals/, `the delivered front door discloses its directory's bootless member; got: ${rendered}`);
82
90
  const outside = join(work, 'docs', 'notes.ts');
83
91
  mkdirSync(join(work, 'docs'), { recursive: true });
84
92
  writeFileSync(outside, 'export const y = 2;\n');
@@ -25,11 +25,18 @@ import { spawnNode } from '../runtime/nodes.js';
25
25
  import { memoryDir } from '../runtime/memory.js';
26
26
  import { clearSessionCache } from '../substrate/session-cache.js';
27
27
  import { renderKnowledgeBlock } from '../substrate/render-node.js';
28
+ import { renderKnowledgeForSubject, renderPreferencesForSubject } from '../substrate/render.js';
29
+ import { assembleNodeSubject } from '../substrate/subject.js';
28
30
  import { renderWorkspaceOpenDocs } from '../substrate/on-read-node.js';
31
+ import { renderWorkspaceOpenDocsForSubject } from '../substrate/on-read.js';
29
32
  import { documentExposedAtOrAbove, emptyContextExposureState, exposedAtOrAbove, exposureTarget, loadContextExposureState, saveContextExposureState, } from '../substrate/injected-store.js';
30
33
  import { readLeaf } from '../../commands/memory/read.js';
31
34
  import { listLeaf } from '../../commands/memory/list.js';
32
35
  import { findLeaf } from '../../commands/memory/find.js';
36
+ import { assembleEventAccounting, docRow, effectivePlan, } from '../../commands/sys/context/resolve.js';
37
+ import { planDelivery } from '../substrate/plan.js';
38
+ import { PlanSet } from '../../commands/sys/context/admin/model.js';
39
+ import { buildRows } from '../../commands/sys/context/admin/docs-panel.js';
33
40
  function freshTranscriptTarget() {
34
41
  return exposureTarget(emptyContextExposureState(), 'transcript');
35
42
  }
@@ -48,6 +55,14 @@ const B = {
48
55
  dupFar: 'BODY-T9-DUP-FROM-FAR',
49
56
  kindSplit: 'BODY-T9-KIND-SPLIT',
50
57
  documentGateWins: 'BODY-T9-DOCUMENT-GATE-WINS',
58
+ codeCraftIndex: 'BODY-T9-CODE-CRAFT-INDEX',
59
+ companionSource: 'BODY-T9-COMPANION-SOURCE',
60
+ companionA: 'BODY-T9-COMPANION-A',
61
+ companionB: 'BODY-T9-COMPANION-B',
62
+ prefRouter: 'BODY-T9-PREF-ROUTER',
63
+ crossPartition: 'BODY-T9-CROSS-PARTITION',
64
+ outsideGuide: 'BODY-T9-OUTSIDE-GUIDE',
65
+ pnameNeighbour: 'BODY-T9-NEIGHBOUR-IN-PNAMEDIR',
51
66
  };
52
67
  let home;
53
68
  let canvasHome;
@@ -58,6 +73,7 @@ let prevProfile;
58
73
  let prevNode;
59
74
  let prevCwd;
60
75
  let cwdProject;
76
+ let outsideProject;
61
77
  let ppreviewDocPath;
62
78
  /** A doc delivered automatically at the top rung by both automatic channels —
63
79
  * every cap observed below is the relationship's, never the author's. */
@@ -157,6 +173,60 @@ before(() => {
157
173
  B.kindSplit,
158
174
  '',
159
175
  ].join('\n'));
176
+ writeDoc(user, 'dev/code-craft/INDEX', [
177
+ '---',
178
+ 'kind: knowledge',
179
+ 'when-and-why-to-read: "When planning code work, this knowledge should be read because it routes the applicable craft guidance"',
180
+ 'surfaces:',
181
+ ' - {on: boot, gate: {kind: {in: [design, spec, plan, developer]}}, at: content}',
182
+ '---',
183
+ '',
184
+ B.codeCraftIndex,
185
+ '',
186
+ ].join('\n'));
187
+ for (let i = 1; i <= 10; i++) {
188
+ writeDoc(user, `dev/code-craft/member-${i}`, bootlessDoc(`When code-craft member ${i} applies`, `BODY-T9-CODE-CRAFT-MEMBER-${i}`));
189
+ }
190
+ // The ANCESTOR half of a content delivery's listings: a bootless sibling of
191
+ // the `dev/code-craft` directory, disclosed because `dev/` is the enclosing
192
+ // directory of the doc whose body delivered.
193
+ writeDoc(user, 'dev/sibling', bootlessDoc('When the dev sibling applies', 'BODY-T9-DEV-SIBLING'));
194
+ writeDoc(user, 't9-companion-source', [
195
+ '---',
196
+ 'kind: knowledge',
197
+ 'when-and-why-to-read: "When companion delivery starts, this knowledge should be read because it proves content reads cascade"',
198
+ 'surfaces:',
199
+ ' - {on: boot, at: content}',
200
+ '---',
201
+ '',
202
+ B.companionSource,
203
+ '',
204
+ ].join('\n'));
205
+ // A companion that FRONTS a directory: its content delivery discloses its own
206
+ // members exactly as the source doc's did, one level deeper in the fixpoint.
207
+ writeDoc(user, 't9-companion-a/INDEX', [
208
+ '---',
209
+ 'kind: knowledge',
210
+ 'when-and-why-to-read: "When the source is read, this knowledge should be read because it proves the first companion route"',
211
+ 'surfaces:',
212
+ ' - {on: memory-read, match: t9-companion-source, at: content}',
213
+ '---',
214
+ '',
215
+ B.companionA,
216
+ '',
217
+ ].join('\n'));
218
+ writeDoc(user, 't9-companion-a/member-1', bootlessDoc('When the companion member applies', 'BODY-T9-COMPANION-A-MEMBER-1'));
219
+ writeDoc(user, 't9-companion-b', [
220
+ '---',
221
+ 'kind: knowledge',
222
+ 'when-and-why-to-read: "When the first companion is read, this knowledge should be read because it proves the route reaches a fixpoint"',
223
+ 'surfaces:',
224
+ ' - {on: memory-read, match: t9-companion-a, at: content}',
225
+ '---',
226
+ '',
227
+ B.companionB,
228
+ '',
229
+ ].join('\n'));
160
230
  writeDoc(user, 't9-document-gate-wins', [
161
231
  '---',
162
232
  'kind: knowledge',
@@ -169,6 +239,78 @@ before(() => {
169
239
  B.documentGateWins,
170
240
  '',
171
241
  ].join('\n'));
242
+ // Cross-partition single emission: a preference body routes a KNOWLEDGE doc
243
+ // that carries boot content of its own. Both partitions render against one
244
+ // exposure state, so that body must be delivered exactly once.
245
+ writeDoc(user, 't9-pref-router', [
246
+ '---',
247
+ 'kind: preference',
248
+ 'when-and-why-to-read: "When the preference partition renders, this preference should be read because it routes a knowledge companion"',
249
+ 'surfaces:',
250
+ ' - {on: boot, at: content}',
251
+ '---',
252
+ '',
253
+ B.prefRouter,
254
+ '',
255
+ ].join('\n'));
256
+ writeDoc(user, 't9-cross-partition', [
257
+ '---',
258
+ 'kind: knowledge',
259
+ 'when-and-why-to-read: "When the preference routes here, this knowledge should be read because it proves one body crosses both partitions once"',
260
+ 'surfaces:',
261
+ ' - {on: boot, at: content}',
262
+ ' - {on: memory-read, match: t9-pref-router, at: content}',
263
+ '---',
264
+ '',
265
+ B.crossPartition,
266
+ '',
267
+ ].join('\n'));
268
+ // Cap safety: a name-tree directory merges across stores, so this user doc
269
+ // sits in the SAME canonical directory as the name-capped project's docs. Its
270
+ // content delivery discloses that directory — and must not raise a capped
271
+ // neighbour above the bare name its relationship allows.
272
+ writeDoc(user, 'pname/user-guide', [
273
+ '---',
274
+ 'kind: knowledge',
275
+ 'when-and-why-to-read: "When the merged directory renders, this knowledge should be read because it discloses that directory"',
276
+ 'surfaces:',
277
+ ' - {on: boot, at: content}',
278
+ '---',
279
+ '',
280
+ B.pnameNeighbour,
281
+ '',
282
+ ].join('\n'));
283
+ // A project OUTSIDE the profile manifest: its store mounts only for a target
284
+ // whose cwd sits inside it, so a workspace-open render of it proves the
285
+ // listings answer from the opened workspace rather than the ambient one.
286
+ const poutside = store('poutside', 'poutside');
287
+ outsideProject = join(home, 'poutside');
288
+ writeDoc(poutside, 't9-ws-guide/INDEX', [
289
+ '---',
290
+ 'kind: knowledge',
291
+ 'when-and-why-to-read: "When that workspace opens, this knowledge should be read because it fronts its guide directory"',
292
+ 'surfaces:',
293
+ ' - {on: workspace-open, at: content}',
294
+ '---',
295
+ '',
296
+ B.outsideGuide,
297
+ '',
298
+ ].join('\n'));
299
+ writeDoc(poutside, 't9-ws-guide/member-1', bootlessDoc('When the outside guide member applies', 'BODY-T9-OUTSIDE-MEMBER-1'));
300
+ // Workspace-open plans project stores only, but a content delivery's
301
+ // read-equivalent attachments answer from the whole opened target.
302
+ writeDoc(user, 'poutside/listing-member', bootlessDoc('When the workspace listing member applies', 'BODY-T9-WORKSPACE-LISTING-MEMBER'));
303
+ writeDoc(user, 't9-workspace-companion', [
304
+ '---',
305
+ 'kind: knowledge',
306
+ 'when-and-why-to-read: "When the workspace guide is read, this companion proves full-view accounting"',
307
+ 'surfaces:',
308
+ ' - {on: memory-read, match: poutside/t9-ws-guide, at: content}',
309
+ '---',
310
+ '',
311
+ 'BODY-T9-WORKSPACE-COMPANION',
312
+ '',
313
+ ].join('\n'));
172
314
  // Hidden-count pair: a top-level dir always renders, so a dir holding only a
173
315
  // bootless doc shows as `<dir>/` + `[+1 more]` — unless its project is `none`.
174
316
  writeDoc(pnone, join('t9-none-hidden', 'probe'), bootlessDoc('When counted', 'HIDDEN-NONE'));
@@ -219,7 +361,10 @@ test('boot delivery caps every profile project at its relationship, including th
219
361
  // does, and the relationship still decides the ceiling.
220
362
  assert.ok(!boot.includes(B.ppreview), `the cwd project is capped at preview — its body must not render; got:\n${boot}`);
221
363
  assert.match(boot, /t9-ppreview-doc {2}# read when: When the cwd project delivers\./, `the cwd project's doc discloses at preview; got:\n${boot}`);
222
- // `name` discloses the bare identifier and nothing else.
364
+ // `name` discloses the bare identifier and nothing else — including when a
365
+ // content delivery in the same merged directory (the user-store neighbour)
366
+ // discloses that directory's listing.
367
+ assert.ok(boot.includes(B.pnameNeighbour), `the merged directory's uncapped document delivers; got:\n${boot}`);
223
368
  assert.ok(!boot.includes(B.pname), 'a name-capped project must not render a body');
224
369
  assert.match(boot, /[├└]─ t9-pname-doc$/m, `the name-capped doc discloses as a bare name; got:\n${boot}`);
225
370
  assert.ok(!boot.includes('When the name project delivers'), 'a name-capped doc discloses no preview line');
@@ -242,6 +387,88 @@ test('surface-entry gates select the rung by node kind while the document gate r
242
387
  const developer = renderKnowledgeBlock(newNode('developer'), freshTranscriptTarget());
243
388
  assert.ok(!developer.includes('t9-document-gate-wins'), `a failing document gate excludes every entry; got:\n${developer}`);
244
389
  assert.ok(!developer.includes(B.documentGateWins), 'a failing document gate excludes its content');
390
+ assert.ok(developer.includes(B.codeCraftIndex), 'the directory document delivers its boot content');
391
+ for (let i = 1; i <= 10; i++) {
392
+ assert.match(developer, new RegExp(`member-${i} {2}# read when: When code-craft member ${i} applies`), `content delivery exposes code-craft member ${i} at preview; got:\n${developer}`);
393
+ }
394
+ // The ancestor listing, not just the doc's own directory.
395
+ assert.match(developer, /sibling {2}# read when: When the dev sibling applies\./, `content delivery exposes the enclosing directory's members too; got:\n${developer}`);
396
+ });
397
+ test('context docs accounting reports content attachment raises from the renderer', () => {
398
+ const node = newNode('developer');
399
+ const subject = assembleNodeSubject(node);
400
+ assert.ok(subject !== null, 'the fixture node assembles a subject');
401
+ const target = { cwd: cwdProject, profileId, nodeId: node };
402
+ const direct = planDelivery(subject, target, 'boot');
403
+ const rendered = assembleEventAccounting({ subject, target, nodeMissing: false }, 'boot', {});
404
+ const rows = effectivePlan(direct, rendered.accounting.deliveries).docs.map((record) => docRow(record, true, subject));
405
+ for (let i = 1; i <= 10; i++) {
406
+ const row = rows.find((candidate) => candidate.name === `dev/code-craft/member-${i}`);
407
+ assert.equal(row?.final_rung, 'preview', `member ${i} is raised to the listing preview rung`);
408
+ assert.equal(row?.delivery_cause, 'directory listing attached to content delivery');
409
+ }
410
+ const companion = rows.find((row) => row.name === 't9-companion-a');
411
+ assert.equal(companion?.final_rung, 'content', 'a memory-read companion reaches its rendered rung');
412
+ assert.equal(companion?.delivery_cause, 'memory-read companion of content delivery');
413
+ const plans = new PlanSet({
414
+ kind: 'developer',
415
+ mode: 'base',
416
+ lifecycle: 'terminal',
417
+ hasManager: true,
418
+ profileId,
419
+ cwd: cwdProject,
420
+ nodeId: node,
421
+ depth: 0,
422
+ event: 'boot',
423
+ file: '',
424
+ docName: '',
425
+ command: '',
426
+ });
427
+ plans.plan('boot');
428
+ const viewerMember = buildRows(plans, 'boot').find((row) => row.name === 'dev/code-craft/member-1');
429
+ const viewerRecord = viewerMember?.records.get('boot');
430
+ assert.equal(viewerRecord?.finalRung, 'preview', 'the admin row reads the renderer-raised rung');
431
+ assert.equal(viewerRecord?.deliveryCause, 'directory listing attached to content delivery');
432
+ plans.dispose();
433
+ });
434
+ test('context docs accounting adds full-view workspace content attachments', () => {
435
+ const node = newNode();
436
+ const subject = assembleNodeSubject(node);
437
+ assert.ok(subject !== null, 'the fixture node assembles a subject');
438
+ const target = { cwd: outsideProject, profileId, nodeId: node };
439
+ const direct = planDelivery(subject, target, 'workspace-open');
440
+ const rendered = assembleEventAccounting({ subject, target, nodeMissing: false }, 'workspace-open', {});
441
+ const rows = effectivePlan(direct, rendered.accounting.deliveries).docs.map((record) => docRow(record, true, subject));
442
+ const listing = rows.find((row) => row.name === 'poutside/listing-member');
443
+ assert.equal(listing?.final_rung, 'preview');
444
+ assert.equal(listing?.delivery_cause, 'directory listing attached to content delivery');
445
+ const companion = rows.find((row) => row.name === 't9-workspace-companion');
446
+ assert.equal(companion?.final_rung, 'content');
447
+ assert.equal(companion?.delivery_cause, 'memory-read companion of content delivery');
448
+ });
449
+ test('boot content triggers memory-read companions to a deduplicated fixpoint', () => {
450
+ const boot = renderKnowledgeBlock(newNode(), freshTranscriptTarget());
451
+ for (const body of [B.companionSource, B.companionA, B.companionB]) {
452
+ assert.equal(boot.split(body).length - 1, 1, `content companion ${body} delivers once; got:\n${boot}`);
453
+ }
454
+ // A companion is read-equivalent in its own right: the directory it fronts
455
+ // discloses its members exactly as the doc that routed to it did.
456
+ assert.match(boot, /member-1 {2}# read when: When the companion member applies\./, `a content companion exposes its own directory's members; got:\n${boot}`);
457
+ });
458
+ test('one body delivered across both boot partitions renders exactly once', () => {
459
+ const node = newNode();
460
+ const subject = assembleNodeSubject(node);
461
+ assert.ok(subject !== null, 'the fixture node assembles a subject');
462
+ const state = emptyContextExposureState();
463
+ // The live boot order: the knowledge block and the preference splice share
464
+ // ONE exposure state, so a knowledge body already delivered as a preference's
465
+ // companion must not render again as knowledge prose.
466
+ const preferences = renderPreferencesForSubject(subject, exposureTarget(state, 'system')).block;
467
+ const knowledge = renderKnowledgeForSubject(subject, node, exposureTarget(state, 'transcript'));
468
+ assert.ok(preferences.includes(B.prefRouter), `the routing preference delivers its body; got:\n${preferences}`);
469
+ assert.ok(preferences.includes(B.crossPartition), `its companion delivers with it; got:\n${preferences}`);
470
+ const occurrences = (preferences.split(B.crossPartition).length - 1) + (knowledge.split(B.crossPartition).length - 1);
471
+ assert.equal(occurrences, 1, `the companion body renders once across both partitions; got:\n${preferences}\n---\n${knowledge}`);
245
472
  });
246
473
  test('a node-local doc floors to name only without boot entries', () => {
247
474
  const node = newNode('explore');
@@ -312,6 +539,17 @@ test('workspace-open clamps each project store the same way boot does', () => {
312
539
  assert.ok(!opened.includes('t9-pnone-doc'), 'the none-capped project delivers nothing at workspace-open');
313
540
  assert.ok(!opened.includes(B.sharedFromPnone), 'nor its side of a name collision');
314
541
  });
542
+ test('workspace-open answers its listings from the workspace it opened, not the ambient one', () => {
543
+ const node = newNode();
544
+ const subject = assembleNodeSubject(node);
545
+ assert.ok(subject !== null, 'the fixture node assembles a subject');
546
+ // The process cwd is `ppreview`; this workspace is outside the profile
547
+ // manifest, so ONLY the opened target's corpus knows its store at all.
548
+ const opened = renderWorkspaceOpenDocsForSubject(subject, join(home, 'poutside'), profileId, freshTranscriptTarget());
549
+ assert.ok(opened.includes(B.outsideGuide), `the opened workspace's guide delivers its body; got:\n${opened}`);
550
+ assert.match(opened, /<memory-listing dir="poutside\/t9-ws-guide">/, `its directory listing renders from that workspace's corpus; got:\n${opened}`);
551
+ assert.match(opened, /\[\[poutside\/t9-ws-guide\/member-1\]\]: When the outside guide member applies\./, `the listing carries that directory's own members; got:\n${opened}`);
552
+ });
315
553
  test('memory list and find record synthetic pages without crediting matched documents', async () => {
316
554
  const node = newNode();
317
555
  process.env['CRTR_NODE_ID'] = node;
@@ -350,6 +588,31 @@ test('memory list and find record synthetic pages without crediting matched docu
350
588
  delete process.env['CRTR_NODE_ID'];
351
589
  }
352
590
  });
591
+ test('an explicit read after boot content adds no duplicate directory attachments', async () => {
592
+ const node = newNode('developer');
593
+ process.env['CRTR_NODE_ID'] = node;
594
+ try {
595
+ const state = loadContextExposureState(node);
596
+ renderKnowledgeBlock(node, exposureTarget(state, 'transcript'));
597
+ saveContextExposureState(node, state);
598
+ const result = (await readLeaf.run({
599
+ name: 'dev/code-craft',
600
+ kind: undefined,
601
+ frontmatter: false,
602
+ scope: undefined,
603
+ dir: undefined,
604
+ }));
605
+ assert.equal(result.content.trim(), B.codeCraftIndex, 'the repeated read returns its body without a new attachment block');
606
+ assert.equal(result.listing.length, 10, 'the deliberate directory read still returns its immediate members');
607
+ const afterRead = loadContextExposureState(node);
608
+ for (let i = 1; i <= 10; i++) {
609
+ assert.ok(documentExposedAtOrAbove(afterRead, join(home, '.crouter', 'memory', 'dev', 'code-craft', `member-${i}.md`), `BODY-T9-CODE-CRAFT-MEMBER-${i}`, 'preview'), `boot content registered member ${i} at preview before the explicit read`);
610
+ }
611
+ }
612
+ finally {
613
+ delete process.env['CRTR_NODE_ID'];
614
+ }
615
+ });
353
616
  test('an explicit `crtr memory read` returns and records the full body a cap withheld', async () => {
354
617
  const node = newNode();
355
618
  process.env['CRTR_NODE_ID'] = node;
@@ -399,8 +399,8 @@ test('reload_auth restores a newly authenticated personal route before the next
399
399
  mkdirSync(userConfigDir, { recursive: true });
400
400
  writeFileSync(userConfigPath, JSON.stringify({
401
401
  modelRoutes: {
402
- personal: { family: 'anthropic', credentialSource: 'user', providerId: personal.provider, models: { medium: personalSpec } },
403
- funded: { family: 'anthropic', credentialSource: 'northlight', providerId: funded.provider, models: { medium: `${funded.provider}/${funded.id}:medium` } },
402
+ personal: { family: 'anthropic', credentialSource: 'user', providerId: personal.provider, models: { ultra: personalSpec, strong: personalSpec, medium: personalSpec, light: personalSpec } },
403
+ funded: { family: 'anthropic', credentialSource: 'northlight', providerId: funded.provider, models: { ultra: `${funded.provider}/${funded.id}:medium`, strong: `${funded.provider}/${funded.id}:medium`, medium: `${funded.provider}/${funded.id}:medium`, light: `${funded.provider}/${funded.id}:medium` } },
404
404
  },
405
405
  modelRouting: { credentialSourceOrder: ['user', 'northlight'], familyOrder: ['anthropic'], strengthFallback: ['medium'] },
406
406
  }));
@@ -137,7 +137,7 @@ export declare function dashboardRowsFromSource(source: CanvasSource, rootId: st
137
137
  * the node ids from one `listFocuses()` query so `viewed` remains visible without
138
138
  * coupling the canvas source to local viewer state. Remote sources ignore that
139
139
  * set. Local disk telemetry/fault/streaming are likewise suppressed remotely. */
140
- export declare function dashboardRowsAllFromSource(source: CanvasSource, localFocusedNodeIds?: ReadonlySet<string>): Promise<DashboardRow[]>;
140
+ export declare function dashboardRowsAllFromSource(source: CanvasSource, localFocusedNodeIds?: ReadonlySet<string>, liveOnly?: boolean): Promise<DashboardRow[]>;
141
141
  export declare function enrichRowsFromSource(source: CanvasSource, rows: DashboardRow[], asks?: Record<string, number>): Promise<void>;
142
142
  /** goal (initial-prompt.md) and session parts are local disk reads — suppressed
143
143
  * to undefined for a remote source. */
@@ -443,10 +443,11 @@ export async function dashboardRowsFromSource(source, rootId) {
443
443
  * the node ids from one `listFocuses()` query so `viewed` remains visible without
444
444
  * coupling the canvas source to local viewer state. Remote sources ignore that
445
445
  * set. Local disk telemetry/fault/streaming are likewise suppressed remotely. */
446
- export async function dashboardRowsAllFromSource(source, localFocusedNodeIds = new Set()) {
446
+ export async function dashboardRowsAllFromSource(source, localFocusedNodeIds = new Set(), liveOnly = false) {
447
447
  const remote = source instanceof RemoteCanvasSource;
448
448
  const focusedNodeIds = remote ? new Set() : localFocusedNodeIds;
449
- return (await source.listNodes()).map((row) => dashboardRowFromNodeRow(row, focusedNodeIds, remote));
449
+ const rows = await source.listNodes(liveOnly ? { status: ['active', 'idle'] } : undefined);
450
+ return rows.map((row) => dashboardRowFromNodeRow(row, focusedNodeIds, remote));
450
451
  }
451
452
  // Lazy enrichment (source-driven) — fold expensive per-node reads into rows on
452
453
  // demand. Ask counts are supplied by the caller (`asks` — a pre-fetched
@@ -2,12 +2,12 @@ import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanAc
2
2
  import { type BindingId } from './keybindings/catalog.js';
3
3
  export declare function configPath(scope: Scope): string | null;
4
4
  export declare function statePath(scope: Scope): string | null;
5
- export declare function readConfig(scope: Scope): ScopeConfig;
5
+ export declare function readConfig(scope: Scope): ResolvedScopeConfig;
6
6
  /** Read the config that lives at an EXACT scope-root dir, rather than the
7
7
  * resolved nearest root for a scope. Lets a caller iterate each visible
8
8
  * project root's own config when several are in scope (nested `.crouter` dirs
9
9
  * or a profile-widened stack), instead of collapsing them onto the nearest. */
10
- export declare function readConfigAtRoot(root: string): ScopeConfig;
10
+ export declare function readConfigAtRoot(root: string): ResolvedScopeConfig;
11
11
  export declare function readState(scope: Scope): ScopeState;
12
12
  type RawConfig = Record<string, unknown>;
13
13
  export declare function updateRawConfigAtomically(scope: Scope, mutate: (latest: RawConfig) => RawConfig): RawConfig;
@@ -26,6 +26,14 @@ export declare function ensureScopeInitialized(scope: Scope, root: string): void
26
26
  /** Normalize the user-owned list of attach actions allowed to bridge through an
27
27
  * occupied tmux root binding. Unknown and non-attach ids have no effect. */
28
28
  export declare function normalizeTmuxPassthrough(raw: unknown): BindingId[];
29
+ type LegacyModelRouteConfig = Omit<ModelRouteConfig, 'models'> & {
30
+ models: Partial<Record<ModelStrength, string>>;
31
+ };
32
+ /** Read configuration exposes legacy sparse routes separately during the
33
+ * one-release cutover; launch code only treats `modelRoutes` as complete. */
34
+ export type ResolvedScopeConfig = ScopeConfig & {
35
+ legacyModelRoutes?: Record<string, LegacyModelRouteConfig>;
36
+ };
29
37
  /** One malformed `bin` declaration. The read path retains these as doctor
30
38
  * findings rather than letting malformed config break a node launch. */
31
39
  export interface BinDeclarationIssue {
@@ -103,7 +111,11 @@ export declare function readRawScopeConfig(scope: Scope): Partial<ScopeConfig> |
103
111
  export interface MergedLaunchConfig {
104
112
  kinds: Record<string, KindConfig>;
105
113
  modelLadders: ModelLaddersConfig;
114
+ /** Complete declared routes. Every route here has all four strengths. */
106
115
  modelRoutes?: Record<string, ModelRouteConfig>;
116
+ /** Read-only compatibility view for sparse routes written before complete
117
+ * routes were required. New route persistence never produces this shape. */
118
+ legacyModelRoutes?: Record<string, LegacyModelRouteConfig>;
107
119
  modelRouting?: ModelRoutingConfig;
108
120
  /** The merged `spawnEnv.allow` set (project stack > profile > user >
109
121
  * builtin, additive) — read by `buildOperationalEnvBase`
@@ -185,6 +197,11 @@ export declare function persistModelLadders(root: string, patch: {
185
197
  anthropic?: Partial<Record<ModelStrength, string>>;
186
198
  openai?: Partial<Record<ModelStrength, string>>;
187
199
  }): void;
200
+ /** Persist one complete route at one exact config root. The route is the only
201
+ * raw route entry touched; unrelated routes and local ladder defaults remain
202
+ * absent. Pass `undefined` to remove this scope's entry, which resumes lower
203
+ * scope inheritance rather than introducing a tombstone. */
204
+ export declare function persistModelRoute(root: string, routeId: string, route: ModelRouteConfig | undefined): void;
188
205
  /** Mutate one exact config root. Setup uses this for profile and explicit
189
206
  * project targets that cannot be represented by the user/project Scope enum. */
190
207
  export declare function updateConfigAtRoot(root: string, mutate: (cfg: ScopeConfig) => void): ScopeConfig;