@north-light/crouter 0.3.217 → 0.3.218
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 +5 -2
- package/dist/api/client.js +7 -1
- package/dist/api/dto/canvas.d.ts +9 -0
- package/dist/api/dto/nodes.d.ts +27 -1
- package/dist/builtin-memory/04-base-worker.md +7 -1
- package/dist/builtin-memory/internal/plugins.md +67 -0
- package/dist/builtin-memory/memory-read-orientation.md +13 -0
- package/dist/clients/attach/chrome/bash-jobs.js +1 -1
- package/dist/clients/attach/photon_rs_bg.wasm +0 -0
- package/dist/clients/attach/viewer.js +429 -429
- package/dist/commands/human/shared.js +1 -1
- package/dist/commands/memory/edit.js +6 -1
- package/dist/commands/memory/lint.d.ts +1 -6
- package/dist/commands/memory/lint.js +18 -126
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +17 -2
- package/dist/commands/memory/read.js +15 -6
- package/dist/commands/memory/shared.d.ts +32 -2
- package/dist/commands/memory/shared.js +179 -0
- package/dist/commands/memory/write.js +6 -1
- package/dist/commands/pkg/browse/catalog.js +2 -0
- package/dist/commands/pkg/browse/model.d.ts +4 -1
- package/dist/commands/pkg/plugin-manage.js +209 -148
- package/dist/core/bash-jobs.d.ts +2 -5
- package/dist/core/bash-jobs.js +4 -8
- package/dist/core/command-plugins/bundle.d.ts +4 -1
- package/dist/core/command-plugins/bundle.js +16 -2
- package/dist/core/human/component-docs.js +1 -0
- package/dist/core/human/scan.d.ts +5 -4
- package/dist/core/human/scan.js +7 -4
- package/dist/core/io.js +3 -2
- package/dist/core/manifest.d.ts +2 -0
- package/dist/core/manifest.js +5 -0
- package/dist/core/memory/extensions.d.ts +30 -0
- package/dist/core/memory/extensions.js +219 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +25 -0
- package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
- package/dist/core/substrate/frontmatter-validation.js +101 -0
- package/dist/core/substrate/index.d.ts +1 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
- package/dist/daemon/api/handlers/canvas.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +4 -3
- package/dist/daemon/api/handlers/nodes.d.ts +1 -0
- package/dist/daemon/api/handlers/nodes.js +101 -19
- package/dist/daemon/api/handlers/reports.d.ts +4 -0
- package/dist/daemon/api/handlers/reports.js +14 -8
- package/dist/daemon/crtrd.js +7 -5
- package/dist/daemon/manage.d.ts +3 -0
- package/dist/daemon/manage.js +14 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
- package/dist/pi-extensions/canvas-bash-valve.js +19 -6
- package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
- package/dist/pi-extensions/canvas-preview-result.js +9 -23
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -3,7 +3,7 @@ import { CORE_COMPONENT_SELECTION_LINES } from '../../core/human/component-docs.
|
|
|
3
3
|
// The reader's missing context is the gap syntax cannot close: an inbox page
|
|
4
4
|
// is opened away from the conversation that produced it, by someone who never
|
|
5
5
|
// followed the work.
|
|
6
|
-
const PAGE_BRIEF = 'Write the page for where it is read. A page projected to the inbox is opened away from this conversation by someone who has not followed the work, so it stands alone; an inline page can lean on what the conversation already said. Title names the topic; subtitle states the decision, your recommendation, and the stakes. Content carries only what is needed to decide, with the evidence behind it below the ask rather than ahead of it. Ask only what changes your next step, and offer only options you would actually take. A page that asks nothing states what happened and what it changes.';
|
|
6
|
+
const PAGE_BRIEF = 'Write the page for where it is read. A page projected to the inbox is opened away from this conversation by someone who has not followed the work, so it stands alone; an inline page can lean on what the conversation already said. Title names the topic; subtitle states the decision, your recommendation, and the stakes. Content carries only what is needed to decide, with the evidence behind it below the ask rather than ahead of it. Ask only what changes your next step, and offer only options you would actually take. The reader takes a page one step at a time, so hold each step — and a stepless page — to one question or request for action; several decisions means several `<Step>` children, never one long scroll of stacked context and asks. A page that asks nothing states what happened and what it changes.';
|
|
7
7
|
const PAGE_AUTHORING = 'Write one `.tsx` module to `$CRTR_CONTEXT_DIR/pages/<name>.tsx` that default-exports a component whose root element is `<Page title="…" subtitle="…">`; a multi-step page puts its content in `<Step>` children. Or send a complete `.html` file with a nonempty `<title>`; HTML is delivered verbatim and collects no response. Delivery is chosen by `human send` flags, never a Page prop. Registry components and React are already in scope, so the module contains no `import` and no `require`. Full JS is yours: state, expressions, `.map` over your data, event handlers, plain elements with Tailwind classes. `usePageHost` is a reserved hook whose product-supplied members are opaque here. Never render submit or dismiss chrome — the host owns it. Display-only pages are first-class: a page with no questions may be sent without inbox or reply delivery. The module is compiled when you submit it and a compile error rejects the submit; there is no pre-check.';
|
|
8
8
|
const DISPLAY_SET_LINE = 'The shadcn display set — `Card`, `Badge`, `Button`, `Table`, `Tabs`, `Separator`, `Alert`, `Progress`, `Accordion` and their parts — is in scope too, for presentation only.';
|
|
9
9
|
/** The one page authoring model for `human -h`. */
|
|
@@ -4,7 +4,7 @@ import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
|
4
4
|
import { readText, writeText } from '../../core/fs-utils.js';
|
|
5
5
|
import { resolveMemoryDoc } from '../../core/memory-resolver.js';
|
|
6
6
|
import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor, readHistoryRecords, } from '../../core/memory/history.js';
|
|
7
|
-
import { DOC_RATIONALE_CONSTRAINT, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, MEMORY_SCOPES, coerceGate, coerceSurface, overlayParam, literalBodySegment, serializeMemoryDocLiteral, } from './shared.js';
|
|
7
|
+
import { DOC_RATIONALE_CONSTRAINT, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, MEMORY_SCOPES, coerceGate, coerceSurface, memoryExtensionCatalogForDoc, memoryExtensionFieldCatalogHelp, overlayParam, parseRequestedExtensionChanges, applyExtensionChanges, literalBodySegment, serializeMemoryDocLiteral, } from './shared.js';
|
|
8
8
|
/** Frontmatter fields an overlay flag cannot express removing. `kind` and the
|
|
9
9
|
* routing line are the document's contract (a doc without them is malformed);
|
|
10
10
|
* `origin` and `last-updated` are runtime provenance. */
|
|
@@ -39,6 +39,8 @@ export const editLeaf = defineLeaf({
|
|
|
39
39
|
overlayParam('gate'),
|
|
40
40
|
overlayParam('slash', {}, 'Presence SETS the field; remove it with `--unset slash`.'),
|
|
41
41
|
{ kind: 'flag', name: 'doc-rationale', type: 'string', required: false, constraint: DOC_RATIONALE_CONSTRAINT },
|
|
42
|
+
{ kind: 'flag', name: 'extension', type: 'string', required: false, repeatable: true, constraint: 'Set one declared plugin field as `extensions.<plugin>.<field>=VALUE`. The path is required in full; booleans accept only true or false, numbers require finite numeric syntax, and strings/enums preserve the literal text after the first =. Every requested field validates before the document is revised.' },
|
|
43
|
+
{ kind: 'flag', name: 'unset-extension', type: 'string', required: false, repeatable: true, constraint: 'Remove one explicitly persisted declared field by its full `extensions.<plugin>.<field>` path. A removed explicit value lets its declaration default apply only in structured output.' },
|
|
42
44
|
{ kind: 'flag', name: 'unset', type: 'enum', choices: [...CLEARABLE_FIELDS], required: false, repeatable: true, constraint: 'Remove a frontmatter field an overlay flag cannot express removing. One field per occurrence. `kind`, `when-and-why-to-read`, `origin`, and `last-updated` are never clearable.' },
|
|
43
45
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Restrict resolution to this scope before editing. A disambiguation filter only \u2014 never a creation target, and never a way to move a doc between stores.' },
|
|
44
46
|
{ kind: 'stdin', name: 'body', required: false, constraint: 'Full replacement body (markdown, no frontmatter), saved byte for byte. Piped on stdin only \u2014 this leaf already claims the one positional for NAME. Absent or empty stdin leaves the existing body untouched, which is how a frontmatter-only revision is made.' },
|
|
@@ -54,6 +56,7 @@ export const editLeaf = defineLeaf({
|
|
|
54
56
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands \u2014 read the revisions or read the doc back.' },
|
|
55
57
|
],
|
|
56
58
|
outputKind: 'object',
|
|
59
|
+
dynamicState: () => memoryExtensionFieldCatalogHelp('edit'),
|
|
57
60
|
effects: [
|
|
58
61
|
'Rewrites memory/<name>.md at the resolved scope and appends one `edit` record to memory/.history/<name>.jsonl.',
|
|
59
62
|
],
|
|
@@ -86,6 +89,7 @@ export const editLeaf = defineLeaf({
|
|
|
86
89
|
if (doc.scope === 'builtin') {
|
|
87
90
|
throw usage(`${doc.name} is a builtin document shipped with the package (read-only) \u2014 it cannot be edited. Override it with a same-named doc at a writable scope instead (\`crtr memory write ${doc.name} ...\`).`, { memory: doc.name, scope: 'builtin' });
|
|
88
91
|
}
|
|
92
|
+
const extensionChanges = parseRequestedExtensionChanges(input['extension'], input['unsetExtension'], memoryExtensionCatalogForDoc(doc));
|
|
89
93
|
const before = readText(doc.path);
|
|
90
94
|
const parsed = parseFrontmatterGeneric(before);
|
|
91
95
|
const previous = { ...(parsed.data ?? {}) };
|
|
@@ -107,6 +111,7 @@ export const editLeaf = defineLeaf({
|
|
|
107
111
|
if (input['slash'] === true)
|
|
108
112
|
frontmatter['slash'] = true;
|
|
109
113
|
setIf('rationale', input['docRationale']);
|
|
114
|
+
applyExtensionChanges(frontmatter, extensionChanges.sets, extensionChanges.unsets);
|
|
110
115
|
for (const field of input['unset'] ?? []) {
|
|
111
116
|
delete frontmatter[field];
|
|
112
117
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { lintSubstrateFrontmatter as lintSubstrateSchema } from '../../core/substrate/frontmatter-validation.js';
|
|
1
2
|
/** Warn when a body short enough to inline still pays a routing line. */
|
|
2
3
|
export declare function lintShortPreviewBody(fm: Record<string, unknown>, body: string): string | null;
|
|
3
4
|
/** The length rule. Caps are computed from the parsed entries — an invalid
|
|
@@ -6,10 +7,4 @@ export declare function lintShortPreviewBody(fm: Record<string, unknown>, body:
|
|
|
6
7
|
* `name`, else the normalized path-derived name) — persona layers under
|
|
7
8
|
* `kinds/` are recognized by it and never capped. */
|
|
8
9
|
export declare function lintBodyLength(fm: Record<string, unknown>, body: string, docName: string): string | null;
|
|
9
|
-
/** Schema checks for a doc living in a substrate memory dir: a memory store
|
|
10
|
-
* holds ONLY substrate docs, so a missing/invalid `kind` is an authoring
|
|
11
|
-
* error here (elsewhere it just means "not a substrate doc"). Fields are
|
|
12
|
-
* checked RAW — the runtime parser silently drops invalid entries, which is
|
|
13
|
-
* exactly the silent tolerance this lint exists to catch at authoring time. */
|
|
14
|
-
export declare function lintSubstrateSchema(fm: Record<string, unknown> | null): string | null;
|
|
15
10
|
export declare const lintLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -13,15 +13,13 @@ import { listInstalledPlugins, listInstalledPluginsInRoot } from '../../core/res
|
|
|
13
13
|
import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from '../../core/scope.js';
|
|
14
14
|
import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
|
|
15
15
|
import { getDefaultProfileId } from '../../core/profiles/default-binding.js';
|
|
16
|
-
import {
|
|
16
|
+
import { normalizeDocName, parseSubstrateFrontmatter, resolveDocName, } from '../../core/substrate/schema.js';
|
|
17
|
+
import { lintSubstrateFrontmatter } from '../../core/substrate/frontmatter-validation.js';
|
|
18
|
+
export { lintSubstrateFrontmatter as lintSubstrateSchema } from '../../core/substrate/frontmatter-validation.js';
|
|
19
|
+
import { memoryExtensionValidationCatalog, validateMemoryExtensionValues } from '../../core/memory/extensions.js';
|
|
17
20
|
import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
|
|
18
21
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
19
22
|
import { descendantStoreRoots } from '../../core/nested-stores.js';
|
|
20
|
-
/** The four retired visibility-axis fields. The cut is hard: an old-format doc
|
|
21
|
-
* fails loudly and names the migration, never gets a dual-parse fallback. */
|
|
22
|
-
const RETIRED_FIELDS = ['system-prompt-visibility', 'file-read-visibility', 'applies-to', 'read-when'];
|
|
23
|
-
/** The frontmatter keys one `surfaces` entry may carry. */
|
|
24
|
-
const SURFACE_ENTRY_KEYS = ['on', 'match', 'match-frontmatter', 'at'];
|
|
25
23
|
/** Rung-scaled body-length caps, measured in WORDS (frontmatter excluded).
|
|
26
24
|
* Words, not lines: house style writes each paragraph as ONE logical line
|
|
27
25
|
* and lets the editor soft-wrap, so a line count measures wrapping style
|
|
@@ -49,8 +47,6 @@ const EVENT_CONTENT_MAX_WORDS = 1000;
|
|
|
49
47
|
const BOOT_PREVIEW_MAX_WORDS = 3000;
|
|
50
48
|
/** A routing line costs more than it saves for a short rule or fact. */
|
|
51
49
|
const PREVIEW_TO_CONTENT_MAX_WORDS = 30;
|
|
52
|
-
/** Rules a doc may suppress via frontmatter `lint-ignore`. */
|
|
53
|
-
const SUPPRESSIBLE_RULES = ['length'];
|
|
54
50
|
function ignoresRule(fm, rule) {
|
|
55
51
|
const v = fm['lint-ignore'];
|
|
56
52
|
return v === rule || (Array.isArray(v) && v.includes(rule));
|
|
@@ -104,120 +100,13 @@ export function lintBodyLength(fm, body, docName) {
|
|
|
104
100
|
}
|
|
105
101
|
return null;
|
|
106
102
|
}
|
|
107
|
-
/** Strict validation of a raw `surfaces` value — the mirror of `coerceSurface`
|
|
108
|
-
* in shared.ts: everything the runtime parser would silently drop or trim is
|
|
109
|
-
* an error here. Returns the first problem found, or null. */
|
|
110
|
-
function lintSurfacesField(v) {
|
|
111
|
-
if (v === undefined)
|
|
112
|
-
return null;
|
|
113
|
-
if (!Array.isArray(v))
|
|
114
|
-
return `invalid surfaces: ${JSON.stringify(v)} (expected a list of {on, at, match?, match-frontmatter?} entries)`;
|
|
115
|
-
for (const raw of v) {
|
|
116
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
117
|
-
return `invalid surfaces entry: ${JSON.stringify(raw)} (expected an {on, at, match?, match-frontmatter?} object)`;
|
|
118
|
-
}
|
|
119
|
-
const rec = raw;
|
|
120
|
-
for (const key of Object.keys(rec)) {
|
|
121
|
-
if (!SURFACE_ENTRY_KEYS.includes(key)) {
|
|
122
|
-
return `invalid surfaces entry: unknown key \`${key}\` (an entry carries only on, match, match-frontmatter, at)`;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const on = rec['on'];
|
|
126
|
-
if (typeof on !== 'string' || !SURFACE_EVENTS.includes(on)) {
|
|
127
|
-
return `invalid surfaces entry \`on\`: ${JSON.stringify(on)} (expected ${SURFACE_EVENTS.join('|')})`;
|
|
128
|
-
}
|
|
129
|
-
const at = rec['at'];
|
|
130
|
-
if (typeof at !== 'string' || !SURFACE_RUNGS.includes(at)) {
|
|
131
|
-
return `invalid surfaces entry \`at\`: ${JSON.stringify(at)} (expected ${SURFACE_RUNGS.join('|')})`;
|
|
132
|
-
}
|
|
133
|
-
let hasMatch = false;
|
|
134
|
-
if (rec['match'] !== undefined) {
|
|
135
|
-
if (on === 'boot' || on === 'workspace-open') {
|
|
136
|
-
return `invalid surfaces entry: \`match\` is meaningless on \`${on}\` — the entry's presence is the match; drop it`;
|
|
137
|
-
}
|
|
138
|
-
const m = rec['match'];
|
|
139
|
-
const globs = typeof m === 'string' ? [m] : Array.isArray(m) && m.every((g) => typeof g === 'string') ? m : null;
|
|
140
|
-
if (globs === null || globs.length === 0 || globs.some((g) => g.trim() === '')) {
|
|
141
|
-
return `invalid surfaces entry \`match\`: ${JSON.stringify(m)} (expected a non-empty glob or non-empty glob list)`;
|
|
142
|
-
}
|
|
143
|
-
hasMatch = true;
|
|
144
|
-
}
|
|
145
|
-
const mf = rec['match-frontmatter'];
|
|
146
|
-
if (mf !== undefined) {
|
|
147
|
-
if (on !== 'read')
|
|
148
|
-
return 'invalid surfaces entry: `match-frontmatter` is a `read`-event predicate only';
|
|
149
|
-
if (mf === null || typeof mf !== 'object' || Array.isArray(mf)) {
|
|
150
|
-
return `invalid surfaces entry \`match-frontmatter\`: ${JSON.stringify(mf)} (expected a field→matcher object)`;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if ((on === 'read' || on === 'memory-read' || on === 'command') && !hasMatch && mf === undefined) {
|
|
154
|
-
return `invalid surfaces entry: a \`${on}\` entry requires \`match\`${on === 'read' ? ' or `match-frontmatter`' : ''}`;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
/** Schema checks for a doc living in a substrate memory dir: a memory store
|
|
160
|
-
* holds ONLY substrate docs, so a missing/invalid `kind` is an authoring
|
|
161
|
-
* error here (elsewhere it just means "not a substrate doc"). Fields are
|
|
162
|
-
* checked RAW — the runtime parser silently drops invalid entries, which is
|
|
163
|
-
* exactly the silent tolerance this lint exists to catch at authoring time. */
|
|
164
|
-
export function lintSubstrateSchema(fm) {
|
|
165
|
-
if (fm === null)
|
|
166
|
-
return 'missing frontmatter: a memory store doc requires `kind: knowledge|preference`';
|
|
167
|
-
if (!isDocKind(fm.kind)) {
|
|
168
|
-
return `invalid kind: ${JSON.stringify(fm.kind)} (expected knowledge|preference)`;
|
|
169
|
-
}
|
|
170
|
-
// The retired `when`/`why` pair was merged into one read-routing field. The
|
|
171
|
-
// hard cut is enforced HERE: an old-shape doc must fail, never be silently
|
|
172
|
-
// read at runtime.
|
|
173
|
-
if ('when' in fm || 'why' in fm) {
|
|
174
|
-
return 'retired `when`/`why` keys: merge them into one `when-and-why-to-read` line — "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the benefit unlocked by reading, not the document summary, rule, or obedience rationale.';
|
|
175
|
-
}
|
|
176
|
-
if (typeof fm['when-and-why-to-read'] !== 'string' || fm['when-and-why-to-read'].trim() === '') {
|
|
177
|
-
return 'missing `when-and-why-to-read`: one routing line — "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the benefit unlocked by reading, not the document summary, rule, or obedience rationale.';
|
|
178
|
-
}
|
|
179
|
-
for (const field of RETIRED_FIELDS) {
|
|
180
|
-
if (field in fm) {
|
|
181
|
-
return `retired \`${field}\` key: the visibility axes were replaced by \`surfaces\` event routing — run \`crtr sys migrate\` to convert old-format docs, or author \`surfaces\` entries by hand (\`crtr memory write -h\`)`;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const unlisted = fm['unlisted'];
|
|
185
|
-
if (unlisted !== undefined && typeof unlisted !== 'boolean') {
|
|
186
|
-
return `invalid unlisted: ${JSON.stringify(unlisted)} (expected a boolean)`;
|
|
187
|
-
}
|
|
188
|
-
const surfacesError = lintSurfacesField(fm['surfaces']);
|
|
189
|
-
if (surfacesError !== null)
|
|
190
|
-
return surfacesError;
|
|
191
|
-
const gate = fm.gate;
|
|
192
|
-
if (gate !== undefined && (gate === null || typeof gate !== 'object' || Array.isArray(gate))) {
|
|
193
|
-
return `invalid gate: ${JSON.stringify(gate)} (expected a field→matcher object)`;
|
|
194
|
-
}
|
|
195
|
-
const slash = fm.slash;
|
|
196
|
-
if (slash !== undefined && typeof slash !== 'boolean') {
|
|
197
|
-
return `invalid slash: ${JSON.stringify(slash)} (expected a boolean)`;
|
|
198
|
-
}
|
|
199
|
-
// rationale (maintainer-facing gap this doc closes) is optional everywhere —
|
|
200
|
-
// no nagging when absent, just a type check when present.
|
|
201
|
-
const rationale = fm.rationale;
|
|
202
|
-
if (rationale !== undefined && typeof rationale !== 'string') {
|
|
203
|
-
return `invalid rationale: ${JSON.stringify(rationale)} (expected a string)`;
|
|
204
|
-
}
|
|
205
|
-
const lintIgnore = fm['lint-ignore'];
|
|
206
|
-
if (lintIgnore !== undefined) {
|
|
207
|
-
const rules = Array.isArray(lintIgnore) ? lintIgnore : [lintIgnore];
|
|
208
|
-
if (rules.length === 0 || !rules.every((r) => typeof r === 'string' && SUPPRESSIBLE_RULES.includes(r))) {
|
|
209
|
-
return `invalid lint-ignore: ${JSON.stringify(lintIgnore)} (the only suppressible rule is \`length\`)`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
103
|
/** Strict-parse one file; push a finding on a YAML error, then run the
|
|
215
104
|
* schema check when the file lives in a substrate store, then validate every
|
|
216
105
|
* `[[canonical/name]]` doc link in the body against the exact resolvable
|
|
217
106
|
* corpus. A dangling link is an authoring error caught HERE, never silently
|
|
218
107
|
* carried; leaf-name fallback is deliberately excluded so links stay stable
|
|
219
108
|
* as the graph grows and another document acquires the same leaf name. */
|
|
220
|
-
function lintFile(file, substrateStore, findings, warnings, corpusNames, fallbackName) {
|
|
109
|
+
function lintFile(file, substrateStore, findings, warnings, corpusNames, fallbackName, scope) {
|
|
221
110
|
let fm;
|
|
222
111
|
let body;
|
|
223
112
|
try {
|
|
@@ -230,10 +119,13 @@ function lintFile(file, substrateStore, findings, warnings, corpusNames, fallbac
|
|
|
230
119
|
}
|
|
231
120
|
if (!substrateStore)
|
|
232
121
|
return;
|
|
233
|
-
const schemaError =
|
|
122
|
+
const schemaError = lintSubstrateFrontmatter(fm);
|
|
234
123
|
if (schemaError !== null)
|
|
235
124
|
findings.push({ path: file, error: schemaError });
|
|
236
125
|
if (fm !== null) {
|
|
126
|
+
for (const issue of validateMemoryExtensionValues(fm['extensions'], memoryExtensionValidationCatalog({ scope, path: file }))) {
|
|
127
|
+
findings.push({ path: file, error: `${issue.path}: ${issue.message}` });
|
|
128
|
+
}
|
|
237
129
|
const lengthError = lintBodyLength(fm, body, resolveDocName(fm, fallbackName));
|
|
238
130
|
if (lengthError !== null)
|
|
239
131
|
findings.push({ path: file, error: lengthError });
|
|
@@ -246,7 +138,7 @@ function lintFile(file, substrateStore, findings, warnings, corpusNames, fallbac
|
|
|
246
138
|
if (entry.on !== 'memory-read')
|
|
247
139
|
continue;
|
|
248
140
|
for (const glob of entry.match ?? []) {
|
|
249
|
-
if (glob === '**' || glob === '*') {
|
|
141
|
+
if ((glob === '**' || glob === '*') && !ignoresRule(fm, 'broad-memory-read')) {
|
|
250
142
|
warnings.push(`${file}: memory-read glob \`${glob}\` fires on every memory read — scope it to a name subtree`);
|
|
251
143
|
}
|
|
252
144
|
}
|
|
@@ -329,7 +221,7 @@ export const lintLeaf = defineLeaf({
|
|
|
329
221
|
for (let i = 1; i < segs.length; i++)
|
|
330
222
|
corpusNames.add(segs.slice(0, i).join('/'));
|
|
331
223
|
}
|
|
332
|
-
const lintDir = (dir) => {
|
|
224
|
+
const lintDir = (dir, scope) => {
|
|
333
225
|
if (!dir || !pathExists(dir))
|
|
334
226
|
return;
|
|
335
227
|
for (const file of walkFiles(dir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
|
|
@@ -338,14 +230,14 @@ export const lintLeaf = defineLeaf({
|
|
|
338
230
|
continue;
|
|
339
231
|
memoryCount += 1;
|
|
340
232
|
const fallbackName = normalizeDocName(relPath.replace(/\.md$/, ''));
|
|
341
|
-
lintFile(file, basename(file) !== 'MEMORY.md', findings, warnings, corpusNames, fallbackName);
|
|
233
|
+
lintFile(file, basename(file) !== 'MEMORY.md', findings, warnings, corpusNames, fallbackName, scope);
|
|
342
234
|
}
|
|
343
235
|
};
|
|
344
236
|
for (const root of projectScopeRoots()) {
|
|
345
|
-
lintDir(join(root, 'memory'));
|
|
237
|
+
lintDir(join(root, 'memory'), 'project');
|
|
346
238
|
for (const plugin of listInstalledPluginsInRoot('project', root)) {
|
|
347
239
|
if (plugin.enabled)
|
|
348
|
-
lintDir(pluginMemoryDir(plugin));
|
|
240
|
+
lintDir(pluginMemoryDir(plugin), 'project');
|
|
349
241
|
}
|
|
350
242
|
}
|
|
351
243
|
// Nested descendant stores: same schema gate, plus a boot-entry warning —
|
|
@@ -353,7 +245,7 @@ export const lintLeaf = defineLeaf({
|
|
|
353
245
|
// surfaces entry there is inert.
|
|
354
246
|
for (const root of descendantStoreRoots(projectScopeRoots())) {
|
|
355
247
|
const dir = join(root, 'memory');
|
|
356
|
-
lintDir(dir);
|
|
248
|
+
lintDir(dir, 'project');
|
|
357
249
|
if (!pathExists(dir))
|
|
358
250
|
continue;
|
|
359
251
|
for (const file of walkFiles(dir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
|
|
@@ -372,10 +264,10 @@ export const lintLeaf = defineLeaf({
|
|
|
372
264
|
}
|
|
373
265
|
}
|
|
374
266
|
for (const scope of ['user', 'builtin']) {
|
|
375
|
-
lintDir(scopeMemoryDir(scope));
|
|
267
|
+
lintDir(scopeMemoryDir(scope), scope);
|
|
376
268
|
for (const plugin of listInstalledPlugins(scope)) {
|
|
377
269
|
if (plugin.enabled)
|
|
378
|
-
lintDir(pluginMemoryDir(plugin));
|
|
270
|
+
lintDir(pluginMemoryDir(plugin), scope);
|
|
379
271
|
}
|
|
380
272
|
}
|
|
381
273
|
// Profile coverage: each managed project has exactly ONE workspace front
|
|
@@ -390,7 +282,7 @@ export const lintLeaf = defineLeaf({
|
|
|
390
282
|
try {
|
|
391
283
|
const { profileId, manifest } = loadProfileManifest(profileIdOrName);
|
|
392
284
|
profileResolved = true;
|
|
393
|
-
lintDir(profileMemoryDir(profileId));
|
|
285
|
+
lintDir(profileMemoryDir(profileId), 'user');
|
|
394
286
|
for (const dir of manifest.projects) {
|
|
395
287
|
profileProjects += 1;
|
|
396
288
|
const doors = frontDoorDocs(join(dir, '.crouter', 'memory'));
|
|
@@ -9,6 +9,7 @@ export interface ListedMemoryDoc {
|
|
|
9
9
|
path: string;
|
|
10
10
|
shortForm: string;
|
|
11
11
|
slash: boolean;
|
|
12
|
+
extensions: Record<string, Record<string, string | boolean | number>>;
|
|
12
13
|
}
|
|
13
14
|
export declare function listMemoryDocs(kindFilter?: string, scopeFilter?: MemoryScope, quiet?: boolean, docs?: readonly MemoryDoc[]): ListedMemoryDoc[];
|
|
14
15
|
export declare const listLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
3
|
+
import { memoryExtensionEffectiveCatalog, projectEffectiveMemoryExtensions } from '../../core/memory/extensions.js';
|
|
4
|
+
import { renderResult } from '../../core/render.js';
|
|
3
5
|
import { parseSubstrateDoc } from '../../core/substrate/schema.js';
|
|
4
6
|
import { paginate } from '../../core/pagination.js';
|
|
5
7
|
import { MEMORY_KINDS, MEMORY_SCOPES, scopeRank } from './shared.js';
|
|
@@ -20,7 +22,15 @@ export function listMemoryDocs(kindFilter, scopeFilter, quiet = false, docs = li
|
|
|
20
22
|
continue;
|
|
21
23
|
if (kindFilter !== undefined && sub.kind !== kindFilter)
|
|
22
24
|
continue;
|
|
23
|
-
addItem({
|
|
25
|
+
addItem({
|
|
26
|
+
name: sub.name,
|
|
27
|
+
kind: sub.kind,
|
|
28
|
+
scope: sub.scope,
|
|
29
|
+
path: sub.path,
|
|
30
|
+
shortForm: sub.shortForm,
|
|
31
|
+
slash: sub.slash,
|
|
32
|
+
extensions: projectEffectiveMemoryExtensions(doc.frontmatter?.['extensions'], memoryExtensionEffectiveCatalog(doc)),
|
|
33
|
+
});
|
|
24
34
|
}
|
|
25
35
|
items.sort((a, b) => {
|
|
26
36
|
const sr = scopeRank(a.scope) - scopeRank(b.scope);
|
|
@@ -48,7 +58,7 @@ export const listLeaf = defineLeaf({
|
|
|
48
58
|
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
49
59
|
],
|
|
50
60
|
output: [
|
|
51
|
-
{ name: 'items', type: 'object[]', required: true, constraint: 'One row per document: {name, short_form, kind, scope}. path is included only with --paths. Every name is readable as-is with `crtr memory read` (a directory segment of a name is readable too — it answers with the directory listing). short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
|
|
61
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'One row per document: {name, short_form, kind, scope, extensions}. extensions carries effective valid plugin-owned metadata; enabled defaults appear here without becoming frontmatter, while disabled and unresolved namespaces are omitted. path is included only with --paths. Every name is readable as-is with `crtr memory read` (a directory segment of a name is readable too — it answers with the directory listing). short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
|
|
52
62
|
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'Opaque token for the next page; null means no more items.' },
|
|
53
63
|
{ name: 'total', type: 'integer | null', required: true, constraint: 'Exact total across all pages when cheap to compute (always cheap here); never null in practice for this leaf.' },
|
|
54
64
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read a document in full or narrow the inventory.' },
|
|
@@ -72,6 +82,7 @@ export const listLeaf = defineLeaf({
|
|
|
72
82
|
short_form: d.shortForm,
|
|
73
83
|
kind: d.kind,
|
|
74
84
|
scope: d.scope,
|
|
85
|
+
extensions: d.extensions,
|
|
75
86
|
...(includePaths ? { path: d.path } : {}),
|
|
76
87
|
})),
|
|
77
88
|
next_cursor: result.next_cursor,
|
|
@@ -79,4 +90,8 @@ export const listLeaf = defineLeaf({
|
|
|
79
90
|
follow_up: 'Read one in full with `crtr memory read <name>`, or revise one with `crtr memory edit <name> --rationale "<why>"` (body, routing, frontmatter, and visibility all go through that one verb, and it records the change). Narrow with --kind / --scope, page with --cursor, or search a topic with `crtr memory find <query>`.',
|
|
80
91
|
};
|
|
81
92
|
},
|
|
93
|
+
render: (result) => {
|
|
94
|
+
const items = result['items'].map(({ extensions: _extensions, ...item }) => item);
|
|
95
|
+
return renderResult({ ...result, items }, listLeaf.help);
|
|
96
|
+
},
|
|
82
97
|
});
|
|
@@ -2,6 +2,7 @@ import { CrtrClient } from '../../api/index.js';
|
|
|
2
2
|
import { interpolateNodePaths } from '../../core/canvas/paths.js';
|
|
3
3
|
import { defineLeaf } from '../../core/command.js';
|
|
4
4
|
import { CrtrError, notFound } from '../../core/errors.js';
|
|
5
|
+
import { memoryExtensionEffectiveCatalog, projectEffectiveMemoryExtensions } from '../../core/memory/extensions.js';
|
|
5
6
|
import { readText, realpathOrSelf } from '../../core/fs-utils.js';
|
|
6
7
|
import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
7
8
|
import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
|
|
@@ -10,6 +11,7 @@ import { expandShellBlocks, hasShellBlocks, makeNodeShellRunner } from '../../co
|
|
|
10
11
|
import { deliveredAtOrAbove, loadInjectedDocs, recordDelivery, saveInjectedDocs } from '../../core/substrate/injected-store.js';
|
|
11
12
|
import { ancestorDirsOf, dirDedupKey, docsByName, isDirName, renderDirListing } from '../../core/substrate/listings.js';
|
|
12
13
|
import { memoryReadDocBlocks } from '../../core/substrate/on-read.js';
|
|
14
|
+
import { renderResult } from '../../core/render.js';
|
|
13
15
|
import { effectiveDocKind, normalizeDocName } from '../../core/substrate/schema.js';
|
|
14
16
|
import { MEMORY_KINDS } from './shared.js';
|
|
15
17
|
export { createMemoryDocSnapshot, resolveMemoryDocs };
|
|
@@ -72,8 +74,9 @@ export const readLeaf = defineLeaf({
|
|
|
72
74
|
{ 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
75
|
{ 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
76
|
{ 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>`.' },
|
|
77
|
+
{ name: 'extensions', type: 'object', required: false, constraint: 'Effective valid plugin-owned metadata, keyed by plugin namespace then field. Includes enabled declaration defaults without writing them to frontmatter; disabled and unresolved namespaces are omitted. Present only on a document read.' },
|
|
75
78
|
{ 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.' },
|
|
76
|
-
{ name: 'follow_up', type: 'string', required:
|
|
79
|
+
{ name: 'follow_up', type: 'string', required: false, constraint: 'Present on a directory read, or on a document read whose body links to further memory documents.' },
|
|
77
80
|
],
|
|
78
81
|
outputKind: 'object',
|
|
79
82
|
effects: [
|
|
@@ -191,11 +194,17 @@ export const readLeaf = defineLeaf({
|
|
|
191
194
|
scope: doc.scope,
|
|
192
195
|
path: doc.path,
|
|
193
196
|
content: finalContent,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
?
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
extensions: projectEffectiveMemoryExtensions(doc.frontmatter?.['extensions'], memoryExtensionEffectiveCatalog(doc)),
|
|
198
|
+
...(links.length > 0
|
|
199
|
+
? {
|
|
200
|
+
links,
|
|
201
|
+
follow_up: 'The `[[name]]` links in the body are further reading — follow one with `crtr memory read <name>` only when the task needs that depth.',
|
|
202
|
+
}
|
|
203
|
+
: {}),
|
|
199
204
|
};
|
|
200
205
|
},
|
|
206
|
+
render: (result) => {
|
|
207
|
+
const { extensions: _extensions, ...withoutExtensions } = result;
|
|
208
|
+
return renderResult(withoutExtensions, readLeaf.help);
|
|
209
|
+
},
|
|
201
210
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { MemoryScope } from '../../core/memory-resolver.js';
|
|
1
|
+
import { type FlagParam } from '../../core/help.js';
|
|
2
|
+
import type { MemoryDoc, MemoryScope } from '../../core/memory-resolver.js';
|
|
3
|
+
import { type MemoryExtensionCatalog } from '../../core/memory/extensions.js';
|
|
4
|
+
import type { MemoryExtensionScalar } from '../../types.js';
|
|
3
5
|
export declare const MEMORY_KINDS: readonly ["knowledge", "preference"];
|
|
4
6
|
export declare const MEMORY_SCOPES: readonly ["user", "project", "profile", "node"];
|
|
5
7
|
/** Scope sort weight matching resolution precedence (node > project stack >
|
|
@@ -64,6 +66,33 @@ export declare function literalBodySegment(source: string): string;
|
|
|
64
66
|
* untransformed. Creation uses `serializeMemoryDoc`, which normalizes a first
|
|
65
67
|
* body into the canonical shape. */
|
|
66
68
|
export declare function serializeMemoryDocLiteral(frontmatter: Record<string, unknown>, bodySegment: string): string;
|
|
69
|
+
export interface RequestedExtensionSet {
|
|
70
|
+
path: string;
|
|
71
|
+
namespace: string;
|
|
72
|
+
field: string;
|
|
73
|
+
value: MemoryExtensionScalar;
|
|
74
|
+
}
|
|
75
|
+
interface RequestedExtensionPath {
|
|
76
|
+
path: string;
|
|
77
|
+
namespace: string;
|
|
78
|
+
field: string;
|
|
79
|
+
}
|
|
80
|
+
/** Resolve declarations at the same scope and path a document will use. The
|
|
81
|
+
* catalog intentionally includes disabled plugins: they still authorize typed
|
|
82
|
+
* edits to values that stay inert until their plugin is enabled again. */
|
|
83
|
+
export declare function memoryExtensionCatalogForDoc(doc: Pick<MemoryDoc, 'scope' | 'path'>): MemoryExtensionCatalog;
|
|
84
|
+
/** A bounded declaration catalog for the two authoring leaves. The manifest is
|
|
85
|
+
* the source for both the type and operation-specific guidance. */
|
|
86
|
+
export declare function memoryExtensionFieldCatalogHelp(operation: 'write' | 'edit'): string | null;
|
|
87
|
+
/** Parse every requested extension mutation before the caller constructs any
|
|
88
|
+
* frontmatter. Set values are deliberately declaration-typed rather than YAML. */
|
|
89
|
+
export declare function parseRequestedExtensionChanges(setRaw: readonly string[] | undefined, unsetRaw: readonly string[] | undefined, catalog: MemoryExtensionCatalog): {
|
|
90
|
+
sets: RequestedExtensionSet[];
|
|
91
|
+
unsets: RequestedExtensionPath[];
|
|
92
|
+
};
|
|
93
|
+
/** Apply only requested extension paths. Existing unknown namespaces remain
|
|
94
|
+
* untouched; an unset prunes the empty namespace and empty root mapping. */
|
|
95
|
+
export declare function applyExtensionChanges(frontmatter: Record<string, unknown>, sets: readonly RequestedExtensionSet[], unsets: readonly RequestedExtensionPath[]): void;
|
|
67
96
|
/** The frontmatter-overlay flags, keyed by flag name, carrying the prose true
|
|
68
97
|
* on BOTH leaves. `write` appends its creation clauses with `withConstraint`;
|
|
69
98
|
* neither leaf restates the other's rules. */
|
|
@@ -80,3 +109,4 @@ export declare const GUIDE_SURFACES = "Every doc appears in its directory\u2019s
|
|
|
80
109
|
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 when-clause: it names an observable circumstance in the reader\u2019s current work, not the doc\u2019s topic reworded as an activity. If the WHEN can be inferred from the title alone, it is not a real trigger. Bad on a todo list: \"When planning or prioritizing work across this profile.\" Good: \"When the user mentions something from their todos, or asks what is still outstanding across this profile.\" 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.";
|
|
81
110
|
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
111
|
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.";
|
|
112
|
+
export {};
|