@north-light/crouter 0.3.226 → 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 +37 -9
- package/dist/daemon/api/bridge.d.ts +35 -4
- package/dist/daemon/api/bridge.js +50 -8
- 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
|
@@ -14,19 +14,7 @@ export declare function rungAtLeast(r: Rung, min: Rung): boolean;
|
|
|
14
14
|
/** The lower of two rungs — the clamp automatic delivery applies to an
|
|
15
15
|
* authored rung against its project relationship's cap. */
|
|
16
16
|
export declare function minRung(a: Rung, b: Rung): Rung;
|
|
17
|
-
|
|
18
|
-
* directory display name). `00-runtime-base/00-authoring` -> `runtime-base/authoring`; `spine` (no
|
|
19
|
-
* prefix) is unchanged. */
|
|
20
|
-
export declare function normalizeNameSegment(segment: string): string;
|
|
21
|
-
/** Normalize a full slash-separated path-derived doc name by stripping the
|
|
22
|
-
* optional numeric prefix from EVERY segment: `01-spine/00-has-manager` ->
|
|
23
|
-
* `spine/has-manager`. This is the identity a doc displays, dedups, and
|
|
24
|
-
* resolves under — distinct from its physical path, which keeps the prefix. */
|
|
25
|
-
export declare function normalizeDocName(name: string): string;
|
|
26
|
-
/** Resolve a doc's identity from its raw frontmatter record: an explicit
|
|
27
|
-
* `name` field wins (trimmed + normalized), else `fallbackName` (the
|
|
28
|
-
* normalized path-derived name). */
|
|
29
|
-
export declare function resolveDocName(fm: Record<string, unknown> | null | undefined, fallbackName: string): string;
|
|
17
|
+
export { normalizeNameSegment, normalizeDocName, resolveLocalName as resolveDocName } from '../memory/identity.js';
|
|
30
18
|
export declare const SURFACE_EVENTS: readonly ["boot", "workspace-open", "read", "memory-read", "command"];
|
|
31
19
|
export type SurfaceEvent = (typeof SURFACE_EVENTS)[number];
|
|
32
20
|
/** The rungs an entry may deliver at. There is no `none` — silence is the
|
|
@@ -42,47 +42,12 @@ export function minRung(a, b) {
|
|
|
42
42
|
return rungRank(a) <= rungRank(b) ? a : b;
|
|
43
43
|
}
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// prefix is NEVER part of the doc's identity: every identity-facing surface —
|
|
50
|
-
// MemoryDoc.name, direct lookup, leaf fallback, `crtr memory read`/`list`,
|
|
51
|
-
// prompt render, on-read display — derives from the NORMALIZED segments, so
|
|
52
|
-
// `00-runtime-base/00-authoring` displays/dedups/resolves as `runtime-base/authoring`. When a doc
|
|
53
|
-
// sets an explicit frontmatter `name`, that normalized name wins instead of
|
|
54
|
-
// the path-derived fallback. Only the physical path keeps the prefix.
|
|
45
|
+
// Identity — owned entirely by core/memory/identity.ts (ordering-prefix
|
|
46
|
+
// normalization, INDEX collapse, explicit-name-then-path local identity,
|
|
47
|
+
// namespace composition). Re-exported here so document parsing and identity
|
|
48
|
+
// derivation stay one rule with one implementation.
|
|
55
49
|
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
/** Strip an optional `NN-` ordering prefix from ONE path segment (file or
|
|
58
|
-
* directory display name). `00-runtime-base/00-authoring` -> `runtime-base/authoring`; `spine` (no
|
|
59
|
-
* prefix) is unchanged. */
|
|
60
|
-
export function normalizeNameSegment(segment) {
|
|
61
|
-
return segment.replace(NUMERIC_PREFIX_RE, '');
|
|
62
|
-
}
|
|
63
|
-
/** Normalize a full slash-separated path-derived doc name by stripping the
|
|
64
|
-
* optional numeric prefix from EVERY segment: `01-spine/00-has-manager` ->
|
|
65
|
-
* `spine/has-manager`. This is the identity a doc displays, dedups, and
|
|
66
|
-
* resolves under — distinct from its physical path, which keeps the prefix. */
|
|
67
|
-
export function normalizeDocName(name) {
|
|
68
|
-
return name.split('/').map(normalizeNameSegment).join('/');
|
|
69
|
-
}
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// Substrate identity — the ONE explicit-name-then-path-fallback rule. A doc's
|
|
72
|
-
// resolver identity is its explicit frontmatter `name` when present (normalized),
|
|
73
|
-
// otherwise the caller-supplied path-derived fallback. Every loader that turns a
|
|
74
|
-
// physical .md file into a named doc (the resolver's `listMemoryDocsInDir` and
|
|
75
|
-
// the explicit file-context loader) calls this ONE helper, so a doc's identity
|
|
76
|
-
// never depends on which path loaded it.
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
/** Resolve a doc's identity from its raw frontmatter record: an explicit
|
|
79
|
-
* `name` field wins (trimmed + normalized), else `fallbackName` (the
|
|
80
|
-
* normalized path-derived name). */
|
|
81
|
-
export function resolveDocName(fm, fallbackName) {
|
|
82
|
-
const raw = fm?.['name'];
|
|
83
|
-
const explicit = typeof raw === 'string' && raw.trim() !== '' ? normalizeDocName(raw.trim()) : '';
|
|
84
|
-
return explicit !== '' ? explicit : fallbackName;
|
|
85
|
-
}
|
|
50
|
+
export { normalizeNameSegment, normalizeDocName, resolveLocalName as resolveDocName } from '../memory/identity.js';
|
|
86
51
|
// ---------------------------------------------------------------------------
|
|
87
52
|
// Surfaces — explicit event routing (design: surfaces-design.md). A doc
|
|
88
53
|
// declares WHEN it delivers as (event, match, rung) entries; matches are
|
|
@@ -2,6 +2,14 @@ import type { MemoryDoc } from '../memory-resolver.js';
|
|
|
2
2
|
import type { SubstrateDoc } from './schema.js';
|
|
3
3
|
type ListAllMemoryDocsFn = () => MemoryDoc[];
|
|
4
4
|
type ParseSubstrateDocFn = (doc: MemoryDoc) => SubstrateDoc | null;
|
|
5
|
+
/** A parsed substrate document with the resolver descriptor it came from. The
|
|
6
|
+
* parsed doc carries canonical identity but not the physical form behind it,
|
|
7
|
+
* so the descriptor is what supplies the routing anchor a `./` memory-read
|
|
8
|
+
* glob matches against. */
|
|
9
|
+
export interface EventCorpusDoc {
|
|
10
|
+
doc: SubstrateDoc;
|
|
11
|
+
source: MemoryDoc;
|
|
12
|
+
}
|
|
5
13
|
/** Called by canvas-doc-substrate.ts on every `session_start`.
|
|
6
14
|
* Resets all cached values so the next access triggers a fresh scan. */
|
|
7
15
|
export declare function clearSessionCache(): void;
|
|
@@ -13,13 +21,15 @@ export declare function clearSessionCache(): void;
|
|
|
13
21
|
* that need every referenceable doc (inline memory-reference inventory) call
|
|
14
22
|
* this directly instead of `cachedAllMemoryDocs`. */
|
|
15
23
|
export declare function cachedAllMemoryDocsInclusive(listFn: ListAllMemoryDocsFn): MemoryDoc[];
|
|
16
|
-
/** allMemoryDocsInclusive
|
|
17
|
-
*
|
|
24
|
+
/** allMemoryDocsInclusive parsed and paired with its descriptors, INCLUDING
|
|
25
|
+
* node scope — the event corpus, and the module's only parse pass. */
|
|
26
|
+
export declare function cachedEventCorpusInclusive(listFn: ListAllMemoryDocsFn, parseFn: ParseSubstrateDocFn): EventCorpusDoc[];
|
|
27
|
+
/** The parsed docs of the inclusive event corpus, without their descriptors. */
|
|
18
28
|
export declare function cachedSubstrateDocsInclusive(listFn: ListAllMemoryDocsFn, parseFn: ParseSubstrateDocFn): SubstrateDoc[];
|
|
19
29
|
/** All memory docs (listAllMemoryDocs()), scanned once per session.
|
|
20
30
|
* `listFn` is injected by the caller to avoid circular imports at module init. */
|
|
21
31
|
export declare function cachedAllMemoryDocs(listFn: ListAllMemoryDocsFn): MemoryDoc[];
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
32
|
+
/** The node-excluded parsed view — render.ts's boot corpus. Projected out of
|
|
33
|
+
* the inclusive parse rather than parsed again. */
|
|
24
34
|
export declare function cachedSubstrateDocs(listFn: ListAllMemoryDocsFn, parseFn: ParseSubstrateDocFn): SubstrateDoc[];
|
|
25
35
|
export {};
|
|
@@ -18,27 +18,33 @@
|
|
|
18
18
|
// Cached entries:
|
|
19
19
|
// • allMemoryDocsInclusive — the full listAllMemoryDocs() corpus, UNFILTERED
|
|
20
20
|
// (includes node scope). This is the ONE underlying
|
|
21
|
-
// filesystem scan
|
|
22
|
-
//
|
|
21
|
+
// filesystem scan the whole module performs per session;
|
|
22
|
+
// every other entry below derives from it by
|
|
23
23
|
// filtering/mapping, never by calling listFn() again.
|
|
24
|
-
// •
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
24
|
+
// • eventCorpusInclusive — allMemoryDocsInclusive parsed once, each surviving
|
|
25
|
+
// SubstrateDoc paired with the resolver descriptor it
|
|
26
|
+
// parsed from. The ONE parse pass; both substrate views
|
|
27
|
+
// below project out of it. on-read.ts consumes the pairs
|
|
28
|
+
// because `./` memory-read globs anchor on the
|
|
29
|
+
// descriptor's canonical routing anchor.
|
|
30
|
+
// • substrateDocsInclusive — the parsed docs alone, still including node
|
|
31
|
+
// scope. Consumers that need every referenceable doc
|
|
32
|
+
// (e.g. the inline memory-reference inventory) use this
|
|
33
|
+
// pair with allMemoryDocsInclusive.
|
|
29
34
|
// • allMemoryDocs — allMemoryDocsInclusive with node-scope docs excluded
|
|
30
35
|
// (boot-only view — see the comment on
|
|
31
36
|
// cachedAllMemoryDocs). Consumed by render.ts.
|
|
32
|
-
// • substrateDocs —
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
+
// • substrateDocs — the same node-excluded view, parsed. Used by
|
|
38
|
+
// render.ts's resolver corpus. The event corpus
|
|
39
|
+
// (on-read.ts) uses the INCLUSIVE pairs instead: node
|
|
40
|
+
// docs' surfaces entries fire on read/memory-read/
|
|
41
|
+
// command like any other store's.
|
|
37
42
|
// Node subjects are deliberately NOT cached: lifecycle, mode, kind, and other
|
|
38
43
|
// gate inputs can change while a resident broker/session remains live. Rendering
|
|
39
44
|
// must always gate against the current persisted node metadata.
|
|
40
45
|
const _cache = {
|
|
41
46
|
allMemoryDocsInclusive: null,
|
|
47
|
+
eventCorpusInclusive: null,
|
|
42
48
|
substrateDocsInclusive: null,
|
|
43
49
|
allMemoryDocs: null,
|
|
44
50
|
substrateDocs: null,
|
|
@@ -47,6 +53,7 @@ const _cache = {
|
|
|
47
53
|
* Resets all cached values so the next access triggers a fresh scan. */
|
|
48
54
|
export function clearSessionCache() {
|
|
49
55
|
_cache.allMemoryDocsInclusive = null;
|
|
56
|
+
_cache.eventCorpusInclusive = null;
|
|
50
57
|
_cache.substrateDocsInclusive = null;
|
|
51
58
|
_cache.allMemoryDocs = null;
|
|
52
59
|
_cache.substrateDocs = null;
|
|
@@ -64,13 +71,24 @@ export function cachedAllMemoryDocsInclusive(listFn) {
|
|
|
64
71
|
}
|
|
65
72
|
return _cache.allMemoryDocsInclusive;
|
|
66
73
|
}
|
|
67
|
-
/** allMemoryDocsInclusive
|
|
68
|
-
*
|
|
74
|
+
/** allMemoryDocsInclusive parsed and paired with its descriptors, INCLUDING
|
|
75
|
+
* node scope — the event corpus, and the module's only parse pass. */
|
|
76
|
+
export function cachedEventCorpusInclusive(listFn, parseFn) {
|
|
77
|
+
if (_cache.eventCorpusInclusive === null) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const source of cachedAllMemoryDocsInclusive(listFn)) {
|
|
80
|
+
const doc = parseFn(source);
|
|
81
|
+
if (doc !== null)
|
|
82
|
+
out.push({ doc, source });
|
|
83
|
+
}
|
|
84
|
+
_cache.eventCorpusInclusive = out;
|
|
85
|
+
}
|
|
86
|
+
return _cache.eventCorpusInclusive;
|
|
87
|
+
}
|
|
88
|
+
/** The parsed docs of the inclusive event corpus, without their descriptors. */
|
|
69
89
|
export function cachedSubstrateDocsInclusive(listFn, parseFn) {
|
|
70
90
|
if (_cache.substrateDocsInclusive === null) {
|
|
71
|
-
_cache.substrateDocsInclusive =
|
|
72
|
-
.map(parseFn)
|
|
73
|
-
.filter((d) => d !== null);
|
|
91
|
+
_cache.substrateDocsInclusive = cachedEventCorpusInclusive(listFn, parseFn).map((e) => e.doc);
|
|
74
92
|
}
|
|
75
93
|
return _cache.substrateDocsInclusive;
|
|
76
94
|
}
|
|
@@ -91,13 +109,13 @@ export function cachedAllMemoryDocs(listFn) {
|
|
|
91
109
|
}
|
|
92
110
|
return _cache.allMemoryDocs;
|
|
93
111
|
}
|
|
94
|
-
/**
|
|
95
|
-
*
|
|
112
|
+
/** The node-excluded parsed view — render.ts's boot corpus. Projected out of
|
|
113
|
+
* the inclusive parse rather than parsed again. */
|
|
96
114
|
export function cachedSubstrateDocs(listFn, parseFn) {
|
|
97
115
|
if (_cache.substrateDocs === null) {
|
|
98
|
-
_cache.substrateDocs =
|
|
99
|
-
.
|
|
100
|
-
.
|
|
116
|
+
_cache.substrateDocs = cachedEventCorpusInclusive(listFn, parseFn)
|
|
117
|
+
.filter((e) => e.doc.scope !== 'node')
|
|
118
|
+
.map((e) => e.doc);
|
|
101
119
|
}
|
|
102
120
|
return _cache.substrateDocs;
|
|
103
121
|
}
|
|
@@ -15,9 +15,9 @@ export declare function underOwningRoot(doc: Pick<SubstrateDoc, 'path'>, absFile
|
|
|
15
15
|
* for non-markdown files); an empty record never satisfies
|
|
16
16
|
* `matchFrontmatter`. */
|
|
17
17
|
export declare function matchesReadEntry(entry: SurfaceEntry, doc: Pick<SubstrateDoc, 'path' | 'scope'>, subject: NodeConfigSubject, absReadFile: string, readFrontmatter: Record<string, unknown>): boolean;
|
|
18
|
-
/** Does a `memory-read` entry on
|
|
19
|
-
* read of
|
|
20
|
-
export declare function matchesMemoryReadEntry(entry: SurfaceEntry,
|
|
18
|
+
/** Does a `memory-read` entry on a carrier anchored at `routingAnchor` fit a
|
|
19
|
+
* resolved read of the doc known by its exact canonical `name`? */
|
|
20
|
+
export declare function matchesMemoryReadEntry(entry: SurfaceEntry, routingAnchor: string, subject: NodeConfigSubject | null, name: string): boolean;
|
|
21
21
|
/** Does a `command` entry fit the executed command string? */
|
|
22
22
|
export declare function matchesCommandEntry(entry: SurfaceEntry, subject: NodeConfigSubject, command: string): boolean;
|
|
23
23
|
/** The doc's delivery rung for a read of `absReadFile` (already realpathed). */
|
|
@@ -25,9 +25,11 @@ export declare function readDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces' | 'p
|
|
|
25
25
|
/** The doc's workspace-open rung. Presence of an entry is the match; callers
|
|
26
26
|
* scope the corpus to project stores mounted by the subject's cwd/profile. */
|
|
27
27
|
export declare function workspaceOpenRung(doc: Pick<SubstrateDoc, 'surfaces'>, subject: NodeConfigSubject): Rung;
|
|
28
|
-
/** The doc's delivery rung for a `crtr memory read` that resolved a doc
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
28
|
+
/** The doc's delivery rung for a `crtr memory read` that resolved a doc known
|
|
29
|
+
* by its exact canonical `name` — no mount-stripped or other alias.
|
|
30
|
+
* `routingAnchor` comes from the doc's resolver descriptor — it is not
|
|
31
|
+
* derivable from the canonical name, which cannot say whether the doc fronts
|
|
32
|
+
* its own directory. */
|
|
33
|
+
export declare function memoryReadDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces'>, routingAnchor: string, subject: NodeConfigSubject | null, name: string): Rung;
|
|
32
34
|
/** The doc's delivery rung for an executed command string. */
|
|
33
35
|
export declare function commandDeliveryRung(doc: Pick<SubstrateDoc, 'surfaces'>, subject: NodeConfigSubject, command: string): Rung;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// AND together — each present constraint must fit, a `match` list is satisfied
|
|
8
8
|
// by any one glob — and multiple entries per event OR together. The `./`
|
|
9
9
|
// prefix is the one relative spelling: it anchors a glob to the doc's own
|
|
10
|
-
// container (its owning repo dir for `read`, its
|
|
10
|
+
// container (its owning repo dir for `read`, its canonical routing anchor for
|
|
11
11
|
// `memory-read`).
|
|
12
12
|
import { basename, isAbsolute, matchesGlob, relative, sep } from 'node:path';
|
|
13
13
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
@@ -77,31 +77,30 @@ export function matchesReadEntry(entry, doc, subject, absReadFile, readFrontmatt
|
|
|
77
77
|
}
|
|
78
78
|
return entry.match !== undefined || entry.matchFrontmatter !== undefined;
|
|
79
79
|
}
|
|
80
|
-
/** One memory-read glob vs the resolved doc's
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
|
|
80
|
+
/** One memory-read glob vs the resolved doc's exact canonical name: plain
|
|
81
|
+
* globs match that name only — no mount-stripped or other alias, per the
|
|
82
|
+
* hard-cut canonical-only routing contract; a `./`-anchored glob matches
|
|
83
|
+
* the name relative to the CARRIER's routing anchor — its own canonical
|
|
84
|
+
* name when the carrier fronts a directory, its canonical parent when it is
|
|
85
|
+
* an ordinary leaf. Names are slash-separated, so path globbing applies
|
|
86
|
+
* segment-wise. */
|
|
87
|
+
function memoryReadGlobMatches(glob, routingAnchor, name) {
|
|
86
88
|
if (glob.startsWith('./')) {
|
|
87
|
-
const dir = carrierName.split('/').slice(0, -1).join('/');
|
|
88
89
|
const stripped = glob.slice(2);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return safeGlob(name.slice(dir.length + 1), stripped);
|
|
95
|
-
});
|
|
90
|
+
if (routingAnchor === '')
|
|
91
|
+
return safeGlob(name, stripped);
|
|
92
|
+
if (!name.startsWith(routingAnchor + '/'))
|
|
93
|
+
return false;
|
|
94
|
+
return safeGlob(name.slice(routingAnchor.length + 1), stripped);
|
|
96
95
|
}
|
|
97
|
-
return
|
|
96
|
+
return safeGlob(name, glob);
|
|
98
97
|
}
|
|
99
|
-
/** Does a `memory-read` entry on
|
|
100
|
-
* read of
|
|
101
|
-
export function matchesMemoryReadEntry(entry,
|
|
98
|
+
/** Does a `memory-read` entry on a carrier anchored at `routingAnchor` fit a
|
|
99
|
+
* resolved read of the doc known by its exact canonical `name`? */
|
|
100
|
+
export function matchesMemoryReadEntry(entry, routingAnchor, subject, name) {
|
|
102
101
|
if (entry.on !== 'memory-read' || entry.match === undefined || !surfaceEntryGatePasses(entry, subject))
|
|
103
102
|
return false;
|
|
104
|
-
return entry.match.some((g) => memoryReadGlobMatches(g,
|
|
103
|
+
return entry.match.some((g) => memoryReadGlobMatches(g, routingAnchor, name));
|
|
105
104
|
}
|
|
106
105
|
/** A command glob is a STRING glob over the whole command line, not a path
|
|
107
106
|
* glob: `*` crosses every character (including `/` — command lines carry
|
|
@@ -154,13 +153,15 @@ export function workspaceOpenRung(doc, subject) {
|
|
|
154
153
|
}
|
|
155
154
|
return r;
|
|
156
155
|
}
|
|
157
|
-
/** The doc's delivery rung for a `crtr memory read` that resolved a doc
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
|
|
156
|
+
/** The doc's delivery rung for a `crtr memory read` that resolved a doc known
|
|
157
|
+
* by its exact canonical `name` — no mount-stripped or other alias.
|
|
158
|
+
* `routingAnchor` comes from the doc's resolver descriptor — it is not
|
|
159
|
+
* derivable from the canonical name, which cannot say whether the doc fronts
|
|
160
|
+
* its own directory. */
|
|
161
|
+
export function memoryReadDeliveryRung(doc, routingAnchor, subject, name) {
|
|
161
162
|
let r = 'none';
|
|
162
163
|
for (const e of doc.surfaces) {
|
|
163
|
-
if (!matchesMemoryReadEntry(e,
|
|
164
|
+
if (!matchesMemoryReadEntry(e, routingAnchor, subject, name))
|
|
164
165
|
continue;
|
|
165
166
|
if (rungRank(e.at) > rungRank(r))
|
|
166
167
|
r = e.at;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
export interface SourceDaemonFixture {
|
|
3
|
+
root: string;
|
|
4
|
+
home: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
canvasHome: string;
|
|
7
|
+
pidfile: string;
|
|
8
|
+
socket: string;
|
|
9
|
+
tcpPort: number;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface SourceDaemonProcess {
|
|
13
|
+
child: ChildProcess;
|
|
14
|
+
exited: Promise<{
|
|
15
|
+
code: number | null;
|
|
16
|
+
signal: NodeJS.Signals | null;
|
|
17
|
+
}>;
|
|
18
|
+
stdout(): string;
|
|
19
|
+
stderr(): string;
|
|
20
|
+
stop(): Promise<{
|
|
21
|
+
code: number | null;
|
|
22
|
+
signal: NodeJS.Signals | null;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare function createSourceDaemonFixture(): Promise<SourceDaemonFixture>;
|
|
26
|
+
export declare function startSourceDaemon(fixture: SourceDaemonFixture): SourceDaemonProcess;
|
|
27
|
+
export declare function healthRequest(port: number, timeoutMs?: number): Promise<boolean>;
|
|
28
|
+
export declare function waitForHealth(process: SourceDaemonProcess, port: number, timeoutMs?: number): Promise<void>;
|
|
29
|
+
export declare function healthWasReadyBeforeExit(process: SourceDaemonProcess, port: number): Promise<boolean>;
|
|
30
|
+
export declare function brokerPidsForCanvas(canvasHome: string): number[];
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { request } from 'node:http';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
function delay(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
async function reserveTcpPort() {
|
|
12
|
+
const server = createServer();
|
|
13
|
+
await new Promise((resolve, reject) => {
|
|
14
|
+
server.once('error', reject);
|
|
15
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
16
|
+
});
|
|
17
|
+
const address = server.address();
|
|
18
|
+
if (address === null || typeof address === 'string')
|
|
19
|
+
throw new Error('test TCP listener did not receive a port');
|
|
20
|
+
await new Promise((resolve, reject) => server.close((error) => error === undefined ? resolve() : reject(error)));
|
|
21
|
+
return address.port;
|
|
22
|
+
}
|
|
23
|
+
export async function createSourceDaemonFixture() {
|
|
24
|
+
const root = mkdtempSync('/tmp/crtr-migration-daemon-');
|
|
25
|
+
const home = join(root, 'home');
|
|
26
|
+
const cwd = join(home, 'work');
|
|
27
|
+
const canvasHome = join(root, 'canvas');
|
|
28
|
+
const pidfile = join(canvasHome, 'ready.pid');
|
|
29
|
+
const socket = join(canvasHome, 'crtrd.sock');
|
|
30
|
+
mkdirSync(cwd, { recursive: true });
|
|
31
|
+
mkdirSync(canvasHome, { recursive: true });
|
|
32
|
+
const tcpPort = await reserveTcpPort();
|
|
33
|
+
return {
|
|
34
|
+
root,
|
|
35
|
+
home,
|
|
36
|
+
cwd,
|
|
37
|
+
canvasHome,
|
|
38
|
+
pidfile,
|
|
39
|
+
socket,
|
|
40
|
+
tcpPort,
|
|
41
|
+
dispose: () => rmSync(root, { recursive: true, force: true }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function startSourceDaemon(fixture) {
|
|
45
|
+
const require = createRequire(import.meta.url);
|
|
46
|
+
const tsxLoader = pathToFileURL(require.resolve('tsx/esm')).href;
|
|
47
|
+
const entry = fileURLToPath(new URL('../../crtrd-cli.ts', import.meta.url));
|
|
48
|
+
const env = {
|
|
49
|
+
...process.env,
|
|
50
|
+
HOME: fixture.home,
|
|
51
|
+
CRTR_HOME: fixture.canvasHome,
|
|
52
|
+
CRTR_PIDFILE: fixture.pidfile,
|
|
53
|
+
CRTRD_TCP: `127.0.0.1:${fixture.tcpPort}`,
|
|
54
|
+
XDG_CACHE_HOME: join(fixture.root, 'cache'),
|
|
55
|
+
XDG_CONFIG_HOME: join(fixture.root, 'config'),
|
|
56
|
+
XDG_DATA_HOME: join(fixture.root, 'data'),
|
|
57
|
+
};
|
|
58
|
+
for (const key of [
|
|
59
|
+
'CRTR_NODE_ID',
|
|
60
|
+
'CRTR_KIND',
|
|
61
|
+
'CRTR_MODE',
|
|
62
|
+
'CRTR_LIFECYCLE',
|
|
63
|
+
'CRTR_NODE_CWD',
|
|
64
|
+
'CRTR_CONTEXT_DIR',
|
|
65
|
+
'CRTR_CYCLES',
|
|
66
|
+
'CRTR_PROFILE_ID',
|
|
67
|
+
'CRTR_PARENT_NODE_ID',
|
|
68
|
+
'CRTR_FRONT_DOOR',
|
|
69
|
+
'CRTR_BROKER_ENGINE',
|
|
70
|
+
'CRTR_PI_BINARY',
|
|
71
|
+
'NODE_OPTIONS',
|
|
72
|
+
])
|
|
73
|
+
delete env[key];
|
|
74
|
+
const child = spawn(process.execPath, [
|
|
75
|
+
'--conditions=crtr-src',
|
|
76
|
+
'--import', tsxLoader,
|
|
77
|
+
entry,
|
|
78
|
+
'--canvas-home', fixture.canvasHome,
|
|
79
|
+
], {
|
|
80
|
+
cwd: fixture.cwd,
|
|
81
|
+
env,
|
|
82
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
83
|
+
});
|
|
84
|
+
let stdout = '';
|
|
85
|
+
let stderr = '';
|
|
86
|
+
child.stdout.setEncoding('utf8');
|
|
87
|
+
child.stderr.setEncoding('utf8');
|
|
88
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
89
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
90
|
+
const exited = new Promise((resolve, reject) => {
|
|
91
|
+
child.once('error', reject);
|
|
92
|
+
child.once('close', (code, signal) => resolve({ code, signal }));
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
child,
|
|
96
|
+
exited,
|
|
97
|
+
stdout: () => stdout,
|
|
98
|
+
stderr: () => stderr,
|
|
99
|
+
stop: async () => {
|
|
100
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
101
|
+
child.kill('SIGTERM');
|
|
102
|
+
const timeout = setTimeout(() => {
|
|
103
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
104
|
+
child.kill('SIGKILL');
|
|
105
|
+
}, 10_000);
|
|
106
|
+
try {
|
|
107
|
+
return await exited;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function healthRequest(port, timeoutMs = 200) {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
let settled = false;
|
|
118
|
+
const finish = (ready) => {
|
|
119
|
+
if (settled)
|
|
120
|
+
return;
|
|
121
|
+
settled = true;
|
|
122
|
+
resolve(ready);
|
|
123
|
+
};
|
|
124
|
+
const req = request({ host: '127.0.0.1', port, path: '/healthz', method: 'GET', agent: false }, (response) => {
|
|
125
|
+
response.resume();
|
|
126
|
+
response.once('end', () => finish(response.statusCode === 200));
|
|
127
|
+
});
|
|
128
|
+
req.once('error', () => finish(false));
|
|
129
|
+
req.setTimeout(timeoutMs, () => {
|
|
130
|
+
req.destroy();
|
|
131
|
+
finish(false);
|
|
132
|
+
});
|
|
133
|
+
req.end();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export async function waitForHealth(process, port, timeoutMs = 20_000) {
|
|
137
|
+
const deadline = Date.now() + timeoutMs;
|
|
138
|
+
while (Date.now() < deadline) {
|
|
139
|
+
if (await healthRequest(port))
|
|
140
|
+
return;
|
|
141
|
+
if (process.child.exitCode !== null || process.child.signalCode !== null) {
|
|
142
|
+
const result = await process.exited;
|
|
143
|
+
throw new Error(`source daemon exited before health readiness (${JSON.stringify(result)}):\n${process.stderr()}`);
|
|
144
|
+
}
|
|
145
|
+
await delay(10);
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`source daemon did not answer /healthz within ${timeoutMs}ms:\n${process.stderr()}`);
|
|
148
|
+
}
|
|
149
|
+
export async function healthWasReadyBeforeExit(process, port) {
|
|
150
|
+
let exited = false;
|
|
151
|
+
void process.exited.then(() => { exited = true; });
|
|
152
|
+
while (!exited) {
|
|
153
|
+
if (await healthRequest(port, 50))
|
|
154
|
+
return true;
|
|
155
|
+
await delay(2);
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
export function brokerPidsForCanvas(canvasHome) {
|
|
160
|
+
let table;
|
|
161
|
+
try {
|
|
162
|
+
table = execFileSync('ps', ['-ax', '-o', 'pid=,command='], { encoding: 'utf8', timeout: 2_000 });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const declaration = `--canvas-home ${canvasHome}`;
|
|
168
|
+
return table.split('\n').flatMap((line) => {
|
|
169
|
+
if (!line.includes('broker-cli') || !line.includes(declaration))
|
|
170
|
+
return [];
|
|
171
|
+
const pid = Number.parseInt(line.trim().split(/\s+/, 1)[0] ?? '', 10);
|
|
172
|
+
return Number.isInteger(pid) ? [pid] : [];
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, watch, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { onDiskMigrationIdentity } from '../../../migrations/registry.js';
|
|
6
|
+
import { brokerPidsForCanvas, createSourceDaemonFixture, healthWasReadyBeforeExit, startSourceDaemon, waitForHealth, } from '../helpers/source-daemon.js';
|
|
7
|
+
const PRE_MIGRATION_DOC = `---
|
|
8
|
+
kind: knowledge
|
|
9
|
+
name: daemon-ordering
|
|
10
|
+
system-prompt-visibility: preview
|
|
11
|
+
file-read-visibility: content
|
|
12
|
+
applies-to:
|
|
13
|
+
- "**/*.ts"
|
|
14
|
+
read-when:
|
|
15
|
+
language: typescript
|
|
16
|
+
---
|
|
17
|
+
# Daemon ordering
|
|
18
|
+
|
|
19
|
+
This document begins in the pre-surfaces schema.
|
|
20
|
+
`;
|
|
21
|
+
function writeUserMemory(home, name, source) {
|
|
22
|
+
const path = join(home, '.crouter', 'memory', name);
|
|
23
|
+
mkdirSync(join(home, '.crouter', 'memory'), { recursive: true });
|
|
24
|
+
writeFileSync(path, source, 'utf8');
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
test('source daemon migrates the pre-migration corpus and writes its marker before API readiness', { timeout: 30_000 }, async () => {
|
|
28
|
+
const fixture = await createSourceDaemonFixture();
|
|
29
|
+
const docPath = writeUserMemory(fixture.home, 'daemon-ordering.md', PRE_MIGRATION_DOC);
|
|
30
|
+
const markerPath = join(fixture.canvasHome, 'on-disk-migrations.json');
|
|
31
|
+
const dbPath = join(fixture.canvasHome, 'canvas.db');
|
|
32
|
+
assert.equal(existsSync(markerPath), false, 'fixture starts without a migration marker');
|
|
33
|
+
assert.equal(existsSync(dbPath), false, 'fixture starts without canvas.db');
|
|
34
|
+
assert.match(readFileSync(docPath, 'utf8'), /system-prompt-visibility:/, 'fixture is genuinely pre-migration');
|
|
35
|
+
const daemon = startSourceDaemon(fixture);
|
|
36
|
+
try {
|
|
37
|
+
const markerDeadline = Date.now() + 20_000;
|
|
38
|
+
while (!existsSync(markerPath) && Date.now() < markerDeadline) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
40
|
+
}
|
|
41
|
+
assert.equal(existsSync(markerPath), true, `marker was not written; stderr:\n${daemon.stderr()}`);
|
|
42
|
+
const migratedAtMarker = readFileSync(docPath, 'utf8');
|
|
43
|
+
assert.doesNotMatch(migratedAtMarker, /system-prompt-visibility:|file-read-visibility:|applies-to:|read-when:/);
|
|
44
|
+
assert.equal(existsSync(fixture.pidfile), false, 'pidfile readiness is not published when the marker first appears');
|
|
45
|
+
assert.equal(existsSync(fixture.socket), false, 'API readiness is not published when the marker first appears');
|
|
46
|
+
await waitForHealth(daemon, fixture.tcpPort);
|
|
47
|
+
const migrated = readFileSync(docPath, 'utf8');
|
|
48
|
+
assert.doesNotMatch(migrated, /system-prompt-visibility:|file-read-visibility:|applies-to:|read-when:/);
|
|
49
|
+
assert.match(migrated, /surfaces:/);
|
|
50
|
+
assert.match(migrated, /on: boot/);
|
|
51
|
+
assert.match(migrated, /on: read/);
|
|
52
|
+
assert.deepEqual(JSON.parse(readFileSync(markerPath, 'utf8')), onDiskMigrationIdentity());
|
|
53
|
+
assert.equal(existsSync(fixture.pidfile), true, 'health readiness follows the pidfile readiness mirror');
|
|
54
|
+
assert.equal(existsSync(fixture.socket), true, 'the unique unix API listener is bound at readiness');
|
|
55
|
+
assert.equal(existsSync(dbPath), true, 'daemon ownership opens canvas.db only after migration');
|
|
56
|
+
assert.ok(statSync(markerPath).mtimeMs <= statSync(fixture.pidfile).mtimeMs, 'the completion marker is written no later than the readiness pidfile');
|
|
57
|
+
assert.ok(statSync(markerPath).mtimeMs <= statSync(fixture.socket).mtimeMs, 'the completion marker is written no later than the API listener');
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
const exit = await daemon.stop();
|
|
61
|
+
fixture.dispose();
|
|
62
|
+
assert.deepEqual(exit, { code: 0, signal: null }, `daemon must stop cleanly; stderr:\n${daemon.stderr()}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
test('a real migration blocker exits before database, pidfile, listeners, or broker effects', { timeout: 30_000 }, async () => {
|
|
66
|
+
const fixture = await createSourceDaemonFixture();
|
|
67
|
+
const blockerPath = writeUserMemory(fixture.home, 'blocked.md', `---\nkind: knowledge\nbroken: [\n---\n# Blocked\n`);
|
|
68
|
+
const effectNames = new Set();
|
|
69
|
+
const effects = watch(fixture.canvasHome, (_event, filename) => {
|
|
70
|
+
if (filename !== null)
|
|
71
|
+
effectNames.add(filename.toString());
|
|
72
|
+
});
|
|
73
|
+
const daemon = startSourceDaemon(fixture);
|
|
74
|
+
const readiness = healthWasReadyBeforeExit(daemon, fixture.tcpPort);
|
|
75
|
+
const exit = await daemon.exited;
|
|
76
|
+
const wasReady = await readiness;
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
78
|
+
effects.close();
|
|
79
|
+
try {
|
|
80
|
+
assert.equal(exit.signal, null);
|
|
81
|
+
assert.notEqual(exit.code, 0, `blocker must exit nonzero; stderr:\n${daemon.stderr()}`);
|
|
82
|
+
assert.match(daemon.stderr(), new RegExp(blockerPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
83
|
+
assert.match(daemon.stderr(), /Repair: crtr sys migrate/);
|
|
84
|
+
assert.equal(wasReady, false, 'the unique TCP endpoint never answered health');
|
|
85
|
+
const databaseFiles = readdirSync(fixture.canvasHome).filter((name) => name.startsWith('canvas.db'));
|
|
86
|
+
assert.deepEqual(databaseFiles, [], 'canvas.db and its journals were never created');
|
|
87
|
+
assert.equal(existsSync(fixture.pidfile), false, 'readiness pidfile was never created');
|
|
88
|
+
assert.equal(existsSync(fixture.socket), false, 'unix API listener was never created');
|
|
89
|
+
assert.equal(existsSync(join(fixture.canvasHome, 'on-disk-migrations.json')), false, 'blocker never gets a completion marker');
|
|
90
|
+
assert.equal(existsSync(join(fixture.canvasHome, 'nodes')), false, 'no node/broker state was created');
|
|
91
|
+
assert.deepEqual(brokerPidsForCanvas(fixture.canvasHome), [], 'no broker process declares the isolated canvas');
|
|
92
|
+
assert.deepEqual([...effectNames].filter((name) => name.startsWith('canvas.db') || name === 'ready.pid' || name === 'crtrd.sock' || name === 'nodes'), [], `filesystem watcher saw no transient daemon effects (all events: ${[...effectNames].join(', ')})`);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
fixture.dispose();
|
|
96
|
+
}
|
|
97
|
+
});
|