@north-light/crouter 0.3.227 → 0.3.228
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +5 -5
- package/dist/api/client.js +3 -3
- package/dist/api/dto/broker-ops.d.ts +8 -0
- package/dist/api/dto/memory.d.ts +8 -7
- package/dist/api/dto/memory.js +2 -2
- package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
- package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
- package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
- package/dist/builtin-memory/design/guide.md +38 -16
- package/dist/builtin-memory/design/roadmap.md +4 -4
- package/dist/builtin-memory/insights/init.md +5 -5
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +6 -6
- package/dist/builtin-memory/internal/plugins.md +1 -1
- package/dist/builtin-memory/plan/guide.md +53 -0
- package/dist/builtin-memory/plan/roadmap.md +10 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
- package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
- package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
- package/dist/clients/attach/chrome/inbox-strip.js +11 -7
- package/dist/clients/attach/session/pane-tag.d.ts +28 -2
- package/dist/clients/attach/session/pane-tag.js +48 -22
- package/dist/clients/attach/session/profile-files.d.ts +6 -0
- package/dist/clients/attach/session/profile-files.js +44 -11
- package/dist/clients/attach/slash/dispatch.js +4 -7
- package/dist/clients/attach/viewer.js +588 -596
- package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
- package/dist/commands/api-client.d.ts +3 -7
- package/dist/commands/api-client.js +7 -12
- package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
- package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
- package/dist/commands/memory/delete.js +51 -24
- package/dist/commands/memory/edit.js +50 -10
- package/dist/commands/memory/find.js +105 -56
- package/dist/commands/memory/history.js +51 -31
- package/dist/commands/memory/lint.d.ts +0 -9
- package/dist/commands/memory/lint.js +78 -322
- package/dist/commands/memory/list.d.ts +12 -4
- package/dist/commands/memory/list.js +64 -25
- package/dist/commands/memory/move.js +103 -73
- package/dist/commands/memory/origin.js +35 -5
- package/dist/commands/memory/read.d.ts +4 -0
- package/dist/commands/memory/read.js +132 -80
- package/dist/commands/memory/shared.d.ts +115 -25
- package/dist/commands/memory/shared.js +331 -74
- package/dist/commands/memory/write.js +72 -21
- package/dist/commands/memory.js +2 -2
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/commands/pkg/market-manage.js +25 -24
- package/dist/commands/pkg/plugin-manage.d.ts +22 -6
- package/dist/commands/pkg/plugin-manage.js +118 -25
- package/dist/commands/pkg/shared.d.ts +8 -0
- package/dist/commands/pkg/shared.js +23 -20
- package/dist/commands/surface-reload.d.ts +5 -0
- package/dist/commands/surface-reload.js +9 -1
- package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
- package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
- package/dist/commands/sys/migrate.js +126 -140
- package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
- package/dist/commands/sys/panels/profiles-panel.js +599 -0
- package/dist/commands/sys/settings-shell.d.ts +4 -2
- package/dist/commands/sys/settings-shell.js +35 -4
- package/dist/commands/sys/settings.js +21 -5
- package/dist/commands/sys/sync-deps.d.ts +2 -0
- package/dist/commands/sys/sync-deps.js +26 -15
- package/dist/commands/sys/sync-project-guidance.js +228 -145
- package/dist/commands/sys/sync-skills.js +28 -17
- package/dist/commands/sys/update.js +11 -3
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
- package/dist/core/__tests__/config-change-delta.test.js +106 -0
- package/dist/core/__tests__/context-intro.test.js +13 -9
- package/dist/core/__tests__/daemon-boot.test.js +22 -16
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
- package/dist/core/__tests__/human-deliver.test.js +19 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
- package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
- package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
- package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
- package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
- package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
- package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
- package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
- package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
- package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
- package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
- package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
- package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
- package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
- package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
- package/dist/core/__tests__/kickoff.test.js +16 -5
- package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
- package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
- package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
- package/dist/core/__tests__/repository-association.test.d.ts +1 -0
- package/dist/core/__tests__/repository-association.test.js +153 -0
- package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
- package/dist/core/__tests__/repository-root-identity.test.js +219 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
- package/dist/core/canvas/canvas.d.ts +3 -0
- package/dist/core/canvas/canvas.js +30 -10
- package/dist/core/canvas/db.js +28 -1
- package/dist/core/canvas/paths.d.ts +4 -6
- package/dist/core/canvas/paths.js +8 -6
- package/dist/core/canvas/render-source.js +2 -2
- package/dist/core/canvas/types.d.ts +9 -13
- package/dist/core/exclusive-lock.d.ts +2 -0
- package/dist/core/exclusive-lock.js +21 -0
- package/dist/core/git.d.ts +0 -1
- package/dist/core/git.js +0 -3
- package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
- package/dist/core/human/feedback-companion.js +3 -0
- package/dist/core/human/scan.js +8 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/memory/doc-link-grammar.js +2 -2
- package/dist/core/memory/history.d.ts +24 -0
- package/dist/core/memory/history.js +66 -1
- package/dist/core/memory/identity.d.ts +65 -0
- package/dist/core/memory/identity.js +185 -0
- package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
- package/dist/core/memory/inline-ref-guidance.js +1 -1
- package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
- package/dist/core/memory/inline-ref-inventory.js +30 -62
- package/dist/core/memory/lint.d.ts +129 -0
- package/dist/core/memory/lint.js +517 -0
- package/dist/core/memory/project-namespace.d.ts +31 -0
- package/dist/core/memory/project-namespace.js +85 -0
- package/dist/core/memory/repository-association.d.ts +31 -0
- package/dist/core/memory/repository-association.js +129 -0
- package/dist/core/memory/tree.d.ts +39 -0
- package/dist/core/memory/tree.js +93 -0
- package/dist/core/memory-resolver.d.ts +193 -90
- package/dist/core/memory-resolver.js +461 -375
- package/dist/core/nested-stores.js +6 -13
- package/dist/core/profiles/select.d.ts +4 -1
- package/dist/core/profiles/select.js +108 -48
- package/dist/core/review/__tests__/capture-origin.test.js +2 -2
- package/dist/core/review/__tests__/stage-identity.test.js +2 -2
- package/dist/core/review/companion.js +11 -2
- package/dist/core/runtime/bearings.d.ts +2 -2
- package/dist/core/runtime/bearings.js +3 -3
- package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
- package/dist/core/runtime/broker/rebind.js +5 -0
- package/dist/core/runtime/broker-extension-render.d.ts +7 -3
- package/dist/core/runtime/broker-extension-render.js +10 -5
- package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
- package/dist/core/runtime/broker-persona-guidance.js +122 -27
- package/dist/core/runtime/deliver-live.d.ts +16 -4
- package/dist/core/runtime/deliver-live.js +29 -15
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +8 -16
- package/dist/core/runtime/lifecycle.js +2 -3
- package/dist/core/runtime/nodes.d.ts +3 -4
- package/dist/core/runtime/nodes.js +3 -4
- package/dist/core/runtime/persona.d.ts +8 -12
- package/dist/core/runtime/persona.js +24 -96
- package/dist/core/runtime/promote.d.ts +2 -2
- package/dist/core/runtime/promote.js +8 -21
- package/dist/core/runtime/revive.js +20 -17
- package/dist/core/runtime/spawn.js +18 -7
- package/dist/core/runtime/tmux-bindings.js +2 -3
- package/dist/core/runtime/warm-pool.js +3 -4
- package/dist/core/scope.js +2 -0
- package/dist/core/self-update.d.ts +0 -2
- package/dist/core/self-update.js +2 -35
- package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
- package/dist/core/substrate/index.d.ts +2 -2
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +43 -27
- package/dist/core/substrate/injected-store.js +208 -104
- package/dist/core/substrate/listings.d.ts +19 -12
- package/dist/core/substrate/listings.js +75 -52
- package/dist/core/substrate/on-read-node.d.ts +4 -7
- package/dist/core/substrate/on-read-node.js +6 -8
- package/dist/core/substrate/on-read.d.ts +22 -25
- package/dist/core/substrate/on-read.js +103 -147
- package/dist/core/substrate/render-node.d.ts +4 -7
- package/dist/core/substrate/render-node.js +5 -7
- package/dist/core/substrate/render.d.ts +21 -3
- package/dist/core/substrate/render.js +291 -223
- package/dist/core/substrate/schema.d.ts +1 -13
- package/dist/core/substrate/schema.js +5 -40
- package/dist/core/substrate/session-cache.d.ts +14 -4
- package/dist/core/substrate/session-cache.js +40 -22
- package/dist/core/substrate/surface-match.d.ts +9 -7
- package/dist/core/substrate/surface-match.js +26 -25
- package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
- package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
- package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
- package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +21 -5
- package/dist/daemon/api/handlers/broker-ops.js +21 -16
- package/dist/daemon/api/handlers/memory.js +2 -0
- package/dist/daemon/crtrd.js +2 -0
- package/dist/daemon/human/finish.js +8 -1
- package/dist/daemon/manage.d.ts +0 -1
- package/dist/daemon/manage.js +7 -16
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/migrations/001-surfaces-frontmatter.js +21 -109
- package/dist/migrations/002-profile-project-memory.js +1 -0
- package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
- package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
- package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
- package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
- package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
- package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
- package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
- package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
- package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
- package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
- package/dist/migrations/__tests__/activation.test.d.ts +1 -0
- package/dist/migrations/__tests__/activation.test.js +149 -0
- package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
- package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
- package/dist/migrations/activation.d.ts +16 -0
- package/dist/migrations/activation.js +78 -0
- package/dist/migrations/convergent.d.ts +14 -3
- package/dist/migrations/convergent.js +21 -10
- package/dist/migrations/corpus.d.ts +78 -0
- package/dist/migrations/corpus.js +497 -0
- package/dist/migrations/frontmatter-splice.d.ts +15 -0
- package/dist/migrations/frontmatter-splice.js +176 -0
- package/dist/migrations/registry.d.ts +6 -1
- package/dist/migrations/registry.js +7 -2
- package/dist/migrations/runner.d.ts +41 -0
- package/dist/migrations/runner.js +81 -0
- package/dist/migrations/types.d.ts +148 -9
- package/dist/migrations/types.js +9 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
- package/dist/pi-extensions/canvas-context-intro.js +50 -46
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
- package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
- package/dist/pi-extensions/canvas-stophook.js +6 -13
- package/dist/shared/generated-context.d.ts +0 -3
- package/dist/shared/generated-context.js +0 -57
- package/dist/shared/tool-groups.js +2 -3
- package/package.json +5 -4
- package/runtime.lock.json +2 -2
- /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
- /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
- /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
- /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
- /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
- /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
- /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
- /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
- /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { realpathOrSelf, readTextIfExists } from '../../core/fs-utils.js';
|
|
3
|
+
import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
4
|
+
import { composeCanonicalName, explicitLocalName, localNameFromPath, validateExplicitLocalName, validateNamespace, } from '../../core/memory/identity.js';
|
|
5
|
+
import { deriveProjectNamespace, } from '../../core/memory/project-namespace.js';
|
|
6
|
+
import { associateRepository, } from '../../core/memory/repository-association.js';
|
|
7
|
+
const SYNTHETIC_NAMESPACE = '__CROUTER_SYNTHETIC_NAMESPACE__';
|
|
8
|
+
const SYNTHETIC_NAMESPACE_SCALAR = '__CROUTER_SYNTHETIC_NAMESPACE_SCALAR__';
|
|
9
|
+
/** Exact source emitted by the superseded migration 003, with only its namespace
|
|
10
|
+
* value parameterized. It is deliberately byte-shaped rather than prose-shaped:
|
|
11
|
+
* a substantive nested guide is never deleted because it merely resembles this. */
|
|
12
|
+
const SYNTHETIC_INDEX_TEMPLATE = [
|
|
13
|
+
'---',
|
|
14
|
+
'kind: knowledge',
|
|
15
|
+
`namespace: ${SYNTHETIC_NAMESPACE_SCALAR}`,
|
|
16
|
+
`when-and-why-to-read: When working in \`${SYNTHETIC_NAMESPACE}\`, this knowledge should be read because it names the namespace every document in this project answers under.`,
|
|
17
|
+
'surfaces:',
|
|
18
|
+
' - on: workspace-open',
|
|
19
|
+
' at: content',
|
|
20
|
+
' - on: read',
|
|
21
|
+
' match: "./**"',
|
|
22
|
+
' at: content',
|
|
23
|
+
'---',
|
|
24
|
+
`# ${SYNTHETIC_NAMESPACE}`,
|
|
25
|
+
'',
|
|
26
|
+
`This project's memory store. Every document here answers at \`${SYNTHETIC_NAMESPACE}/<local name>\`.`,
|
|
27
|
+
'',
|
|
28
|
+
].join('\n');
|
|
29
|
+
export function planRepositories(corpus) {
|
|
30
|
+
const blockers = [];
|
|
31
|
+
const associated = [];
|
|
32
|
+
const groups = new Map();
|
|
33
|
+
const rootsInCorpusOrder = [];
|
|
34
|
+
for (const store of corpus.stores) {
|
|
35
|
+
if (!isNativeProjectStore(store))
|
|
36
|
+
continue;
|
|
37
|
+
const ownerDir = realpathOrSelf(ownerDirOf(store));
|
|
38
|
+
const result = associateRepository(ownerDir);
|
|
39
|
+
if (!result.ok) {
|
|
40
|
+
blockers.push({
|
|
41
|
+
kind: 'association',
|
|
42
|
+
message: `project memory store at ${store.key} owned by ${ownerDir} cannot be associated with a repository (${result.reason}) — repair the repository boundary and re-run`,
|
|
43
|
+
storeRoot: store.key,
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const row = {
|
|
48
|
+
store,
|
|
49
|
+
ownerDir,
|
|
50
|
+
association: result.association,
|
|
51
|
+
rootDoc: store.snapshot.docs.find((doc) => localNameFromPath(doc.relPath) === '') ?? null,
|
|
52
|
+
};
|
|
53
|
+
associated.push(row);
|
|
54
|
+
const groupingKey = result.association.kind === 'git'
|
|
55
|
+
? result.association.repositoryKey
|
|
56
|
+
: `path:${result.association.repositoryRoot}`;
|
|
57
|
+
let group = groups.get(groupingKey);
|
|
58
|
+
if (group === undefined) {
|
|
59
|
+
group = {
|
|
60
|
+
repositoryKey: result.association.repositoryKey,
|
|
61
|
+
roots: new Map(),
|
|
62
|
+
};
|
|
63
|
+
groups.set(groupingKey, group);
|
|
64
|
+
}
|
|
65
|
+
let root = group.roots.get(result.association.repositoryRoot);
|
|
66
|
+
if (root === undefined) {
|
|
67
|
+
root = {
|
|
68
|
+
association: result.association,
|
|
69
|
+
stores: [],
|
|
70
|
+
rootStore: null,
|
|
71
|
+
index: missingRootIndex(),
|
|
72
|
+
declaration: { state: 'missing' },
|
|
73
|
+
};
|
|
74
|
+
group.roots.set(result.association.repositoryRoot, root);
|
|
75
|
+
rootsInCorpusOrder.push(root);
|
|
76
|
+
}
|
|
77
|
+
root.stores.push(row);
|
|
78
|
+
if (result.association.ownerRelativePath === '' && root.rootStore === null)
|
|
79
|
+
root.rootStore = row;
|
|
80
|
+
}
|
|
81
|
+
const nestedByStore = new Map();
|
|
82
|
+
for (const row of associated) {
|
|
83
|
+
if (row.association.ownerRelativePath === '') {
|
|
84
|
+
nestedByStore.set(row.store.key, { declaration: { state: 'missing' }, action: 'none' });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const declaration = legacyNestedDeclarationFacts(row.rootDoc);
|
|
88
|
+
let action = 'none';
|
|
89
|
+
if (declaration.state === 'invalid') {
|
|
90
|
+
blockers.push(invalidDeclarationBlocker(row.store.key, row.rootDoc?.path ?? row.store.key, declaration, 'nested project memory store'));
|
|
91
|
+
}
|
|
92
|
+
else if (declaration.state === 'valid') {
|
|
93
|
+
action = row.rootDoc !== null && isSupersededSyntheticIndex(row.rootDoc.source, declaration.value)
|
|
94
|
+
? 'synthetic-index-removed'
|
|
95
|
+
: 'removed';
|
|
96
|
+
}
|
|
97
|
+
nestedByStore.set(row.store.key, { declaration, action });
|
|
98
|
+
}
|
|
99
|
+
for (const root of rootsInCorpusOrder) {
|
|
100
|
+
root.index = readRootIndex(root);
|
|
101
|
+
root.declaration = root.index.error === null
|
|
102
|
+
? declarationFacts(root.index.frontmatter?.['namespace'])
|
|
103
|
+
: { state: 'invalid', reason: root.index.error, raw: undefined };
|
|
104
|
+
if (root.declaration.state === 'invalid') {
|
|
105
|
+
blockers.push(invalidDeclarationBlocker(root.rootStore?.store.key ?? dirname(root.association.declarationPath), root.association.declarationPath, root.declaration, 'repository root'));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const selectedByRoot = new Map();
|
|
109
|
+
for (const group of groups.values())
|
|
110
|
+
selectGroup(group, selectedByRoot, blockers);
|
|
111
|
+
const repositories = [];
|
|
112
|
+
for (const root of rootsInCorpusOrder) {
|
|
113
|
+
const selected = selectedByRoot.get(root.association.repositoryRoot);
|
|
114
|
+
if (selected !== undefined)
|
|
115
|
+
repositories.push(selected.decision);
|
|
116
|
+
}
|
|
117
|
+
const prefixes = new Map();
|
|
118
|
+
for (const row of associated) {
|
|
119
|
+
const selected = selectedByRoot.get(row.association.repositoryRoot);
|
|
120
|
+
const nested = nestedByStore.get(row.store.key);
|
|
121
|
+
if (selected === undefined || nested.declaration.state === 'invalid')
|
|
122
|
+
continue;
|
|
123
|
+
const isRoot = row.association.ownerRelativePath === '';
|
|
124
|
+
const effectivePrefix = composeCanonicalName(selected.decision.namespace, row.association.ownerRelativePath);
|
|
125
|
+
const { oldPrefix, oldRule } = oldAuthority(row.store, isRoot, selected.declaration, nested.declaration, effectivePrefix);
|
|
126
|
+
prefixes.set(row.store.key, {
|
|
127
|
+
store: row.store,
|
|
128
|
+
storeKey: row.store.key,
|
|
129
|
+
storeRoot: row.store.key,
|
|
130
|
+
ownerDir: row.ownerDir,
|
|
131
|
+
repositoryKey: row.association.repositoryKey,
|
|
132
|
+
repositoryRoot: row.association.repositoryRoot,
|
|
133
|
+
ownerRelativePath: row.association.ownerRelativePath,
|
|
134
|
+
oldPrefix,
|
|
135
|
+
oldRule,
|
|
136
|
+
effectivePrefix,
|
|
137
|
+
nestedDeclarationAction: nested.action,
|
|
138
|
+
legacyDeclaration: !isRoot && nested.declaration.state === 'valid' ? nested.declaration.value : null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
addOwnerPathCollisionBlockers(associated, selectedByRoot, blockers);
|
|
142
|
+
return { repositories, prefixes, blockers };
|
|
143
|
+
}
|
|
144
|
+
/** What a store's documents answered under BEFORE this run, from file truth
|
|
145
|
+
* alone. A store's own valid declaration proves its old prefix outright.
|
|
146
|
+
*
|
|
147
|
+
* A nested store that declares nothing is in one of two states, and they are not
|
|
148
|
+
* the same authority: pre-003, where nothing up the tree declared anything and
|
|
149
|
+
* its documents answered at bare local names; or already cut, where the
|
|
150
|
+
* repository root's kept declaration is exactly what gave them their prefix. The
|
|
151
|
+
* kept root declaration plus the absence of any explicit `name:` this cut would
|
|
152
|
+
* have rewritten identifies the second — that pair is the corpus this migration
|
|
153
|
+
* leaves behind, so reading it as pre-003 would report an identity rename for
|
|
154
|
+
* every document that never moves. */
|
|
155
|
+
function oldAuthority(store, isRoot, rootDeclaration, nestedDeclaration, effectivePrefix) {
|
|
156
|
+
const own = isRoot ? rootDeclaration : nestedDeclaration;
|
|
157
|
+
if (own.state === 'valid')
|
|
158
|
+
return { oldPrefix: own.value, oldRule: 'per-store-declaration' };
|
|
159
|
+
if (!isRoot && rootDeclaration.state === 'valid' && !carriesLegacyExplicitIdentity(store, effectivePrefix)) {
|
|
160
|
+
return { oldPrefix: effectivePrefix, oldRule: 'per-store-declaration' };
|
|
161
|
+
}
|
|
162
|
+
return { oldPrefix: '', oldRule: 'pre-003' };
|
|
163
|
+
}
|
|
164
|
+
/** An explicit `name:` this cut never leaves behind: one on a store root
|
|
165
|
+
* document, or one that is not a store-local name under the store's own prefix —
|
|
166
|
+
* a fully qualified pre-003 name is the shape that matters. Either proves the
|
|
167
|
+
* store still carries pre-003 identity even though it declares nothing. */
|
|
168
|
+
function carriesLegacyExplicitIdentity(store, effectivePrefix) {
|
|
169
|
+
return store.snapshot.docs.some((doc) => {
|
|
170
|
+
const explicit = explicitLocalName(doc.frontmatter);
|
|
171
|
+
if (explicit === null)
|
|
172
|
+
return false;
|
|
173
|
+
return localNameFromPath(doc.relPath) === '' || !validateExplicitLocalName(explicit, effectivePrefix).ok;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function selectGroup(group, selectedByRoot, blockers) {
|
|
177
|
+
const roots = [...group.roots.values()];
|
|
178
|
+
if (roots.some((root) => root.declaration.state === 'invalid'))
|
|
179
|
+
return;
|
|
180
|
+
const declared = roots.filter((root) => root.declaration.state === 'valid');
|
|
181
|
+
const values = [...new Set(declared.map((root) => root.declaration.value))];
|
|
182
|
+
if (values.length > 1) {
|
|
183
|
+
const paths = declared.map((root) => root.association.declarationPath);
|
|
184
|
+
blockers.push({
|
|
185
|
+
kind: 'namespace',
|
|
186
|
+
message: `linked repository roots for ${group.repositoryKey} declare different namespaces (${values.map((value) => `\`${value}\``).join(', ')}) — make every root declaration agree and re-run`,
|
|
187
|
+
paths,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (values.length === 1) {
|
|
192
|
+
const namespace = values[0];
|
|
193
|
+
for (const root of roots) {
|
|
194
|
+
const action = root.declaration.state === 'valid' ? 'kept' : 'filled-linked-root';
|
|
195
|
+
selectedByRoot.set(root.association.repositoryRoot, {
|
|
196
|
+
declaration: root.declaration,
|
|
197
|
+
decision: repositoryDecision(group, root, namespace, 'declared', action),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const derivingRoot = roots[0];
|
|
203
|
+
const derived = deriveProjectNamespace({
|
|
204
|
+
ownerDir: derivingRoot.association.repositoryRoot,
|
|
205
|
+
rootFrontmatter: derivingRoot.index.frontmatter,
|
|
206
|
+
...(derivingRoot.index.exists ? { rootBody: derivingRoot.index.body } : {}),
|
|
207
|
+
});
|
|
208
|
+
if (!derived.ok) {
|
|
209
|
+
blockers.push({
|
|
210
|
+
kind: 'namespace',
|
|
211
|
+
message: `repository at ${derivingRoot.association.repositoryRoot} cannot derive a namespace (${derived.reason}) — declare a valid \`namespace:\` on ${derivingRoot.association.declarationPath} and re-run`,
|
|
212
|
+
storeRoot: derivingRoot.rootStore?.store.key ?? dirname(derivingRoot.association.declarationPath),
|
|
213
|
+
...(derivingRoot.index.exists ? { path: derivingRoot.association.declarationPath } : {}),
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const root of roots) {
|
|
218
|
+
selectedByRoot.set(root.association.repositoryRoot, {
|
|
219
|
+
declaration: root.declaration,
|
|
220
|
+
decision: repositoryDecision(group, root, derived.namespace, derived.source, root.index.exists ? 'kept' : 'created'),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function repositoryDecision(group, root, namespace, source, action) {
|
|
225
|
+
return {
|
|
226
|
+
repositoryKey: group.repositoryKey,
|
|
227
|
+
repositoryRoot: root.association.repositoryRoot,
|
|
228
|
+
declarationPath: root.association.declarationPath,
|
|
229
|
+
namespace,
|
|
230
|
+
source,
|
|
231
|
+
action,
|
|
232
|
+
storeKeys: root.stores.map((row) => row.store.key),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function readRootIndex(root) {
|
|
236
|
+
const declarationRealPath = realpathOrSelf(root.association.declarationPath);
|
|
237
|
+
const declarationDoc = root.rootStore?.store.snapshot.docs.find((doc) => realpathOrSelf(doc.path) === declarationRealPath);
|
|
238
|
+
if (declarationDoc !== undefined) {
|
|
239
|
+
return {
|
|
240
|
+
exists: true,
|
|
241
|
+
frontmatter: declarationDoc.frontmatter,
|
|
242
|
+
body: declarationDoc.body,
|
|
243
|
+
error: null,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const source = readTextIfExists(root.association.declarationPath);
|
|
248
|
+
if (source === null)
|
|
249
|
+
return missingRootIndex();
|
|
250
|
+
const parsed = parseFrontmatterGeneric(source);
|
|
251
|
+
return {
|
|
252
|
+
exists: true,
|
|
253
|
+
frontmatter: parsed.data,
|
|
254
|
+
body: parsed.body,
|
|
255
|
+
error: null,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
return {
|
|
260
|
+
exists: true,
|
|
261
|
+
frontmatter: null,
|
|
262
|
+
body: '',
|
|
263
|
+
error: `cannot read or parse ${root.association.declarationPath} (${firstLine(error)})`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function missingRootIndex() {
|
|
268
|
+
return { exists: false, frontmatter: null, body: '', error: null };
|
|
269
|
+
}
|
|
270
|
+
function legacyNestedDeclarationFacts(rootDoc) {
|
|
271
|
+
if (rootDoc?.frontmatter === null
|
|
272
|
+
|| rootDoc?.frontmatter === undefined
|
|
273
|
+
|| !Object.prototype.hasOwnProperty.call(rootDoc.frontmatter, 'namespace')) {
|
|
274
|
+
return { state: 'missing' };
|
|
275
|
+
}
|
|
276
|
+
return authoredDeclarationFacts(rootDoc.frontmatter['namespace']);
|
|
277
|
+
}
|
|
278
|
+
function declarationFacts(raw) {
|
|
279
|
+
if (raw === undefined || raw === null || raw === '')
|
|
280
|
+
return { state: 'missing' };
|
|
281
|
+
return authoredDeclarationFacts(raw);
|
|
282
|
+
}
|
|
283
|
+
function authoredDeclarationFacts(raw) {
|
|
284
|
+
if (typeof raw !== 'string') {
|
|
285
|
+
return {
|
|
286
|
+
state: 'invalid',
|
|
287
|
+
reason: `\`namespace:\` is non-string (${JSON.stringify(raw)})`,
|
|
288
|
+
raw,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const valid = validateNamespace(raw);
|
|
292
|
+
return valid.ok
|
|
293
|
+
? { state: 'valid', value: raw }
|
|
294
|
+
: { state: 'invalid', reason: `\`namespace: ${raw}\` is invalid (${valid.reason})`, raw };
|
|
295
|
+
}
|
|
296
|
+
function invalidDeclarationBlocker(storeRoot, path, declaration, subject) {
|
|
297
|
+
return {
|
|
298
|
+
kind: 'namespace',
|
|
299
|
+
message: `${subject} has an invalid namespace declaration at ${path} (${declaration.reason}) — correct it and re-run; migration never replaces an unprovable identity`,
|
|
300
|
+
storeRoot,
|
|
301
|
+
path,
|
|
302
|
+
...(typeof declaration.raw === 'string' ? { name: declaration.raw } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function addOwnerPathCollisionBlockers(associated, selectedByRoot, blockers) {
|
|
306
|
+
const collisions = new Map();
|
|
307
|
+
for (const row of associated) {
|
|
308
|
+
const key = `${row.association.repositoryRoot}\u0000${row.association.ownerRelativePath}`;
|
|
309
|
+
const bucket = collisions.get(key);
|
|
310
|
+
if (bucket === undefined)
|
|
311
|
+
collisions.set(key, [row]);
|
|
312
|
+
else
|
|
313
|
+
bucket.push(row);
|
|
314
|
+
}
|
|
315
|
+
for (const rows of collisions.values()) {
|
|
316
|
+
if (rows.length < 2)
|
|
317
|
+
continue;
|
|
318
|
+
const first = rows[0];
|
|
319
|
+
const selected = selectedByRoot.get(first.association.repositoryRoot);
|
|
320
|
+
const effectivePrefix = selected === undefined
|
|
321
|
+
? first.association.ownerRelativePath
|
|
322
|
+
: composeCanonicalName(selected.decision.namespace, first.association.ownerRelativePath);
|
|
323
|
+
const paths = rows.map((row) => row.ownerDir);
|
|
324
|
+
blockers.push({
|
|
325
|
+
kind: 'collision',
|
|
326
|
+
message: `directories ${paths.join(', ')} in repository ${first.association.repositoryRoot} normalize to the same owner path \`${first.association.ownerRelativePath}\` and effective prefix \`${effectivePrefix}\` — rename one directory and re-run`,
|
|
327
|
+
storeRoot: first.store.key,
|
|
328
|
+
name: effectivePrefix,
|
|
329
|
+
paths,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function isSupersededSyntheticIndex(source, namespace) {
|
|
334
|
+
return source === SYNTHETIC_INDEX_TEMPLATE
|
|
335
|
+
.replaceAll(SYNTHETIC_NAMESPACE_SCALAR, renderScalar(namespace))
|
|
336
|
+
.replaceAll(SYNTHETIC_NAMESPACE, namespace);
|
|
337
|
+
}
|
|
338
|
+
function renderScalar(value) {
|
|
339
|
+
return /^[A-Za-z][A-Za-z0-9._\-/]*$/.test(value) ? value : JSON.stringify(value);
|
|
340
|
+
}
|
|
341
|
+
function ownerDirOf(store) {
|
|
342
|
+
return store.descriptor.ownerDir ?? dirname(dirname(store.descriptor.storeRoot));
|
|
343
|
+
}
|
|
344
|
+
function isNativeProjectStore(store) {
|
|
345
|
+
return store.descriptor.scope === 'project' && store.descriptor.plugin === undefined;
|
|
346
|
+
}
|
|
347
|
+
function firstLine(error) {
|
|
348
|
+
return (error instanceof Error ? error.message : String(error)).split('\n')[0] ?? String(error);
|
|
349
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { withExclusiveDirectoryLockAsync } from '../../core/exclusive-lock.js';
|
|
9
|
+
import { activateOnDiskMigrations, runForcedOnDiskMigrations } from '../activation.js';
|
|
10
|
+
function result() {
|
|
11
|
+
return {
|
|
12
|
+
blockers: [],
|
|
13
|
+
finalVerificationSucceeded: true,
|
|
14
|
+
wrote: { profileManifests: [], documents: [], corpusConsumers: [], corpusIdentities: [] },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function deferred() {
|
|
18
|
+
let resolve;
|
|
19
|
+
const promise = new Promise((done) => { resolve = done; });
|
|
20
|
+
return { promise, resolve };
|
|
21
|
+
}
|
|
22
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
async function waitFor(check) {
|
|
24
|
+
const deadline = Date.now() + 2_000;
|
|
25
|
+
while (!check()) {
|
|
26
|
+
if (Date.now() >= deadline)
|
|
27
|
+
throw new Error('timed out waiting for migration race fixture');
|
|
28
|
+
await delay(5);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function childActivation(home, release) {
|
|
32
|
+
const moduleUrl = pathToFileURL(join(process.cwd(), 'src/migrations/activation.ts')).href;
|
|
33
|
+
const script = `
|
|
34
|
+
const { existsSync, writeFileSync } = await import('node:fs');
|
|
35
|
+
const { join } = await import('node:path');
|
|
36
|
+
const { activateOnDiskMigrations } = await import(${JSON.stringify(moduleUrl)});
|
|
37
|
+
await activateOnDiskMigrations({
|
|
38
|
+
timeoutMs: 5000,
|
|
39
|
+
runner: async () => {
|
|
40
|
+
writeFileSync(join(${JSON.stringify(home)}, 'entered.' + process.pid), '');
|
|
41
|
+
while (!existsSync(${JSON.stringify(release)})) await new Promise((resolve) => setTimeout(resolve, 5));
|
|
42
|
+
return { blockers: [], finalVerificationSucceeded: true, wrote: { profileManifests: [], documents: [], corpusConsumers: [], corpusIdentities: [] } };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
`;
|
|
46
|
+
const child = spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx/esm', '--input-type=module', '-e', script], {
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
env: { ...process.env, CRTR_HOME: home },
|
|
49
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
50
|
+
});
|
|
51
|
+
let stderr = '';
|
|
52
|
+
child.stderr?.setEncoding('utf8').on('data', (chunk) => { stderr += String(chunk); });
|
|
53
|
+
const done = new Promise((resolve, reject) => {
|
|
54
|
+
child.once('error', reject);
|
|
55
|
+
child.once('close', (code) => code === 0 ? resolve() : reject(new Error(`activation child exited ${code}: ${stderr}`)));
|
|
56
|
+
});
|
|
57
|
+
return done;
|
|
58
|
+
}
|
|
59
|
+
test('the asynchronous lock remains owned until its operation promise settles', async () => {
|
|
60
|
+
const home = mkdtempSync(join(tmpdir(), 'crtr-async-lock-'));
|
|
61
|
+
const lock = join(home, 'exclusive.lock');
|
|
62
|
+
const gate = deferred();
|
|
63
|
+
const entered = deferred();
|
|
64
|
+
let secondEntered = false;
|
|
65
|
+
try {
|
|
66
|
+
const first = withExclusiveDirectoryLockAsync(lock, async () => {
|
|
67
|
+
entered.resolve();
|
|
68
|
+
await gate.promise;
|
|
69
|
+
}, { timeoutMs: 1_000 });
|
|
70
|
+
await entered.promise;
|
|
71
|
+
assert.equal(existsSync(lock), true);
|
|
72
|
+
const second = withExclusiveDirectoryLockAsync(lock, async () => { secondEntered = true; }, { timeoutMs: 1_000 });
|
|
73
|
+
await delay(30);
|
|
74
|
+
assert.equal(secondEntered, false);
|
|
75
|
+
assert.equal(existsSync(lock), true);
|
|
76
|
+
gate.resolve();
|
|
77
|
+
await Promise.all([first, second]);
|
|
78
|
+
assert.equal(secondEntered, true);
|
|
79
|
+
assert.equal(existsSync(lock), false);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
rmSync(home, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
test('activation and a forced migration serialize, and forced still runs after the marker write', async () => {
|
|
86
|
+
const home = mkdtempSync(join(tmpdir(), 'crtr-activation-forced-'));
|
|
87
|
+
const previous = process.env['CRTR_HOME'];
|
|
88
|
+
process.env['CRTR_HOME'] = home;
|
|
89
|
+
const gate = deferred();
|
|
90
|
+
const entered = deferred();
|
|
91
|
+
let calls = 0;
|
|
92
|
+
let active = 0;
|
|
93
|
+
let maxActive = 0;
|
|
94
|
+
const runner = async () => {
|
|
95
|
+
calls += 1;
|
|
96
|
+
active += 1;
|
|
97
|
+
maxActive = Math.max(maxActive, active);
|
|
98
|
+
try {
|
|
99
|
+
if (calls === 1) {
|
|
100
|
+
entered.resolve();
|
|
101
|
+
await gate.promise;
|
|
102
|
+
}
|
|
103
|
+
return result();
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
active -= 1;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
const activation = activateOnDiskMigrations({ timeoutMs: 1_000, runner });
|
|
111
|
+
await entered.promise;
|
|
112
|
+
const forced = runForcedOnDiskMigrations({ timeoutMs: 1_000, runner });
|
|
113
|
+
await delay(30);
|
|
114
|
+
assert.equal(calls, 1);
|
|
115
|
+
gate.resolve();
|
|
116
|
+
await Promise.all([activation, forced]);
|
|
117
|
+
assert.equal(calls, 2);
|
|
118
|
+
assert.equal(maxActive, 1);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
if (previous === undefined)
|
|
122
|
+
delete process.env['CRTR_HOME'];
|
|
123
|
+
else
|
|
124
|
+
process.env['CRTR_HOME'] = previous;
|
|
125
|
+
rmSync(home, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
test('a waiting process rechecks the marker so only one process runs migration', async () => {
|
|
129
|
+
const home = mkdtempSync(join(tmpdir(), 'crtr-activation-process-race-'));
|
|
130
|
+
const release = join(home, 'release');
|
|
131
|
+
mkdirSync(home, { recursive: true });
|
|
132
|
+
try {
|
|
133
|
+
const first = childActivation(home, release);
|
|
134
|
+
await waitFor(() => readdirSync(home).some((entry) => entry.startsWith('entered.')));
|
|
135
|
+
const second = childActivation(home, release);
|
|
136
|
+
await delay(50);
|
|
137
|
+
assert.equal(readdirSync(home).filter((entry) => entry.startsWith('entered.')).length, 1);
|
|
138
|
+
writeFileSync(release, '');
|
|
139
|
+
await Promise.all([first, second]);
|
|
140
|
+
assert.equal(readdirSync(home).filter((entry) => entry.startsWith('entered.')).length, 1);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
rmSync(home, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { onDiskMigrationMarkerPath } from '../../core/canvas/paths.js';
|
|
7
|
+
import { activateOnDiskMigrations, runForcedOnDiskMigrations } from '../activation.js';
|
|
8
|
+
import { onDiskMigrationIdentity, STATE_MIGRATIONS } from '../registry.js';
|
|
9
|
+
function result(options = {}) {
|
|
10
|
+
const writes = options.writes ?? [];
|
|
11
|
+
return {
|
|
12
|
+
blockers: options.blockers ?? [],
|
|
13
|
+
finalVerificationSucceeded: options.verified ?? true,
|
|
14
|
+
wrote: { profileManifests: writes, documents: [], corpusConsumers: [], corpusIdentities: [] },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async function inHome(run) {
|
|
18
|
+
const home = mkdtempSync(join(tmpdir(), 'crtr-migration-activation-'));
|
|
19
|
+
const previous = process.env['CRTR_HOME'];
|
|
20
|
+
process.env['CRTR_HOME'] = home;
|
|
21
|
+
try {
|
|
22
|
+
await run(home);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
if (previous === undefined)
|
|
26
|
+
delete process.env['CRTR_HOME'];
|
|
27
|
+
else
|
|
28
|
+
process.env['CRTR_HOME'] = previous;
|
|
29
|
+
rmSync(home, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function marker() {
|
|
33
|
+
return JSON.parse(readFileSync(onDiskMigrationMarkerPath(), 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
test('activation skips only an exact current marker', async () => {
|
|
36
|
+
await inHome(async () => {
|
|
37
|
+
let runs = 0;
|
|
38
|
+
const runner = async () => { runs += 1; return result(); };
|
|
39
|
+
writeFileSync(onDiskMigrationMarkerPath(), JSON.stringify(onDiskMigrationIdentity()));
|
|
40
|
+
assert.equal(await activateOnDiskMigrations({ timeoutMs: 1_000, runner }), null);
|
|
41
|
+
assert.equal(runs, 0);
|
|
42
|
+
for (const invalid of [undefined, '{broken', JSON.stringify(onDiskMigrationIdentity().slice(0, -1))]) {
|
|
43
|
+
rmSync(onDiskMigrationMarkerPath(), { force: true });
|
|
44
|
+
if (invalid !== undefined)
|
|
45
|
+
writeFileSync(onDiskMigrationMarkerPath(), invalid);
|
|
46
|
+
assert.notEqual(await activateOnDiskMigrations({ timeoutMs: 1_000, runner }), null);
|
|
47
|
+
}
|
|
48
|
+
assert.equal(runs, 3);
|
|
49
|
+
assert.deepEqual(marker(), onDiskMigrationIdentity());
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
// The repository-root cut renamed migration 003, and the rename is the whole
|
|
53
|
+
// invalidation mechanism: every home carrying the superseded identity must run
|
|
54
|
+
// the corpus lane again. These ids are written literally rather than through
|
|
55
|
+
// `onDiskMigrationIdentity()` so a rename that silently keeps the old marker
|
|
56
|
+
// valid fails here.
|
|
57
|
+
test('a superseded 003 identity reruns the corpus lane and the repository-root identity skips it', async () => {
|
|
58
|
+
await inHome(async () => {
|
|
59
|
+
let runs = 0;
|
|
60
|
+
const runner = async () => { runs += 1; return result(); };
|
|
61
|
+
writeFileSync(onDiskMigrationMarkerPath(), JSON.stringify([
|
|
62
|
+
{ id: '001-surfaces-frontmatter', lane: 'convergent' },
|
|
63
|
+
{ id: '002-profile-project-memory', lane: 'profile-manifest' },
|
|
64
|
+
{ id: '003-workspace-qualified-memory-identity', lane: 'corpus' },
|
|
65
|
+
]));
|
|
66
|
+
assert.notEqual(await activateOnDiskMigrations({ timeoutMs: 1_000, runner }), null, 'the superseded 003 identity must not satisfy activation');
|
|
67
|
+
assert.equal(runs, 1);
|
|
68
|
+
const repositoryRootIdentity = [
|
|
69
|
+
{ id: '001-surfaces-frontmatter', lane: 'convergent' },
|
|
70
|
+
{ id: '002-profile-project-memory', lane: 'profile-manifest' },
|
|
71
|
+
{ id: '003-repository-root-memory-identity', lane: 'corpus' },
|
|
72
|
+
];
|
|
73
|
+
assert.deepEqual(marker(), repositoryRootIdentity, 'the rerun records the repository-root identity in order');
|
|
74
|
+
writeFileSync(onDiskMigrationMarkerPath(), JSON.stringify(repositoryRootIdentity));
|
|
75
|
+
assert.equal(await activateOnDiskMigrations({ timeoutMs: 1_000, runner }), null);
|
|
76
|
+
assert.equal(runs, 1, 'the repository-root marker skips the run');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
test('journaled database entries do not change the on-disk marker identity', () => {
|
|
80
|
+
const before = onDiskMigrationIdentity();
|
|
81
|
+
const registry = STATE_MIGRATIONS;
|
|
82
|
+
registry.push({ lane: 'journaled', description: 'test database migration', apply: () => undefined });
|
|
83
|
+
try {
|
|
84
|
+
assert.deepEqual(onDiskMigrationIdentity(), before);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
registry.pop();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
test('forced runs ignore current markers while dry runs leave marker bytes untouched', async () => {
|
|
91
|
+
await inHome(async () => {
|
|
92
|
+
const current = JSON.stringify(onDiskMigrationIdentity());
|
|
93
|
+
writeFileSync(onDiskMigrationMarkerPath(), current);
|
|
94
|
+
let runs = 0;
|
|
95
|
+
await runForcedOnDiskMigrations({
|
|
96
|
+
timeoutMs: 1_000,
|
|
97
|
+
runner: async () => { runs += 1; return result(); },
|
|
98
|
+
});
|
|
99
|
+
assert.equal(runs, 1);
|
|
100
|
+
assert.deepEqual(marker(), onDiskMigrationIdentity());
|
|
101
|
+
writeFileSync(onDiskMigrationMarkerPath(), '{malformed but preserved');
|
|
102
|
+
await runForcedOnDiskMigrations({
|
|
103
|
+
timeoutMs: 1_000,
|
|
104
|
+
dryRun: true,
|
|
105
|
+
runner: async () => { runs += 1; return result({ verified: false }); },
|
|
106
|
+
});
|
|
107
|
+
assert.equal(readFileSync(onDiskMigrationMarkerPath(), 'utf8'), '{malformed but preserved');
|
|
108
|
+
assert.equal(runs, 2);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
test('failure leaves the marker absent and a later activation converges', async () => {
|
|
112
|
+
await inHome(async () => {
|
|
113
|
+
await assert.rejects(activateOnDiskMigrations({ timeoutMs: 1_000, runner: async () => { throw new Error('interrupted'); } }), /interrupted/);
|
|
114
|
+
assert.equal(existsSync(onDiskMigrationMarkerPath()), false);
|
|
115
|
+
await activateOnDiskMigrations({ timeoutMs: 1_000, runner: async () => result() });
|
|
116
|
+
assert.deepEqual(marker(), onDiskMigrationIdentity());
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
test('an unreadable profile blocker aborts startup effects with its path and repair command', async () => {
|
|
120
|
+
await inHome(async (home) => {
|
|
121
|
+
const blockedPath = join(home, 'profiles', 'bad-deadbeef', 'profile.json');
|
|
122
|
+
const writtenPath = join(home, 'profiles', 'root', 'profile.json');
|
|
123
|
+
const effects = { database: false, pidfile: false, api: false, broker: false };
|
|
124
|
+
const startup = async () => {
|
|
125
|
+
await activateOnDiskMigrations({
|
|
126
|
+
timeoutMs: 1_000,
|
|
127
|
+
runner: async () => result({
|
|
128
|
+
verified: false,
|
|
129
|
+
writes: [writtenPath],
|
|
130
|
+
blockers: [{ kind: 'parse-failure', message: 'profile manifest is not valid JSON', path: blockedPath }],
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
effects.database = true;
|
|
134
|
+
effects.pidfile = true;
|
|
135
|
+
effects.api = true;
|
|
136
|
+
effects.broker = true;
|
|
137
|
+
};
|
|
138
|
+
await assert.rejects(startup(), (error) => {
|
|
139
|
+
assert.ok(error instanceof Error);
|
|
140
|
+
assert.match(error.message, /on-disk migration blocked by 1 finding/);
|
|
141
|
+
assert.match(error.message, new RegExp(blockedPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
142
|
+
assert.match(error.message, new RegExp(writtenPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
143
|
+
assert.match(error.message, /Repair: crtr sys migrate/);
|
|
144
|
+
return true;
|
|
145
|
+
});
|
|
146
|
+
assert.deepEqual(effects, { database: false, pidfile: false, api: false, broker: false });
|
|
147
|
+
assert.equal(existsSync(onDiskMigrationMarkerPath()), false);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|