@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
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// The corpus-lane runner: discover EVERY memory store a machine can reach,
|
|
2
|
+
// snapshot them together, let each corpus migration plan across the whole set,
|
|
3
|
+
// then apply in two global phases.
|
|
4
|
+
//
|
|
5
|
+
// Why global: a document's canonical identity is an ADDRESS other stores point
|
|
6
|
+
// at through `[[links]]` and `memory-read` routes. A per-store lane can rename
|
|
7
|
+
// an identity it owns but cannot see the consumers that break, so the corpus
|
|
8
|
+
// lane plans over every store at once — and one blocker anywhere writes nothing
|
|
9
|
+
// anywhere, because a half-applied identity cut leaves dangling addresses.
|
|
10
|
+
//
|
|
11
|
+
// Why two phases: consumers are rewritten first, identities second, both across
|
|
12
|
+
// the ENTIRE corpus. A store whose identity changed must not be readable at its
|
|
13
|
+
// new address before every pointer to the old one has moved.
|
|
14
|
+
//
|
|
15
|
+
// Discovery is filesystem-only and never opens canvas.db. A migration runs
|
|
16
|
+
// BEFORE the daemon on a cold home (Northlight's entrypoint does exactly that),
|
|
17
|
+
// so node target metadata comes from each node's durable meta.json — the source
|
|
18
|
+
// of truth the database index is rebuilt from — through its existing read-only
|
|
19
|
+
// helper. Missing or corrupt metadata remains a global blocker.
|
|
20
|
+
import { readdirSync } from 'node:fs';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { readMetaObject } from '../core/canvas/meta-file.js';
|
|
23
|
+
import { nodeMetaPath, nodesRoot } from '../core/canvas/paths.js';
|
|
24
|
+
import { atomicWriteText, listDirs, pathExists, realpathOrSelf, removePath, walkFiles } from '../core/fs-utils.js';
|
|
25
|
+
import { memoryStoresInPrecedence, } from '../core/memory-resolver.js';
|
|
26
|
+
import { descendantStoreRootsExhaustive } from '../core/nested-stores.js';
|
|
27
|
+
import { profileMemoryDir } from '../core/profiles/manifest.js';
|
|
28
|
+
import { listInstalledPlugins, listInstalledPluginsInRoot } from '../core/resolver.js';
|
|
29
|
+
import { memoryDir as nodeMemoryDir } from '../core/runtime/memory.js';
|
|
30
|
+
import { NEUTRAL_PROJECT_MEMORY, pluginMemoryDir, projectScopeRoots, scopeRoot } from '../core/scope.js';
|
|
31
|
+
import { CRTR_DIR_NAME } from '../types.js';
|
|
32
|
+
import { foldDocChange, loadStoreSnapshot } from './convergent.js';
|
|
33
|
+
import { STATE_MIGRATIONS } from './registry.js';
|
|
34
|
+
function firstLine(e) {
|
|
35
|
+
return (e instanceof Error ? e.message : String(e)).split('\n')[0] ?? '';
|
|
36
|
+
}
|
|
37
|
+
function targetId(target) {
|
|
38
|
+
return `${target.profileId ?? '-'}:${target.nodeId ?? '-'}:${realpathOrSelf(target.cwd)}`;
|
|
39
|
+
}
|
|
40
|
+
function pluginStoreDescriptor(plugin) {
|
|
41
|
+
const storeRoot = pluginMemoryDir(plugin);
|
|
42
|
+
return {
|
|
43
|
+
scope: plugin.scope,
|
|
44
|
+
storeRoot,
|
|
45
|
+
namespace: plugin.name,
|
|
46
|
+
plugin: plugin.name,
|
|
47
|
+
mountStatus: pathExists(storeRoot) ? 'ready' : 'absent',
|
|
48
|
+
projectMemory: NEUTRAL_PROJECT_MEMORY,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function nodeStoreDescriptor(storeRoot) {
|
|
52
|
+
return {
|
|
53
|
+
scope: 'node',
|
|
54
|
+
storeRoot,
|
|
55
|
+
namespace: '',
|
|
56
|
+
mountStatus: pathExists(storeRoot) ? 'ready' : 'absent',
|
|
57
|
+
projectMemory: NEUTRAL_PROJECT_MEMORY,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Node stores holding at least one document. An empty node store contributes
|
|
61
|
+
* no identity and no reference, so it stays out of the corpus — which is what
|
|
62
|
+
* keeps discovery cheap across thousands of nodes AND keeps the daemon
|
|
63
|
+
* optional on a home whose nodes have never written memory. */
|
|
64
|
+
function nodeStoresWithDocs() {
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const nodeId of listDirs(nodesRoot())) {
|
|
67
|
+
const storeRoot = nodeMemoryDir(nodeId);
|
|
68
|
+
if (!pathExists(storeRoot))
|
|
69
|
+
continue;
|
|
70
|
+
const docs = walkFiles(storeRoot, (n) => n.endsWith('.md') && n !== 'SKILL.md', (d) => d.startsWith('.'));
|
|
71
|
+
if (docs.length > 0)
|
|
72
|
+
out.push({ nodeId, storeRoot });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** Discover the whole reachable corpus: every store, the targets that mount
|
|
77
|
+
* them, and the frontier searched to find them. `profiles` are the manifest
|
|
78
|
+
* lane's post-migration snapshots — a pre-migration manifest reads as corrupt,
|
|
79
|
+
* so discovery must run after that lane, not against `listProfiles()`. */
|
|
80
|
+
export async function discoverCorpus(opts) {
|
|
81
|
+
const cwd = realpathOrSelf(opts.cwd ?? process.cwd());
|
|
82
|
+
const selectedProfileId = process.env['CRTR_PROFILE_ID'] || null;
|
|
83
|
+
const explicitDirs = opts.explicitDirs ?? [];
|
|
84
|
+
const roots = [];
|
|
85
|
+
const rootSeen = new Set();
|
|
86
|
+
const addRoot = (row) => {
|
|
87
|
+
const key = `${row.reason}\u0000${realpathOrSelf(row.root)}\u0000${row.detail ?? ''}`;
|
|
88
|
+
if (rootSeen.has(key))
|
|
89
|
+
return;
|
|
90
|
+
rootSeen.add(key);
|
|
91
|
+
roots.push(row);
|
|
92
|
+
};
|
|
93
|
+
const projectRoots = new Map();
|
|
94
|
+
const addProjectRoot = (root, reason, profileId) => {
|
|
95
|
+
const key = realpathOrSelf(root);
|
|
96
|
+
let entry = projectRoots.get(key);
|
|
97
|
+
if (entry === undefined) {
|
|
98
|
+
entry = { root, reason, profileIds: [] };
|
|
99
|
+
projectRoots.set(key, entry);
|
|
100
|
+
}
|
|
101
|
+
if (!entry.profileIds.includes(profileId))
|
|
102
|
+
entry.profileIds.push(profileId);
|
|
103
|
+
};
|
|
104
|
+
const contexts = [
|
|
105
|
+
{ reason: 'cwd-ancestor', profileId: selectedProfileId, roots: projectScopeRoots(cwd, selectedProfileId) },
|
|
106
|
+
...opts.profiles.map(({ profileId, manifest }) => ({
|
|
107
|
+
reason: 'profile-project',
|
|
108
|
+
profileId,
|
|
109
|
+
detail: profileId,
|
|
110
|
+
roots: [
|
|
111
|
+
...new Set(manifest.projects.flatMap((project) => [
|
|
112
|
+
join(project.path, CRTR_DIR_NAME),
|
|
113
|
+
...projectScopeRoots(project.path, null),
|
|
114
|
+
])),
|
|
115
|
+
],
|
|
116
|
+
})),
|
|
117
|
+
...explicitDirs.map((dir) => ({
|
|
118
|
+
reason: 'explicit-dir',
|
|
119
|
+
profileId: selectedProfileId,
|
|
120
|
+
detail: dir,
|
|
121
|
+
roots: [join(realpathOrSelf(dir), CRTR_DIR_NAME)],
|
|
122
|
+
})),
|
|
123
|
+
];
|
|
124
|
+
const descendantCache = new Map();
|
|
125
|
+
/** Memoized by the root set: two profiles naming the same projects, or a
|
|
126
|
+
* target-discovered root already walked, must not pay for the walk twice. */
|
|
127
|
+
const descendantsUnder = (rootsIn) => {
|
|
128
|
+
const key = rootsIn.map((root) => realpathOrSelf(root)).sort().join('\n');
|
|
129
|
+
let found = descendantCache.get(key);
|
|
130
|
+
if (found === undefined) {
|
|
131
|
+
found = descendantStoreRootsExhaustive([...rootsIn]);
|
|
132
|
+
descendantCache.set(key, found);
|
|
133
|
+
}
|
|
134
|
+
return found;
|
|
135
|
+
};
|
|
136
|
+
/** Take one set of project roots into discovery: the roots themselves, then
|
|
137
|
+
* every store nested below them. Both kinds are project roots in their own
|
|
138
|
+
* right — each earns a project target whose own precedence walk can reach
|
|
139
|
+
* further roots, which is why the target loop below calls this again. */
|
|
140
|
+
const expandRoots = (rootsIn, reason, profileId, detail) => {
|
|
141
|
+
const detailRow = detail === undefined ? {} : { detail };
|
|
142
|
+
for (const root of rootsIn) {
|
|
143
|
+
addRoot({ reason, root, boundary: reason === 'explicit-dir' ? 'store' : 'ancestors', ...detailRow });
|
|
144
|
+
addProjectRoot(root, reason, profileId);
|
|
145
|
+
}
|
|
146
|
+
for (const descendant of descendantsUnder(rootsIn)) {
|
|
147
|
+
addRoot({ reason: 'descendant', root: descendant, boundary: 'descendants', ...detailRow });
|
|
148
|
+
addProjectRoot(descendant, 'descendant', profileId);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
for (const context of contexts)
|
|
152
|
+
expandRoots(context.roots, context.reason, context.profileId, context.detail);
|
|
153
|
+
const blockers = [];
|
|
154
|
+
const stores = new Map();
|
|
155
|
+
const register = (descriptor, discovery, target) => {
|
|
156
|
+
const storeRoot = descriptor.storeRoot;
|
|
157
|
+
if (storeRoot === '' || !pathExists(storeRoot))
|
|
158
|
+
return;
|
|
159
|
+
const key = realpathOrSelf(storeRoot);
|
|
160
|
+
let record = stores.get(key);
|
|
161
|
+
if (record === undefined) {
|
|
162
|
+
record = {
|
|
163
|
+
key,
|
|
164
|
+
// First descriptor wins, matching the resolver's first-position dedup.
|
|
165
|
+
descriptor,
|
|
166
|
+
discovery: [],
|
|
167
|
+
discoverySeen: new Set(),
|
|
168
|
+
targets: [],
|
|
169
|
+
writable: descriptor.scope !== 'builtin',
|
|
170
|
+
};
|
|
171
|
+
stores.set(key, record);
|
|
172
|
+
try {
|
|
173
|
+
readdirSync(key);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
blockers.push({
|
|
177
|
+
kind: 'inaccessible-root',
|
|
178
|
+
message: `memory store cannot be listed (${firstLine(e)}) — discovery cannot see its documents; fix the permissions and re-run`,
|
|
179
|
+
storeRoot: key,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const discoveryKey = `${discovery.reason}\u0000${discovery.root}\u0000${discovery.targetId ?? ''}`;
|
|
184
|
+
if (!record.discoverySeen.has(discoveryKey)) {
|
|
185
|
+
record.discoverySeen.add(discoveryKey);
|
|
186
|
+
record.discovery.push(discovery);
|
|
187
|
+
}
|
|
188
|
+
if (target !== undefined) {
|
|
189
|
+
if (!record.targets.includes(target.id))
|
|
190
|
+
record.targets.push(target.id);
|
|
191
|
+
if (!target.storeKeys.includes(key))
|
|
192
|
+
target.storeKeys.push(key);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const targets = [];
|
|
196
|
+
const targetsById = new Map();
|
|
197
|
+
const addTarget = (kind, origin, target) => {
|
|
198
|
+
const id = targetId(target);
|
|
199
|
+
if (targetsById.has(id))
|
|
200
|
+
return;
|
|
201
|
+
const record = { id, kind, origin, target, storeKeys: [] };
|
|
202
|
+
targetsById.set(id, record);
|
|
203
|
+
targets.push(record);
|
|
204
|
+
};
|
|
205
|
+
addTarget('cwd', cwd, { cwd, profileId: selectedProfileId, nodeId: null });
|
|
206
|
+
for (const { profileId, manifest } of opts.profiles) {
|
|
207
|
+
const home = manifest.home !== null && manifest.home !== '' ? manifest.home : null;
|
|
208
|
+
addTarget('profile', profileId, {
|
|
209
|
+
cwd: home ?? manifest.projects[0]?.path ?? cwd,
|
|
210
|
+
profileId,
|
|
211
|
+
nodeId: null,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/** One project target per (root, profile) membership. Re-run whenever the
|
|
215
|
+
* root set grows: `addTarget` dedupes by target id. */
|
|
216
|
+
const addProjectTargets = () => {
|
|
217
|
+
for (const { root, profileIds } of [...projectRoots.values()]) {
|
|
218
|
+
for (const profileId of [...profileIds]) {
|
|
219
|
+
addTarget('project', dirname(root), { cwd: dirname(root), profileId, nodeId: null });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const nodeStores = nodeStoresWithDocs();
|
|
224
|
+
if (pathExists(nodesRoot()))
|
|
225
|
+
addRoot({ reason: 'node', root: nodesRoot(), boundary: 'nodes' });
|
|
226
|
+
const nodeMetadata = { available: true, nodes: 0 };
|
|
227
|
+
for (const { nodeId, storeRoot } of nodeStores) {
|
|
228
|
+
const metaPath = nodeMetaPath(nodeId);
|
|
229
|
+
const meta = readMetaObject(metaPath);
|
|
230
|
+
if (meta === null) {
|
|
231
|
+
nodeMetadata.available = false;
|
|
232
|
+
nodeMetadata.error ??= `unreadable metadata: ${metaPath}`;
|
|
233
|
+
blockers.push({
|
|
234
|
+
kind: 'inaccessible-target',
|
|
235
|
+
message: `node ${nodeId} holds memory documents but its durable metadata is unreadable at ${metaPath} — restore or repair that file, then re-run`,
|
|
236
|
+
storeRoot: realpathOrSelf(storeRoot),
|
|
237
|
+
path: metaPath,
|
|
238
|
+
});
|
|
239
|
+
register(nodeStoreDescriptor(storeRoot), { reason: 'node', root: storeRoot });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
nodeMetadata.nodes += 1;
|
|
243
|
+
const nodeCwd = typeof meta['cwd'] === 'string' && meta['cwd'] !== '' ? meta['cwd'] : cwd;
|
|
244
|
+
const profileId = typeof meta['profile_id'] === 'string' && meta['profile_id'] !== '' ? meta['profile_id'] : null;
|
|
245
|
+
addTarget('node', nodeId, { cwd: nodeCwd, profileId, nodeId });
|
|
246
|
+
}
|
|
247
|
+
// Runtime default `includeDescendants: false`: this is the store stack a link
|
|
248
|
+
// or route actually resolves through. A descendant store is reached by its
|
|
249
|
+
// own owner-dir target instead.
|
|
250
|
+
const walkTarget = (target) => {
|
|
251
|
+
for (const descriptor of memoryStoresInPrecedence(target.target)) {
|
|
252
|
+
if (descriptor.plugin !== undefined) {
|
|
253
|
+
addRoot({ reason: 'plugin', root: descriptor.storeRoot, boundary: 'store', detail: descriptor.plugin });
|
|
254
|
+
register(descriptor, { reason: 'plugin', root: descriptor.storeRoot, targetId: target.id }, target);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (descriptor.scope === 'project') {
|
|
258
|
+
const crtrRoot = dirname(descriptor.storeRoot);
|
|
259
|
+
const known = projectRoots.get(realpathOrSelf(crtrRoot));
|
|
260
|
+
if (known === undefined) {
|
|
261
|
+
// Reached only through this target's own ancestor walk — a node's cwd
|
|
262
|
+
// or a profile home outside every manifest project. It enters
|
|
263
|
+
// discovery WHOLE: the stores nested below it are corpus too, and the
|
|
264
|
+
// loop below gives it and them their own project targets.
|
|
265
|
+
expandRoots([crtrRoot], 'cwd-ancestor', target.target.profileId, target.id);
|
|
266
|
+
}
|
|
267
|
+
register(descriptor, { reason: known?.reason ?? 'cwd-ancestor', root: crtrRoot, targetId: target.id }, target);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const reason = descriptor.scope === 'user'
|
|
271
|
+
? 'user'
|
|
272
|
+
: descriptor.scope === 'profile'
|
|
273
|
+
? 'profile'
|
|
274
|
+
: descriptor.scope === 'node'
|
|
275
|
+
? 'node'
|
|
276
|
+
: 'builtin';
|
|
277
|
+
addRoot({ reason, root: descriptor.storeRoot, boundary: 'store' });
|
|
278
|
+
register(descriptor, { reason, root: descriptor.storeRoot, targetId: target.id }, target);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
// Discovery runs to a FIXPOINT: a target's store walk can name a project root
|
|
282
|
+
// no manifest and no ancestor walk knew, that root's descendants are corpus
|
|
283
|
+
// too, and each of them gets a target whose own walk can name another root.
|
|
284
|
+
// Terminates because a root expands once (`projectRoots` dedupes by realpath)
|
|
285
|
+
// and a target is walked once.
|
|
286
|
+
const walkedTargets = new Set();
|
|
287
|
+
for (;;) {
|
|
288
|
+
addProjectTargets();
|
|
289
|
+
const pending = targets.filter((target) => !walkedTargets.has(target.id));
|
|
290
|
+
if (pending.length === 0)
|
|
291
|
+
break;
|
|
292
|
+
for (const target of pending) {
|
|
293
|
+
walkedTargets.add(target.id);
|
|
294
|
+
walkTarget(target);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Stores no target walk reaches. A DISABLED plugin's documents still hold
|
|
298
|
+
// identities and still point at other stores, so the corpus must see them —
|
|
299
|
+
// `memoryStoresInPrecedence` mounts only enabled ones.
|
|
300
|
+
const userRoot = scopeRoot('user');
|
|
301
|
+
if (userRoot !== null)
|
|
302
|
+
addRoot({ reason: 'plugin', root: userRoot, boundary: 'plugins' });
|
|
303
|
+
const extraPlugins = [...listInstalledPlugins('user')];
|
|
304
|
+
for (const { root } of projectRoots.values()) {
|
|
305
|
+
addRoot({ reason: 'plugin', root, boundary: 'plugins' });
|
|
306
|
+
extraPlugins.push(...listInstalledPluginsInRoot('project', root));
|
|
307
|
+
}
|
|
308
|
+
for (const plugin of extraPlugins) {
|
|
309
|
+
const descriptor = pluginStoreDescriptor(plugin);
|
|
310
|
+
addRoot({ reason: 'plugin', root: descriptor.storeRoot, boundary: 'store', detail: plugin.name });
|
|
311
|
+
register(descriptor, { reason: 'plugin', root: descriptor.storeRoot });
|
|
312
|
+
}
|
|
313
|
+
// A profile store the selected profile never mounts (no target selects it).
|
|
314
|
+
for (const { profileId } of opts.profiles) {
|
|
315
|
+
const storeRoot = profileMemoryDir(profileId);
|
|
316
|
+
addRoot({ reason: 'profile', root: storeRoot, boundary: 'store', detail: profileId });
|
|
317
|
+
register({ scope: 'profile', storeRoot, namespace: '', mountStatus: pathExists(storeRoot) ? 'ready' : 'absent', projectMemory: NEUTRAL_PROJECT_MEMORY }, { reason: 'profile', root: storeRoot });
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
roots,
|
|
321
|
+
stores: [...stores.values()].map((record) => ({
|
|
322
|
+
key: record.key,
|
|
323
|
+
descriptor: record.descriptor,
|
|
324
|
+
discovery: record.discovery,
|
|
325
|
+
targets: record.targets,
|
|
326
|
+
writable: record.writable,
|
|
327
|
+
})),
|
|
328
|
+
targets: targets.map((record) => ({
|
|
329
|
+
id: record.id,
|
|
330
|
+
kind: record.kind,
|
|
331
|
+
origin: record.origin,
|
|
332
|
+
target: record.target,
|
|
333
|
+
storeKeys: record.storeKeys,
|
|
334
|
+
})),
|
|
335
|
+
blockers,
|
|
336
|
+
nodeMetadata,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Snapshots
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
/** Read every discovered store's documents. `preloaded` supplies the per-store
|
|
343
|
+
* convergent lane's FOLDED snapshots so the corpus plans against the bytes a
|
|
344
|
+
* write run leaves behind — identical under `--dry-run`, where the same fold
|
|
345
|
+
* ran and nothing was written. */
|
|
346
|
+
export function loadCorpusSnapshots(discovery, opts = {}) {
|
|
347
|
+
const snapshots = new Map();
|
|
348
|
+
const skipped = [];
|
|
349
|
+
for (const store of discovery.stores) {
|
|
350
|
+
const preloaded = opts.preloaded?.get(store.key);
|
|
351
|
+
if (preloaded !== undefined) {
|
|
352
|
+
snapshots.set(store.key, preloaded);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const loaded = loadStoreSnapshot(store.descriptor.storeRoot);
|
|
356
|
+
snapshots.set(store.key, loaded.snapshot);
|
|
357
|
+
for (const row of loaded.skipped)
|
|
358
|
+
skipped.push({ storeRoot: store.key, ...row });
|
|
359
|
+
}
|
|
360
|
+
return { snapshots, skipped };
|
|
361
|
+
}
|
|
362
|
+
export function parseFailureBlockers(skipped) {
|
|
363
|
+
return skipped.map((row) => ({
|
|
364
|
+
kind: 'parse-failure',
|
|
365
|
+
message: `${row.relPath}: frontmatter is not valid YAML (${row.error}) — the document's identity cannot be read; fix the file and re-run`,
|
|
366
|
+
storeRoot: row.storeRoot,
|
|
367
|
+
path: join(row.storeRoot, row.relPath),
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
function makeCorpusSnapshot(stores, targets, roots) {
|
|
371
|
+
const byKey = new Map(stores.map((store) => [store.key, store]));
|
|
372
|
+
const byTarget = new Map(targets.map((target) => [target.id, target]));
|
|
373
|
+
return {
|
|
374
|
+
stores,
|
|
375
|
+
targets,
|
|
376
|
+
roots,
|
|
377
|
+
store: (key) => byKey.get(key),
|
|
378
|
+
storesForTarget: (id) => (byTarget.get(id)?.storeKeys ?? [])
|
|
379
|
+
.map((key) => byKey.get(key))
|
|
380
|
+
.filter((store) => store !== undefined),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
export function buildCorpusSnapshot(discovery, snapshots) {
|
|
384
|
+
const stores = discovery.stores.map((store) => ({
|
|
385
|
+
...store,
|
|
386
|
+
snapshot: snapshots.get(store.key) ?? { root: store.descriptor.storeRoot, docs: [] },
|
|
387
|
+
}));
|
|
388
|
+
return makeCorpusSnapshot(stores, discovery.targets, discovery.roots);
|
|
389
|
+
}
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// The runner
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
function cloneCorpus(corpus) {
|
|
394
|
+
const stores = corpus.stores.map((store) => ({
|
|
395
|
+
...store,
|
|
396
|
+
snapshot: { root: store.snapshot.root, docs: [...store.snapshot.docs] },
|
|
397
|
+
}));
|
|
398
|
+
return makeCorpusSnapshot(stores, corpus.targets, corpus.roots);
|
|
399
|
+
}
|
|
400
|
+
/** Fold a planned change into the working corpus so migration k+1 plans against
|
|
401
|
+
* post-change state. A change outside every store is a migration defect, except
|
|
402
|
+
* an identity change creating this plan's repository-root declaration: absent
|
|
403
|
+
* root stores are intentionally not synthesized during planning. */
|
|
404
|
+
function foldCorpusChange(corpus, change, allowedOutsidePaths = new Set()) {
|
|
405
|
+
let owner;
|
|
406
|
+
for (const store of corpus.stores) {
|
|
407
|
+
if (!change.path.startsWith(`${store.snapshot.root}/`))
|
|
408
|
+
continue;
|
|
409
|
+
if (owner === undefined || store.snapshot.root.length > owner.snapshot.root.length)
|
|
410
|
+
owner = store;
|
|
411
|
+
}
|
|
412
|
+
if (owner === undefined) {
|
|
413
|
+
if (allowedOutsidePaths.has(change.path))
|
|
414
|
+
return;
|
|
415
|
+
throw new Error(`corpus migration: planned change outside every discovered store: ${change.path}`);
|
|
416
|
+
}
|
|
417
|
+
foldDocChange(owner.snapshot, change);
|
|
418
|
+
}
|
|
419
|
+
/** Plan every corpus migration over the whole corpus, then — only when the
|
|
420
|
+
* global plan is blocker-free — write every migration's consumer rewrites
|
|
421
|
+
* before any identity rewrite. `rebuild` re-reads the corpus from file truth
|
|
422
|
+
* for the post-write `verify` pass. */
|
|
423
|
+
export function runCorpusMigrations(corpus, opts = {}) {
|
|
424
|
+
const migrations = (opts.migrations ?? STATE_MIGRATIONS).filter((m) => m.lane === 'corpus');
|
|
425
|
+
const blockers = [...(opts.blockers ?? [])];
|
|
426
|
+
const planned = [];
|
|
427
|
+
const repositories = [];
|
|
428
|
+
const storePrefixes = [];
|
|
429
|
+
const renames = [];
|
|
430
|
+
const references = [];
|
|
431
|
+
const consumers = [];
|
|
432
|
+
const identities = [];
|
|
433
|
+
const result = (verified, wrote) => ({
|
|
434
|
+
migrations: migrations.length,
|
|
435
|
+
blocked: blockers.length > 0 && migrations.length > 0,
|
|
436
|
+
blockers,
|
|
437
|
+
planned,
|
|
438
|
+
repositories,
|
|
439
|
+
storePrefixes,
|
|
440
|
+
renames,
|
|
441
|
+
references,
|
|
442
|
+
wrote,
|
|
443
|
+
verified,
|
|
444
|
+
});
|
|
445
|
+
const noWrites = { consumers: [], identities: [] };
|
|
446
|
+
if (migrations.length === 0)
|
|
447
|
+
return result(false, noWrites);
|
|
448
|
+
const working = cloneCorpus(corpus);
|
|
449
|
+
for (const migration of migrations) {
|
|
450
|
+
const plan = migration.plan(working);
|
|
451
|
+
blockers.push(...plan.blockers);
|
|
452
|
+
repositories.push(...plan.repositories);
|
|
453
|
+
storePrefixes.push(...plan.storePrefixes);
|
|
454
|
+
renames.push(...plan.renames);
|
|
455
|
+
references.push(...plan.references);
|
|
456
|
+
for (const change of plan.consumers)
|
|
457
|
+
foldCorpusChange(working, change);
|
|
458
|
+
const allowedOutsidePaths = new Set(plan.repositories.map((repository) => repository.declarationPath));
|
|
459
|
+
for (const change of plan.identities)
|
|
460
|
+
foldCorpusChange(working, change, allowedOutsidePaths);
|
|
461
|
+
consumers.push(...plan.consumers);
|
|
462
|
+
identities.push(...plan.identities);
|
|
463
|
+
planned.push({
|
|
464
|
+
migration: migration.description,
|
|
465
|
+
consumers: plan.consumers.map((c) => c.path),
|
|
466
|
+
identities: plan.identities.map((c) => c.path),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const wrote = { consumers: consumers.map((c) => c.path), identities: identities.map((c) => c.path) };
|
|
470
|
+
if (blockers.length > 0)
|
|
471
|
+
return result(false, noWrites);
|
|
472
|
+
if (opts.dryRun === true)
|
|
473
|
+
return result(false, wrote);
|
|
474
|
+
// The phase barrier is GLOBAL: every migration's consumer rewrites land
|
|
475
|
+
// before any identity rewrite, so no pointer resolves to a moved address.
|
|
476
|
+
for (const change of consumers) {
|
|
477
|
+
if (change.after === null)
|
|
478
|
+
removePath(change.path);
|
|
479
|
+
else
|
|
480
|
+
atomicWriteText(change.path, change.after);
|
|
481
|
+
}
|
|
482
|
+
for (const change of identities) {
|
|
483
|
+
if (change.after === null)
|
|
484
|
+
removePath(change.path);
|
|
485
|
+
else
|
|
486
|
+
atomicWriteText(change.path, change.after);
|
|
487
|
+
}
|
|
488
|
+
if (opts.rebuild === undefined)
|
|
489
|
+
return result(false, wrote);
|
|
490
|
+
const rebuilt = opts.rebuild();
|
|
491
|
+
for (const migration of migrations) {
|
|
492
|
+
for (const finding of migration.verify?.(rebuilt) ?? []) {
|
|
493
|
+
blockers.push({ ...finding, kind: 'verification' });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return result(true, wrote);
|
|
497
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Split a complete document into its raw frontmatter YAML and byte-preserved body. */
|
|
2
|
+
export declare function splitDocSource(source: string, docPath: string): {
|
|
3
|
+
rawYaml: string;
|
|
4
|
+
body: string;
|
|
5
|
+
};
|
|
6
|
+
/** Remove complete top-level mapping-key lines, including multiline values. */
|
|
7
|
+
export declare function dropTopLevelKeys(rawYaml: string, keys: readonly string[], docPath: string): string;
|
|
8
|
+
/** Remove selected items from a top-level block sequence without re-rendering retained bytes. */
|
|
9
|
+
export declare function dropSequenceItems(rawYaml: string, key: string, drop: (item: unknown) => boolean, docPath: string): string;
|
|
10
|
+
/** Append a rendered top-level mapping pair while retaining existing YAML bytes. */
|
|
11
|
+
export declare function appendRenderedKey(rawYaml: string, key: string, value: unknown, docPath: string): string;
|
|
12
|
+
/** Prove that a transformed YAML block parses to exactly the expected mapping. */
|
|
13
|
+
export declare function proveFrontmatterRoundTrip(after: string, expected: Record<string, unknown>, docPath: string): void;
|
|
14
|
+
/** Replace only the YAML block in a complete document; the body remains byte-for-byte unchanged. */
|
|
15
|
+
export declare function rebuildDocSource(source: string, newYaml: string, docPath: string): string;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { isMap, isSeq, parse as parseYaml, parseDocument, stringify } from 'yaml';
|
|
2
|
+
import { parseFrontmatterGeneric } from '../core/frontmatter.js';
|
|
3
|
+
function deepEqual(a, b) {
|
|
4
|
+
if (a === b)
|
|
5
|
+
return true;
|
|
6
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
7
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
8
|
+
return false;
|
|
9
|
+
return a.every((value, index) => deepEqual(value, b[index]));
|
|
10
|
+
}
|
|
11
|
+
if (a !== null && b !== null && typeof a === 'object' && typeof b === 'object') {
|
|
12
|
+
const ak = Object.keys(a);
|
|
13
|
+
const bk = Object.keys(b);
|
|
14
|
+
if (ak.length !== bk.length)
|
|
15
|
+
return false;
|
|
16
|
+
return ak.every((key) => deepEqual(a[key], b[key]));
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function yamlMap(rawYaml, docPath) {
|
|
21
|
+
if (rawYaml.trim() === '')
|
|
22
|
+
return [];
|
|
23
|
+
const document = parseDocument(rawYaml);
|
|
24
|
+
if (document.errors.length > 0 || !isMap(document.contents)) {
|
|
25
|
+
throw new Error(`frontmatter splice: unparseable frontmatter in ${docPath}`);
|
|
26
|
+
}
|
|
27
|
+
return document.contents.items;
|
|
28
|
+
}
|
|
29
|
+
function keyLineSpan(rawYaml, pair, docPath) {
|
|
30
|
+
const keyRange = pair.key?.range;
|
|
31
|
+
const valueRange = pair.value?.range;
|
|
32
|
+
if (keyRange === undefined)
|
|
33
|
+
throw new Error(`frontmatter splice: rangeless key in ${docPath}`);
|
|
34
|
+
const start = rawYaml.lastIndexOf('\n', keyRange[0] - 1) + 1;
|
|
35
|
+
let end = valueRange?.[2] ?? keyRange[2];
|
|
36
|
+
if (!(end > 0 && rawYaml[end - 1] === '\n')) {
|
|
37
|
+
while (end < rawYaml.length && (rawYaml[end] === ' ' || rawYaml[end] === '\t' || rawYaml[end] === '\r'))
|
|
38
|
+
end++;
|
|
39
|
+
if (rawYaml[end] === '\n')
|
|
40
|
+
end++;
|
|
41
|
+
}
|
|
42
|
+
return [start, end];
|
|
43
|
+
}
|
|
44
|
+
function parseRecord(rawYaml, docPath) {
|
|
45
|
+
yamlMap(rawYaml, docPath);
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = parseYaml(rawYaml);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error(`frontmatter splice: unparseable frontmatter in ${docPath}`);
|
|
52
|
+
}
|
|
53
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
54
|
+
throw new Error(`frontmatter splice: frontmatter is not a mapping in ${docPath}`);
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
/** Split a complete document into its raw frontmatter YAML and byte-preserved body. */
|
|
59
|
+
export function splitDocSource(source, docPath) {
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = parseFrontmatterGeneric(source);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new Error(`frontmatter splice: unparseable frontmatter in ${docPath}`, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
if (parsed.raw === '' && parsed.body === source) {
|
|
68
|
+
throw new Error(`frontmatter splice: cannot locate frontmatter block in ${docPath}`);
|
|
69
|
+
}
|
|
70
|
+
return { rawYaml: parsed.raw, body: parsed.body };
|
|
71
|
+
}
|
|
72
|
+
/** Remove complete top-level mapping-key lines, including multiline values. */
|
|
73
|
+
export function dropTopLevelKeys(rawYaml, keys, docPath) {
|
|
74
|
+
const pairs = yamlMap(rawYaml, docPath);
|
|
75
|
+
const names = new Set(keys);
|
|
76
|
+
const spans = pairs
|
|
77
|
+
.filter((pair) => names.has(String(pair.key)))
|
|
78
|
+
.map((pair) => keyLineSpan(rawYaml, pair, docPath))
|
|
79
|
+
.sort((a, b) => b[0] - a[0]);
|
|
80
|
+
let after = rawYaml;
|
|
81
|
+
for (const [start, end] of spans)
|
|
82
|
+
after = after.slice(0, start) + after.slice(end);
|
|
83
|
+
const expected = parseRecord(rawYaml, docPath);
|
|
84
|
+
for (const key of keys)
|
|
85
|
+
delete expected[key];
|
|
86
|
+
proveFrontmatterRoundTrip(after, expected, docPath);
|
|
87
|
+
return after;
|
|
88
|
+
}
|
|
89
|
+
/** Remove selected items from a top-level block sequence without re-rendering retained bytes. */
|
|
90
|
+
export function dropSequenceItems(rawYaml, key, drop, docPath) {
|
|
91
|
+
const pairs = yamlMap(rawYaml, docPath);
|
|
92
|
+
const pair = pairs.find((candidate) => String(candidate.key) === key);
|
|
93
|
+
if (pair === undefined)
|
|
94
|
+
return rawYaml;
|
|
95
|
+
if (!isSeq(pair.value))
|
|
96
|
+
throw new Error(`frontmatter splice: ${key} is not a sequence in ${docPath}`);
|
|
97
|
+
const items = pair.value.items;
|
|
98
|
+
const dropped = items.map((item) => drop(item.toJSON?.()));
|
|
99
|
+
if (!dropped.some(Boolean))
|
|
100
|
+
return rawYaml;
|
|
101
|
+
const expected = parseRecord(rawYaml, docPath);
|
|
102
|
+
const original = expected[key];
|
|
103
|
+
if (!Array.isArray(original))
|
|
104
|
+
throw new Error(`frontmatter splice: ${key} is not a sequence in ${docPath}`);
|
|
105
|
+
const retained = original.filter((_, index) => !dropped[index]);
|
|
106
|
+
if (retained.length === 0) {
|
|
107
|
+
delete expected[key];
|
|
108
|
+
const after = dropTopLevelKeys(rawYaml, [key], docPath);
|
|
109
|
+
proveFrontmatterRoundTrip(after, expected, docPath);
|
|
110
|
+
return after;
|
|
111
|
+
}
|
|
112
|
+
const sequenceRange = pair.value.range;
|
|
113
|
+
if (sequenceRange === undefined)
|
|
114
|
+
throw new Error(`frontmatter splice: rangeless sequence in ${docPath}`);
|
|
115
|
+
const lineStarts = items.map((item) => {
|
|
116
|
+
const range = item.range;
|
|
117
|
+
if (range === undefined)
|
|
118
|
+
throw new Error(`frontmatter splice: rangeless sequence item in ${docPath}`);
|
|
119
|
+
return rawYaml.lastIndexOf('\n', range[0] - 1) + 1;
|
|
120
|
+
});
|
|
121
|
+
if (new Set(lineStarts).size !== lineStarts.length) {
|
|
122
|
+
throw new Error(`frontmatter splice: flow sequence cannot be spliced byte-precisely in ${docPath}`);
|
|
123
|
+
}
|
|
124
|
+
const spans = [];
|
|
125
|
+
for (let index = 0; index < items.length; index++) {
|
|
126
|
+
if (!dropped[index])
|
|
127
|
+
continue;
|
|
128
|
+
const start = lineStarts[index];
|
|
129
|
+
const end = lineStarts[index + 1] ?? sequenceRange[2];
|
|
130
|
+
spans.push([start, end]);
|
|
131
|
+
}
|
|
132
|
+
spans.sort((a, b) => b[0] - a[0]);
|
|
133
|
+
let after = rawYaml;
|
|
134
|
+
for (const [start, end] of spans)
|
|
135
|
+
after = after.slice(0, start) + after.slice(end);
|
|
136
|
+
expected[key] = retained;
|
|
137
|
+
proveFrontmatterRoundTrip(after, expected, docPath);
|
|
138
|
+
return after;
|
|
139
|
+
}
|
|
140
|
+
/** Append a rendered top-level mapping pair while retaining existing YAML bytes. */
|
|
141
|
+
export function appendRenderedKey(rawYaml, key, value, docPath) {
|
|
142
|
+
const expected = parseRecord(rawYaml, docPath);
|
|
143
|
+
expected[key] = value;
|
|
144
|
+
const block = stringify({ [key]: value }, { lineWidth: 0 });
|
|
145
|
+
let after = rawYaml === '' ? block : (rawYaml.endsWith('\n') ? rawYaml : rawYaml + '\n') + block;
|
|
146
|
+
after = after.replace(/\r?\n$/, '');
|
|
147
|
+
proveFrontmatterRoundTrip(after, expected, docPath);
|
|
148
|
+
return after;
|
|
149
|
+
}
|
|
150
|
+
/** Prove that a transformed YAML block parses to exactly the expected mapping. */
|
|
151
|
+
export function proveFrontmatterRoundTrip(after, expected, docPath) {
|
|
152
|
+
let actual;
|
|
153
|
+
try {
|
|
154
|
+
actual = parseYaml(after === '' ? '{}' : after);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw new Error(`frontmatter splice: transformed frontmatter does not parse in ${docPath}`, { cause: error });
|
|
158
|
+
}
|
|
159
|
+
if (!deepEqual(actual ?? {}, expected)) {
|
|
160
|
+
throw new Error(`frontmatter splice: spliced frontmatter does not round-trip in ${docPath}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Replace only the YAML block in a complete document; the body remains byte-for-byte unchanged. */
|
|
164
|
+
export function rebuildDocSource(source, newYaml, docPath) {
|
|
165
|
+
const { rawYaml, body } = splitDocSource(source, docPath);
|
|
166
|
+
const bodyStart = source.length - body.length;
|
|
167
|
+
const firstNewline = source.indexOf('\n');
|
|
168
|
+
const rawStart = firstNewline < 0 ? -1 : firstNewline + 1;
|
|
169
|
+
const rawEnd = rawStart < 0 ? -1 : rawStart + rawYaml.length;
|
|
170
|
+
if (rawYaml === '' && bodyStart === source.length) {
|
|
171
|
+
throw new Error(`frontmatter splice: cannot locate frontmatter block in ${docPath}`);
|
|
172
|
+
}
|
|
173
|
+
if (rawStart < 0 || rawEnd > bodyStart)
|
|
174
|
+
throw new Error(`frontmatter splice: cannot locate frontmatter block in ${docPath}`);
|
|
175
|
+
return source.slice(0, rawStart) + newYaml + source.slice(rawEnd);
|
|
176
|
+
}
|