@north-light/crouter 0.3.227 → 0.3.228
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 -5
- package/dist/api/client.js +3 -3
- package/dist/api/dto/broker-ops.d.ts +8 -0
- package/dist/api/dto/memory.d.ts +8 -7
- package/dist/api/dto/memory.js +2 -2
- package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
- package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
- package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
- package/dist/builtin-memory/design/guide.md +38 -16
- package/dist/builtin-memory/design/roadmap.md +4 -4
- package/dist/builtin-memory/insights/init.md +5 -5
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +6 -6
- package/dist/builtin-memory/internal/plugins.md +1 -1
- package/dist/builtin-memory/plan/guide.md +53 -0
- package/dist/builtin-memory/plan/roadmap.md +10 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
- package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
- package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
- package/dist/clients/attach/chrome/inbox-strip.js +11 -7
- package/dist/clients/attach/session/pane-tag.d.ts +28 -2
- package/dist/clients/attach/session/pane-tag.js +48 -22
- package/dist/clients/attach/session/profile-files.d.ts +6 -0
- package/dist/clients/attach/session/profile-files.js +44 -11
- package/dist/clients/attach/slash/dispatch.js +4 -7
- package/dist/clients/attach/viewer.js +588 -596
- package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
- package/dist/commands/api-client.d.ts +3 -7
- package/dist/commands/api-client.js +7 -12
- package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
- package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
- package/dist/commands/memory/delete.js +51 -24
- package/dist/commands/memory/edit.js +50 -10
- package/dist/commands/memory/find.js +105 -56
- package/dist/commands/memory/history.js +51 -31
- package/dist/commands/memory/lint.d.ts +0 -9
- package/dist/commands/memory/lint.js +78 -322
- package/dist/commands/memory/list.d.ts +12 -4
- package/dist/commands/memory/list.js +64 -25
- package/dist/commands/memory/move.js +103 -73
- package/dist/commands/memory/origin.js +35 -5
- package/dist/commands/memory/read.d.ts +4 -0
- package/dist/commands/memory/read.js +132 -80
- package/dist/commands/memory/shared.d.ts +115 -25
- package/dist/commands/memory/shared.js +331 -74
- package/dist/commands/memory/write.js +72 -21
- package/dist/commands/memory.js +2 -2
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/commands/pkg/market-manage.js +25 -24
- package/dist/commands/pkg/plugin-manage.d.ts +22 -6
- package/dist/commands/pkg/plugin-manage.js +118 -25
- package/dist/commands/pkg/shared.d.ts +8 -0
- package/dist/commands/pkg/shared.js +23 -20
- package/dist/commands/surface-reload.d.ts +5 -0
- package/dist/commands/surface-reload.js +9 -1
- package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
- package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
- package/dist/commands/sys/migrate.js +126 -140
- package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
- package/dist/commands/sys/panels/profiles-panel.js +599 -0
- package/dist/commands/sys/settings-shell.d.ts +4 -2
- package/dist/commands/sys/settings-shell.js +35 -4
- package/dist/commands/sys/settings.js +21 -5
- package/dist/commands/sys/sync-deps.d.ts +2 -0
- package/dist/commands/sys/sync-deps.js +26 -15
- package/dist/commands/sys/sync-project-guidance.js +228 -145
- package/dist/commands/sys/sync-skills.js +28 -17
- package/dist/commands/sys/update.js +11 -3
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
- package/dist/core/__tests__/config-change-delta.test.js +106 -0
- package/dist/core/__tests__/context-intro.test.js +13 -9
- package/dist/core/__tests__/daemon-boot.test.js +22 -16
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
- package/dist/core/__tests__/human-deliver.test.js +19 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
- package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
- package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
- package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
- package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
- package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
- package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
- package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
- package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
- package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
- package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
- package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
- package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
- package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
- package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
- package/dist/core/__tests__/kickoff.test.js +16 -5
- package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
- package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
- package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
- package/dist/core/__tests__/repository-association.test.d.ts +1 -0
- package/dist/core/__tests__/repository-association.test.js +153 -0
- package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
- package/dist/core/__tests__/repository-root-identity.test.js +219 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
- package/dist/core/canvas/canvas.d.ts +3 -0
- package/dist/core/canvas/canvas.js +30 -10
- package/dist/core/canvas/db.js +28 -1
- package/dist/core/canvas/paths.d.ts +4 -6
- package/dist/core/canvas/paths.js +8 -6
- package/dist/core/canvas/render-source.js +2 -2
- package/dist/core/canvas/types.d.ts +9 -13
- package/dist/core/exclusive-lock.d.ts +2 -0
- package/dist/core/exclusive-lock.js +21 -0
- package/dist/core/git.d.ts +0 -1
- package/dist/core/git.js +0 -3
- package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
- package/dist/core/human/feedback-companion.js +3 -0
- package/dist/core/human/scan.js +8 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/memory/doc-link-grammar.js +2 -2
- package/dist/core/memory/history.d.ts +24 -0
- package/dist/core/memory/history.js +66 -1
- package/dist/core/memory/identity.d.ts +65 -0
- package/dist/core/memory/identity.js +185 -0
- package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
- package/dist/core/memory/inline-ref-guidance.js +1 -1
- package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
- package/dist/core/memory/inline-ref-inventory.js +30 -62
- package/dist/core/memory/lint.d.ts +129 -0
- package/dist/core/memory/lint.js +517 -0
- package/dist/core/memory/project-namespace.d.ts +31 -0
- package/dist/core/memory/project-namespace.js +85 -0
- package/dist/core/memory/repository-association.d.ts +31 -0
- package/dist/core/memory/repository-association.js +129 -0
- package/dist/core/memory/tree.d.ts +39 -0
- package/dist/core/memory/tree.js +93 -0
- package/dist/core/memory-resolver.d.ts +193 -90
- package/dist/core/memory-resolver.js +461 -375
- package/dist/core/nested-stores.js +6 -13
- package/dist/core/profiles/select.d.ts +4 -1
- package/dist/core/profiles/select.js +108 -48
- package/dist/core/review/__tests__/capture-origin.test.js +2 -2
- package/dist/core/review/__tests__/stage-identity.test.js +2 -2
- package/dist/core/review/companion.js +11 -2
- package/dist/core/runtime/bearings.d.ts +2 -2
- package/dist/core/runtime/bearings.js +3 -3
- package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
- package/dist/core/runtime/broker/rebind.js +5 -0
- package/dist/core/runtime/broker-extension-render.d.ts +7 -3
- package/dist/core/runtime/broker-extension-render.js +10 -5
- package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
- package/dist/core/runtime/broker-persona-guidance.js +122 -27
- package/dist/core/runtime/deliver-live.d.ts +16 -4
- package/dist/core/runtime/deliver-live.js +29 -15
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +8 -16
- package/dist/core/runtime/lifecycle.js +2 -3
- package/dist/core/runtime/nodes.d.ts +3 -4
- package/dist/core/runtime/nodes.js +3 -4
- package/dist/core/runtime/persona.d.ts +8 -12
- package/dist/core/runtime/persona.js +24 -96
- package/dist/core/runtime/promote.d.ts +2 -2
- package/dist/core/runtime/promote.js +8 -21
- package/dist/core/runtime/revive.js +20 -17
- package/dist/core/runtime/spawn.js +18 -7
- package/dist/core/runtime/tmux-bindings.js +2 -3
- package/dist/core/runtime/warm-pool.js +3 -4
- package/dist/core/scope.js +2 -0
- package/dist/core/self-update.d.ts +0 -2
- package/dist/core/self-update.js +2 -35
- package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
- package/dist/core/substrate/index.d.ts +2 -2
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +43 -27
- package/dist/core/substrate/injected-store.js +208 -104
- package/dist/core/substrate/listings.d.ts +19 -12
- package/dist/core/substrate/listings.js +75 -52
- package/dist/core/substrate/on-read-node.d.ts +4 -7
- package/dist/core/substrate/on-read-node.js +6 -8
- package/dist/core/substrate/on-read.d.ts +22 -25
- package/dist/core/substrate/on-read.js +103 -147
- package/dist/core/substrate/render-node.d.ts +4 -7
- package/dist/core/substrate/render-node.js +5 -7
- package/dist/core/substrate/render.d.ts +21 -3
- package/dist/core/substrate/render.js +291 -223
- package/dist/core/substrate/schema.d.ts +1 -13
- package/dist/core/substrate/schema.js +5 -40
- package/dist/core/substrate/session-cache.d.ts +14 -4
- package/dist/core/substrate/session-cache.js +40 -22
- package/dist/core/substrate/surface-match.d.ts +9 -7
- package/dist/core/substrate/surface-match.js +26 -25
- package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
- package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
- package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
- package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +21 -5
- package/dist/daemon/api/handlers/broker-ops.js +21 -16
- package/dist/daemon/api/handlers/memory.js +2 -0
- package/dist/daemon/crtrd.js +2 -0
- package/dist/daemon/human/finish.js +8 -1
- package/dist/daemon/manage.d.ts +0 -1
- package/dist/daemon/manage.js +7 -16
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/migrations/001-surfaces-frontmatter.js +21 -109
- package/dist/migrations/002-profile-project-memory.js +1 -0
- package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
- package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
- package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
- package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
- package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
- package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
- package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
- package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
- package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
- package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
- package/dist/migrations/__tests__/activation.test.d.ts +1 -0
- package/dist/migrations/__tests__/activation.test.js +149 -0
- package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
- package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
- package/dist/migrations/activation.d.ts +16 -0
- package/dist/migrations/activation.js +78 -0
- package/dist/migrations/convergent.d.ts +14 -3
- package/dist/migrations/convergent.js +21 -10
- package/dist/migrations/corpus.d.ts +78 -0
- package/dist/migrations/corpus.js +497 -0
- package/dist/migrations/frontmatter-splice.d.ts +15 -0
- package/dist/migrations/frontmatter-splice.js +176 -0
- package/dist/migrations/registry.d.ts +6 -1
- package/dist/migrations/registry.js +7 -2
- package/dist/migrations/runner.d.ts +41 -0
- package/dist/migrations/runner.js +81 -0
- package/dist/migrations/types.d.ts +148 -9
- package/dist/migrations/types.js +9 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
- package/dist/pi-extensions/canvas-context-intro.js +50 -46
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
- package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
- package/dist/pi-extensions/canvas-stophook.js +6 -13
- package/dist/shared/generated-context.d.ts +0 -3
- package/dist/shared/generated-context.js +0 -57
- package/dist/shared/tool-groups.js +2 -3
- package/package.json +5 -4
- package/runtime.lock.json +2 -2
- /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
- /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
- /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
- /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
- /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
- /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
- /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
- /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
- /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
1
|
import { defineLeaf } from '../../core/command.js';
|
|
3
2
|
import { CrtrError, notFound, usage } from '../../core/errors.js';
|
|
4
|
-
import { nativeMemoryStoresInPrecedence,
|
|
3
|
+
import { nativeMemoryStoresInPrecedence, resolveMemoryDoc, resolveMemoryDocInStore, } from '../../core/memory-resolver.js';
|
|
5
4
|
import { renderResult } from '../../core/render.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
/** Locate a document's revision log. A live doc
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
import { historyLogPathForDoc, readHistoryRecords, resolveDeletedHistoryLogPath, summarizeChange, unifiedDiff, } from '../../core/memory/history.js';
|
|
6
|
+
import { requireCanonicalName, requireLocalNameInStore, requireMountedStore, resolveReadSelector, selectorParam, } from './shared.js';
|
|
7
|
+
import { stripNamespace } from '../../core/memory/identity.js';
|
|
8
|
+
/** Locate a document's revision log. A live doc is resolved to its physical
|
|
9
|
+
* descriptor first; a deleted doc is looked up in the selected store(s) by the
|
|
10
|
+
* exact canonical name, stripping only that store's declared project namespace.
|
|
11
|
+
* There is no scope-prefix or leaf fallback. */
|
|
12
|
+
function resolveLog(nameRaw, selectorInput) {
|
|
13
|
+
const selector = resolveReadSelector(selectorInput);
|
|
14
|
+
if (selector.store !== null)
|
|
15
|
+
requireMountedStore(selector.store);
|
|
15
16
|
let doc;
|
|
16
17
|
try {
|
|
17
|
-
doc =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
18
|
+
doc = selector.store !== null
|
|
19
|
+
? resolveMemoryDocInStore(nameRaw, selector.store)
|
|
20
|
+
: resolveMemoryDoc(nameRaw, { includeDescendants: true, ...(selector.scope ? { scope: selector.scope } : {}) });
|
|
21
21
|
}
|
|
22
22
|
catch (e) {
|
|
23
23
|
if (!(e instanceof CrtrError && e.code === 'not_found'))
|
|
@@ -36,17 +36,20 @@ function resolveLog(nameRaw, scopeArg) {
|
|
|
36
36
|
scope: 'builtin',
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
-
return { name: doc.name, scope: doc.scope, logPath:
|
|
39
|
+
return { name: doc.name, scope: doc.scope, logPath: historyLogPathForDoc(doc) };
|
|
40
40
|
}
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
const canonicalName = requireCanonicalName(nameRaw);
|
|
42
|
+
const stores = selector.store !== null
|
|
43
|
+
? [{ scope: selector.store.scope, memoryDir: selector.store.storeRoot, store: selector.store }]
|
|
44
|
+
: nativeMemoryStoresInPrecedence(selector.scope, true);
|
|
45
|
+
for (const store of stores) {
|
|
46
|
+
if (selector.store === null && store.store.namespace !== '' && stripNamespace(canonicalName, store.store.namespace) === null)
|
|
47
|
+
continue;
|
|
48
|
+
const localName = requireLocalNameInStore(store.store, canonicalName);
|
|
49
|
+
const deleted = resolveDeletedHistoryLogPath(store.memoryDir, localName);
|
|
50
|
+
if (deleted !== null) {
|
|
51
|
+
return { name: canonicalName, scope: store.scope, logPath: deleted.logPath, deleted };
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
throw notFound(`no memory document or revision history found: ${nameRaw}`, {
|
|
52
55
|
memory: nameRaw,
|
|
@@ -64,20 +67,25 @@ export const historyLeaf = defineLeaf({
|
|
|
64
67
|
'A record whose `before` differs from the previous record\u2019s `after` is the evidence of an out-of-band direct file edit; nothing prevents those, and nothing detects them beyond this.\n\n' +
|
|
65
68
|
'The log is a plain JSONL file at `log_path`, one JSON object per line \u2014 an external consumer reads it directly rather than through this command.',
|
|
66
69
|
params: [
|
|
67
|
-
{ kind: 'positional', name: 'name', required: true, constraint: '
|
|
70
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Full canonical memory name (a project document includes its namespace), resolved exactly. Deleted history uses the same canonical name and the selected store\u2019s physical sidecars; there is no leaf or scope-prefix fallback. Builtin and installed-plugin docs are read-only corpora and carry no history.' },
|
|
71
|
+
selectorParam('scope-read'),
|
|
72
|
+
selectorParam('dir'),
|
|
73
|
+
selectorParam('profile'),
|
|
68
74
|
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 10, constraint: 'How many revisions to return, newest first. Default 10.' },
|
|
69
75
|
{ kind: 'flag', name: 'diff', type: 'bool', required: false, constraint: 'Include a unified diff per listed revision, computed from that record\u2019s own before/after texts.' },
|
|
70
76
|
{ kind: 'flag', name: 'revision', type: 'int', required: false, constraint: 'Show ONE revision in full \u2014 its metadata plus the complete before and after document texts. 1-based position in the log: the number `crtr memory edit` reports. Overrides --limit and --diff.' },
|
|
71
|
-
|
|
77
|
+
// Scope, --dir, and --profile are shared selector contracts above.
|
|
72
78
|
],
|
|
73
79
|
output: [
|
|
74
80
|
{ name: 'name', type: 'string', required: true, constraint: 'Resolved document name.' },
|
|
75
81
|
{ name: 'scope', type: 'string', required: true, constraint: 'Scope whose store holds the log: node, project, profile, or user.' },
|
|
76
82
|
{ name: 'log_path', type: 'string', required: true, constraint: 'Absolute path to the append-only JSONL log \u2014 the direct-consumption pointer, one self-contained record per line.' },
|
|
83
|
+
{ name: 'history_candidates', type: 'object[]', required: false, constraint: 'Deleted-history physical candidates: {path, representation, winner}. The directory-index form is listed first and wins deterministically.' },
|
|
84
|
+
{ name: 'history_collision', type: 'object', required: false, constraint: 'Present when deleted history has both physical forms: {paths, winner}.' },
|
|
77
85
|
{ name: 'total', type: 'number', required: true, constraint: 'Total revisions in the log.' },
|
|
78
86
|
{ name: 'revisions', type: 'object[]', required: false, constraint: 'Newest-first, capped by --limit. Each: {revision, op, at, node, rationale, body_changed, frontmatter_changed, diff}. `node` and `rationale` are present only when recorded; `diff` only with --diff. Absent when --revision selects one record.' },
|
|
79
87
|
{ name: 'record', type: 'object', required: false, constraint: 'Present only with --revision: {revision, op, at, node, cwd, rationale, verbatim, before, after} \u2014 the full record including both complete document texts.' },
|
|
80
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands \u2014 the flag variants and the doc read.' },
|
|
88
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands \u2014 the flag variants and the doc read, or, for a deleted document, the revision carrying its retained final text.' },
|
|
81
89
|
],
|
|
82
90
|
outputKind: 'object',
|
|
83
91
|
effects: ['None. Read-only.'],
|
|
@@ -123,10 +131,16 @@ export const historyLeaf = defineLeaf({
|
|
|
123
131
|
run: async (input) => {
|
|
124
132
|
const nameRaw = input['name'];
|
|
125
133
|
const scopeArg = input['scope'];
|
|
134
|
+
const dirArg = input['dir'];
|
|
135
|
+
const profileArg = input['profile'];
|
|
126
136
|
const limit = input['limit'] ?? 10;
|
|
127
137
|
const wantDiff = input['diff'] === true;
|
|
128
138
|
const revisionArg = input['revision'];
|
|
129
|
-
const { name, scope, logPath } = resolveLog(nameRaw, scopeArg);
|
|
139
|
+
const { name, scope, logPath, deleted } = resolveLog(nameRaw, { scope: scopeArg, dir: dirArg, profile: profileArg });
|
|
140
|
+
const historyMeta = deleted === undefined ? {} : {
|
|
141
|
+
history_candidates: deleted.candidates,
|
|
142
|
+
...(deleted.collision === undefined ? {} : { history_collision: deleted.collision }),
|
|
143
|
+
};
|
|
130
144
|
const records = readHistoryRecords(logPath);
|
|
131
145
|
if (records.length === 0) {
|
|
132
146
|
throw notFound(`no revision history recorded for ${name}`, {
|
|
@@ -146,9 +160,12 @@ export const historyLeaf = defineLeaf({
|
|
|
146
160
|
name,
|
|
147
161
|
scope,
|
|
148
162
|
log_path: logPath,
|
|
163
|
+
...historyMeta,
|
|
149
164
|
total: records.length,
|
|
150
165
|
record: { revision: revisionArg, ...r },
|
|
151
|
-
follow_up:
|
|
166
|
+
follow_up: deleted === undefined
|
|
167
|
+
? `Read the document as it stands with \`crtr memory read ${name}\`, or list the revisions with \`crtr memory history ${name} --diff\`.`
|
|
168
|
+
: `${name} is deleted — no read answers to it. This record's \`before\` is its text at that revision; the last revision (${records.length}) carries the text it was deleted with. List the revisions with \`crtr memory history ${name} --diff\`.`,
|
|
152
169
|
};
|
|
153
170
|
}
|
|
154
171
|
const revisions = records
|
|
@@ -171,9 +188,12 @@ export const historyLeaf = defineLeaf({
|
|
|
171
188
|
name,
|
|
172
189
|
scope,
|
|
173
190
|
log_path: logPath,
|
|
191
|
+
...historyMeta,
|
|
174
192
|
total: records.length,
|
|
175
193
|
revisions,
|
|
176
|
-
follow_up:
|
|
194
|
+
follow_up: deleted === undefined
|
|
195
|
+
? `Add --diff for per-revision unified diffs, --revision N for one record's full before/after text, or read the document as it stands with \`crtr memory read ${name}\`.`
|
|
196
|
+
: `${name} is deleted and only this log survives — no read answers to it. Add --diff for per-revision unified diffs, or recover its final text with \`crtr memory history ${name} --revision ${records.length}\`, whose \`before\` is the document as it stood when deleted.`,
|
|
177
197
|
};
|
|
178
198
|
},
|
|
179
199
|
});
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export { lintSubstrateFrontmatter as lintSubstrateSchema } from '../../core/substrate/frontmatter-validation.js';
|
|
2
|
-
/** Warn when a body short enough to inline still pays a routing line. */
|
|
3
|
-
export declare function lintShortPreviewBody(fm: Record<string, unknown>, body: string): string | null;
|
|
4
|
-
/** The length rule. Caps are computed from the parsed entries — an invalid
|
|
5
|
-
* entry already fails the schema check, so it simply matches no cap here.
|
|
6
|
-
* `docName` is the doc's canonical resolver identity (explicit frontmatter
|
|
7
|
-
* `name`, else the normalized path-derived name) — persona layers under
|
|
8
|
-
* `kinds/` are recognized by it and never capped. */
|
|
9
|
-
export declare function lintBodyLength(fm: Record<string, unknown>, body: string, docName: string): string | null;
|
|
10
1
|
export declare const lintLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -1,341 +1,97 @@
|
|
|
1
|
-
// `crtr memory lint` — the
|
|
2
|
-
// (
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
import { basename, join, relative, sep } from 'node:path';
|
|
1
|
+
// `crtr memory lint` — the selector/help wrapper over the memory lint engine
|
|
2
|
+
// (`core/memory/lint.ts`). Every rule lives in the engine, which migration
|
|
3
|
+
// verification, the plugin candidate gate, and the build's builtin gate consume
|
|
4
|
+
// too; this leaf only picks WHAT to lint (a target view, or one exact store
|
|
5
|
+
// with --dir) and shapes the findings for the CLI.
|
|
7
6
|
import { defineLeaf } from '../../core/command.js';
|
|
8
|
-
import { general } from '../../core/errors.js';
|
|
9
7
|
import { warn } from '../../core/output.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* Words, not lines: house style writes each paragraph as ONE logical line
|
|
25
|
-
* and lets the editor soft-wrap, so a line count measures wrapping style
|
|
26
|
-
* rather than context cost — words track what the reader actually pays.
|
|
27
|
-
*
|
|
28
|
-
* A `boot` entry at `content` inlines the whole body into every agent's
|
|
29
|
-
* system prompt; a `content` entry on any other event (workspace-open, read,
|
|
30
|
-
* memory-read, command) surfaces the whole body whenever the entry fires —
|
|
31
|
-
* both cap at 1000 words. A `boot` entry at `preview` routes a deliberate
|
|
32
|
-
* reader into the whole body, so its cap is the longest doc worth reading
|
|
33
|
-
* end-to-end (calibrated to the humanizer doc, 2936 words). `name` entries
|
|
34
|
-
* and surface-less docs never cap: such a doc is only reached deliberately
|
|
35
|
-
* (a listing, browse, or a [[link]]), so its length is the reader's choice.
|
|
36
|
-
* The strictest applicable cap wins.
|
|
37
|
-
*
|
|
38
|
-
* Persona layers — canonical names under `kinds/`, the docs the prompt
|
|
39
|
-
* render composes into an agent's persona — are structurally exempt: an
|
|
40
|
-
* agent reads its whole persona by construction, so persona length is a
|
|
41
|
-
* persona-design choice, not an authoring smell. Suppression elsewhere is
|
|
42
|
-
* deliberate and per-doc: `lint-ignore: length` in the frontmatter,
|
|
43
|
-
* surfaced ONLY by the finding itself — never advertised in authoring
|
|
44
|
-
* help. */
|
|
45
|
-
const BOOT_CONTENT_MAX_WORDS = 1000;
|
|
46
|
-
const EVENT_CONTENT_MAX_WORDS = 1000;
|
|
47
|
-
const BOOT_PREVIEW_MAX_WORDS = 3000;
|
|
48
|
-
/** A routing line costs more than it saves for a short rule or fact. */
|
|
49
|
-
const PREVIEW_TO_CONTENT_MAX_WORDS = 30;
|
|
50
|
-
function ignoresRule(fm, rule) {
|
|
51
|
-
const v = fm['lint-ignore'];
|
|
52
|
-
return v === rule || (Array.isArray(v) && v.includes(rule));
|
|
8
|
+
import { ExitCode } from '../../types.js';
|
|
9
|
+
import { lintMemoryStore, lintMemoryTarget, resolvableCanonicalNames, } from '../../core/memory/lint.js';
|
|
10
|
+
import { loadMemoryTargetView } from '../../core/memory-resolver.js';
|
|
11
|
+
import { resolveReadSelector, selectorParam } from './shared.js';
|
|
12
|
+
function findingRow(finding) {
|
|
13
|
+
return {
|
|
14
|
+
rule: finding.rule,
|
|
15
|
+
path: finding.path,
|
|
16
|
+
message: finding.message,
|
|
17
|
+
...(finding.name === undefined ? {} : { name: finding.name }),
|
|
18
|
+
...(finding.scope === undefined ? {} : { scope: finding.scope }),
|
|
19
|
+
...(finding.storeRoot === undefined ? {} : { store_root: finding.storeRoot }),
|
|
20
|
+
...(finding.paths === undefined ? {} : { paths: [...finding.paths] }),
|
|
21
|
+
};
|
|
53
22
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
23
|
+
/** The canonical names a `--dir` store's own links and routes must resolve in:
|
|
24
|
+
* the target view an agent standing in that project sees. Never the store in
|
|
25
|
+
* isolation — a project doc legitimately links a user or builtin doc. */
|
|
26
|
+
function resolvableNamesForStore(store) {
|
|
27
|
+
const view = loadMemoryTargetView({
|
|
28
|
+
cwd: store.ownerDir ?? store.storeRoot,
|
|
29
|
+
profileId: process.env['CRTR_PROFILE_ID'] || null,
|
|
30
|
+
nodeId: null,
|
|
31
|
+
}, { quiet: true, includeDescendants: true });
|
|
32
|
+
return resolvableCanonicalNames(view.docs);
|
|
57
33
|
}
|
|
58
|
-
/** The doc's surfaces entries as the runtime will read them. Used by the cap
|
|
59
|
-
* checks AFTER the strict schema check has run — for a doc that passes it,
|
|
60
|
-
* the tolerant parse and the strict contract agree. */
|
|
61
|
-
function parsedSurfaces(fm) {
|
|
62
|
-
return parseSubstrateFrontmatter(fm)?.surfaces ?? [];
|
|
63
|
-
}
|
|
64
|
-
/** Warn when a body short enough to inline still pays a routing line. */
|
|
65
|
-
export function lintShortPreviewBody(fm, body) {
|
|
66
|
-
const words = countBodyWords(body);
|
|
67
|
-
if (words >= PREVIEW_TO_CONTENT_MAX_WORDS)
|
|
68
|
-
return null;
|
|
69
|
-
const previewEvents = [...new Set(parsedSurfaces(fm).filter((e) => e.at === 'preview').map((e) => e.on))];
|
|
70
|
-
if (previewEvents.length === 0)
|
|
71
|
-
return null;
|
|
72
|
-
const noun = previewEvents.length === 1 ? 'entry delivers' : 'entries deliver';
|
|
73
|
-
return `body is ${words} words but the ${previewEvents.join('/')} ${noun} \`preview\`; use \`at: content\` so agents receive the whole rule without a separate memory read`;
|
|
74
|
-
}
|
|
75
|
-
/** The length rule. Caps are computed from the parsed entries — an invalid
|
|
76
|
-
* entry already fails the schema check, so it simply matches no cap here.
|
|
77
|
-
* `docName` is the doc's canonical resolver identity (explicit frontmatter
|
|
78
|
-
* `name`, else the normalized path-derived name) — persona layers under
|
|
79
|
-
* `kinds/` are recognized by it and never capped. */
|
|
80
|
-
export function lintBodyLength(fm, body, docName) {
|
|
81
|
-
if (ignoresRule(fm, 'length'))
|
|
82
|
-
return null;
|
|
83
|
-
// Persona exemption: render composes an agent's persona from the docs
|
|
84
|
-
// resolving under `kinds/...`, and an agent reads its whole persona by
|
|
85
|
-
// construction — persona length is persona design, never an authoring smell.
|
|
86
|
-
if (docName === 'kinds' || docName.startsWith('kinds/'))
|
|
87
|
-
return null;
|
|
88
|
-
const entries = parsedSurfaces(fm);
|
|
89
|
-
const words = countBodyWords(body);
|
|
90
|
-
const remedy = 'Keep the load-bearing core here and split the depth into [[linked]] reference docs carrying no surfaces at all (the listing and the link are how they are found, so they cost nothing until followed). Split by subject: each leaf covers a different subject a task might need on its own; never split off “further evidence”, examples, or references — a references leaf is never followed, so supporting material stays next to the point it supports or gets cut. Keep it whole — `lint-ignore: length` in the frontmatter — only when every reader who surfaces this doc genuinely benefits from reading 100% of it, or it is one indivisible body of knowledge; then splitting just adds hops.';
|
|
91
|
-
if (entries.some((e) => e.on === 'boot' && e.at === 'content') && words > BOOT_CONTENT_MAX_WORDS) {
|
|
92
|
-
return `body is ${words} words but a boot entry at \`content\` inlines every word into every agent's system prompt — capped at ${BOOT_CONTENT_MAX_WORDS} words (boot preview gets ${BOOT_PREVIEW_MAX_WORDS}; name entries are never capped). ${remedy}`;
|
|
93
|
-
}
|
|
94
|
-
const contentEvents = [...new Set(entries.filter((e) => e.on !== 'boot' && e.at === 'content').map((e) => e.on))];
|
|
95
|
-
if (contentEvents.length > 0 && words > EVENT_CONTENT_MAX_WORDS) {
|
|
96
|
-
return `body is ${words} words, over the ${EVENT_CONTENT_MAX_WORDS}-word cap for a \`content\` entry on ${contentEvents.join('/')} (the whole body delivers every time the entry fires; name entries are never capped). ${remedy}`;
|
|
97
|
-
}
|
|
98
|
-
if (entries.some((e) => e.on === 'boot' && e.at === 'preview') && words > BOOT_PREVIEW_MAX_WORDS) {
|
|
99
|
-
return `body is ${words} words, over the ${BOOT_PREVIEW_MAX_WORDS}-word cap for boot preview (the routing line invites every reader into the whole body, so the cap is the longest doc worth reading end-to-end; boot content is capped at ${BOOT_CONTENT_MAX_WORDS} words; name entries are never capped). ${remedy}`;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
/** Strict-parse one file; push a finding on a YAML error, then run the
|
|
104
|
-
* schema check when the file lives in a substrate store, then validate every
|
|
105
|
-
* `[[canonical/name]]` doc link in the body against the exact resolvable
|
|
106
|
-
* corpus. A dangling link is an authoring error caught HERE, never silently
|
|
107
|
-
* carried; leaf-name fallback is deliberately excluded so links stay stable
|
|
108
|
-
* as the graph grows and another document acquires the same leaf name. */
|
|
109
|
-
function lintFile(file, substrateStore, findings, warnings, corpusNames, fallbackName, scope) {
|
|
110
|
-
let fm;
|
|
111
|
-
let body;
|
|
112
|
-
try {
|
|
113
|
-
({ data: fm, body } = parseFrontmatterGeneric(readText(file)));
|
|
114
|
-
}
|
|
115
|
-
catch (e) {
|
|
116
|
-
const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
|
|
117
|
-
findings.push({ path: file, error: `invalid YAML frontmatter: ${msg}` });
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (!substrateStore)
|
|
121
|
-
return;
|
|
122
|
-
const schemaError = lintSubstrateFrontmatter(fm);
|
|
123
|
-
if (schemaError !== null)
|
|
124
|
-
findings.push({ path: file, error: schemaError });
|
|
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
|
-
}
|
|
129
|
-
const lengthError = lintBodyLength(fm, body, resolveDocName(fm, fallbackName));
|
|
130
|
-
if (lengthError !== null)
|
|
131
|
-
findings.push({ path: file, error: lengthError });
|
|
132
|
-
const shortPreviewWarning = lintShortPreviewBody(fm, body);
|
|
133
|
-
if (shortPreviewWarning !== null)
|
|
134
|
-
warnings.push(`${file}: ${shortPreviewWarning}`);
|
|
135
|
-
// An over-broad memory-read glob fires on EVERY memory read — almost
|
|
136
|
-
// always an authoring accident, so flag it without failing the corpus.
|
|
137
|
-
for (const entry of parsedSurfaces(fm)) {
|
|
138
|
-
if (entry.on !== 'memory-read')
|
|
139
|
-
continue;
|
|
140
|
-
for (const glob of entry.match ?? []) {
|
|
141
|
-
if ((glob === '**' || glob === '*') && !ignoresRule(fm, 'broad-memory-read')) {
|
|
142
|
-
warnings.push(`${file}: memory-read glob \`${glob}\` fires on every memory read — scope it to a name subtree`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
for (const name of docLinkNames(body)) {
|
|
148
|
-
if (!corpusNames.has(name)) {
|
|
149
|
-
findings.push({
|
|
150
|
-
path: file,
|
|
151
|
-
error: `dangling doc link [[${name}]]: no memory document or directory has that exact canonical name — retarget the link (\`crtr memory find ${name.split('/').pop()}\`) or drop it`,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/** The files in a project store carrying a workspace front-door entry
|
|
157
|
-
* ({on: workspace-open, at: content}). Raw fm scan; a missing store or an
|
|
158
|
-
* unparseable doc contributes zero (the YAML failure is its own finding). */
|
|
159
|
-
function frontDoorDocs(storeDir) {
|
|
160
|
-
const hits = [];
|
|
161
|
-
if (!pathExists(storeDir))
|
|
162
|
-
return hits;
|
|
163
|
-
for (const file of walkFiles(storeDir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
|
|
164
|
-
if (basename(file) === 'MEMORY.md')
|
|
165
|
-
continue;
|
|
166
|
-
let fm;
|
|
167
|
-
try {
|
|
168
|
-
fm = parseFrontmatterGeneric(readText(file)).data;
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (fm === null)
|
|
174
|
-
continue;
|
|
175
|
-
const entries = parsedSurfaces(fm);
|
|
176
|
-
if (entries.some((e) => e.on === 'workspace-open' && e.at === 'content'))
|
|
177
|
-
hits.push(file);
|
|
178
|
-
}
|
|
179
|
-
return hits;
|
|
180
|
-
}
|
|
181
|
-
const FRONT_DOOR_REMEDY = 'author the project\'s operating guide as an ordinary doc with `surfaces: [{on: workspace-open, at: content}, {on: read, match: "./**", at: content}]`; run `crtr memory write -h` first';
|
|
182
34
|
export const lintLeaf = defineLeaf({
|
|
183
35
|
name: 'lint',
|
|
184
|
-
description: 'validate frontmatter and
|
|
185
|
-
whenToUse: 'you authored or migrated documents and want the authoring-time gate
|
|
36
|
+
description: 'validate memory identity, frontmatter, and references across a target view or one exact store',
|
|
37
|
+
whenToUse: 'you authored, moved, or migrated documents and want the authoring-time gate before shipping them. It strict-parses every doc the selection covers and fails on: invalid YAML or substrate schema, a repository whose root store declares no valid `namespace:` (so the runtime refuses to mount it), a `namespace:` outside the repository-root store\u2019s root INDEX.md, two directories in one repository whose paths normalize to the same prefix, a `name:` on a root INDEX.md or a non-root `name:` that is not a valid store-LOCAL name, two files in one store owning one canonical identity, a `[[link]]` or literal `memory-read` route naming no document or directory in the view (a directory\u2019s own document answers at the directory name, never at `<dir>/INDEX`), a body longer than its delivery entries earn, and a repository whose root store’s workspace front door is missing or sits anywhere but its root INDEX.md. One canonical identity held by SEVERAL stores is legitimate (a linked worktree sharing its main checkout\u2019s namespace), so it is reported as a notice with the view\u2019s winner and every candidate, never failed. CI-friendly: non-zero exit on any error, warnings and notices stay non-fatal.',
|
|
186
38
|
help: {
|
|
187
39
|
name: 'memory lint',
|
|
188
|
-
summary: '
|
|
189
|
-
params: [
|
|
40
|
+
summary: 'validate canonical identity, frontmatter, and references across the memory corpus',
|
|
41
|
+
params: [
|
|
42
|
+
selectorParam('scope-read', {}, 'Lints only that source class. Cross-store candidate notices and project front-door coverage need the unfiltered view, so a scope filter suppresses them.'),
|
|
43
|
+
selectorParam('dir', {}, 'Lints that one store even when the runtime refuses to mount it — the only way to check a project store before its repository namespace is declared. Its links and routes are resolved against the target view that project\u2019s own directory sees, and an unmounted store defers those reference checks (its identities are undetermined until the namespace exists).'),
|
|
44
|
+
],
|
|
190
45
|
output: [
|
|
191
|
-
{ name: 'checked', type: 'number', required: true, constraint: '
|
|
192
|
-
{ name: '
|
|
193
|
-
{ name: '
|
|
194
|
-
{ name: '
|
|
46
|
+
{ name: 'checked', type: 'number', required: true, constraint: 'Markdown files examined.' },
|
|
47
|
+
{ name: 'stores', type: 'number', required: true, constraint: 'Physical memory stores linted.' },
|
|
48
|
+
{ name: 'profile_projects', type: 'number', required: false, constraint: 'Projects the selected profile manages that were checked for a workspace front door — zero under a scope filter excluding project stores. Absent under --dir.' },
|
|
49
|
+
{ name: 'findings', type: 'object[]', required: true, constraint: 'One row per ERROR: {rule, path, message, name?, scope?, store_root?, paths?}. `rule` names the violated rule (yaml, schema, extension, length, store-namespace, namespace-placement, root-name, root-index-unaddressable, local-name, canonical-collision, dangling-link, memory-read-route, front-door, owner-path-collision). `paths` carries every file sharing a collided identity, read winner first. Empty when green.' },
|
|
50
|
+
{ name: 'warnings', type: 'object[]', required: true, constraint: 'Same row shape, non-fatal authoring smells: short-preview, broad-memory-read, nested-store-boot, an advisory missing front door, a wildcard memory-read glob matching nothing.' },
|
|
51
|
+
{ name: 'notices', type: 'object[]', required: true, constraint: 'Same row shape, inspection data rather than faults: cross-store-candidates (one canonical name held by several stores, winner first) and reference-check-deferred.' },
|
|
52
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next command.' },
|
|
195
53
|
],
|
|
196
54
|
outputKind: 'object',
|
|
197
|
-
effects: ['None. Read-only. Exits non-zero when any finding exists.'],
|
|
55
|
+
effects: ['None. Read-only. Exits non-zero when any error finding exists.'],
|
|
198
56
|
},
|
|
199
|
-
run: async () => {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
corpusNames.add(doc.name);
|
|
220
|
-
const segs = doc.name.split('/');
|
|
221
|
-
for (let i = 1; i < segs.length; i++)
|
|
222
|
-
corpusNames.add(segs.slice(0, i).join('/'));
|
|
223
|
-
}
|
|
224
|
-
const lintDir = (dir, scope) => {
|
|
225
|
-
if (!dir || !pathExists(dir))
|
|
226
|
-
return;
|
|
227
|
-
for (const file of walkFiles(dir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
|
|
228
|
-
const relPath = relative(dir, file).split(sep).join('/');
|
|
229
|
-
if (!relPath)
|
|
230
|
-
continue;
|
|
231
|
-
memoryCount += 1;
|
|
232
|
-
const fallbackName = normalizeDocName(relPath.replace(/\.md$/, ''));
|
|
233
|
-
lintFile(file, basename(file) !== 'MEMORY.md', findings, warnings, corpusNames, fallbackName, scope);
|
|
234
|
-
}
|
|
57
|
+
run: async (input) => {
|
|
58
|
+
const selector = resolveReadSelector({
|
|
59
|
+
scope: input['scope'],
|
|
60
|
+
dir: input['dir'],
|
|
61
|
+
});
|
|
62
|
+
const result = selector.store !== null
|
|
63
|
+
? lintMemoryStore(selector.store, {
|
|
64
|
+
resolvable: resolvableNamesForStore(selector.store),
|
|
65
|
+
// Asked about directly, not as a profile-managed project: a missing
|
|
66
|
+
// front door is a gap to report, not a failed gate.
|
|
67
|
+
frontDoor: 'advisory',
|
|
68
|
+
})
|
|
69
|
+
: lintMemoryTarget({ ...(selector.scope === undefined ? {} : { scope: selector.scope }) });
|
|
70
|
+
const errors = result.errors.map(findingRow);
|
|
71
|
+
const warnings = result.warnings.map(findingRow);
|
|
72
|
+
const notices = result.notices.map(findingRow);
|
|
73
|
+
const counts = {
|
|
74
|
+
checked: result.checked,
|
|
75
|
+
stores: result.storesLinted,
|
|
76
|
+
...('profileProjects' in result ? { profile_projects: result.profileProjects } : {}),
|
|
235
77
|
};
|
|
236
|
-
for (const
|
|
237
|
-
|
|
238
|
-
for (const plugin of listInstalledPluginsInRoot('project', root)) {
|
|
239
|
-
if (plugin.enabled)
|
|
240
|
-
lintDir(pluginMemoryDir(plugin), 'project');
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
// Nested descendant stores: same schema gate, plus a boot-entry warning —
|
|
244
|
-
// the boot catalog deliberately never includes nested stores, so a boot
|
|
245
|
-
// surfaces entry there is inert.
|
|
246
|
-
for (const root of descendantStoreRoots(projectScopeRoots())) {
|
|
247
|
-
const dir = join(root, 'memory');
|
|
248
|
-
lintDir(dir, 'project');
|
|
249
|
-
if (!pathExists(dir))
|
|
250
|
-
continue;
|
|
251
|
-
for (const file of walkFiles(dir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
|
|
252
|
-
if (basename(file) === 'MEMORY.md')
|
|
253
|
-
continue;
|
|
254
|
-
let fm;
|
|
255
|
-
try {
|
|
256
|
-
fm = parseFrontmatterGeneric(readText(file)).data;
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
continue; // invalid YAML is already a finding from lintDir
|
|
260
|
-
}
|
|
261
|
-
if (fm !== null && parsedSurfaces(fm).some((e) => e.on === 'boot')) {
|
|
262
|
-
warnings.push(`${file}: nested-store docs never ride the boot catalog, so its boot surfaces entries are inert — drop them`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
for (const scope of ['user', 'builtin']) {
|
|
267
|
-
lintDir(scopeMemoryDir(scope), scope);
|
|
268
|
-
for (const plugin of listInstalledPlugins(scope)) {
|
|
269
|
-
if (plugin.enabled)
|
|
270
|
-
lintDir(pluginMemoryDir(plugin), scope);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// Profile coverage: each managed project has exactly ONE workspace front
|
|
274
|
-
// door — a doc carrying {on: workspace-open, at: content}, the guide that
|
|
275
|
-
// enters first-message context when cwd/profile mounts the store. Zero
|
|
276
|
-
// means agents open the workspace blind; two means both deliver whole at
|
|
277
|
-
// every open, and the store needs consolidating.
|
|
278
|
-
let profileProjects = 0;
|
|
279
|
-
let profileResolved = false;
|
|
280
|
-
const profileIdOrName = process.env['CRTR_PROFILE_ID'] || getDefaultProfileId(process.cwd());
|
|
281
|
-
if (profileIdOrName) {
|
|
282
|
-
try {
|
|
283
|
-
const { profileId, manifest } = loadProfileManifest(profileIdOrName);
|
|
284
|
-
profileResolved = true;
|
|
285
|
-
lintDir(profileMemoryDir(profileId), 'user');
|
|
286
|
-
for (const project of manifest.projects) {
|
|
287
|
-
profileProjects += 1;
|
|
288
|
-
const dir = project.path;
|
|
289
|
-
const doors = frontDoorDocs(join(dir, '.crouter', 'memory'));
|
|
290
|
-
if (doors.length === 0) {
|
|
291
|
-
findings.push({
|
|
292
|
-
path: dir,
|
|
293
|
-
error: `profile "${manifest.name}" manages this dir but no doc in its .crouter/memory carries a {on: workspace-open, at: content} surfaces entry — ${FRONT_DOOR_REMEDY}`,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
else if (doors.length > 1) {
|
|
297
|
-
findings.push({
|
|
298
|
-
path: dir,
|
|
299
|
-
error: `multiple workspace front doors (${doors.length}): ${doors.join(', ')} — exactly one doc per project store may carry {on: workspace-open, at: content}; consolidate the others into it or lower their entries`,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
// Unresolvable profile: profile coverage does not apply. Treat cwd as
|
|
306
|
-
// unprofiled for the advisory front-door warning below.
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// Without a selected, resolvable profile there is no managed-project
|
|
310
|
-
// finding to carry this signal, so keep a non-fatal cwd adoption warning.
|
|
311
|
-
if (!profileResolved) {
|
|
312
|
-
if (frontDoorDocs(join(workingDirectory, '.crouter', 'memory')).length === 0) {
|
|
313
|
-
warnings.push(`${workingDirectory}: no workspace front door — ${FRONT_DOOR_REMEDY}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
const checked = memoryCount;
|
|
317
|
-
// Warnings are surfaced on stderr and returned structurally, but never
|
|
318
|
-
// convert a green corpus into a failed authoring gate.
|
|
319
|
-
for (const warning of warnings)
|
|
320
|
-
warn(`memory lint: warning: ${warning}`);
|
|
321
|
-
if (findings.length > 0) {
|
|
322
|
-
// Human/agent path renders only the message — surface every offender as
|
|
323
|
-
// a scoped stderr notice (the --json path carries them in details too).
|
|
324
|
-
for (const f of findings)
|
|
325
|
-
warn(`memory lint: ${f.path}: ${f.error}`);
|
|
326
|
-
throw general(`memory lint: ${findings.length} finding(s) across ${checked} files`, {
|
|
327
|
-
checked,
|
|
328
|
-
findings: findings.map((f) => ({ path: f.path, error: f.error })),
|
|
329
|
-
warnings,
|
|
330
|
-
next: 'Fix each doc (quote YAML values containing `: `; use a valid kind; run `crtr sys migrate` for retired visibility fields; give each surfaces entry a valid on/at and the match its event requires; retarget or drop dangling [[links]]; split an over-length body into [[linked]] surface-less reference docs); give every profile-managed project exactly one workspace front door; then re-run `crtr memory lint`.',
|
|
331
|
-
});
|
|
78
|
+
for (const finding of [...result.warnings, ...result.errors]) {
|
|
79
|
+
warn(`memory lint: ${finding.rule}: ${finding.path}: ${finding.message}`);
|
|
332
80
|
}
|
|
81
|
+
// A failing lint still owes its declared rows — the findings ARE the
|
|
82
|
+
// result. It fails by exit code, not by throwing them away.
|
|
83
|
+
if (errors.length > 0)
|
|
84
|
+
process.exitCode = ExitCode.GENERAL;
|
|
333
85
|
return {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
findings: [],
|
|
86
|
+
...counts,
|
|
87
|
+
findings: errors,
|
|
337
88
|
warnings,
|
|
338
|
-
|
|
89
|
+
notices,
|
|
90
|
+
follow_up: errors.length > 0
|
|
91
|
+
? `${errors.length} error(s) across ${result.checked} files in ${result.storesLinted} store(s) — fix each, then re-run. A repository store that does not mount is repaired with \`crtr sys migrate --dir <repository-root>\`; an owner-path collision is fixed by renaming one of the directories named in the finding; a canonical collision is recovered with \`crtr memory move <name> --to <new-free-name>\` (it selects the first physical path in stable lexical order); a dangling link or route is retargeted with the name \`crtr memory find <leaf>\` reports, or dropped; an over-length body splits into [[linked]] surface-less reference docs.`
|
|
92
|
+
: warnings.length > 0
|
|
93
|
+
? 'No errors — review the warnings above, then revise with `crtr memory edit <name> --rationale "<why>"`.'
|
|
94
|
+
: 'No errors. Notices are inspection data, not faults: a canonical name held by several stores is reachable per-store with `crtr memory read <name> --dir <project>`.',
|
|
339
95
|
};
|
|
340
96
|
},
|
|
341
97
|
});
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type { MemoryDoc, MemoryScope } from '../../core/memory-resolver.js';
|
|
2
|
-
/**
|
|
3
|
-
* Shared in-process consumers retain `memory list`'s scope ordering and
|
|
4
|
-
* identity behavior without rescanning the corpus once per output page. */
|
|
2
|
+
/** One inventory row before pagination/output shaping. */
|
|
5
3
|
export interface ListedMemoryDoc {
|
|
6
4
|
name: string;
|
|
7
5
|
kind: string;
|
|
8
6
|
scope: MemoryScope;
|
|
9
7
|
path: string;
|
|
8
|
+
storeRoot: string;
|
|
9
|
+
physicalRelativePath: string;
|
|
10
10
|
shortForm: string;
|
|
11
11
|
slash: boolean;
|
|
12
12
|
extensions: Record<string, Record<string, string | boolean | number>>;
|
|
13
|
+
/** Every physical document sharing this canonical name in the same corpus,
|
|
14
|
+
* in source order — the winner first. */
|
|
15
|
+
candidates: readonly MemoryDoc[];
|
|
13
16
|
}
|
|
14
|
-
|
|
17
|
+
/** The inventory over one corpus. A target view is winner-only: one row per
|
|
18
|
+
* canonical identity, first in source order, because that is the document a
|
|
19
|
+
* read of the name returns. An exact store (`--dir`) is not deduped — the
|
|
20
|
+
* point of naming a store is to see what it physically holds, including a pair
|
|
21
|
+
* that shares one identity. */
|
|
22
|
+
export declare function listMemoryDocs(kindFilter: string | undefined, docs: readonly MemoryDoc[], dedup: boolean): ListedMemoryDoc[];
|
|
15
23
|
export declare const listLeaf: import("../../core/command.js").LeafDef;
|