@north-light/crouter 0.3.216 → 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 +516 -516
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +2 -2
- package/dist/clients/inbox/tui/input.js +12 -8
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +9 -3
- 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 +33 -3
- package/dist/commands/memory/shared.js +180 -1
- 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/__tests__/human-cancel-guard.test.js +30 -12
- 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/__tests__/page.test.js +12 -0
- package/dist/core/human/component-docs.js +6 -1
- package/dist/core/human/page-schema.d.ts +14 -0
- package/dist/core/human/page-schema.js +24 -7
- package/dist/core/human/page.d.ts +2 -0
- package/dist/core/human/page.js +17 -6
- package/dist/core/human/scan.d.ts +5 -4
- package/dist/core/human/scan.js +7 -4
- package/dist/core/human/tickets.js +9 -2
- 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
|
@@ -14,8 +14,8 @@ optionsPanel.handleKey('', enter);
|
|
|
14
14
|
optionsPanel.handleKey('y', key());
|
|
15
15
|
assert.deepEqual(optionResponses, { choice: { selectedOptionIds: ['yes'], comments: [{ id: 'option:yes', anchor: { kind: 'option', optionId: 'yes' }, text: 'ship it' }] } });
|
|
16
16
|
optionsPanel.unmount();
|
|
17
|
-
//
|
|
18
|
-
const multi = manifest([{ id: 'toppings', kind: 'options', step: 0, config: { mode: 'multi', options: [{ id: 'mush', label: 'Mushroom' }, { id: 'onion', label: 'Onion' }] } }]);
|
|
17
|
+
// A required multi-select refuses an empty Enter; selection confirmation lands on Summary before submit.
|
|
18
|
+
const multi = manifest([{ id: 'toppings', kind: 'options', step: 0, config: { mode: 'multi', required: true, options: [{ id: 'mush', label: 'Mushroom' }, { id: 'onion', label: 'Onion' }] } }]);
|
|
19
19
|
let multiResponses;
|
|
20
20
|
const multiPanel = mountPanel({ manifest: multi, cols: 80, rows: 24, onComplete: (r) => { multiResponses = r; } });
|
|
21
21
|
multiPanel.handleKey('', enter);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isResponseBearingSlot } from '../../../core/human/page-schema.js';
|
|
2
2
|
import { panItemReview, verticalCursor, wordLeftIndex, wordRightIndex } from './render.js';
|
|
3
|
-
import { commentAnchors, commentIdFor, pageComplete, slotActions, slotAnswered, terminalAnswerable } from './slots.js';
|
|
3
|
+
import { commentAnchors, commentIdFor, pageComplete, responseAnswers, slotActions, slotAnswered, terminalAnswerable } from './slots.js';
|
|
4
4
|
/** Only a single-mode options/cards pick jumps ahead; a table pick leaves the user on the table. */
|
|
5
5
|
const autoAdvances = (slot, mode) => mode === 'single' && (slot.kind === 'options' || slot.kind === 'cards');
|
|
6
6
|
export function handleKeypress(input, key, state, render, exit) {
|
|
@@ -143,18 +143,22 @@ function handleItemReview(input, key, state, render, exit) {
|
|
|
143
143
|
render();
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
|
-
if (slot.kind === 'table' && terminalAnswerable(slot)) {
|
|
147
|
-
const r = responseFor(slot, state.responses.get(slot.id));
|
|
148
|
-
setResponse(state, slot, r);
|
|
149
|
-
advanceToNextIncomplete(state);
|
|
150
|
-
render();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
146
|
if (slotAnswered(state, slot)) {
|
|
154
147
|
advanceToNextIncomplete(state);
|
|
155
148
|
render();
|
|
156
149
|
return;
|
|
157
150
|
}
|
|
151
|
+
// Enter settles a question whose current value already answers it: an optional picker
|
|
152
|
+
// left empty is a real answer, not a skipped one.
|
|
153
|
+
if (terminalAnswerable(slot)) {
|
|
154
|
+
const settled = responseFor(slot, state.responses.get(slot.id));
|
|
155
|
+
if (responseAnswers(state, slot, settled)) {
|
|
156
|
+
setResponse(state, slot, settled);
|
|
157
|
+
advanceToNextIncomplete(state);
|
|
158
|
+
render();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
158
162
|
if (isResponseBearingSlot(slot)) {
|
|
159
163
|
state.hint = terminalAnswerable(slot) ? 'Select at least one option (space to toggle), or q to go back' : 'This must be answered in Northlight';
|
|
160
164
|
render();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CommentAnchor, PageManifest, PageSlot, TableConfig } from '../../../core/human/page-schema.js';
|
|
1
|
+
import type { CommentAnchor, PageManifest, PageSlot, SlotResponse, TableConfig } from '../../../core/human/page-schema.js';
|
|
2
2
|
import type { TuiState } from './types.js';
|
|
3
3
|
export type SelectGroup = 'option' | 'card' | 'row' | 'column';
|
|
4
4
|
export type ActionRow = {
|
|
@@ -24,6 +24,8 @@ export declare function describeSlot(slot: PageSlot): string;
|
|
|
24
24
|
export declare function terminalAnswerable(slot: PageSlot): boolean;
|
|
25
25
|
export declare function slotActions(slot: PageSlot): ActionRow[];
|
|
26
26
|
export declare function tableMarkdown(config: TableConfig): string;
|
|
27
|
+
/** Whether this value would be accepted as the slot's answer — the same reading crtrd applies on submit. */
|
|
28
|
+
export declare function responseAnswers(state: Pick<TuiState, 'manifest'>, slot: PageSlot, response: SlotResponse): boolean;
|
|
27
29
|
export declare function slotAnswered(state: TuiState, slot: PageSlot): boolean;
|
|
28
30
|
export declare function pageComplete(state: Pick<TuiState, 'manifest' | 'responses'>): boolean;
|
|
29
31
|
export declare function commentIdFor(anchor: CommentAnchor): string;
|
|
@@ -88,17 +88,23 @@ export function tableMarkdown(config) {
|
|
|
88
88
|
const divider = `| ${config.columns.map((column) => column.align === 'right' ? '---:' : '---').join(' | ')} |`;
|
|
89
89
|
return [header, divider, ...(config.rows ?? []).map((row) => `| ${config.columns.map((column) => escapeCell(row.cells[column.id], column.mono === true)).join(' | ')} |`)].join('\n');
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
/** Whether this value would be accepted as the slot's answer — the same reading crtrd applies on submit. */
|
|
92
|
+
export function responseAnswers(state, slot, response) {
|
|
93
|
+
if (slot.id === undefined)
|
|
93
94
|
return false;
|
|
94
95
|
try {
|
|
95
|
-
validatePartialPageResponses(state.manifest, { [slot.id]:
|
|
96
|
+
validatePartialPageResponses(state.manifest, { [slot.id]: response });
|
|
96
97
|
return true;
|
|
97
98
|
}
|
|
98
99
|
catch {
|
|
99
100
|
return false;
|
|
100
101
|
}
|
|
101
102
|
}
|
|
103
|
+
export function slotAnswered(state, slot) {
|
|
104
|
+
if (!terminalAnswerable(slot) || slot.id === undefined || !state.responses.has(slot.id))
|
|
105
|
+
return false;
|
|
106
|
+
return responseAnswers(state, slot, state.responses.get(slot.id));
|
|
107
|
+
}
|
|
102
108
|
export function pageComplete(state) {
|
|
103
109
|
if (state.manifest.slots.some((slot) => isResponseBearingSlot(slot) && !terminalAnswerable(slot)))
|
|
104
110
|
return false;
|
|
@@ -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. */
|
|
@@ -77,6 +106,7 @@ export declare function overlayParam(name: string, overrides?: Partial<FlagParam
|
|
|
77
106
|
* happening. Same field, same prose, two flag names that cannot be confused. */
|
|
78
107
|
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.";
|
|
79
108
|
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.";
|
|
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.";
|
|
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 {};
|