@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.
- package/dist/api/dto/health.d.ts +2 -0
- package/dist/builtin-memory/internal/memory-loading.md +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +5 -3
- package/dist/clients/attach/input/controller.d.ts +4 -0
- package/dist/clients/attach/input/controller.js +8 -0
- package/dist/clients/attach/session/bindings.d.ts +3 -0
- package/dist/clients/attach/session/bindings.js +7 -0
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +3 -3
- package/dist/clients/attach/viewer.js +616 -616
- package/dist/commands/memory/read.js +15 -38
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
- package/dist/commands/sys/context/admin/model.d.ts +7 -2
- package/dist/commands/sys/context/admin/model.js +7 -1
- package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
- package/dist/commands/sys/context/admin/read-view.js +9 -4
- package/dist/commands/sys/context/resolve.d.ts +32 -3
- package/dist/commands/sys/context/resolve.js +74 -25
- package/dist/commands/sys/panels/models-panel.d.ts +28 -5
- package/dist/commands/sys/panels/models-panel.js +558 -75
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes-config.test.js +142 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +2 -2
- package/dist/core/canvas/render-source.d.ts +1 -1
- package/dist/core/canvas/render-source.js +3 -2
- package/dist/core/config.d.ts +19 -2
- package/dist/core/config.js +162 -47
- package/dist/core/keybindings/catalog.js +9 -7
- package/dist/core/model-routes.js +10 -3
- package/dist/core/substrate/listings.d.ts +1 -1
- package/dist/core/substrate/listings.js +6 -2
- package/dist/core/substrate/on-read.d.ts +61 -5
- package/dist/core/substrate/on-read.js +129 -22
- package/dist/core/substrate/render.d.ts +3 -2
- package/dist/core/substrate/render.js +133 -38
- package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
- package/dist/daemon/api/handlers/canvas.js +18 -2
- package/dist/daemon/api/handlers/health.js +17 -12
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
- 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
|
|
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
|
-
|
|
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
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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):
|
|
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):
|
|
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;
|