@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { appendFileSync, renameSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { defineLeaf } from '../../core/command.js';
|
|
4
|
+
import { CrtrError, notFound, usage } from '../../core/errors.js';
|
|
5
|
+
import { ensureDir, pathExists, readText, realpathOrSelf, writeText } from '../../core/fs-utils.js';
|
|
6
|
+
import { listAllMemoryDocs, resolveMemoryDoc } from '../../core/memory-resolver.js';
|
|
7
|
+
import { findDocLinks, isDocLinkName } from '../../core/memory/doc-link-grammar.js';
|
|
8
|
+
import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor } from '../../core/memory/history.js';
|
|
9
|
+
import { docsByName, isDirName } from '../../core/substrate/listings.js';
|
|
10
|
+
import { normalizeDocName } from '../../core/substrate/schema.js';
|
|
11
|
+
import { MEMORY_SCOPES, literalBodySegment, memoryFilePath, pruneEmptyParents } from './shared.js';
|
|
12
|
+
/** Replace every `[[oldName]]` doc link in a full document source with
|
|
13
|
+
* `[[newName]]`, splicing only within the literal body segment so the
|
|
14
|
+
* frontmatter block is never touched. Link spans come from the shared
|
|
15
|
+
* doc-link grammar (code blocks and inline code excluded), applied back to
|
|
16
|
+
* front so earlier offsets stay valid. */
|
|
17
|
+
function rewriteRefs(source, oldName, newName) {
|
|
18
|
+
const body = literalBodySegment(source);
|
|
19
|
+
const prefixLen = source.length - body.length;
|
|
20
|
+
const links = findDocLinks(body).filter((l) => l.name === oldName);
|
|
21
|
+
let text = source;
|
|
22
|
+
for (let i = links.length - 1; i >= 0; i -= 1) {
|
|
23
|
+
const l = links[i];
|
|
24
|
+
text = text.slice(0, prefixLen + l.start) + `[[${newName}]]` + text.slice(prefixLen + l.end);
|
|
25
|
+
}
|
|
26
|
+
return { text, count: links.length };
|
|
27
|
+
}
|
|
28
|
+
export const moveLeaf = defineLeaf({
|
|
29
|
+
name: 'move',
|
|
30
|
+
description: 'rename or relocate a memory document, rewriting inbound links',
|
|
31
|
+
whenToUse: 'a stored document should answer to a different name — renamed in place or relocated under another directory. The sanctioned way to move one, so you never mv the markdown off disk: this verb carries the document’s revision history along and rewrites every inbound `[[ref]]` across writable stores, so authored links break loudly or not at all. Moves stay within the document’s own store; one document per move — a directory never moves as a unit, so relocate its members individually.',
|
|
32
|
+
help: {
|
|
33
|
+
name: 'memory move',
|
|
34
|
+
summary: 'move memory/<name>.md to memory/<new-name>.md in its store, rewriting inbound [[refs]] corpus-wide',
|
|
35
|
+
params: [
|
|
36
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`), resolved as `read` resolves it: exact identity/direct path by precedence node > project stack > profile > user, then bare leaf fallback. Builtin and installed-plugin docs are read-only and refuse to move. A directory name is rejected — move member docs individually.' },
|
|
37
|
+
{ kind: 'flag', name: 'to', type: 'string', required: true, constraint: 'The new canonical name, in the same store: `/`-joined segments of [A-Za-z0-9_-], no `NN-` ordering prefix (identity is prefix-stripped, so a prefixed name would not answer to itself). Must not already answer to any document in the corpus.' },
|
|
38
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Restrict resolution to this scope before moving. Use it to disambiguate a name present at multiple scopes. builtin is not a choice — builtin docs ship with the package and cannot move.' },
|
|
39
|
+
],
|
|
40
|
+
output: [
|
|
41
|
+
{ name: 'name', type: 'string', required: true, constraint: 'The document’s new canonical name.' },
|
|
42
|
+
{ name: 'previous_name', type: 'string', required: true, constraint: 'The name it answered to before the move.' },
|
|
43
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope of the store the document moved within: node, user, project, or profile.' },
|
|
44
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path of the document at its new location.' },
|
|
45
|
+
{ name: 'moved', type: 'boolean', required: true, constraint: 'Always true on success — the file and its revision log now live at the new name.' },
|
|
46
|
+
{ name: 'log_path', type: 'string', required: true, constraint: 'Absolute path to the revision log at its new location, ending in a `move` record that names the previous canonical name.' },
|
|
47
|
+
{ name: 'refs_rewritten', type: 'object[]', required: true, constraint: 'Documents whose bodies were rewritten from `[[<previous_name>]]` to `[[<name>]]`. Each: {name, scope, path, refs}. Empty when nothing linked to the document — or when the moved document was shadowed by a same-named doc at a nearer scope, in which case inbound refs keep resolving to that survivor and are left alone.' },
|
|
48
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read it back at the new name, verify the corpus with lint.' },
|
|
49
|
+
],
|
|
50
|
+
outputKind: 'object',
|
|
51
|
+
effects: [
|
|
52
|
+
'Moves memory/<name>.md to memory/<new-name>.md within its store (creating parent directories, pruning any it leaves empty) and moves memory/.history/<name>.jsonl with it, appending a `move` record naming the previous canonical name.',
|
|
53
|
+
'Rewrites every inbound `[[<name>]]` doc link across writable stores to `[[<new-name>]]`, appending an `edit` record to each rewritten document’s revision log. Aborts before any write when a read-only (builtin or installed-plugin) document carries such a link.',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
run: async (input) => {
|
|
57
|
+
const nameRaw = input['name'];
|
|
58
|
+
const newName = (input['to'] ?? '').trim();
|
|
59
|
+
const scopeArg = input['scope'];
|
|
60
|
+
let doc;
|
|
61
|
+
try {
|
|
62
|
+
doc = resolveMemoryDoc(nameRaw, { includeDescendants: true, ...(scopeArg !== undefined ? { scope: scopeArg } : {}) });
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (e instanceof CrtrError && e.code === 'not_found') {
|
|
66
|
+
const dirName = normalizeDocName(nameRaw.replace(/\/+$/, ''));
|
|
67
|
+
if (dirName !== '' && isDirName(docsByName(listAllMemoryDocs(undefined, true, true)), dirName)) {
|
|
68
|
+
throw usage(`${dirName} is a directory, and a directory never moves as a unit — move its member docs individually.`, {
|
|
69
|
+
memory: dirName,
|
|
70
|
+
next: `Run \`crtr memory read ${dirName}\` to list the members, then \`crtr memory move <member> --to <new-name>\` per doc.`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
throw notFound(`memory document not found: ${nameRaw}`, {
|
|
74
|
+
memory: nameRaw,
|
|
75
|
+
next: 'Run `crtr memory find <query>` to locate it, or `crtr memory list` to browse the inventory. If it exists at another scope, pass --scope.',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
throw e; // ambiguous / usage propagate with their own recovery guidance
|
|
79
|
+
}
|
|
80
|
+
if (doc.plugin !== undefined) {
|
|
81
|
+
throw usage(`${doc.name} belongs to the installed plugin "${doc.plugin}" — it is not a scope doc and cannot be moved here. Manage plugin docs with \`crtr pkg\`.`, { memory: doc.name, plugin: doc.plugin });
|
|
82
|
+
}
|
|
83
|
+
if (doc.scope === 'builtin') {
|
|
84
|
+
throw usage(`${doc.name} is a builtin document shipped with the package (read-only) — it cannot be moved. Override it with a same-named doc at a writable scope instead (\`crtr memory write ${doc.name} ...\`).`, { memory: doc.name, scope: 'builtin' });
|
|
85
|
+
}
|
|
86
|
+
// An explicit frontmatter `name` pins identity independent of the file's
|
|
87
|
+
// location, so a physical move would change nothing a reader can see.
|
|
88
|
+
const explicitName = doc.frontmatter?.['name'];
|
|
89
|
+
if (typeof explicitName === 'string' && explicitName.trim() !== '') {
|
|
90
|
+
throw usage(`${doc.name} pins its identity with an explicit \`name\` frontmatter field, so its name is not path-derived and a file move would not rename it. Edit the field with \`crtr memory edit ${doc.name}\` instead.`, { memory: doc.name, path: doc.path });
|
|
91
|
+
}
|
|
92
|
+
if (newName === '')
|
|
93
|
+
throw usage('--to requires the new canonical name');
|
|
94
|
+
if (!isDocLinkName(newName)) {
|
|
95
|
+
throw usage(`invalid new name: ${newName} — a canonical name is one or more [A-Za-z0-9_-] segments joined by \`/\``);
|
|
96
|
+
}
|
|
97
|
+
// Identity is derived from the path with `NN-` ordering prefixes stripped,
|
|
98
|
+
// so a prefixed target name would derive a different identity than asked.
|
|
99
|
+
if (normalizeDocName(newName) !== newName) {
|
|
100
|
+
throw usage(`invalid new name: ${newName} — a \`NN-\` ordering prefix is stripped from derived identity, so this doc would answer to ${normalizeDocName(newName)} instead. Name it that directly.`);
|
|
101
|
+
}
|
|
102
|
+
if (newName === doc.name)
|
|
103
|
+
throw usage(`${doc.name} already answers to that name — nothing to move`);
|
|
104
|
+
const newPath = memoryFilePath(doc.root, newName);
|
|
105
|
+
if (pathExists(newPath)) {
|
|
106
|
+
throw usage(`the target is occupied: ${newPath} already exists — pick a free name or delete the occupant first`, { memory: newName, path: newPath });
|
|
107
|
+
}
|
|
108
|
+
const corpus = listAllMemoryDocs(undefined, true, true);
|
|
109
|
+
const byName = docsByName(corpus);
|
|
110
|
+
const taken = byName.get(newName);
|
|
111
|
+
if (taken !== undefined) {
|
|
112
|
+
throw usage(`${newName} already answers to a document (${taken.scope} scope, ${taken.path}) — moving onto it would make every rewritten ref ambiguous. Pick a free name.`, { memory: newName, path: taken.path });
|
|
113
|
+
}
|
|
114
|
+
// Refs are rewritten only when this doc is what `[[<name>]]` actually
|
|
115
|
+
// resolves to. Moving a doc shadowed by a same-named nearer doc leaves
|
|
116
|
+
// every ref pointing at that surviving winner, so they are left alone.
|
|
117
|
+
const movedReal = realpathOrSelf(doc.path);
|
|
118
|
+
const winner = byName.get(doc.name);
|
|
119
|
+
const isWinner = winner !== undefined && realpathOrSelf(winner.path) === movedReal;
|
|
120
|
+
const referencing = [];
|
|
121
|
+
const blocked = [];
|
|
122
|
+
if (isWinner) {
|
|
123
|
+
const seenFiles = new Set([movedReal]);
|
|
124
|
+
for (const d of corpus) {
|
|
125
|
+
const real = realpathOrSelf(d.path);
|
|
126
|
+
if (seenFiles.has(real))
|
|
127
|
+
continue;
|
|
128
|
+
seenFiles.add(real);
|
|
129
|
+
if (!findDocLinks(d.body).some((l) => l.name === doc.name))
|
|
130
|
+
continue;
|
|
131
|
+
if (d.plugin !== undefined || d.scope === 'builtin')
|
|
132
|
+
blocked.push(d);
|
|
133
|
+
else
|
|
134
|
+
referencing.push(d);
|
|
135
|
+
}
|
|
136
|
+
if (blocked.length > 0) {
|
|
137
|
+
throw usage(`cannot move ${doc.name}: read-only docs link to it with [[${doc.name}]] and their refs cannot be rewritten — ` +
|
|
138
|
+
blocked.map((d) => `${d.plugin !== undefined ? `plugin ${d.plugin}` : d.scope}: ${d.name} (${d.path})`).join('; ') +
|
|
139
|
+
'. Nothing was changed.', { memory: doc.name });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// All checks passed — mutate. Inbound refs first, each an ordinary `edit`
|
|
143
|
+
// in that doc's own log; the mechanical splice touches only body bytes, so
|
|
144
|
+
// `last-updated` (recency of the knowledge itself) is deliberately not
|
|
145
|
+
// restamped.
|
|
146
|
+
const refsRewritten = [];
|
|
147
|
+
for (const d of referencing) {
|
|
148
|
+
const before = readText(d.path);
|
|
149
|
+
const { text: after, count } = rewriteRefs(before, doc.name, newName);
|
|
150
|
+
if (count === 0)
|
|
151
|
+
continue;
|
|
152
|
+
writeText(d.path, after);
|
|
153
|
+
appendHistoryRecord(historyLogPathFor(d.root, d.path), buildHistoryRecord({ op: 'edit', rationale: `\`crtr memory move\`: [[${doc.name}]] → [[${newName}]]`, before, after }));
|
|
154
|
+
refsRewritten.push({ name: d.name, scope: d.scope, path: d.path, refs: count });
|
|
155
|
+
}
|
|
156
|
+
// The doc itself: self-refs rewrite in the same motion as the move.
|
|
157
|
+
const before = readText(doc.path);
|
|
158
|
+
const after = isWinner ? rewriteRefs(before, doc.name, newName).text : before;
|
|
159
|
+
writeText(newPath, after);
|
|
160
|
+
rmSync(doc.path);
|
|
161
|
+
pruneEmptyParents(resolvePath(doc.path), doc.name.split('/').length);
|
|
162
|
+
// The revision log travels with the doc — history answers to the name the
|
|
163
|
+
// doc now answers to. A log already at the target (a prior, deleted life
|
|
164
|
+
// of that name) keeps its records; this doc's records append after them.
|
|
165
|
+
const oldLog = historyLogPathFor(doc.root, doc.path);
|
|
166
|
+
const newLog = historyLogPathFor(doc.root, newPath);
|
|
167
|
+
if (pathExists(oldLog)) {
|
|
168
|
+
ensureDir(dirname(newLog));
|
|
169
|
+
if (pathExists(newLog)) {
|
|
170
|
+
appendFileSync(newLog, readText(oldLog), 'utf8');
|
|
171
|
+
rmSync(oldLog);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
renameSync(oldLog, newLog);
|
|
175
|
+
}
|
|
176
|
+
pruneEmptyParents(resolvePath(oldLog), doc.name.split('/').length);
|
|
177
|
+
}
|
|
178
|
+
appendHistoryRecord(newLog, buildHistoryRecord({ op: 'move', from: doc.name, before, after }));
|
|
179
|
+
const refsNote = isWinner
|
|
180
|
+
? refsRewritten.length > 0
|
|
181
|
+
? ` Rewrote [[${doc.name}]] in ${refsRewritten.length} doc${refsRewritten.length === 1 ? '' : 's'}.`
|
|
182
|
+
: ' No inbound refs needed rewriting.'
|
|
183
|
+
: ` Inbound [[${doc.name}]] refs were left alone: a same-named doc at a nearer scope still answers to that name.`;
|
|
184
|
+
return {
|
|
185
|
+
name: newName,
|
|
186
|
+
previous_name: doc.name,
|
|
187
|
+
scope: doc.scope,
|
|
188
|
+
path: newPath,
|
|
189
|
+
moved: true,
|
|
190
|
+
log_path: newLog,
|
|
191
|
+
refs_rewritten: refsRewritten,
|
|
192
|
+
follow_up: `Moved.${refsNote} Read it back with \`crtr memory read ${newName}\`, and run \`crtr memory lint\` to verify no link dangles.`,
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
});
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { CrtrClient } from '../../api/index.js';
|
|
2
|
+
import { interpolateNodePaths } from '../../core/canvas/paths.js';
|
|
1
3
|
import { defineLeaf } from '../../core/command.js';
|
|
2
4
|
import { CrtrError, notFound } from '../../core/errors.js';
|
|
3
|
-
import {
|
|
4
|
-
import { effectiveDocKind, parseSubstrateDoc, previewLine } from '../../core/substrate/schema.js';
|
|
5
|
-
import { displayName, indexDirOf, isIndexName } from '../../core/substrate/ceiling.js';
|
|
6
|
-
import { interpolateNodePaths } from '../../core/canvas/paths.js';
|
|
7
|
-
import { readText } from '../../core/fs-utils.js';
|
|
5
|
+
import { readText, realpathOrSelf } from '../../core/fs-utils.js';
|
|
8
6
|
import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
9
7
|
import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
|
|
8
|
+
import { createMemoryDocSnapshot, listAllMemoryDocs, resolveMemoryDoc, resolveMemoryDocs } from '../../core/memory-resolver.js';
|
|
10
9
|
import { expandShellBlocks, hasShellBlocks, makeNodeShellRunner } from '../../core/runtime/shell-expansion.js';
|
|
10
|
+
import { deliveredAtOrAbove, loadInjectedDocs, recordDelivery, saveInjectedDocs } from '../../core/substrate/injected-store.js';
|
|
11
|
+
import { ancestorDirsOf, dirDedupKey, docsByName, isDirName, renderDirListing } from '../../core/substrate/listings.js';
|
|
12
|
+
import { memoryReadDocBlocks } from '../../core/substrate/on-read.js';
|
|
13
|
+
import { effectiveDocKind, normalizeDocName } from '../../core/substrate/schema.js';
|
|
11
14
|
import { MEMORY_KINDS } from './shared.js';
|
|
12
15
|
export { createMemoryDocSnapshot, resolveMemoryDocs };
|
|
13
16
|
/** Load the body at an already-resolved memory path with the same frontmatter
|
|
@@ -20,117 +23,56 @@ export function readMemoryDocContent(path, includeFrontmatter = false) {
|
|
|
20
23
|
const nodeId = process.env['CRTR_NODE_ID'];
|
|
21
24
|
return nodeId ? interpolateNodePaths(content, nodeId) : content;
|
|
22
25
|
}
|
|
23
|
-
/** The
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* top-level namespace merges across stores, so its "directory" would
|
|
32
|
-
* enumerate the entire corpus. */
|
|
33
|
-
function indexRoutes(doc) {
|
|
34
|
-
// A dir INDEX resolves two ways: the explicit `dir/INDEX` name, or the bare
|
|
35
|
-
// dir name (which carries the dir as its identity while the path is the
|
|
36
|
-
// INDEX.md). Detect from the path so both shapes route identically.
|
|
37
|
-
if (!doc.path.endsWith('/INDEX.md'))
|
|
38
|
-
return [];
|
|
39
|
-
const dir = isIndexName(doc.name) ? indexDirOf(doc.name) : doc.name;
|
|
40
|
-
if (dir === '')
|
|
41
|
-
return [];
|
|
42
|
-
const canonicalName = `${dir}/INDEX`;
|
|
43
|
-
let corpus;
|
|
26
|
+
/** The node-config subject for memory-read-event gate evaluation. A CLI
|
|
27
|
+
* process cannot take it from canvas-db (V-1); one `/v1` request supplies it.
|
|
28
|
+
* With no node identity or no reachable daemon the answer is null — gated
|
|
29
|
+
* docs then skip while ungated docs still deliver. Never autostarts a daemon:
|
|
30
|
+
* a memory read must not boot the runtime as a side effect. */
|
|
31
|
+
async function fetchSubject(nodeId) {
|
|
32
|
+
if (nodeId === undefined)
|
|
33
|
+
return null;
|
|
44
34
|
try {
|
|
45
|
-
|
|
35
|
+
return await CrtrClient.forLocalSocket({ autostart: false }).nodeSubject(nodeId);
|
|
46
36
|
}
|
|
47
37
|
catch {
|
|
48
|
-
return
|
|
38
|
+
return null;
|
|
49
39
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
for (const d of corpus)
|
|
55
|
-
if (!byName.has(d.name))
|
|
56
|
-
byName.set(d.name, d);
|
|
57
|
-
const prefix = dir + '/';
|
|
58
|
-
// Keyed by RENDERED identity (`displayName`), not `.name`: one physical
|
|
59
|
-
// directory INDEX answers to two names — `listAllMemoryDocs` emits the
|
|
60
|
-
// path-derived `dir/INDEX`, while `resolveMemoryDoc('dir')` returns it
|
|
61
|
-
// carrying the bare dir as its identity. Keying on `.name` let a body link
|
|
62
|
-
// to a sub-INDEX (written the normal way, by bare dir name) miss the entry
|
|
63
|
-
// the directory scan already added, and the doc rendered twice.
|
|
64
|
-
const members = new Map();
|
|
65
|
-
for (const [name, d] of byName) {
|
|
66
|
-
if (!name.startsWith(prefix) || name === canonicalName)
|
|
67
|
-
continue;
|
|
68
|
-
// The shallowest INDEX between the gating dir and this doc stands in for
|
|
69
|
-
// it (an INDEX finds itself here, keeping its own single line).
|
|
70
|
-
const segs = name.slice(prefix.length).split('/');
|
|
71
|
-
let member = d;
|
|
72
|
-
for (let i = 1; i < segs.length; i++) {
|
|
73
|
-
const gate = byName.get(`${prefix}${segs.slice(0, i).join('/')}/INDEX`);
|
|
74
|
-
if (gate !== undefined) {
|
|
75
|
-
member = gate;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (!members.has(displayName(member.name)))
|
|
80
|
-
members.set(displayName(member.name), member);
|
|
40
|
+
}
|
|
41
|
+
function corpusDocs() {
|
|
42
|
+
try {
|
|
43
|
+
return listAllMemoryDocs(undefined, true, true);
|
|
81
44
|
}
|
|
82
|
-
|
|
83
|
-
// store-root front door whose explicit frontmatter name dodges the root
|
|
84
|
-
// check above lands here. Fall back to the plain links field.
|
|
85
|
-
if (members.size === 0)
|
|
45
|
+
catch {
|
|
86
46
|
return [];
|
|
87
|
-
// Union the body's authored links so one list carries everything this INDEX
|
|
88
|
-
// routes to — the routes list subsumes `links` on an INDEX read.
|
|
89
|
-
for (const linkName of docLinkNames(doc.body)) {
|
|
90
|
-
let linked;
|
|
91
|
-
try {
|
|
92
|
-
linked = resolveMemoryDoc(linkName, { includeDescendants: true });
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
continue; // dangling link — lint's finding, not read's
|
|
96
|
-
}
|
|
97
|
-
if (displayName(linked.name) !== linkName)
|
|
98
|
-
continue;
|
|
99
|
-
if (linkName === displayName(canonicalName) || linkName === displayName(doc.name))
|
|
100
|
-
continue;
|
|
101
|
-
if (!members.has(linkName))
|
|
102
|
-
members.set(linkName, linked);
|
|
103
47
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.sort((a, b) => a.label.localeCompare(b.label))
|
|
112
|
-
.map((e) => e.line);
|
|
48
|
+
}
|
|
49
|
+
function attr(s) {
|
|
50
|
+
return s
|
|
51
|
+
.replace(/&/g, '&')
|
|
52
|
+
.replace(/"/g, '"')
|
|
53
|
+
.replace(/</g, '<')
|
|
54
|
+
.replace(/>/g, '>');
|
|
113
55
|
}
|
|
114
56
|
export const readLeaf = defineLeaf({
|
|
115
57
|
name: 'read',
|
|
116
58
|
description: 'load a memory document body by name',
|
|
117
|
-
whenToUse: 'a task in front of you matches a stored document and you already know its name — read it before improvising. Exact identity and direct-path matches resolve by scope precedence (node > project stack > profile > user > builtin); bare leaf-name fallback is considered only when no such match exists. You name the document by its crtr identifier, never a file path — do not cat or find the markdown off disk. Reach for `crtr memory find` first when you do not yet know which document applies.',
|
|
59
|
+
whenToUse: 'a task in front of you matches a stored document and you already know its name — read it before improvising. Exact identity and direct-path matches resolve by scope precedence (node > project stack > profile > user > builtin); bare leaf-name fallback is considered only when no such match exists. A directory name is also a valid target: it returns the neighborhood listing, and walking directories is the sanctioned browse move. You name the document by its crtr identifier, never a file path — do not cat or find the markdown off disk. Reach for `crtr memory find` first when you do not yet know which document applies.',
|
|
118
60
|
help: {
|
|
119
61
|
name: 'memory read',
|
|
120
|
-
summary: 'resolve a path-derived name to its document body
|
|
62
|
+
summary: 'resolve a path-derived name to its document body (frontmatter stripped unless --frontmatter), or a directory name to its listing',
|
|
121
63
|
params: [
|
|
122
|
-
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Exact identity and direct-path matches resolve by scope precedence: node > project stack > profile > user > builtin. A bare leaf falls back only when no exact/direct match exists.' },
|
|
64
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Exact identity and direct-path matches resolve by scope precedence: node > project stack > profile > user > builtin. A bare leaf falls back only when no exact/direct match exists. A directory name returns that directory\u2019s listing instead of a document.' },
|
|
123
65
|
{ kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Narrows resolution when the name is ambiguous across kinds.' },
|
|
124
66
|
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'When present, includes the YAML frontmatter in the returned body. Off by default — only the body is returned.' },
|
|
125
67
|
],
|
|
126
68
|
output: [
|
|
127
|
-
{ name: 'name', type: 'string', required: true, constraint: 'Resolved document name.' },
|
|
128
|
-
{ name: 'kind', type: 'string', required:
|
|
129
|
-
{ name: 'scope', type: 'string', required:
|
|
130
|
-
{ name: 'path', type: 'string', required:
|
|
131
|
-
{ name: 'content', type: 'string', required:
|
|
132
|
-
{ name: '
|
|
133
|
-
{ name: '
|
|
69
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Resolved document name, or the directory name on a directory read.' },
|
|
70
|
+
{ name: 'kind', type: 'string', required: false, constraint: 'Resolved kind: knowledge or preference. Absent on a directory read.' },
|
|
71
|
+
{ name: 'scope', type: 'string', required: false, constraint: 'Scope the document was resolved from: node, project, profile, user, or builtin. Absent on a directory read.' },
|
|
72
|
+
{ name: 'path', type: 'string', required: false, constraint: 'Absolute path to the document on disk. Revise it with `crtr memory edit`, never by editing this file — an edit records why the change happened and lands in the doc’s revision history. Absent on a directory read.' },
|
|
73
|
+
{ name: 'content', type: 'string', required: false, constraint: 'Document body. Frontmatter stripped unless --frontmatter is set. A document may embed shell as `!`cmd`` or a ```! fenced block; each runs once per read, in the current working directory, and is replaced by its output. Read `path` off disk when you need the literal unexecuted text. May be preceded by one `<auto-loaded-context>` block carrying the doc\u2019s neighborhood listings and any docs routed to this read. Absent on a directory read.' },
|
|
74
|
+
{ name: 'listing', type: 'string[]', required: false, constraint: 'Present only on a directory read: one `[[name]]: <when-and-why>` line per member doc, one bare `[[name]]` per subdirectory. Follow any line with `crtr memory read <name>`.' },
|
|
75
|
+
{ name: 'links', type: 'string[]', required: false, constraint: 'Canonical names this document links to via `[[name]]` that resolve in the current corpus — further reading, loaded only on demand with `crtr memory read <name>`. A directory link returns that directory\u2019s listing. Omitted when the body carries no resolvable links.' },
|
|
134
76
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Hints at variant flags or next commands.' },
|
|
135
77
|
],
|
|
136
78
|
outputKind: 'object',
|
|
@@ -156,52 +98,104 @@ export const readLeaf = defineLeaf({
|
|
|
156
98
|
if (!(e instanceof CrtrError && e.code === 'not_found'))
|
|
157
99
|
throw e;
|
|
158
100
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// it gates, subsuming the links field. Non-INDEX reads keep plain links.
|
|
174
|
-
const routes = indexRoutes(doc);
|
|
175
|
-
const links = routes.length > 0 ? [] : docLinkNames(doc.body).filter((linkName) => {
|
|
176
|
-
try {
|
|
177
|
-
// `resolveMemoryDoc` permits leaf-name fallback for interactive reads;
|
|
178
|
-
// a stored graph edge does not. Compare the resolved canonical name
|
|
179
|
-
// so an old shorthand never masquerades as a first-class doc link.
|
|
180
|
-
return displayName(resolveMemoryDoc(linkName, { includeDescendants: true }).name) === linkName;
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return false;
|
|
101
|
+
const nodeId = process.env['CRTR_NODE_ID'] || undefined;
|
|
102
|
+
if (doc === undefined) {
|
|
103
|
+
// No doc answers for a directory — the bare listing does. An explicit
|
|
104
|
+
// dir read is a deliberate act, so every line renders (unlisted
|
|
105
|
+
// excluded), never dedup-filtered; rendered members still record at
|
|
106
|
+
// preview so unsolicited channels stay quiet about them later.
|
|
107
|
+
const dirName = normalizeDocName(nameRaw.replace(/\/+$/, ''));
|
|
108
|
+
const byName = docsByName(corpusDocs());
|
|
109
|
+
if (dirName !== '' && isDirName(byName, dirName)) {
|
|
110
|
+
const seen = nodeId !== undefined ? loadInjectedDocs(nodeId) : null;
|
|
111
|
+
const listing = renderDirListing(byName, dirName, seen, false);
|
|
112
|
+
if (nodeId !== undefined && seen !== null) {
|
|
113
|
+
recordDelivery(seen, dirDedupKey(dirName), 'preview');
|
|
114
|
+
saveInjectedDocs(nodeId, seen);
|
|
184
115
|
}
|
|
116
|
+
return {
|
|
117
|
+
name: dirName,
|
|
118
|
+
listing,
|
|
119
|
+
follow_up: 'Each line is readable with `crtr memory read <name>` — a `[[name]]: <line>` entry is a doc, a bare `[[name]]` is a subdirectory whose read browses deeper. Browse the whole inventory with `crtr memory list`.',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
throw notFound(`memory document not found: ${nameRaw}`, {
|
|
123
|
+
memory: nameRaw,
|
|
124
|
+
next: 'Run `crtr memory find <query>` to discover documents, or `crtr memory list` to browse the inventory.',
|
|
185
125
|
});
|
|
186
|
-
return {
|
|
187
|
-
name: doc.name,
|
|
188
|
-
kind,
|
|
189
|
-
scope: doc.scope,
|
|
190
|
-
path: doc.path,
|
|
191
|
-
content,
|
|
192
|
-
...(routes.length > 0 ? { routes } : {}),
|
|
193
|
-
...(links.length > 0 ? { links } : {}),
|
|
194
|
-
follow_up: (routes.length > 0
|
|
195
|
-
? 'This INDEX gates the docs listed in `routes` — follow one with `crtr memory read <name>` when the task in front of you matches its line. '
|
|
196
|
-
: links.length > 0
|
|
197
|
-
? 'The `[[name]]` links in the body are further reading — follow one with `crtr memory read <name>` only when the task needs that depth. '
|
|
198
|
-
: '') +
|
|
199
|
-
'Use --frontmatter on this same command to inspect the YAML frontmatter, or revise the doc with `crtr memory edit` (recorded, with a rationale). Browse the inventory with `crtr memory list`.',
|
|
200
|
-
};
|
|
201
126
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
127
|
+
const kind = effectiveDocKind(doc);
|
|
128
|
+
const raw = readMemoryDocContent(doc.path, includeFrontmatter);
|
|
129
|
+
// A read is an explicit act by an agent that can already run shell, so a
|
|
130
|
+
// document's embedded commands run here and their output lands inline —
|
|
131
|
+
// the same contract as invoking the document as a slash command. Nothing
|
|
132
|
+
// executes on the auto-load path, which never reaches this leaf.
|
|
133
|
+
const content = hasShellBlocks(raw)
|
|
134
|
+
? await expandShellBlocks(raw, makeNodeShellRunner({ cwd: process.cwd() }))
|
|
135
|
+
: raw;
|
|
136
|
+
const byName = docsByName(corpusDocs());
|
|
137
|
+
// `[[name]]` doc links are pointers, never transclusion: surface which
|
|
138
|
+
// linked names actually resolve so the reader can follow one when the
|
|
139
|
+
// task needs that depth, without ever auto-loading a linked body. A link
|
|
140
|
+
// may also name a directory — a browse link to its listing.
|
|
141
|
+
const links = docLinkNames(doc.body).filter((linkName) => {
|
|
142
|
+
if (isDirName(byName, linkName))
|
|
143
|
+
return true;
|
|
144
|
+
try {
|
|
145
|
+
// `resolveMemoryDoc` permits leaf-name fallback for interactive reads;
|
|
146
|
+
// a stored graph edge does not. Compare the resolved canonical name
|
|
147
|
+
// so an old shorthand never masquerades as a first-class doc link.
|
|
148
|
+
return resolveMemoryDoc(linkName, { includeDescendants: true }).name === linkName;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
205
153
|
});
|
|
154
|
+
// Reading a doc discloses where it sits (its directory's listing and each
|
|
155
|
+
// ancestor's, once per transcript) and fires the memory-read event
|
|
156
|
+
// (corpus docs whose `memory-read` entries match the resolved name).
|
|
157
|
+
// Both prepend in one <auto-loaded-context> block. Without a node
|
|
158
|
+
// identity the call is stateless: listings render undeduped and nothing
|
|
159
|
+
// records.
|
|
160
|
+
const seen = nodeId !== undefined ? loadInjectedDocs(nodeId) : null;
|
|
161
|
+
const subject = await fetchSubject(nodeId);
|
|
162
|
+
const excludeReal = realpathOrSelf(doc.path);
|
|
163
|
+
// The explicit read IS a content delivery: record it first so unsolicited
|
|
164
|
+
// channels (including this doc's own line in its directory listing) stay
|
|
165
|
+
// quiet about a doc whose full body is already in the transcript.
|
|
166
|
+
if (seen !== null)
|
|
167
|
+
recordDelivery(seen, excludeReal, 'content');
|
|
168
|
+
const blocks = [];
|
|
169
|
+
for (const dir of ancestorDirsOf(doc.name)) {
|
|
170
|
+
if (seen !== null && deliveredAtOrAbove(seen, dirDedupKey(dir), 'preview'))
|
|
171
|
+
continue;
|
|
172
|
+
const lines = renderDirListing(byName, dir, seen, true);
|
|
173
|
+
if (seen !== null)
|
|
174
|
+
recordDelivery(seen, dirDedupKey(dir), 'preview');
|
|
175
|
+
if (lines.length > 0)
|
|
176
|
+
blocks.push(`<memory-listing dir="${attr(dir)}">\n${lines.join('\n')}\n</memory-listing>`);
|
|
177
|
+
}
|
|
178
|
+
const targetNames = [doc.name];
|
|
179
|
+
if (doc.plugin !== undefined) {
|
|
180
|
+
const slash = doc.name.indexOf('/');
|
|
181
|
+
if (slash > 0)
|
|
182
|
+
targetNames.push(doc.name.slice(slash + 1));
|
|
183
|
+
}
|
|
184
|
+
blocks.push(...memoryReadDocBlocks(subject, excludeReal, targetNames, seen ?? new Map()));
|
|
185
|
+
if (nodeId !== undefined && seen !== null)
|
|
186
|
+
saveInjectedDocs(nodeId, seen);
|
|
187
|
+
const finalContent = blocks.length === 0 ? content : `<auto-loaded-context>\n${blocks.join('\n')}\n</auto-loaded-context>\n\n${content}`;
|
|
188
|
+
return {
|
|
189
|
+
name: doc.name,
|
|
190
|
+
kind,
|
|
191
|
+
scope: doc.scope,
|
|
192
|
+
path: doc.path,
|
|
193
|
+
content: finalContent,
|
|
194
|
+
...(links.length > 0 ? { links } : {}),
|
|
195
|
+
follow_up: (links.length > 0
|
|
196
|
+
? 'The `[[name]]` links in the body are further reading — follow one with `crtr memory read <name>` only when the task needs that depth. '
|
|
197
|
+
: '') +
|
|
198
|
+
'Use --frontmatter on this same command to inspect the YAML frontmatter, or revise the doc with `crtr memory edit` (recorded, with a rationale). Browse the inventory with `crtr memory list`.',
|
|
199
|
+
};
|
|
206
200
|
},
|
|
207
201
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { FlagParam } from '../../core/help.js';
|
|
2
2
|
import type { MemoryScope } from '../../core/memory-resolver.js';
|
|
3
3
|
export declare const MEMORY_KINDS: readonly ["knowledge", "preference"];
|
|
4
|
-
export declare const VISIBILITY_RUNGS: readonly ["none", "name", "preview", "content"];
|
|
5
4
|
export declare const MEMORY_SCOPES: readonly ["user", "project", "profile", "node"];
|
|
6
5
|
/** Scope sort weight matching resolution precedence (node > project stack >
|
|
7
6
|
* profile > user > builtin). Used by `list` for its "scope then kind then
|
|
@@ -25,21 +24,23 @@ export declare function resolveWriteTarget(scopeArg: string | undefined, profile
|
|
|
25
24
|
scope: MemoryScope;
|
|
26
25
|
memoryDir: string;
|
|
27
26
|
};
|
|
27
|
+
/** Remove a just-removed file's now-empty parent directories up to (never
|
|
28
|
+
* including) its tree root, so removing the last entry under an `area/`
|
|
29
|
+
* prefix does not orphan an empty directory. The root is derived by walking
|
|
30
|
+
* up one dirname per name segment: a doc named `a/b` sits at `<root>/a/b.md`,
|
|
31
|
+
* so its root is two dirnames above — the same arithmetic holds for a
|
|
32
|
+
* `.history/<name>.jsonl` sidecar. Stops at the first non-empty ancestor. */
|
|
33
|
+
export declare function pruneEmptyParents(filePath: string, nameSegments: number): void;
|
|
28
34
|
/** Map a path-derived name (`topic` or `area/topic`) to its file path under a
|
|
29
35
|
* memory dir, guarding against traversal/absolute escapes. */
|
|
30
36
|
export declare function memoryFilePath(memoryDir: string, name: string): string;
|
|
31
37
|
export declare function coerceGate(raw: string): Record<string, unknown>;
|
|
32
|
-
/** Coerce
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
export declare function
|
|
38
|
-
/** Coerce repeatable `--applies-to` values (already an array from the argv
|
|
39
|
-
* parser) to the schema's glob form: one value stays a bare string, several
|
|
40
|
-
* become an array. Each value is trimmed; empty entries are rejected loudly
|
|
41
|
-
* (a silently-dropped glob is a routing bug waiting to happen). */
|
|
42
|
-
export declare function coerceAppliesTo(values: string[]): unknown;
|
|
38
|
+
/** Coerce one `--surface` value into a validated surfaces entry. Strict where
|
|
39
|
+
* the runtime parser is tolerant: a flag that would be silently dropped or
|
|
40
|
+
* trimmed at render time is an authoring mistake and fails HERE. The entry is
|
|
41
|
+
* returned in canonical key order with a single glob kept as a bare string,
|
|
42
|
+
* so the stored YAML stays as compact as the flag that authored it. */
|
|
43
|
+
export declare function coerceSurface(raw: string): Record<string, unknown>;
|
|
43
44
|
/** Provenance for a doc at the moment it is created: when, where, and which
|
|
44
45
|
* node's conversation authored it. Read from the env the runtime injects into
|
|
45
46
|
* every node's pi process (CRTR_NODE_ID/KIND/MODE/NODE_CWD); when invoked
|
|
@@ -50,8 +51,8 @@ export declare function coerceAppliesTo(values: string[]): unknown;
|
|
|
50
51
|
export declare function buildOrigin(): Record<string, unknown>;
|
|
51
52
|
/** Serialize a substrate frontmatter record + body into a complete `.md`
|
|
52
53
|
* document. Frontmatter is emitted as a `---` fenced YAML block (the `yaml`
|
|
53
|
-
* package — the same one the parser uses — so nested gate maps and
|
|
54
|
-
*
|
|
54
|
+
* package — the same one the parser uses — so nested gate maps and surfaces
|
|
55
|
+
* entry lists round-trip), in canonical field order with preserved extras last. */
|
|
55
56
|
export declare function serializeMemoryDoc(frontmatter: Record<string, unknown>, body: string): string;
|
|
56
57
|
/** Everything after the closing frontmatter fence, byte for byte — leading blank
|
|
57
58
|
* lines and indentation intact. Source without a fence is all body. */
|
|
@@ -75,7 +76,7 @@ export declare function overlayParam(name: string, overrides?: Partial<FlagParam
|
|
|
75
76
|
* `--doc-rationale`, because `--rationale` there means why THIS REVISION is
|
|
76
77
|
* happening. Same field, same prose, two flag names that cannot be confused. */
|
|
77
78
|
export declare const DOC_RATIONALE_CONSTRAINT = "Frontmatter rationale \u2014 the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting the flag preserves an existing rationale unchanged.";
|
|
78
|
-
export declare const
|
|
79
|
+
export declare const GUIDE_SURFACES = "Every doc appears in its directory\u2019s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing \u2014 entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc\u2019s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file\u2019s absolute path and basename, `./`-anchored globs vs its path relative to the store\u2019s owning repo dir, `match-frontmatter` predicates over the read file\u2019s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc\u2019s canonical name, `./` anchored to this doc\u2019s own name directory); `command` fires when a matching shell command runs (globs vs the whole command string, `*` crossing `/`). `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly \u2014 a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.";
|
|
79
80
|
export declare const GUIDE_ROUTING_LINE = "The routing line (--when-and-why-to-read) is the only text an agent reads before deciding to load the doc. The test for its because-clause: if it can be derived by paraphrasing the doc\u2019s advice, it is a restatement, not a payoff \u2014 a real payoff names a consequence in the reader\u2019s world that the document itself never asserts. Bad: \"because only genuine first principles belong in taste memory.\" Bad: \"because keeping the test loop fast and free of speculative tests protects the development pace\" \u2014 the doc\u2019s rule as an outcome, derivable straight from its advice. Good: \"because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation.\" Someone mid-task who has not read the doc must be able to decide from this line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user one sharp question instead of improvising.";
|
|
80
|
-
export declare const GUIDE_PREDICATE_VOCABULARY = "Gate and
|
|
81
|
-
export declare const GUIDE_DOC_LINKS = "Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes
|
|
81
|
+
export declare const GUIDE_PREDICATE_VOCABULARY = "Gate and match-frontmatter share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.";
|
|
82
|
+
export declare const GUIDE_DOC_LINKS = "Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes. A bare directory name is a valid link too: following it returns that directory\u2019s listing, a browse entrance rather than a doc. Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document or directory, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias or label form.";
|