@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Regression: `crtr surface reload` refused every pane whose start command came
|
|
2
|
+
// from a respawn path that shell-quotes the node id (the Markdown file-open
|
|
3
|
+
// bridge, the review companion launch). The old gate compared the raw command
|
|
4
|
+
// line to the canonical `crtr surface attach to <id>`, so
|
|
5
|
+
// `crtr surface attach to '<id>'` read as a viewer/node mismatch and the reload
|
|
6
|
+
// exited 2 — permanently, for that pane.
|
|
7
|
+
// Run with: node --conditions=crtr-src --import tsx/esm --test 'src/commands/__tests__/surface-reload-target.test.ts'
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { attachTargetNode } from '../surface-reload.js';
|
|
11
|
+
const NODE = '3zl47w7d-mt0cp6we-ed07d4c5';
|
|
12
|
+
test('a viewer command line names its node whether or not the id is shell-quoted', () => {
|
|
13
|
+
assert.equal(attachTargetNode(`crtr surface attach to ${NODE}`), NODE);
|
|
14
|
+
assert.equal(attachTargetNode(`crtr surface attach to '${NODE}'`), NODE);
|
|
15
|
+
assert.equal(attachTargetNode(`crtr surface attach to '${NODE}' --observer`), NODE);
|
|
16
|
+
assert.equal(attachTargetNode(`crtr surface attach to ${NODE} --observer`), NODE);
|
|
17
|
+
});
|
|
18
|
+
test('a command line for another node does not read as this node', () => {
|
|
19
|
+
assert.notEqual(attachTargetNode(`crtr surface attach to '3zl47w7d-other-00000000'`), NODE);
|
|
20
|
+
assert.equal(attachTargetNode('zsh -l'), undefined);
|
|
21
|
+
});
|
|
@@ -45,13 +45,9 @@ export declare function readColdStartDiagnostic(): string | undefined;
|
|
|
45
45
|
* `onColdSocket` is fire-and-forget — `ensureDaemon()` spawns crtrd and
|
|
46
46
|
* returns immediately, so the client's OWN `/healthz` poll is the only
|
|
47
47
|
* deadline the initiating CLI invocation actually waits on. It must never be
|
|
48
|
-
* narrower than the window `spawnDaemon`'s `verifyDaemonStartup` allows
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* and a 10s client poll would still give up and report `daemon_unavailable`
|
|
52
|
-
* against it. Passing the SAME `DAEMON_VERIFY_WINDOW_MS`
|
|
53
|
-
* here keeps the two in lockstep — one authoritative window, not two that can
|
|
54
|
-
* drift apart — without adding any retry/fallback poll loop. */
|
|
48
|
+
* narrower than the window `spawnDaemon`'s `verifyDaemonStartup` allows.
|
|
49
|
+
* Both callers use the shared startup policy so a valid on-disk migration
|
|
50
|
+
* cannot outlast the initiating CLI command. */
|
|
55
51
|
export declare function cliClient(opts?: {
|
|
56
52
|
timeoutMs?: number;
|
|
57
53
|
}): CrtrClient;
|
|
@@ -18,7 +18,8 @@ import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { CrtrClient, ApiError } from '../api/index.js';
|
|
20
20
|
import { apiErrorToCliError } from '../core/io.js';
|
|
21
|
-
import { ensureDaemon
|
|
21
|
+
import { ensureDaemon } from '../daemon/manage.js';
|
|
22
|
+
import { DAEMON_STARTUP_WINDOW_MS } from '../daemon/startup-policy.js';
|
|
22
23
|
import { readState } from '../core/config.js';
|
|
23
24
|
import { resolveRemoteCanvasTarget } from '../core/remote-canvas-target.js';
|
|
24
25
|
import { RemoteCanvasSource } from '../core/canvas/remote-canvas-source.js';
|
|
@@ -119,13 +120,9 @@ export function readColdStartDiagnostic() {
|
|
|
119
120
|
* `onColdSocket` is fire-and-forget — `ensureDaemon()` spawns crtrd and
|
|
120
121
|
* returns immediately, so the client's OWN `/healthz` poll is the only
|
|
121
122
|
* deadline the initiating CLI invocation actually waits on. It must never be
|
|
122
|
-
* narrower than the window `spawnDaemon`'s `verifyDaemonStartup` allows
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* and a 10s client poll would still give up and report `daemon_unavailable`
|
|
126
|
-
* against it. Passing the SAME `DAEMON_VERIFY_WINDOW_MS`
|
|
127
|
-
* here keeps the two in lockstep — one authoritative window, not two that can
|
|
128
|
-
* drift apart — without adding any retry/fallback poll loop. */
|
|
123
|
+
* narrower than the window `spawnDaemon`'s `verifyDaemonStartup` allows.
|
|
124
|
+
* Both callers use the shared startup policy so a valid on-disk migration
|
|
125
|
+
* cannot outlast the initiating CLI command. */
|
|
129
126
|
export function cliClient(opts) {
|
|
130
127
|
if (autostartDisabled()) {
|
|
131
128
|
return CrtrClient.forLocalSocket({ autostart: false, ...(opts?.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}) });
|
|
@@ -133,15 +130,13 @@ export function cliClient(opts) {
|
|
|
133
130
|
return CrtrClient.forLocalSocket({
|
|
134
131
|
...(opts?.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
135
132
|
onColdSocket: async () => {
|
|
136
|
-
//
|
|
137
|
-
// to the SAME window spawnDaemon's verifyDaemonStartup uses) owns the
|
|
138
|
-
// "did it come up?" wait and the loud failure.
|
|
133
|
+
// The client's bounded /healthz poll owns the readiness wait and failure.
|
|
139
134
|
ensureDaemon();
|
|
140
135
|
},
|
|
141
136
|
// On a cold-start timeout, enrich the bare `daemon_unavailable` with the
|
|
142
137
|
// tail of crtrd's own stderr log instead of discarding it.
|
|
143
138
|
coldStartDiagnostic: readColdStartDiagnostic,
|
|
144
|
-
coldStartPollWindowMs:
|
|
139
|
+
coldStartPollWindowMs: DAEMON_STARTUP_WINDOW_MS,
|
|
145
140
|
});
|
|
146
141
|
}
|
|
147
142
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Regressions for the memory leaves' store selection and mutation guards. Each
|
|
2
|
+
// case reproduces a failure the leaves actually shipped: an explicit --profile
|
|
3
|
+
// silently answering from the ambient profile, a project root deletable out
|
|
4
|
+
// from under its own namespace, `move --dir` rewriting links on behalf of a
|
|
5
|
+
// duplicate that does not own the address, an `NN-` ordering prefix accepted as
|
|
6
|
+
// an address, a `name:` pin that could not be retired in place, a numeric
|
|
7
|
+
// `namespace:` mounting lint-clean, `lint` throwing its declared rows away on
|
|
8
|
+
// failure, and deleted history recommending a read that cannot succeed.
|
|
9
|
+
//
|
|
10
|
+
// Fixture: one repository-root store declaring `namespace: acme` and a nested
|
|
11
|
+
// store whose effective prefix is derived as `acme/child`; plus two single-store
|
|
12
|
+
// projects for the lint cases and two profiles under a temp HOME.
|
|
13
|
+
//
|
|
14
|
+
// Run: node --conditions=crtr-src --import tsx/esm --test --test-concurrency=4 --test-timeout=10000 src/commands/memory/__tests__/command-selector-and-mutation-guards.test.ts
|
|
15
|
+
import { test, beforeEach, after } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { resetScopeCache } from '../../../core/scope.js';
|
|
21
|
+
import { closeDb } from '../../../core/canvas/db.js';
|
|
22
|
+
import { createProfile, profileMemoryDir } from '../../../core/profiles/manifest.js';
|
|
23
|
+
import { openProjectMemoryStore } from '../../../core/memory-resolver.js';
|
|
24
|
+
import { appendHistoryRecord, buildHistoryRecord, readHistoryRecords } from '../../../core/memory/history.js';
|
|
25
|
+
import { CrtrError } from '../../../core/errors.js';
|
|
26
|
+
import { deleteLeaf } from '../delete.js';
|
|
27
|
+
import { editLeaf } from '../edit.js';
|
|
28
|
+
import { historyLeaf } from '../history.js';
|
|
29
|
+
import { lintLeaf } from '../lint.js';
|
|
30
|
+
import { moveLeaf } from '../move.js';
|
|
31
|
+
import { originLeaf } from '../origin.js';
|
|
32
|
+
import { readLeaf } from '../read.js';
|
|
33
|
+
import { writeLeaf } from '../write.js';
|
|
34
|
+
let home;
|
|
35
|
+
let canvasHome;
|
|
36
|
+
let base;
|
|
37
|
+
let root;
|
|
38
|
+
let child;
|
|
39
|
+
let badns;
|
|
40
|
+
let dangling;
|
|
41
|
+
let profileB;
|
|
42
|
+
let nestedWrite;
|
|
43
|
+
let nestedEmpty;
|
|
44
|
+
const prevEnv = {};
|
|
45
|
+
const prevCwd = process.cwd();
|
|
46
|
+
function writeDoc(path, frontmatter, body) {
|
|
47
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
48
|
+
writeFileSync(path, `---\n${frontmatter}\n---\n\n${body}\n`, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
function storeDir(project) {
|
|
51
|
+
return join(project, '.crouter', 'memory');
|
|
52
|
+
}
|
|
53
|
+
/** The error a leaf threw, for `code`/`message` assertions. */
|
|
54
|
+
async function thrownByAsync(run) {
|
|
55
|
+
try {
|
|
56
|
+
await run();
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
assert.ok(e instanceof CrtrError, `expected a CrtrError, got ${String(e)}`);
|
|
60
|
+
return e;
|
|
61
|
+
}
|
|
62
|
+
assert.fail('expected the leaf to throw');
|
|
63
|
+
}
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
for (const key of ['HOME', 'CRTR_HOME', 'CRTR_PROFILE_ID', 'CRTR_NODE_ID']) {
|
|
66
|
+
if (!(key in prevEnv))
|
|
67
|
+
prevEnv[key] = process.env[key];
|
|
68
|
+
}
|
|
69
|
+
home = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-memcmd-home-')));
|
|
70
|
+
canvasHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-memcmd-canvas-')));
|
|
71
|
+
base = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-memcmd-')));
|
|
72
|
+
process.env['HOME'] = home;
|
|
73
|
+
process.env['CRTR_HOME'] = canvasHome;
|
|
74
|
+
delete process.env['CRTR_NODE_ID'];
|
|
75
|
+
mkdirSync(join(home, '.crouter', 'memory'), { recursive: true });
|
|
76
|
+
root = join(base, 'root');
|
|
77
|
+
child = join(root, 'child');
|
|
78
|
+
nestedWrite = join(root, 'nested-write');
|
|
79
|
+
nestedEmpty = join(root, 'nested-empty');
|
|
80
|
+
badns = join(base, 'badns');
|
|
81
|
+
dangling = join(base, 'dangling');
|
|
82
|
+
mkdirSync(child, { recursive: true });
|
|
83
|
+
mkdirSync(nestedWrite, { recursive: true });
|
|
84
|
+
mkdirSync(nestedEmpty, { recursive: true });
|
|
85
|
+
// --- repository root, declaring the namespace for every nested store ---
|
|
86
|
+
writeDoc(join(storeDir(root), 'INDEX.md'), 'kind: knowledge\nnamespace: acme\nwhen-and-why-to-read: "When the root acme store is addressed, this knowledge should be read because it names what the store holds."', 'ROOT STORE INDEX');
|
|
87
|
+
writeDoc(join(storeDir(root), 'foo.md'), 'kind: knowledge', 'ROOT FOO BODY');
|
|
88
|
+
writeDoc(join(storeDir(root), 'linker.md'), 'kind: knowledge', 'ROOT LINKER BODY linking [[acme/foo]] once.');
|
|
89
|
+
writeDoc(join(storeDir(root), 'disposable.md'), 'kind: knowledge', 'ROOT DISPOSABLE BODY');
|
|
90
|
+
writeDoc(join(storeDir(root), '04-ordered.md'), 'kind: knowledge', 'ROOT ORDERED BODY');
|
|
91
|
+
// A `name:` pin on a file whose own path already derives a different identity.
|
|
92
|
+
writeDoc(join(storeDir(root), 'x.md'), 'kind: knowledge\nname: y', 'ROOT PINNED BODY');
|
|
93
|
+
appendHistoryRecord(join(storeDir(root), '.history', 'x.jsonl'), buildHistoryRecord({ op: 'create', before: '', after: 'ROOT PINNED BODY' }));
|
|
94
|
+
// --- child: a nested store deriving `acme/child`; cwd lives here ---
|
|
95
|
+
writeDoc(join(storeDir(child), 'INDEX.md'), 'kind: knowledge\nwhen-and-why-to-read: "When the child guide is addressed, this knowledge should be read because it names what the nested store holds."', 'CHILD STORE INDEX');
|
|
96
|
+
writeDoc(join(storeDir(child), 'foo.md'), 'kind: knowledge', 'CHILD FOO BODY');
|
|
97
|
+
// This nested store has content but no guide, so a write at its exact prefix
|
|
98
|
+
// must create metadata-free INDEX.md rather than a second declaration.
|
|
99
|
+
writeDoc(join(storeDir(nestedWrite), 'topic.md'), 'kind: knowledge', 'NESTED WRITE TOPIC');
|
|
100
|
+
// --- lint fixtures ---
|
|
101
|
+
writeDoc(join(storeDir(badns), 'INDEX.md'), 'kind: knowledge\nnamespace: 42', 'BADNS STORE INDEX');
|
|
102
|
+
writeDoc(join(storeDir(dangling), 'INDEX.md'), 'kind: knowledge\nnamespace: dang\nwhen-and-why-to-read: "When the dang store is addressed, this knowledge should be read because it names what the store holds."', 'DANG STORE INDEX');
|
|
103
|
+
writeDoc(join(storeDir(dangling), 'broken.md'), 'kind: knowledge', 'Links [[dang/nowhere]], which no document answers to.');
|
|
104
|
+
// --- two profiles, both holding the SAME canonical name ---
|
|
105
|
+
const a = createProfile('memcmd-a');
|
|
106
|
+
const b = createProfile('memcmd-b');
|
|
107
|
+
profileB = profileMemoryDir(b.profileId);
|
|
108
|
+
writeDoc(join(profileMemoryDir(a.profileId), 'pdoc.md'), 'kind: knowledge', 'PROFILE A BODY');
|
|
109
|
+
writeDoc(join(profileB, 'pdoc.md'), 'kind: knowledge', 'PROFILE B BODY');
|
|
110
|
+
process.env['CRTR_PROFILE_ID'] = a.profileId;
|
|
111
|
+
process.chdir(child);
|
|
112
|
+
resetScopeCache();
|
|
113
|
+
});
|
|
114
|
+
after(() => {
|
|
115
|
+
closeDb();
|
|
116
|
+
process.chdir(prevCwd);
|
|
117
|
+
resetScopeCache();
|
|
118
|
+
for (const dir of [home, canvasHome, base])
|
|
119
|
+
rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
for (const [key, value] of Object.entries(prevEnv)) {
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
delete process.env[key];
|
|
123
|
+
else
|
|
124
|
+
process.env[key] = value;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
test('an explicit --profile selects that profile store for edit, origin, and history', async () => {
|
|
128
|
+
const target = join(profileB, 'pdoc.md');
|
|
129
|
+
const selector = { scope: 'profile', profile: 'memcmd-b' };
|
|
130
|
+
const edited = (await editLeaf.run({
|
|
131
|
+
...selector,
|
|
132
|
+
name: 'pdoc',
|
|
133
|
+
rationale: 'regression: --profile must not answer from the ambient profile',
|
|
134
|
+
body: 'PROFILE B BODY REVISED',
|
|
135
|
+
}));
|
|
136
|
+
assert.equal(edited.path, target, 'the edit landed in the named profile store, not the ambient one');
|
|
137
|
+
assert.match(readFileSync(target, 'utf8'), /PROFILE B BODY REVISED/);
|
|
138
|
+
const origin = (await originLeaf.run({ ...selector, name: 'pdoc' }));
|
|
139
|
+
assert.equal(origin.path, target);
|
|
140
|
+
assert.equal(origin.scope, 'profile');
|
|
141
|
+
const history = (await historyLeaf.run({ ...selector, name: 'pdoc' }));
|
|
142
|
+
assert.equal(history.log_path, join(profileB, '.history', 'pdoc.jsonl'));
|
|
143
|
+
assert.equal(history.total, 1);
|
|
144
|
+
});
|
|
145
|
+
test('only the repository-root INDEX is immutable; nested guides move and delete normally', async () => {
|
|
146
|
+
const deleteErr = await thrownByAsync(() => deleteLeaf.run({ name: 'acme', dir: root }));
|
|
147
|
+
assert.equal(deleteErr.code, 'usage');
|
|
148
|
+
assert.match(deleteErr.message, /repository-root document/);
|
|
149
|
+
assert.ok(existsSync(join(storeDir(root), 'INDEX.md')), 'the namespace declaration is still on disk');
|
|
150
|
+
const moveErr = await thrownByAsync(() => moveLeaf.run({ name: 'acme', to: 'acme/other', dir: root }));
|
|
151
|
+
assert.equal(moveErr.code, 'usage');
|
|
152
|
+
assert.match(moveErr.message, /repository-root document/);
|
|
153
|
+
const moved = (await moveLeaf.run({ name: 'acme/child', to: 'acme/child/guide', dir: child }));
|
|
154
|
+
assert.equal(moved.path, join(storeDir(child), 'guide.md'));
|
|
155
|
+
assert.ok(!existsSync(join(storeDir(child), 'INDEX.md')));
|
|
156
|
+
writeDoc(join(storeDir(child), 'INDEX.md'), 'kind: knowledge\nwhen-and-why-to-read: "When the child guide is addressed, this knowledge should be read because it names what the nested store holds."', 'CHILD STORE INDEX');
|
|
157
|
+
const deleted = (await deleteLeaf.run({ name: 'acme/child', dir: child }));
|
|
158
|
+
assert.equal(deleted.path, join(storeDir(child), 'INDEX.md'));
|
|
159
|
+
assert.ok(!existsSync(join(storeDir(child), 'INDEX.md')));
|
|
160
|
+
});
|
|
161
|
+
test('nested stores cannot initialize a namespace, but writes to their exact prefix create a metadata-free guide', async () => {
|
|
162
|
+
const initErr = await thrownByAsync(() => writeLeaf.run({
|
|
163
|
+
name: 'acme/nested-empty',
|
|
164
|
+
kind: 'knowledge',
|
|
165
|
+
dir: nestedEmpty,
|
|
166
|
+
body: 'must not initialize a nested store',
|
|
167
|
+
}));
|
|
168
|
+
assert.equal(initErr.code, 'usage');
|
|
169
|
+
assert.match(initErr.message, /only the repository root declares a namespace/);
|
|
170
|
+
assert.match(String(initErr.details?.['next']), /crtr memory write <namespace> --dir <repositoryRoot>/);
|
|
171
|
+
const created = (await writeLeaf.run({
|
|
172
|
+
name: 'acme/nested-write',
|
|
173
|
+
kind: 'knowledge',
|
|
174
|
+
dir: nestedWrite,
|
|
175
|
+
body: 'NESTED GUIDE BODY',
|
|
176
|
+
}));
|
|
177
|
+
assert.equal(created.name, 'acme/nested-write');
|
|
178
|
+
assert.equal(created.path, join(storeDir(nestedWrite), 'INDEX.md'));
|
|
179
|
+
const text = readFileSync(created.path, 'utf8');
|
|
180
|
+
assert.ok(!/^namespace:/m.test(text), `nested guide must not declare a namespace; got:\n${text}`);
|
|
181
|
+
});
|
|
182
|
+
test('editing the repository-root guide preserves its namespace declaration', async () => {
|
|
183
|
+
await editLeaf.run({
|
|
184
|
+
name: 'acme',
|
|
185
|
+
dir: root,
|
|
186
|
+
rationale: 'regression: root namespace must remain immutable during edits',
|
|
187
|
+
body: 'ROOT STORE INDEX REVISED',
|
|
188
|
+
});
|
|
189
|
+
assert.match(readFileSync(join(storeDir(root), 'INDEX.md'), 'utf8'), /^namespace: acme$/m);
|
|
190
|
+
});
|
|
191
|
+
test('move and delete recover same-store collisions by stable physical path, while edit and origin refuse', async () => {
|
|
192
|
+
const collisionLeaf = join(storeDir(root), 'collision.md');
|
|
193
|
+
const collisionIndex = join(storeDir(root), 'collision', 'INDEX.md');
|
|
194
|
+
writeDoc(collisionLeaf, 'kind: knowledge', 'COLLISION LEAF');
|
|
195
|
+
writeDoc(collisionIndex, 'kind: knowledge', 'COLLISION INDEX');
|
|
196
|
+
const editErr = await thrownByAsync(() => editLeaf.run({
|
|
197
|
+
name: 'acme/collision',
|
|
198
|
+
dir: root,
|
|
199
|
+
rationale: 'collision must remain ambiguous to edit',
|
|
200
|
+
body: 'must not edit',
|
|
201
|
+
}));
|
|
202
|
+
assert.equal(editErr.code, 'usage');
|
|
203
|
+
assert.match(editErr.message, /owned by 2 files/);
|
|
204
|
+
assert.match(String(editErr.details?.['next']), /memory move acme\/collision --to <new-free-name>/);
|
|
205
|
+
const originErr = await thrownByAsync(() => originLeaf.run({ name: 'acme/collision', dir: root }));
|
|
206
|
+
assert.equal(originErr.code, 'usage');
|
|
207
|
+
assert.match(originErr.message, /owned by 2 files/);
|
|
208
|
+
try {
|
|
209
|
+
const lint = (await lintLeaf.run({ dir: root }));
|
|
210
|
+
const collision = lint.findings.find((finding) => finding.rule === 'canonical-collision');
|
|
211
|
+
assert.ok(collision, 'lint reports the physical collision');
|
|
212
|
+
assert.match(collision.message, /memory move acme\/collision --to <new-free-name>/);
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
process.exitCode = 0;
|
|
216
|
+
}
|
|
217
|
+
const moved = (await moveLeaf.run({ name: 'acme/collision', to: 'acme/recovered', dir: root }));
|
|
218
|
+
assert.equal(moved.path, join(storeDir(root), 'recovered.md'));
|
|
219
|
+
assert.ok(!existsSync(collisionLeaf), 'move selected the first lexical physical path');
|
|
220
|
+
assert.ok(existsSync(collisionIndex), 'move left the other representation untouched');
|
|
221
|
+
assert.ok(moved.follow_up.includes('first physical path') && moved.follow_up.includes(collisionLeaf));
|
|
222
|
+
const deleteLeafPath = join(storeDir(root), 'delete-collision.md');
|
|
223
|
+
const deleteIndexPath = join(storeDir(root), 'delete-collision', 'INDEX.md');
|
|
224
|
+
writeDoc(deleteLeafPath, 'kind: knowledge', 'DELETE COLLISION LEAF');
|
|
225
|
+
writeDoc(deleteIndexPath, 'kind: knowledge', 'DELETE COLLISION INDEX');
|
|
226
|
+
const deleted = (await deleteLeaf.run({ name: 'acme/delete-collision', dir: root }));
|
|
227
|
+
assert.equal(deleted.path, deleteLeafPath);
|
|
228
|
+
assert.ok(!existsSync(deleteLeafPath), 'delete selected the first lexical physical path');
|
|
229
|
+
assert.ok(existsSync(deleteIndexPath), 'delete left the other representation untouched');
|
|
230
|
+
assert.ok(deleted.follow_up.includes('first physical path') && deleted.follow_up.includes(deleteLeafPath));
|
|
231
|
+
});
|
|
232
|
+
test('move --dir addresses the selected repository store at its full effective name and rewrites its links', async () => {
|
|
233
|
+
const linker = join(storeDir(root), 'linker.md');
|
|
234
|
+
const result = (await moveLeaf.run({ name: 'acme/foo', to: 'acme/renamed', dir: root }));
|
|
235
|
+
assert.deepEqual(result.refs_rewritten.map((ref) => ref.path), [linker]);
|
|
236
|
+
assert.match(readFileSync(linker, 'utf8'), /\[\[acme\/renamed\]\]/, 'the root link follows its moved target');
|
|
237
|
+
const childDoc = await readLeaf.run({ name: 'acme/child/foo', kind: undefined, frontmatter: false, scope: undefined, dir: child });
|
|
238
|
+
assert.match(childDoc.content, /CHILD FOO BODY/, 'the nested effective prefix remains independently addressable through --dir');
|
|
239
|
+
});
|
|
240
|
+
test('an `NN-` ordering prefix is physical-path-only: not an address, not a mutation destination', async () => {
|
|
241
|
+
const read = (await readLeaf.run({ name: 'acme/ordered', kind: undefined, frontmatter: false, scope: undefined, dir: root }));
|
|
242
|
+
assert.match(read.content, /ROOT ORDERED BODY/, 'the path-derived identity strips the prefix');
|
|
243
|
+
const query = await thrownByAsync(() => readLeaf.run({ name: 'acme/04-ordered', kind: undefined, frontmatter: false, scope: undefined, dir: root }));
|
|
244
|
+
assert.equal(query.code, 'not_found', 'the prefixed spelling is not a canonical identity');
|
|
245
|
+
const mutation = await thrownByAsync(() => moveLeaf.run({ name: 'acme/ordered', to: 'acme/05-renamed', dir: root }));
|
|
246
|
+
assert.equal(mutation.code, 'usage');
|
|
247
|
+
assert.match(mutation.message, /ordering prefix/);
|
|
248
|
+
assert.ok(!existsSync(join(storeDir(root), '05-renamed.md')), 'nothing was renamed');
|
|
249
|
+
assert.ok(!existsSync(join(storeDir(root), 'renamed.md')), 'and nothing was silently renamed to the stripped form');
|
|
250
|
+
});
|
|
251
|
+
test('moving a `name:` pin back to the file’s own path identity retires the pin in place', async () => {
|
|
252
|
+
const path = join(storeDir(root), 'x.md');
|
|
253
|
+
const result = (await moveLeaf.run({ name: 'acme/y', to: 'acme/x', dir: root }));
|
|
254
|
+
assert.equal(result.path, path, 'the file stays exactly where it already sat');
|
|
255
|
+
const text = readFileSync(path, 'utf8');
|
|
256
|
+
assert.ok(!/^name:/m.test(text), `the pin is gone; got:\n${text}`);
|
|
257
|
+
assert.match(text, /ROOT PINNED BODY/);
|
|
258
|
+
const log = join(storeDir(root), '.history', 'x.jsonl');
|
|
259
|
+
assert.equal(result.log_path, log);
|
|
260
|
+
assert.equal(readHistoryRecords(log).length, 2, 'the existing log gained the move record and was not duplicated');
|
|
261
|
+
assert.ok(!existsSync(join(storeDir(root), '.history', 'y.jsonl')), 'no sidecar was created under the retired pin');
|
|
262
|
+
});
|
|
263
|
+
test('a numeric `namespace:` does not mount and lint reports it', async () => {
|
|
264
|
+
assert.equal(openProjectMemoryStore(badns).mountStatus, 'invalid', 'a non-string namespace is not coerced into one');
|
|
265
|
+
try {
|
|
266
|
+
const result = (await lintLeaf.run({ dir: badns }));
|
|
267
|
+
assert.ok(result.findings.some((f) => f.rule === 'store-namespace'), `lint reports the undeclared namespace; got: ${JSON.stringify(result.findings)}`);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
process.exitCode = 0;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
test('a failing lint returns its declared rows and fails by exit code', async () => {
|
|
274
|
+
try {
|
|
275
|
+
const result = (await lintLeaf.run({ dir: dangling }));
|
|
276
|
+
assert.equal(process.exitCode, 1, 'a failing lint exits non-zero');
|
|
277
|
+
assert.ok(result.checked > 0, 'the declared counts survive the failure');
|
|
278
|
+
assert.ok(Array.isArray(result.warnings) && Array.isArray(result.notices), 'so do the non-fatal rows');
|
|
279
|
+
assert.ok(result.findings.some((f) => f.rule === 'dangling-link'), `the findings carry the error rows; got: ${JSON.stringify(result.findings)}`);
|
|
280
|
+
assert.match(result.follow_up, /error/);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
process.exitCode = 0;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
test('deleted history points at the revision holding the retained text, never a read that cannot answer', async () => {
|
|
287
|
+
await deleteLeaf.run({ name: 'acme/disposable', dir: root });
|
|
288
|
+
const result = (await historyLeaf.run({ name: 'acme/disposable', dir: root }));
|
|
289
|
+
assert.equal(result.total, 1);
|
|
290
|
+
assert.match(result.follow_up, /--revision 1/, 'the follow-up names the revision carrying the final text');
|
|
291
|
+
assert.ok(!result.follow_up.includes('memory read acme/disposable'), `a read of a deleted document is guaranteed to fail and must not be recommended; got: ${result.follow_up}`);
|
|
292
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Regressions for repository-root memory identity linting: nested stores derive
|
|
2
|
+
// their prefix and carry no declaration or required front door, while root-only
|
|
3
|
+
// declarations, owner-path collisions, and profile coverage remain enforced.
|
|
4
|
+
//
|
|
5
|
+
// Run: node --conditions=crtr-src --import tsx/esm --test --test-timeout=10000 src/commands/memory/__tests__/repository-root-lint.test.ts
|
|
6
|
+
import { after, beforeEach, test } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { closeDb } from '../../../core/canvas/db.js';
|
|
12
|
+
import { lintMemoryStore, lintMemoryTarget } from '../../../core/memory/lint.js';
|
|
13
|
+
import { openProjectMemoryStore } from '../../../core/memory-resolver.js';
|
|
14
|
+
import { createProfile } from '../../../core/profiles/manifest.js';
|
|
15
|
+
import { resetScopeCache } from '../../../core/scope.js';
|
|
16
|
+
let home;
|
|
17
|
+
let canvasHome;
|
|
18
|
+
let base;
|
|
19
|
+
const previousEnv = {};
|
|
20
|
+
function storeDir(owner) {
|
|
21
|
+
return join(owner, '.crouter', 'memory');
|
|
22
|
+
}
|
|
23
|
+
function writeDoc(path, frontmatter, body = 'DOCUMENT BODY') {
|
|
24
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
25
|
+
writeFileSync(path, `---\n${frontmatter}\n---\n\n${body}\n`, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
function rootFrontmatter(namespace, frontDoor = false) {
|
|
28
|
+
const surfaces = frontDoor
|
|
29
|
+
? '\nsurfaces:\n - on: workspace-open\n at: content\n - on: read\n match: "./**"\n at: content'
|
|
30
|
+
: '';
|
|
31
|
+
return `kind: knowledge\nnamespace: ${namespace}\nwhen-and-why-to-read: "When this repository is opened, this knowledge should be read because it orients work in the repository."${surfaces}`;
|
|
32
|
+
}
|
|
33
|
+
function nestedFrontmatter() {
|
|
34
|
+
return 'kind: knowledge\nwhen-and-why-to-read: "When this nested store is addressed, this knowledge should be read because it orients work in its area."';
|
|
35
|
+
}
|
|
36
|
+
function makeRepository(name, frontDoor = true) {
|
|
37
|
+
const root = join(base, name);
|
|
38
|
+
writeDoc(join(storeDir(root), 'INDEX.md'), rootFrontmatter(name, frontDoor), 'ROOT GUIDE');
|
|
39
|
+
return root;
|
|
40
|
+
}
|
|
41
|
+
function target(cwd, profileId = null) {
|
|
42
|
+
return { cwd, profileId, nodeId: null };
|
|
43
|
+
}
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
for (const key of ['HOME', 'CRTR_HOME', 'CRTR_PROFILE_ID', 'CRTR_NODE_ID']) {
|
|
46
|
+
if (!(key in previousEnv))
|
|
47
|
+
previousEnv[key] = process.env[key];
|
|
48
|
+
}
|
|
49
|
+
home = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-root-lint-home-')));
|
|
50
|
+
canvasHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-root-lint-canvas-')));
|
|
51
|
+
base = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-root-lint-')));
|
|
52
|
+
process.env['HOME'] = home;
|
|
53
|
+
process.env['CRTR_HOME'] = canvasHome;
|
|
54
|
+
delete process.env['CRTR_PROFILE_ID'];
|
|
55
|
+
delete process.env['CRTR_NODE_ID'];
|
|
56
|
+
mkdirSync(join(home, '.crouter', 'memory'), { recursive: true });
|
|
57
|
+
resetScopeCache();
|
|
58
|
+
});
|
|
59
|
+
after(() => {
|
|
60
|
+
closeDb();
|
|
61
|
+
resetScopeCache();
|
|
62
|
+
for (const dir of [home, canvasHome, base])
|
|
63
|
+
rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
for (const [key, value] of Object.entries(previousEnv)) {
|
|
65
|
+
if (value === undefined)
|
|
66
|
+
delete process.env[key];
|
|
67
|
+
else
|
|
68
|
+
process.env[key] = value;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
test('a root declaration and substantive metadata-free nested index are clean', () => {
|
|
72
|
+
const root = makeRepository('acme', false);
|
|
73
|
+
const nested = join(root, 'packages', 'api');
|
|
74
|
+
writeDoc(join(storeDir(nested), 'INDEX.md'), nestedFrontmatter(), 'NESTED GUIDE');
|
|
75
|
+
assert.equal(lintMemoryStore(openProjectMemoryStore(root)).ok, true, 'one repository-root declaration is valid');
|
|
76
|
+
const nestedResult = lintMemoryStore(openProjectMemoryStore(nested));
|
|
77
|
+
assert.equal(nestedResult.ok, true, JSON.stringify(nestedResult.findings));
|
|
78
|
+
assert.equal(openProjectMemoryStore(nested).namespace, 'acme/packages/api');
|
|
79
|
+
});
|
|
80
|
+
test('nested declarations and root names are both rejected', () => {
|
|
81
|
+
const root = makeRepository('acme');
|
|
82
|
+
const nested = join(root, 'packages', 'api');
|
|
83
|
+
writeDoc(join(storeDir(nested), 'INDEX.md'), `${nestedFrontmatter()}\nnamespace: nested\nname: forbidden`, 'NESTED GUIDE');
|
|
84
|
+
const result = lintMemoryStore(openProjectMemoryStore(nested));
|
|
85
|
+
assert.deepEqual(result.errors.map((finding) => finding.rule), ['namespace-placement', 'root-name']);
|
|
86
|
+
assert.match(result.errors[0].message, /repository-root store/);
|
|
87
|
+
assert.match(result.errors[0].message, new RegExp(root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
88
|
+
});
|
|
89
|
+
test('a nested non-root workspace-open guide remains an error', () => {
|
|
90
|
+
const root = makeRepository('acme');
|
|
91
|
+
const nested = join(root, 'packages', 'api');
|
|
92
|
+
writeDoc(join(storeDir(nested), 'INDEX.md'), nestedFrontmatter(), 'NESTED GUIDE');
|
|
93
|
+
writeDoc(join(storeDir(nested), 'second-guide.md'), `${nestedFrontmatter()}\nsurfaces:\n - on: workspace-open\n at: content`, 'SECOND GUIDE');
|
|
94
|
+
const result = lintMemoryStore(openProjectMemoryStore(nested));
|
|
95
|
+
assert.deepEqual(result.errors.map((finding) => finding.rule), ['front-door']);
|
|
96
|
+
});
|
|
97
|
+
test('owner-path collisions report one collision rather than a second namespace failure per store', () => {
|
|
98
|
+
const root = makeRepository('acme');
|
|
99
|
+
const first = join(root, 'api!');
|
|
100
|
+
const second = join(root, 'api?');
|
|
101
|
+
writeDoc(join(storeDir(first), 'INDEX.md'), nestedFrontmatter(), 'FIRST');
|
|
102
|
+
writeDoc(join(storeDir(second), 'INDEX.md'), nestedFrontmatter(), 'SECOND');
|
|
103
|
+
const result = lintMemoryTarget({ target: target(root), includeDescendants: true });
|
|
104
|
+
const collisionFindings = result.errors.filter((finding) => finding.rule === 'owner-path-collision');
|
|
105
|
+
assert.equal(collisionFindings.length, 2, JSON.stringify(result.errors));
|
|
106
|
+
assert.equal(result.errors.some((finding) => finding.rule === 'store-namespace'), false, JSON.stringify(result.errors));
|
|
107
|
+
for (const finding of collisionFindings) {
|
|
108
|
+
assert.match(finding.message, /api!/);
|
|
109
|
+
assert.match(finding.message, /api\?/);
|
|
110
|
+
assert.equal(result.errors.filter((candidate) => candidate.storeRoot === finding.storeRoot).length, 1, `the colliding store ${finding.storeRoot} emits only its collision finding`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
test('profile front-door coverage keeps all four repository-root cases distinct', () => {
|
|
114
|
+
// (a) Managed owners with no store still owe a root front door.
|
|
115
|
+
const absent = join(base, 'absent');
|
|
116
|
+
mkdirSync(absent, { recursive: true });
|
|
117
|
+
const absentProfile = createProfile('root-lint-absent', [{ path: absent, memory: 'content' }]);
|
|
118
|
+
const absentResult = lintMemoryTarget({ target: target(base, absentProfile.profileId) });
|
|
119
|
+
const absentDoors = absentResult.errors.filter((finding) => finding.rule === 'front-door');
|
|
120
|
+
assert.equal(absentDoors.length, 1, JSON.stringify(absentResult.errors));
|
|
121
|
+
assert.equal(absentDoors[0].path, absent);
|
|
122
|
+
// (b) A root store without its pair emits the required finding once, not once
|
|
123
|
+
// from the target view and again from managed-owner coverage.
|
|
124
|
+
const missing = makeRepository('missing-door', false);
|
|
125
|
+
const missingProfile = createProfile('root-lint-missing', [{ path: missing, memory: 'content' }]);
|
|
126
|
+
const missingResult = lintMemoryTarget({ target: target(missing, missingProfile.profileId) });
|
|
127
|
+
const missingDoors = missingResult.errors.filter((finding) => finding.rule === 'front-door');
|
|
128
|
+
assert.equal(missingDoors.length, 1, JSON.stringify(missingResult.errors));
|
|
129
|
+
assert.equal(missingDoors[0].path, missing);
|
|
130
|
+
// (c) A profile pointing at a nested owner does not make that store owe a
|
|
131
|
+
// front door, but the associated repository root still owes its pair.
|
|
132
|
+
const nestedManagedRoot = makeRepository('nested-managed', false);
|
|
133
|
+
const nestedManaged = join(nestedManagedRoot, 'area');
|
|
134
|
+
writeDoc(join(storeDir(nestedManaged), 'INDEX.md'), nestedFrontmatter(), 'NESTED GUIDE');
|
|
135
|
+
const nestedProfile = createProfile('root-lint-nested', [{ path: nestedManaged, memory: 'content' }]);
|
|
136
|
+
const nestedResult = lintMemoryTarget({ target: target(nestedManaged, nestedProfile.profileId) });
|
|
137
|
+
const nestedDoors = nestedResult.errors.filter((finding) => finding.rule === 'front-door');
|
|
138
|
+
assert.equal(nestedDoors.length, 1, JSON.stringify(nestedResult.errors));
|
|
139
|
+
assert.equal(nestedDoors[0].path, nestedManagedRoot);
|
|
140
|
+
// (d) A nested store with no door is clean when its repository root has one.
|
|
141
|
+
const cleanRoot = makeRepository('nested-clean');
|
|
142
|
+
const cleanNested = join(cleanRoot, 'area');
|
|
143
|
+
writeDoc(join(storeDir(cleanNested), 'INDEX.md'), nestedFrontmatter(), 'NESTED GUIDE');
|
|
144
|
+
const cleanResult = lintMemoryTarget({ target: target(cleanRoot) });
|
|
145
|
+
assert.equal(cleanResult.findings.length, 0, JSON.stringify(cleanResult.findings));
|
|
146
|
+
});
|