@north-light/crouter 0.3.208 → 0.3.209
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/client.d.ts +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/broker-ops.d.ts +2 -12
- package/dist/api/dto/nodes.d.ts +16 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base.md +3 -2
- package/dist/builtin-memory/01-spine/00-has-manager.md +3 -2
- package/dist/builtin-memory/01-spine/01-no-manager.md +3 -2
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -2
- package/dist/builtin-memory/02-lifecycle/01-resident.md +3 -2
- package/dist/builtin-memory/04-base-worker.md +3 -2
- package/dist/builtin-memory/04-orchestration-kernel.md +3 -2
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/design/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/developer/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/explore/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +3 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -2
- package/dist/builtin-memory/advisor/council.md +0 -2
- package/dist/builtin-memory/design.md +3 -2
- package/dist/builtin-memory/development.md +3 -2
- package/dist/builtin-memory/insights/capture.md +1 -3
- package/dist/builtin-memory/insights/init.md +1 -3
- package/dist/builtin-memory/insights/listen.md +3 -2
- package/dist/builtin-memory/internal/INDEX.md +5 -4
- package/dist/builtin-memory/internal/agent-shaping.md +5 -4
- package/dist/builtin-memory/internal/examples/INDEX.md +3 -2
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -2
- package/dist/builtin-memory/internal/marketplaces.md +3 -2
- package/dist/builtin-memory/internal/memory-loading.md +31 -20
- package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -2
- package/dist/builtin-memory/internal/plugins.md +7 -6
- package/dist/builtin-memory/internal/storage-tiers.md +3 -2
- package/dist/builtin-memory/plan/roadmap.md +3 -2
- package/dist/builtin-memory/spec/guide.md +0 -2
- package/dist/builtin-memory/spec/requirements.md +0 -2
- package/dist/builtin-memory/spec/roadmap.md +3 -2
- package/dist/builtin-memory/testing.md +1 -3
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +3 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +1 -4
- package/dist/clients/attach/viewer.js +366 -366
- package/dist/commands/memory/delete.js +3 -18
- package/dist/commands/memory/edit.js +23 -30
- package/dist/commands/memory/history.js +1 -1
- package/dist/commands/memory/lint.d.ts +7 -8
- package/dist/commands/memory/lint.js +178 -132
- package/dist/commands/memory/list.d.ts +0 -1
- package/dist/commands/memory/list.js +2 -12
- package/dist/commands/memory/move.d.ts +1 -0
- package/dist/commands/memory/move.js +195 -0
- package/dist/commands/memory/read.js +135 -141
- package/dist/commands/memory/shared.d.ts +18 -17
- package/dist/commands/memory/shared.js +93 -39
- package/dist/commands/memory/write.js +23 -33
- package/dist/commands/memory.js +5 -4
- package/dist/commands/pkg/browse/catalog.js +2 -4
- package/dist/commands/pkg/browse/doc-view.js +17 -11
- package/dist/commands/pkg/browse/model.d.ts +7 -9
- package/dist/commands/sys/migrate.d.ts +1 -0
- package/dist/commands/sys/migrate.js +106 -0
- package/dist/commands/sys/sync-deps.js +5 -10
- package/dist/commands/sys/sync-project-guidance.js +36 -16
- package/dist/commands/sys/sync-skills.js +8 -4
- package/dist/commands/sys.js +3 -2
- package/dist/core/__tests__/inline-memory-refs.test.js +8 -5
- package/dist/core/__tests__/memory-resolver-precedence.test.js +5 -4
- package/dist/core/__tests__/nested-store-discovery.test.js +1 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +3 -3
- package/dist/core/__tests__/on-read-dedup-resume.test.js +12 -12
- package/dist/core/__tests__/on-read-nested-store.test.js +23 -20
- package/dist/core/canvas/db.d.ts +3 -1
- package/dist/core/canvas/db.js +12 -2
- package/dist/core/memory/history.d.ts +4 -1
- package/dist/core/memory/history.js +1 -0
- package/dist/core/memory/inline-ref-inventory.d.ts +5 -4
- package/dist/core/memory/inline-ref-inventory.js +23 -19
- package/dist/core/memory-resolver.d.ts +4 -4
- package/dist/core/memory-resolver.js +16 -43
- package/dist/core/runtime/bearings.d.ts +4 -3
- package/dist/core/runtime/bearings.js +4 -4
- package/dist/core/runtime/broker-extension-render.d.ts +3 -2
- package/dist/core/runtime/broker-extension-render.js +3 -3
- package/dist/core/runtime/memory.js +2 -3
- package/dist/core/substrate/index.d.ts +7 -4
- package/dist/core/substrate/index.js +6 -4
- package/dist/core/substrate/injected-store.d.ts +24 -12
- package/dist/core/substrate/injected-store.js +80 -33
- package/dist/core/substrate/listings.d.ts +21 -0
- package/dist/core/substrate/listings.js +88 -0
- package/dist/core/substrate/on-read-node.d.ts +5 -5
- package/dist/core/substrate/on-read-node.js +4 -5
- package/dist/core/substrate/on-read.d.ts +25 -4
- package/dist/core/substrate/on-read.js +81 -102
- package/dist/core/substrate/render-node.d.ts +5 -2
- package/dist/core/substrate/render-node.js +5 -3
- package/dist/core/substrate/render.d.ts +9 -8
- package/dist/core/substrate/render.js +104 -96
- package/dist/core/substrate/schema.d.ts +34 -18
- package/dist/core/substrate/schema.js +75 -32
- package/dist/core/substrate/surface-match.d.ts +32 -0
- package/dist/core/substrate/surface-match.js +179 -0
- package/dist/daemon/api/handlers/nodes.js +9 -0
- package/dist/migrations/001-surfaces-frontmatter.d.ts +2 -0
- package/dist/migrations/001-surfaces-frontmatter.js +276 -0
- package/dist/migrations/convergent.d.ts +31 -0
- package/dist/migrations/convergent.js +71 -0
- package/dist/migrations/registry.d.ts +2 -0
- package/dist/migrations/registry.js +19 -0
- package/dist/migrations/types.d.ts +40 -0
- package/dist/migrations/types.js +11 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +2 -1
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +2 -1
- package/dist/pi-extensions/canvas-doc-substrate.js +57 -34
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/core/substrate/ceiling.d.ts +0 -17
- package/dist/core/substrate/ceiling.js +0 -67
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Two boot targets, one shape per kind: each kind renders as ONE wrapped block —
|
|
4
4
|
// intro prose → GROUPED content → an update directive. The render is grouped
|
|
5
|
-
// by
|
|
5
|
+
// by boot rung, not interleaved: `content`-rung docs render FIRST as
|
|
6
6
|
// clean prose bodies (verbatim, no tree chrome, no name label — this is what
|
|
7
7
|
// makes content-rung the persona-prose mechanism), in general→specific order;
|
|
8
|
-
// then the `preview`+`name`
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// knowledge" unconditionally:
|
|
8
|
+
// then the `preview`+`name` docs render as ONE catalog tree, each at its own
|
|
9
|
+
// rung (preview → a `# read when:` routing line; name → a bare entry); a doc
|
|
10
|
+
// with NO boot entry leaks only into its dir's `[+N more]` count, never a
|
|
11
|
+
// name — EXCEPT a node-local doc, whose contract is "node-local rides into
|
|
12
|
+
// knowledge" unconditionally: renderKnowledgeForSubject floors a bootless
|
|
13
13
|
// node-local doc up to `name` so it still shows its bare name rather than
|
|
14
14
|
// collapsing into a hidden count. Only gate evaluation removes a node-local
|
|
15
15
|
// doc outright.
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
//
|
|
28
28
|
// The pipeline is explicitly TWO-STEP and must never run in the other order:
|
|
29
29
|
// 1. SELECT WINNERS — MemoryDoc → parseSubstrateDoc → (null-filter
|
|
30
|
-
// non-substrate) →
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// node-local store
|
|
30
|
+
// non-substrate) → per-doc boot rung (`bootRung`, the max `at` over the
|
|
31
|
+
// doc's `on: boot` surfaces entries) → gatePasses → first-wins DEDUP by
|
|
32
|
+
// name over the resolver's precedence order (nearest project > user >
|
|
33
|
+
// builtin). Node-local docs follow the same gate rule, but the
|
|
34
|
+
// knowledge-block render floors a bootless node-local doc's rung to
|
|
35
|
+
// `name` — the one rung-floor exemption, scoped to the node-local store
|
|
36
|
+
// only.
|
|
36
37
|
// 2. RENDER IN DISPLAY ORDER — split winners into content vs. the rest;
|
|
37
38
|
// order content general→specific (builtin, user, project outermost→
|
|
38
39
|
// nearest, node-local last) then by tree position then filename; place
|
|
@@ -43,29 +44,28 @@
|
|
|
43
44
|
// orderings and must not be conflated.
|
|
44
45
|
//
|
|
45
46
|
// Pure + defensive: reads the resolver, canvas-db (subject assembly), and the
|
|
46
|
-
// node-local memory dir; performs no writes and no side effects
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
47
|
+
// node-local memory dir; performs no writes and no side effects beyond the
|
|
48
|
+
// caller-supplied `seen` delivery map (write-only seeding — boot docs render
|
|
49
|
+
// every session and are never skipped). A single malformed doc never throws
|
|
50
|
+
// the whole render — parsing is isolated upstream (parseSubstrateDoc returns
|
|
51
|
+
// null for a non-substrate doc; per-file node-local loads are wrapped), and
|
|
52
|
+
// tree construction is pure string work.
|
|
50
53
|
import { relative, sep } from 'node:path';
|
|
51
54
|
import { interpolateNodePaths } from '../canvas/paths.js';
|
|
52
55
|
import { listAllMemoryDocs, resolveMemoryDoc } from '../memory-resolver.js';
|
|
53
56
|
import { subKindsAvailableTo } from '../config.js';
|
|
54
57
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
55
|
-
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
58
|
+
import { pathExists, readText, realpathOrSelf, walkFiles } from '../fs-utils.js';
|
|
56
59
|
import { memoryDir } from '../runtime/memory.js';
|
|
57
60
|
import { projectScopeRoots } from '../scope.js';
|
|
61
|
+
import { gatePasses } from './gate.js';
|
|
58
62
|
// De-barreled to LEAF modules on purpose: importing from './index.js' would
|
|
59
63
|
// pull subject.js (canvas-db) transitively, re-tainting every CLI consumer of
|
|
60
64
|
// the pure render fns. The canvas-db wrappers (renderPreferencesSection /
|
|
61
65
|
// renderKnowledgeBlock) live in render-node.js instead.
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
64
|
-
import { normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, rungRank, } from './schema.js';
|
|
66
|
+
import { recordDelivery } from './injected-store.js';
|
|
67
|
+
import { bootRung, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, rungRank, } from './schema.js';
|
|
65
68
|
import { cachedSubstrateDocs } from './session-cache.js';
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Step 1 — winner selection (ceiling + gate + first-wins dedup).
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
69
|
/** First-wins dedup by (already-normalized) name, preserving the input's
|
|
70
70
|
* precedence order. The array's ORDER is the precedence signal — callers must
|
|
71
71
|
* pass docs already ordered highest-precedence-first; this function never
|
|
@@ -82,18 +82,14 @@ function dedupeFirstWins(docs) {
|
|
|
82
82
|
return out;
|
|
83
83
|
}
|
|
84
84
|
/** STEP 1 — the resolver-provided WINNER docs of one `kind`, eligible at boot
|
|
85
|
-
* for `subject`: parsed (non-substrate docs null-filtered),
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* Ceilings are computed over the WHOLE corpus (cross-kind) BEFORE the kind
|
|
95
|
-
* filter, and the effective rung is written back into systemPromptVisibility —
|
|
96
|
-
* but each doc keeps its canonical logical identifier, such as `taste/INDEX`. */
|
|
85
|
+
* for `subject`: parsed (non-substrate docs null-filtered), gate-passed, then
|
|
86
|
+
* first-wins DEDUPED by name over the resolver's precedence order (nearest
|
|
87
|
+
* project > user > builtin) — each at its own boot rung, INCLUDING docs with
|
|
88
|
+
* NO boot entry (the tree counts them into `[+N more]`). This is winner
|
|
89
|
+
* selection ONLY; display ordering is a separate, later step (see
|
|
90
|
+
* renderGrouped) so a builtin doc can never mask a project/user override by
|
|
91
|
+
* virtue of sorting first. Uses the per-session cache so the full corpus is
|
|
92
|
+
* scanned + parsed at most once per session across the boot-render calls. */
|
|
97
93
|
function selectWinners(subject, kind) {
|
|
98
94
|
let docs;
|
|
99
95
|
try {
|
|
@@ -102,17 +98,29 @@ function selectWinners(subject, kind) {
|
|
|
102
98
|
catch {
|
|
103
99
|
return [];
|
|
104
100
|
}
|
|
105
|
-
const ceil = buildCeilingIndex(docs);
|
|
106
101
|
const eligible = docs
|
|
107
|
-
.map((d) => {
|
|
108
|
-
const rung = effectiveSystemPromptRung(d, ceil);
|
|
109
|
-
return rung === d.systemPromptVisibility ? d : { ...d, systemPromptVisibility: rung };
|
|
110
|
-
})
|
|
111
102
|
.filter((d) => d.kind === kind)
|
|
112
|
-
.filter((d) => gatePasses(d, subject))
|
|
103
|
+
.filter((d) => gatePasses(d, subject))
|
|
104
|
+
.map((d) => ({ ...d, bootRung: bootRung(d) }));
|
|
113
105
|
return dedupeFirstWins(eligible);
|
|
114
106
|
}
|
|
115
107
|
// ---------------------------------------------------------------------------
|
|
108
|
+
// Boot-delivery seeding — the injected-docs map grows at render time.
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
/** Record every VISIBLY rendered winner into the caller's transcript dedup map
|
|
111
|
+
* at its displayed rung (write-only — boot renders every session and never
|
|
112
|
+
* skips). Seeding is what lets a later listing line or on-read injection skip
|
|
113
|
+
* a doc the boot catalog already discloses at that rank. */
|
|
114
|
+
function seedBootDelivery(seen, docs) {
|
|
115
|
+
if (seen === undefined)
|
|
116
|
+
return;
|
|
117
|
+
for (const d of docs) {
|
|
118
|
+
if (!isVisible(d.bootRung))
|
|
119
|
+
continue;
|
|
120
|
+
recordDelivery(seen, realpathOrSelf(d.path), d.bootRung);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
116
124
|
// Step 2 — display ordering + grouped render.
|
|
117
125
|
// ---------------------------------------------------------------------------
|
|
118
126
|
/** Content-prose ordering rank: general → specific. builtin=0, user=1,
|
|
@@ -171,15 +179,15 @@ function compareTreePosition(a, b) {
|
|
|
171
179
|
/** STEP 2 — split deduped `winners` by rung and render each group in its
|
|
172
180
|
* display order: `content` docs first, as clean prose (general→specific,
|
|
173
181
|
* then tree position, then filename — no tree chrome, no name label); every
|
|
174
|
-
* other winner (preview/name/hidden-
|
|
175
|
-
* as before. `nodeLocalSet` marks which winners came from the
|
|
176
|
-
* store (ranked most-specific in content ordering); omit it for a
|
|
177
|
-
* resolver render. */
|
|
182
|
+
* other winner (preview/name/hidden-bootless) folds into ONE catalog tree
|
|
183
|
+
* exactly as before. `nodeLocalSet` marks which winners came from the
|
|
184
|
+
* node-local store (ranked most-specific in content ordering); omit it for a
|
|
185
|
+
* pure resolver render. */
|
|
178
186
|
function renderGrouped(winners, rootLabel, nodeLocalSet = new Set()) {
|
|
179
187
|
const content = [];
|
|
180
188
|
const rest = [];
|
|
181
189
|
for (const d of winners) {
|
|
182
|
-
if (d.
|
|
190
|
+
if (d.bootRung === 'content')
|
|
183
191
|
content.push(d);
|
|
184
192
|
else
|
|
185
193
|
rest.push(d);
|
|
@@ -204,10 +212,10 @@ function renderGrouped(winners, rootLabel, nodeLocalSet = new Set()) {
|
|
|
204
212
|
* Node-local docs follow the same GATE rule as every other store — gate
|
|
205
213
|
* evaluation is the only thing that removes a doc here. Rung is different:
|
|
206
214
|
* node-local's contract is "node-local rides into knowledge" unconditionally,
|
|
207
|
-
* so a
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
215
|
+
* so a doc with no boot entry is retained by this function — it is the caller
|
|
216
|
+
* (renderKnowledgeForSubject) that floors a bootless node-local doc up to
|
|
217
|
+
* `name` so it still shows its bare name instead of collapsing into a
|
|
218
|
+
* `[+N more]` count. */
|
|
211
219
|
function nodeLocalDocs(nodeId, subject) {
|
|
212
220
|
const dir = memoryDir(nodeId);
|
|
213
221
|
if (!pathExists(dir))
|
|
@@ -246,7 +254,6 @@ function newDir(path) {
|
|
|
246
254
|
path,
|
|
247
255
|
children: new Map(),
|
|
248
256
|
leaves: [],
|
|
249
|
-
index: null,
|
|
250
257
|
hiddenHere: 0,
|
|
251
258
|
renders: false,
|
|
252
259
|
ownCount: 0,
|
|
@@ -281,9 +288,9 @@ function isVisible(rung) {
|
|
|
281
288
|
return rungRank(rung) >= rungRank('name');
|
|
282
289
|
}
|
|
283
290
|
/** Bottom-up pass: decide which dirs render and where hidden counts land. A dir
|
|
284
|
-
* renders when it has ≥1 visible leaf,
|
|
285
|
-
*
|
|
286
|
-
*
|
|
291
|
+
* renders when it has ≥1 visible leaf, ≥1 rendering child dir, or it is
|
|
292
|
+
* top-level (a direct child of the root always renders so its subtree count
|
|
293
|
+
* is visible — the root itself always renders). Bootless docs in a
|
|
287
294
|
* NON-rendering dir bubble up to the nearest rendering ancestor's `[+N more]`;
|
|
288
295
|
* a rendering dir keeps its own subtree count. Returns the count to bubble up. */
|
|
289
296
|
function resolveDir(dir, isTopLevel) {
|
|
@@ -292,10 +299,8 @@ function resolveDir(dir, isTopLevel) {
|
|
|
292
299
|
totalHidden += resolveDir(child, dir.path === '');
|
|
293
300
|
}
|
|
294
301
|
const hasVisibleLeaf = dir.leaves.length > 0;
|
|
295
|
-
const hasVisibleIndex = dir.index !== null && isVisible(dir.index.systemPromptVisibility);
|
|
296
302
|
const anyChildRenders = [...dir.children.values()].some((c) => c.renders);
|
|
297
|
-
dir.renders =
|
|
298
|
-
hasVisibleLeaf || hasVisibleIndex || anyChildRenders || isTopLevel || dir.path === '';
|
|
303
|
+
dir.renders = hasVisibleLeaf || anyChildRenders || isTopLevel || dir.path === '';
|
|
299
304
|
dir.ownCount = dir.renders ? totalHidden : 0;
|
|
300
305
|
return dir.renders ? 0 : totalHidden;
|
|
301
306
|
}
|
|
@@ -306,9 +311,17 @@ function itemSortKey(item) {
|
|
|
306
311
|
return leafSegment(item.doc.name);
|
|
307
312
|
return '';
|
|
308
313
|
}
|
|
309
|
-
/**
|
|
314
|
+
/** DISPLAY-ORDER PIN ONLY: a leaf whose last name segment is `INDEX` sorts
|
|
315
|
+
* first among its directory's items. `INDEX` carries no mechanism anywhere in
|
|
316
|
+
* the substrate anymore — these are ordinary docs — but the ex-router docs
|
|
317
|
+
* keeping that name are each their dir's orientation entry, and localeCompare
|
|
318
|
+
* would drop them mid-list. */
|
|
319
|
+
function isIndexPinned(item) {
|
|
320
|
+
return item.kind === 'leaf' && leafSegment(item.doc.name) === 'INDEX';
|
|
321
|
+
}
|
|
322
|
+
/** Render one canonical logical doc entry at its boot rung. */
|
|
310
323
|
function renderDocEntry(doc, label, entryPrefix, childPrefix, lines) {
|
|
311
|
-
switch (doc.
|
|
324
|
+
switch (doc.bootRung) {
|
|
312
325
|
case 'preview': {
|
|
313
326
|
const pl = previewLine(doc);
|
|
314
327
|
lines.push(pl === '' ? `${entryPrefix}${label}` : `${entryPrefix}${label} # read when: ${pl}`);
|
|
@@ -329,23 +342,19 @@ function renderDocEntry(doc, label, entryPrefix, childPrefix, lines) {
|
|
|
329
342
|
}
|
|
330
343
|
}
|
|
331
344
|
/** Render the children of a rendering dir, with `childPrefix` carrying the tree
|
|
332
|
-
* guides for this depth. Order:
|
|
333
|
-
* child dirs and leaves intermixed alphabetically, then the `[+N more]`
|
|
334
|
-
* last. */
|
|
345
|
+
* guides for this depth. Order: an `INDEX`-named leaf first (display pin),
|
|
346
|
+
* then child dirs and leaves intermixed alphabetically, then the `[+N more]`
|
|
347
|
+
* count last. */
|
|
335
348
|
function renderChildren(dir, childPrefix, lines) {
|
|
336
|
-
const
|
|
349
|
+
const ordered = [];
|
|
337
350
|
for (const child of dir.children.values()) {
|
|
338
351
|
if (child.renders)
|
|
339
|
-
|
|
352
|
+
ordered.push({ kind: 'dir', node: child });
|
|
340
353
|
}
|
|
341
354
|
for (const leaf of dir.leaves)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (dir.index !== null && isVisible(dir.index.systemPromptVisibility)) {
|
|
346
|
-
ordered.push({ kind: 'index', doc: dir.index });
|
|
347
|
-
}
|
|
348
|
-
ordered.push(...middle);
|
|
355
|
+
ordered.push({ kind: 'leaf', doc: leaf });
|
|
356
|
+
ordered.sort((a, b) => Number(isIndexPinned(b)) - Number(isIndexPinned(a)) ||
|
|
357
|
+
itemSortKey(a).localeCompare(itemSortKey(b)));
|
|
349
358
|
if (dir.ownCount > 0)
|
|
350
359
|
ordered.push({ kind: 'more', count: dir.ownCount });
|
|
351
360
|
ordered.forEach((item, i) => {
|
|
@@ -360,7 +369,6 @@ function renderChildren(dir, childPrefix, lines) {
|
|
|
360
369
|
lines.push(`${entryPrefix}${item.node.segment}/`);
|
|
361
370
|
renderChildren(item.node, nextPrefix, lines);
|
|
362
371
|
break;
|
|
363
|
-
case 'index':
|
|
364
372
|
case 'leaf':
|
|
365
373
|
renderDocEntry(item.doc, leafSegment(item.doc.name), entryPrefix, nextPrefix, lines);
|
|
366
374
|
break;
|
|
@@ -385,18 +393,11 @@ function buildTree(docs, rootLabel) {
|
|
|
385
393
|
if (seen.has(d.name))
|
|
386
394
|
continue; // first-wins cross-scope dedup
|
|
387
395
|
seen.add(d.name);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
else {
|
|
394
|
-
const parent = ensureDir(root, parentDirOf(d.name));
|
|
395
|
-
if (isVisible(d.systemPromptVisibility))
|
|
396
|
-
parent.leaves.push(d);
|
|
397
|
-
else
|
|
398
|
-
parent.hiddenHere += 1;
|
|
399
|
-
}
|
|
396
|
+
const parent = ensureDir(root, parentDirOf(d.name));
|
|
397
|
+
if (isVisible(d.bootRung))
|
|
398
|
+
parent.leaves.push(d);
|
|
399
|
+
else
|
|
400
|
+
parent.hiddenHere += 1;
|
|
400
401
|
}
|
|
401
402
|
resolveDir(root, false);
|
|
402
403
|
const lines = [rootLabel];
|
|
@@ -416,7 +417,9 @@ const KNOWLEDGE_INTRO = 'Knowledge documents are what you consult — playbooks
|
|
|
416
417
|
'references on the user, projects, and this node. They are aggregated from your memory stores: ' +
|
|
417
418
|
'user-global (`~/.crouter/memory/`), every ancestor project store (`<dir>/.crouter/memory/`) ' +
|
|
418
419
|
"from the cwd upward, the selected profile's memory store (when one is selected), and node-local " +
|
|
419
|
-
"(this node's `context/memory/`). To read one, run `crtr memory read <name
|
|
420
|
+
"(this node's `context/memory/`). To read one, run `crtr memory read <name>`; a directory " +
|
|
421
|
+
'name is a valid read target too — it answers with its listing, and walking directories is ' +
|
|
422
|
+
'the sanctioned way to browse. ' +
|
|
420
423
|
'Each doc exists to prevent a specific mistake you would make without it, so when your task ' +
|
|
421
424
|
'matches one — by its name or its `# read when:` line — read it before you act; consulting it ' +
|
|
422
425
|
'only after the work is done forfeits the entire reason it exists. ' +
|
|
@@ -485,12 +488,13 @@ function buildSubPersonaMenu(kind) {
|
|
|
485
488
|
* preview/name catalog tree. ALWAYS returns a non-empty `<memory
|
|
486
489
|
* kind="preference">` block — the guidance frame is unconditional, so the
|
|
487
490
|
* system-prompt splice is never empty even when no preference is eligible. */
|
|
488
|
-
function buildPreferencesBlock(subject) {
|
|
491
|
+
function buildPreferencesBlock(subject, seen) {
|
|
489
492
|
let body = MEMORY_USAGE_GUIDANCE;
|
|
490
493
|
let contentDocs = [];
|
|
491
494
|
let tree = '';
|
|
492
495
|
if (subject !== null) {
|
|
493
496
|
const winners = selectWinners(subject, 'preference');
|
|
497
|
+
seedBootDelivery(seen, winners);
|
|
494
498
|
const grouped = renderGrouped(winners, 'preferences');
|
|
495
499
|
contentDocs = grouped.contentDocs;
|
|
496
500
|
tree = grouped.tree;
|
|
@@ -508,8 +512,8 @@ function buildPreferencesBlock(subject) {
|
|
|
508
512
|
}
|
|
509
513
|
return { block: `<memory kind="preference">\n${body}\n</memory>`, contentDocs, tree };
|
|
510
514
|
}
|
|
511
|
-
export function renderPreferencesForSubject(subject) {
|
|
512
|
-
return buildPreferencesBlock(subject);
|
|
515
|
+
export function renderPreferencesForSubject(subject, seen) {
|
|
516
|
+
return buildPreferencesBlock(subject, seen);
|
|
513
517
|
}
|
|
514
518
|
// ---------------------------------------------------------------------------
|
|
515
519
|
// 2. Knowledge — `<memory kind="knowledge">` (the first-message bearings).
|
|
@@ -521,18 +525,22 @@ export function renderPreferencesForSubject(subject) {
|
|
|
521
525
|
* first-wins dedup over a same-named node-local doc. GROUPED by rung exactly
|
|
522
526
|
* like the preference block: content prose first (general→specific,
|
|
523
527
|
* node-local last), then the preview/name catalog tree. Node-local docs are
|
|
524
|
-
* floored HERE — a
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
export function renderKnowledgeForSubject(subject, nodeId) {
|
|
531
|
-
const nodeLocal = nodeLocalDocs(nodeId, subject).map((d) =>
|
|
528
|
+
* floored HERE — a bootless node-local doc's rung is bumped to `name` before
|
|
529
|
+
* grouping, so it still surfaces as a bare-name tree entry rather than
|
|
530
|
+
* collapsing into a `[+N more]` count (its body still never renders: floored
|
|
531
|
+
* docs never reach the `content` rung). Procedural guidance and factual
|
|
532
|
+
* references both live here as `knowledge`. Returns '' when nothing is
|
|
533
|
+
* eligible. */
|
|
534
|
+
export function renderKnowledgeForSubject(subject, nodeId, seen) {
|
|
535
|
+
const nodeLocal = nodeLocalDocs(nodeId, subject).map((d) => {
|
|
536
|
+
const r = bootRung(d);
|
|
537
|
+
return { ...d, bootRung: isVisible(r) ? r : 'name' };
|
|
538
|
+
});
|
|
532
539
|
const nodeLocalSet = new Set(nodeLocal);
|
|
533
540
|
// Resolver winners first: first-wins dedup keeps the resolver's copy over a
|
|
534
541
|
// same-named node-local doc, matching the original precedence.
|
|
535
542
|
const winners = dedupeFirstWins([...selectWinners(subject, 'knowledge'), ...nodeLocal]);
|
|
543
|
+
seedBootDelivery(seen, winners);
|
|
536
544
|
const { contentProse, tree } = renderGrouped(winners, 'knowledge', nodeLocalSet);
|
|
537
545
|
if (contentProse === '' && tree === '')
|
|
538
546
|
return '';
|
|
@@ -7,10 +7,8 @@ export declare const RUNGS: readonly ["none", "name", "preview", "content"];
|
|
|
7
7
|
export type Rung = (typeof RUNGS)[number];
|
|
8
8
|
/** Ordinal of a rung on the ladder (none=0 … content=3). */
|
|
9
9
|
export declare function rungRank(r: Rung): number;
|
|
10
|
-
/** Does rung `r` disclose at least as much as `min`?
|
|
11
|
-
* `rungAtLeast(doc.systemPromptVisibility, 'name')` ⇒ "shows at boot at all". */
|
|
10
|
+
/** Does rung `r` disclose at least as much as `min`? */
|
|
12
11
|
export declare function rungAtLeast(r: Rung, min: Rung): boolean;
|
|
13
|
-
export declare const FALLBACK_RUNG: Rung;
|
|
14
12
|
/** Strip an optional `NN-` ordering prefix from ONE path segment (file or
|
|
15
13
|
* directory display name). `00-runtime-base` -> `runtime-base`; `spine` (no
|
|
16
14
|
* prefix) is unchanged. */
|
|
@@ -24,6 +22,32 @@ export declare function normalizeDocName(name: string): string;
|
|
|
24
22
|
* `name` field wins (trimmed + normalized), else `fallbackName` (the
|
|
25
23
|
* normalized path-derived name). */
|
|
26
24
|
export declare function resolveDocName(fm: Record<string, unknown> | null | undefined, fallbackName: string): string;
|
|
25
|
+
export declare const SURFACE_EVENTS: readonly ["boot", "workspace-open", "read", "memory-read", "command"];
|
|
26
|
+
export type SurfaceEvent = (typeof SURFACE_EVENTS)[number];
|
|
27
|
+
/** The rungs an entry may deliver at. There is no `none` — silence is the
|
|
28
|
+
* absence of an entry. (`Rung`'s `none` survives only as the internal
|
|
29
|
+
* ranking floor, e.g. `bootRung` of a doc with no boot entry.) */
|
|
30
|
+
export declare const SURFACE_RUNGS: readonly ["name", "preview", "content"];
|
|
31
|
+
export type SurfaceRung = (typeof SURFACE_RUNGS)[number];
|
|
32
|
+
/** One routing entry: when event `on` exposes a subject that fits `match`
|
|
33
|
+
* (and `matchFrontmatter`, `read` only), the doc delivers at rung `at`.
|
|
34
|
+
* Constraints within one entry AND together (each present constraint must
|
|
35
|
+
* fit; a `match` list is satisfied by any one glob); multiple entries per
|
|
36
|
+
* event OR together. */
|
|
37
|
+
export interface SurfaceEntry {
|
|
38
|
+
on: SurfaceEvent;
|
|
39
|
+
/** Globs vs the event's subject namespace. Required on read/memory-read/
|
|
40
|
+
* command (a read entry may carry `matchFrontmatter` instead); meaningless
|
|
41
|
+
* on boot/workspace-open (presence of the entry is the match). */
|
|
42
|
+
match?: string[];
|
|
43
|
+
/** Predicate over the read file's own YAML frontmatter — `read` event only.
|
|
44
|
+
* Frontmatter key `match-frontmatter`. */
|
|
45
|
+
matchFrontmatter?: GatePredicate;
|
|
46
|
+
at: SurfaceRung;
|
|
47
|
+
}
|
|
48
|
+
/** A doc's boot rung: the highest `at` over its `boot` entries, `none` when
|
|
49
|
+
* it carries no boot entry. The boot render's per-doc selection input. */
|
|
50
|
+
export declare function bootRung(doc: Pick<SubstrateSchema, 'surfaces'>): Rung;
|
|
27
51
|
/** A gate predicate tree, evaluated by predicate.ts (`evalCondition`) against
|
|
28
52
|
* the node-config subject. Typed loosely on purpose — the matcher engine owns
|
|
29
53
|
* validation; structurally it is a field→matcher map with optional
|
|
@@ -46,25 +70,17 @@ export interface SubstrateSchema {
|
|
|
46
70
|
/** Human-facing abbreviation for `crtr memory list`. NEVER loaded into an
|
|
47
71
|
* agent's context (design §3). Empty string when absent. */
|
|
48
72
|
shortForm: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
/** Suppress this doc from directory listings (default false). Suppression,
|
|
74
|
+
* not a type — the doc is otherwise ordinary. */
|
|
75
|
+
unlisted: boolean;
|
|
76
|
+
/** Explicit event routing. Empty = no explicit routing (listings still
|
|
77
|
+
* apply, unless `unlisted`). Invalid entries are dropped by the tolerant
|
|
78
|
+
* runtime parser; lint enforces strictly. */
|
|
79
|
+
surfaces: SurfaceEntry[];
|
|
53
80
|
/** Optional eligibility predicate over the node's own config. Absent ⇒ always
|
|
54
81
|
* eligible. An empty `{}` is carried as-is and is inert (never matches) — see
|
|
55
82
|
* `gatePasses`. */
|
|
56
83
|
gate?: GatePredicate;
|
|
57
|
-
/** Explicit file-context routes. `.` is the project workspace-open target;
|
|
58
|
-
* every other value is a glob matched after an actual file read. A single
|
|
59
|
-
* value is normalized to a 1-list. */
|
|
60
|
-
appliesTo?: string[];
|
|
61
|
-
/** Optional condition over the READ FILE's own frontmatter — the on-read
|
|
62
|
-
* frontmatter trigger (Stream A native rules). Same coercion + predicate
|
|
63
|
-
* vocabulary as `gate` (non-null non-array object carried; empty `{}` inert),
|
|
64
|
-
* but evaluated by `evalCondition` against the read file's parsed YAML rather
|
|
65
|
-
* than the node subject. Absent ⇒ no frontmatter trigger. Frontmatter key
|
|
66
|
-
* `read-when`. */
|
|
67
|
-
readWhen?: GatePredicate;
|
|
68
84
|
/** Opt-in: this doc is invocable as a pi slash command (`/<name>`, `/` in a
|
|
69
85
|
* nested name rendered as `:`). Default `false` — most docs are consulted,
|
|
70
86
|
* not invoked. Frontmatter key `slash`. */
|
|
@@ -4,13 +4,9 @@
|
|
|
4
4
|
// downstream track (CLI verbs, boot render, on-read render, migrator) builds
|
|
5
5
|
// against — pure and side-effect free. See design-substrate.md §4 (schema).
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// runtime parser is tolerant by contract (it maps over many docs and must
|
|
11
|
-
// never throw), so a doc missing/with an invalid rung falls back to the neutral
|
|
12
|
-
// floor `none` — a malformed doc renders invisible rather than crashing, and
|
|
13
|
-
// lint flags it. Valid docs never hit the fallback.
|
|
7
|
+
// The runtime parser is tolerant by contract (it maps over many docs and must
|
|
8
|
+
// never throw): invalid `surfaces` entries are dropped rather than crashing a
|
|
9
|
+
// render, and `crtr memory lint` owns strict authoring-time enforcement.
|
|
14
10
|
// ---------------------------------------------------------------------------
|
|
15
11
|
// Kinds — the two semantic kinds (design §3): `knowledge` (consult — procedural
|
|
16
12
|
// playbooks + factual references merged) vs `preference` (behave — standing
|
|
@@ -31,19 +27,11 @@ export const RUNGS = ['none', 'name', 'preview', 'content'];
|
|
|
31
27
|
export function rungRank(r) {
|
|
32
28
|
return RUNGS.indexOf(r);
|
|
33
29
|
}
|
|
34
|
-
/** Does rung `r` disclose at least as much as `min`?
|
|
35
|
-
* `rungAtLeast(doc.systemPromptVisibility, 'name')` ⇒ "shows at boot at all". */
|
|
30
|
+
/** Does rung `r` disclose at least as much as `min`? */
|
|
36
31
|
export function rungAtLeast(r, min) {
|
|
37
32
|
return rungRank(r) >= rungRank(min);
|
|
38
33
|
}
|
|
39
34
|
// ---------------------------------------------------------------------------
|
|
40
|
-
// The neutral fallback rung used only when a doc omits a visibility field or
|
|
41
|
-
// carries an invalid one. NOT a default an author may lean on — authoring-time
|
|
42
|
-
// enforcement requires explicit rungs; this is purely the runtime parser's
|
|
43
|
-
// never-throw floor (a malformed doc renders invisible, and lint flags it).
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
export const FALLBACK_RUNG = 'none';
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
35
|
// Display-name normalization — the optional `NN-` ordering pin. A doc's
|
|
48
36
|
// physical path may carry a two-digit numeric prefix on a file or directory
|
|
49
37
|
// segment (`00-runtime-base.md`, `01-spine/`) purely to pin structural order
|
|
@@ -86,6 +74,31 @@ export function resolveDocName(fm, fallbackName) {
|
|
|
86
74
|
return explicit !== '' ? explicit : fallbackName;
|
|
87
75
|
}
|
|
88
76
|
// ---------------------------------------------------------------------------
|
|
77
|
+
// Surfaces — explicit event routing (design: surfaces-design.md). A doc
|
|
78
|
+
// declares WHEN it delivers as (event, match, rung) entries; matches are
|
|
79
|
+
// always globs, always absolute in the event's namespace, with `./` as the
|
|
80
|
+
// one relative spelling (anchoring to the doc's own container). A doc with
|
|
81
|
+
// no `surfaces` at all does exactly one thing: appears in its directory's
|
|
82
|
+
// listing.
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
export const SURFACE_EVENTS = ['boot', 'workspace-open', 'read', 'memory-read', 'command'];
|
|
85
|
+
/** The rungs an entry may deliver at. There is no `none` — silence is the
|
|
86
|
+
* absence of an entry. (`Rung`'s `none` survives only as the internal
|
|
87
|
+
* ranking floor, e.g. `bootRung` of a doc with no boot entry.) */
|
|
88
|
+
export const SURFACE_RUNGS = ['name', 'preview', 'content'];
|
|
89
|
+
/** A doc's boot rung: the highest `at` over its `boot` entries, `none` when
|
|
90
|
+
* it carries no boot entry. The boot render's per-doc selection input. */
|
|
91
|
+
export function bootRung(doc) {
|
|
92
|
+
let r = 'none';
|
|
93
|
+
for (const e of doc.surfaces) {
|
|
94
|
+
if (e.on !== 'boot')
|
|
95
|
+
continue;
|
|
96
|
+
if (rungRank(e.at) > rungRank(r))
|
|
97
|
+
r = e.at;
|
|
98
|
+
}
|
|
99
|
+
return r;
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
89
102
|
// Parse / validate.
|
|
90
103
|
// ---------------------------------------------------------------------------
|
|
91
104
|
/** Parse a raw frontmatter record (from `parseFrontmatterGeneric`, via the
|
|
@@ -106,11 +119,9 @@ export function parseSubstrateFrontmatter(fm) {
|
|
|
106
119
|
kind,
|
|
107
120
|
whenAndWhyToRead: strField(fm['when-and-why-to-read']),
|
|
108
121
|
shortForm: strField(fm['short-form']),
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
unlisted: fm['unlisted'] === true,
|
|
123
|
+
surfaces: parseSurfaces(fm['surfaces']),
|
|
111
124
|
gate: parseGate(fm.gate),
|
|
112
|
-
appliesTo: parseAppliesTo(fm['applies-to']),
|
|
113
|
-
readWhen: parseGate(fm['read-when']),
|
|
114
125
|
slash: fm.slash === true,
|
|
115
126
|
rationale: parseRationale(fm['rationale']),
|
|
116
127
|
};
|
|
@@ -161,13 +172,6 @@ function strField(v) {
|
|
|
161
172
|
function parseRationale(v) {
|
|
162
173
|
return typeof v === 'string' && v.trim() !== '' ? v : undefined;
|
|
163
174
|
}
|
|
164
|
-
/** Resolve a visibility field to a ladder rung, falling back to the neutral
|
|
165
|
-
* floor when absent/invalid. */
|
|
166
|
-
function parseRung(v, fallback) {
|
|
167
|
-
return typeof v === 'string' && RUNGS.includes(v)
|
|
168
|
-
? v
|
|
169
|
-
: fallback;
|
|
170
|
-
}
|
|
171
175
|
/** A gate is engaged only when frontmatter carries a non-null, non-array object
|
|
172
176
|
* (the predicate vocabulary's field→matcher map). Anything else (absent, null,
|
|
173
177
|
* scalar, array) ⇒ no gate ⇒ always eligible — the safe default that never
|
|
@@ -177,14 +181,53 @@ function parseGate(v) {
|
|
|
177
181
|
? v
|
|
178
182
|
: undefined;
|
|
179
183
|
}
|
|
180
|
-
/**
|
|
181
|
-
*
|
|
182
|
-
|
|
184
|
+
/** Tolerant `surfaces` parse: a non-array yields `[]`; invalid entries are
|
|
185
|
+
* dropped (bad `on`, bad `at`, a read/memory-read/command entry left with
|
|
186
|
+
* neither `match` nor — read only — `match-frontmatter`). `match-frontmatter`
|
|
187
|
+
* on a non-read event is dropped from the entry, which survives iff it still
|
|
188
|
+
* carries a `match`. Lint owns strict enforcement; the runtime parser maps
|
|
189
|
+
* over many docs and must never throw. */
|
|
190
|
+
function parseSurfaces(v) {
|
|
191
|
+
if (!Array.isArray(v))
|
|
192
|
+
return [];
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const raw of v) {
|
|
195
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
196
|
+
continue;
|
|
197
|
+
const rec = raw;
|
|
198
|
+
const on = rec['on'];
|
|
199
|
+
if (typeof on !== 'string' || !SURFACE_EVENTS.includes(on))
|
|
200
|
+
continue;
|
|
201
|
+
const at = rec['at'];
|
|
202
|
+
if (typeof at !== 'string' || !SURFACE_RUNGS.includes(at))
|
|
203
|
+
continue;
|
|
204
|
+
const match = parseMatchGlobs(rec['match']);
|
|
205
|
+
const matchFrontmatter = on === 'read' ? parseGate(rec['match-frontmatter']) : undefined;
|
|
206
|
+
if ((on === 'read' || on === 'memory-read' || on === 'command') &&
|
|
207
|
+
match === undefined &&
|
|
208
|
+
matchFrontmatter === undefined) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
out.push({
|
|
212
|
+
on: on,
|
|
213
|
+
at: at,
|
|
214
|
+
...(match !== undefined ? { match } : {}),
|
|
215
|
+
...(matchFrontmatter !== undefined ? { matchFrontmatter } : {}),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
/** Normalize an entry's `match` to a non-empty glob list, or undefined.
|
|
221
|
+
* Accepts a string or an array of strings; blanks are dropped. */
|
|
222
|
+
function parseMatchGlobs(v) {
|
|
183
223
|
if (typeof v === 'string') {
|
|
184
|
-
|
|
224
|
+
const t = v.trim();
|
|
225
|
+
return t === '' ? undefined : [t];
|
|
185
226
|
}
|
|
186
227
|
if (Array.isArray(v)) {
|
|
187
|
-
const globs = v
|
|
228
|
+
const globs = v
|
|
229
|
+
.filter((g) => typeof g === 'string' && g.trim() !== '')
|
|
230
|
+
.map((g) => g.trim());
|
|
188
231
|
return globs.length > 0 ? globs : undefined;
|
|
189
232
|
}
|
|
190
233
|
return undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Rung, type SubstrateDoc, type SurfaceEntry } from './schema.js';
|
|
2
|
+
/** The project directory owning a doc's store: the path segment stack above
|
|
3
|
+
* the doc's `.crouter` dir. `null` when the doc's path carries no `.crouter`
|
|
4
|
+
* segment (builtin docs). */
|
|
5
|
+
export declare function owningRootOf(doc: Pick<SubstrateDoc, 'path'>): string | null;
|
|
6
|
+
/** True when the (already-realpathed) file sits beneath the doc's store's
|
|
7
|
+
* owning directory. Callers additionally guard on `doc.scope === 'project'`:
|
|
8
|
+
* a profile store's path computes an owning root of `~`, and user/builtin/
|
|
9
|
+
* node docs likewise have no project-owning dir, so a `./`-anchored glob
|
|
10
|
+
* outside a project store must never match. */
|
|
11
|
+
export declare function underOwningRoot(doc: Pick<SubstrateDoc, 'path'>, absFile: string): boolean;
|
|
12
|
+
/** Does a `read` entry fit the read file? `absReadFile` must already be
|
|
13
|
+
* realpathed. `readFrontmatter` is the read file's own parsed YAML (empty
|
|
14
|
+
* for non-markdown files); an empty record never satisfies
|
|
15
|
+
* `matchFrontmatter`. */
|
|
16
|
+
export declare function matchesReadEntry(entry: SurfaceEntry, doc: Pick<SubstrateDoc, 'path' | 'scope'>, absReadFile: string, readFrontmatter: Record<string, unknown>): boolean;
|
|
17
|
+
/** Does a `memory-read` entry on the doc named `carrierName` fit a resolved
|
|
18
|
+
* read of a doc known by `targetNames`? */
|
|
19
|
+
export declare function matchesMemoryReadEntry(entry: SurfaceEntry, carrierName: string, targetNames: string[]): boolean;
|
|
20
|
+
/** Does a `command` entry fit the executed command string? */
|
|
21
|
+
export declare function matchesCommandEntry(entry: SurfaceEntry, command: string): boolean;
|
|
22
|
+
/** The doc's delivery rung for a read of `absReadFile` (already realpathed). */
|
|
23
|
+
export declare function readDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces' | 'path' | 'scope'>, absReadFile: string, readFrontmatter: Record<string, unknown>): Rung;
|
|
24
|
+
/** The doc's workspace-open rung. Presence of an entry is the match; callers
|
|
25
|
+
* scope the corpus to project stores mounted by the subject's cwd/profile. */
|
|
26
|
+
export declare function workspaceOpenRung(doc: Pick<SubstrateDoc, 'surfaces'>): Rung;
|
|
27
|
+
/** The doc's delivery rung for a `crtr memory read` that resolved a doc
|
|
28
|
+
* known by `targetNames` (canonical name, plus the mount-stripped name for
|
|
29
|
+
* plugin-mounted docs). The doc's own `name` anchors `./` globs. */
|
|
30
|
+
export declare function memoryReadDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces' | 'name'>, targetNames: string[]): Rung;
|
|
31
|
+
/** The doc's delivery rung for an executed command string. */
|
|
32
|
+
export declare function commandDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces'>, command: string): Rung;
|