@ornexus/neocortex 4.60.72 → 4.60.73
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/release-source.json +1 -1
- package/dist/sbom.cdx.json +24 -24
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +2 -2
- package/packages/client/dist/adapters/adapter-registry.js +108 -1
- package/packages/client/dist/adapters/antigravity-adapter.js +77 -2
- package/packages/client/dist/adapters/claude-code-adapter.js +79 -3
- package/packages/client/dist/adapters/codex-adapter.js +80 -2
- package/packages/client/dist/adapters/cursor-adapter.js +115 -4
- package/packages/client/dist/adapters/gemini-adapter.js +71 -2
- package/packages/client/dist/adapters/grok-adapter.js +42 -2
- package/packages/client/dist/adapters/index.js +22 -1
- package/packages/client/dist/adapters/platform-detector.js +212 -1
- package/packages/client/dist/adapters/target-adapter.js +12 -0
- package/packages/client/dist/adapters/vscode-adapter.js +72 -2
- package/packages/client/dist/agent/refresh-stubs.js +417 -2
- package/packages/client/dist/agent/update-agent-yaml.js +102 -1
- package/packages/client/dist/agent/update-description.js +251 -1
- package/packages/client/dist/cache/crypto-utils.js +78 -1
- package/packages/client/dist/cache/encrypted-cache.js +108 -1
- package/packages/client/dist/cache/in-memory-asset-cache.js +73 -1
- package/packages/client/dist/cache/index.js +13 -1
- package/packages/client/dist/cache/protected-pi-boundary.js +44 -1
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +127 -2
- package/packages/client/dist/checkpoint/index.js +11 -1
- package/packages/client/dist/checkpoint/shared-checkpoint-types.js +32 -1
- package/packages/client/dist/cli.js +358 -5
- package/packages/client/dist/commands/activate.js +394 -8
- package/packages/client/dist/commands/architecture-answers.js +274 -7
- package/packages/client/dist/commands/architecture-memory-terminal.js +253 -4
- package/packages/client/dist/commands/architecture-non-ui-continuation.js +100 -1
- package/packages/client/dist/commands/architecture-root-binding.js +51 -1
- package/packages/client/dist/commands/cache-status.js +112 -2
- package/packages/client/dist/commands/config.js +88 -1
- package/packages/client/dist/commands/doctor.js +635 -2
- package/packages/client/dist/commands/invoke.js +7200 -143
- package/packages/client/dist/commands/legacy-epic-terminal.js +71 -1
- package/packages/client/dist/commands/legacy-story-branch.js +96 -1
- package/packages/client/dist/commands/legacy-story-pr.js +276 -1
- package/packages/client/dist/commands/mutation-qa-checkpoint.js +285 -5
- package/packages/client/dist/commands/mutation-qa-gate.js +54 -1
- package/packages/client/dist/commands/mutation-qa-git.js +41 -1
- package/packages/client/dist/commands/mutation-qa-policy.js +102 -1
- package/packages/client/dist/commands/mutation-qa-run.js +219 -3
- package/packages/client/dist/commands/mutation-qa-verdict.js +149 -1
- package/packages/client/dist/commands/mutation-qa.js +220 -1
- package/packages/client/dist/commands/native-agent.js +136 -2
- package/packages/client/dist/commands/planning-diagnosis.js +43 -1
- package/packages/client/dist/commands/planning-generation.js +53 -1
- package/packages/client/dist/commands/planning-memory.js +40 -1
- package/packages/client/dist/commands/planning-research.js +42 -1
- package/packages/client/dist/commands/private-session.js +75 -1
- package/packages/client/dist/commands/property-qa.js +139 -1
- package/packages/client/dist/commands/qa-control.js +16 -1
- package/packages/client/dist/commands/qa-scan-browser-launch.js +55 -1
- package/packages/client/dist/commands/qa-scan-browser-route.js +28 -1
- package/packages/client/dist/commands/qa-scan-capture.js +663 -5
- package/packages/client/dist/commands/qa-scan-discovery.js +376 -3
- package/packages/client/dist/commands/qa-scan-review.js +826 -5
- package/packages/client/dist/commands/qa-synthetic-backend-capture.js +266 -1
- package/packages/client/dist/commands/qa-synthetic-backend-review.js +297 -1
- package/packages/client/dist/commands/qa-synthetic-backend.js +340 -2
- package/packages/client/dist/commands/qa-synthetic-broker-port.js +85 -1
- package/packages/client/dist/commands/qa-synthetic-browser.js +39 -1
- package/packages/client/dist/commands/refactor-characterization.js +671 -21
- package/packages/client/dist/commands/refactor-commit.js +399 -13
- package/packages/client/dist/commands/refactor-control.js +142 -3
- package/packages/client/dist/commands/refactor-cpp-assertions.js +93 -3
- package/packages/client/dist/commands/refactor-review.js +283 -5
- package/packages/client/dist/commands/refactor-root.js +588 -7
- package/packages/client/dist/commands/refresh-memory.js +156 -3
- package/packages/client/dist/commands/shared-architecture-non-ui.js +24 -1
- package/packages/client/dist/commands/story-checkpoint.js +171 -1
- package/packages/client/dist/config/profile-directory.js +13 -1
- package/packages/client/dist/config/resolver-selection.js +278 -1
- package/packages/client/dist/config/secure-config.js +281 -12
- package/packages/client/dist/config.js +223 -2
- package/packages/client/dist/constants.js +25 -1
- package/packages/client/dist/context/context-collector.js +222 -2
- package/packages/client/dist/context/context-sanitizer.js +145 -1
- package/packages/client/dist/continuity/autonomous-governance.js +285 -1
- package/packages/client/dist/continuity/continuity-client-state-store.js +1587 -1
- package/packages/client/dist/continuity/invoke-hooks.js +129 -1
- package/packages/client/dist/continuity/migrations/001-initial-schema.js +12 -2
- package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +12 -2
- package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +12 -2
- package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +12 -2
- package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +12 -2
- package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +10 -2
- package/packages/client/dist/continuity/native-agent-store.js +69 -1
- package/packages/client/dist/continuity/native-agent-types.js +90 -1
- package/packages/client/dist/continuity/outbox.js +117 -1
- package/packages/client/dist/continuity/runner-bootstrap-policy.js +172 -1
- package/packages/client/dist/continuity/sqlite-store.d.ts +22 -1
- package/packages/client/dist/continuity/sqlite-store.js +2612 -81
- package/packages/client/dist/effects/invoke-effect-gateways.js +62 -1
- package/packages/client/dist/errors/error-messages.js +228 -2
- package/packages/client/dist/errors/frontmatter-diagnostics.js +125 -1
- package/packages/client/dist/evidence/collector.js +347 -1
- package/packages/client/dist/evidence/index.js +1 -1
- package/packages/client/dist/evidence/types.js +2 -0
- package/packages/client/dist/fs/atomic-state-replace.js +152 -1
- package/packages/client/dist/generated/command-registry.generated.js +4076 -1
- package/packages/client/dist/generated/public-projections.generated.js +149 -1
- package/packages/client/dist/git/direct-yolo-c01-retry.js +138 -4
- package/packages/client/dist/git/direct-yolo-checkpoint.js +1234 -4
- package/packages/client/dist/git/epic-operational-state.d.ts +198 -0
- package/packages/client/dist/git/epic-operational-state.js +672 -0
- package/packages/client/dist/git/git-lifecycle-step-ids.js +14 -1
- package/packages/client/dist/git/git-lifecycle-store.d.ts +9 -1
- package/packages/client/dist/git/git-lifecycle-store.js +1981 -9
- package/packages/client/dist/git/legacy-story-branch-checkpoint.js +259 -9
- package/packages/client/dist/git/legacy-story-branch-setup.js +348 -4
- package/packages/client/dist/git/legacy-story-pr.js +3538 -70
- package/packages/client/dist/git/secure-filesystem-boundary.js +182 -5
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +284 -1
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +237 -1
- package/packages/client/dist/i18n/first-run.js +60 -2
- package/packages/client/dist/index.js +47 -1
- package/packages/client/dist/invocation/invoke-input.js +24 -1
- package/packages/client/dist/license/index.js +5 -1
- package/packages/client/dist/license/license-client.js +304 -1
- package/packages/client/dist/machine/fingerprint.js +163 -2
- package/packages/client/dist/machine/index.js +5 -1
- package/packages/client/dist/memory/project-memory-path-scope.js +62 -1
- package/packages/client/dist/memory/project-memory-writer.js +1007 -40
- package/packages/client/dist/memory/shared-project-memory-types.js +253 -2
- package/packages/client/dist/migrations/epic-lifecycle-backfill.d.ts +97 -0
- package/packages/client/dist/migrations/epic-lifecycle-backfill.js +1060 -0
- package/packages/client/dist/migrations/project-identity-capability.js +50 -1
- package/packages/client/dist/migrations/project-identity.js +493 -7
- package/packages/client/dist/migrations/state-json.d.ts +22 -0
- package/packages/client/dist/migrations/state-json.js +393 -4
- package/packages/client/dist/policy/architecture-policy.js +184 -2
- package/packages/client/dist/policy/index.js +7 -1
- package/packages/client/dist/policy/shared-policy-types.js +38 -0
- package/packages/client/dist/policy/subagent-approval-policy.js +55 -1
- package/packages/client/dist/private-execution/clean-baseline.js +279 -2
- package/packages/client/dist/private-execution/codex-executor.js +574 -4
- package/packages/client/dist/private-execution/codex-host-runtime.js +137 -1
- package/packages/client/dist/private-execution/codex-lifecycle-guard.js +97 -1
- package/packages/client/dist/private-execution/codex-scoped-permissions.js +128 -1
- package/packages/client/dist/private-execution/delivery.js +410 -13
- package/packages/client/dist/private-execution/diagnosis-root-review.js +294 -2
- package/packages/client/dist/private-execution/frozen-handoff.js +19 -6
- package/packages/client/dist/private-execution/implementation-validation.js +106 -2
- package/packages/client/dist/private-execution/isolated-admission.js +1432 -23
- package/packages/client/dist/private-execution/isolated-session.js +725 -5
- package/packages/client/dist/private-execution/isolated-workspace.js +122 -1
- package/packages/client/dist/private-execution/pact-consumer-runtime.js +409 -3
- package/packages/client/dist/private-execution/pact-test-source.js +258 -1
- package/packages/client/dist/private-execution/pact-toolchain.js +290 -3
- package/packages/client/dist/private-execution/pact-write-set.js +172 -1
- package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +406 -5
- package/packages/client/dist/private-execution/planning-generation-checkpoint.js +462 -5
- package/packages/client/dist/private-execution/planning-memory-checkpoint.js +139 -3
- package/packages/client/dist/private-execution/planning-research-checkpoint.js +325 -4
- package/packages/client/dist/private-execution/private-report-store.js +42 -1
- package/packages/client/dist/private-execution/property-test-candidate.js +103 -1
- package/packages/client/dist/private-execution/property-test-choices.js +147 -3
- package/packages/client/dist/private-execution/property-test-plan.js +271 -7
- package/packages/client/dist/private-execution/property-test-review.js +258 -4
- package/packages/client/dist/private-execution/property-test-runtime.js +372 -9
- package/packages/client/dist/private-execution/property-test-source.js +139 -1
- package/packages/client/dist/private-execution/root-checkpoint.js +80 -1
- package/packages/client/dist/private-execution/root-memory-git-evidence.js +135 -3
- package/packages/client/dist/private-execution/root-memory-recovery.js +93 -1
- package/packages/client/dist/private-execution/root-memory-request.js +29 -1
- package/packages/client/dist/private-execution/root-memory-scope.js +178 -1
- package/packages/client/dist/private-execution/root-memory-writer.js +102 -1
- package/packages/client/dist/private-execution/session-lease.js +63 -1
- package/packages/client/dist/private-execution/shared-private-admission-types.js +50 -1
- package/packages/client/dist/private-execution/shared-project-validation.js +66 -1
- package/packages/client/dist/private-execution/shared-root-memory-types.js +61 -1
- package/packages/client/dist/private-execution/ux-source-validation.js +372 -1
- package/packages/client/dist/private-execution/ux-write-set.js +191 -2
- package/packages/client/dist/registry/registry-reconciliation.js +234 -1
- package/packages/client/dist/resilience/circuit-breaker.js +170 -1
- package/packages/client/dist/resilience/degradation-manager.js +164 -1
- package/packages/client/dist/resilience/freshness-indicator.js +100 -1
- package/packages/client/dist/resilience/index.js +8 -1
- package/packages/client/dist/resilience/recovery-detector.js +74 -1
- package/packages/client/dist/resolvers/asset-resolver.js +13 -0
- package/packages/client/dist/resolvers/local-resolver.js +255 -8
- package/packages/client/dist/resolvers/remote-resolver.js +233 -1
- package/packages/client/dist/runner/binary.js +404 -1
- package/packages/client/dist/runner/cli.js +1571 -17
- package/packages/client/dist/runner/headless/adapters.js +49 -1
- package/packages/client/dist/runner/headless/claude-code-adapter.js +79 -1
- package/packages/client/dist/runner/headless/continuous-story-workspace.js +1628 -11
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +69 -1
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +110 -2
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +1236 -8
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +318 -2
- package/packages/client/dist/runner/headless/fake-adapter.js +62 -1
- package/packages/client/dist/runner/headless/managed-profile.js +132 -1
- package/packages/client/dist/runner/headless/opencode-adapter.js +145 -2
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +43 -1
- package/packages/client/dist/runner/headless/resolver.js +231 -1
- package/packages/client/dist/runner/headless/run-yolo.js +885 -1
- package/packages/client/dist/runner/headless/run-yoloop.js +2574 -2
- package/packages/client/dist/runner/headless/types.js +574 -2
- package/packages/client/dist/runner/local-console.js +396 -13
- package/packages/client/dist/runner/portal-sync-worker.js +1487 -1
- package/packages/client/dist/runner/project-identity.js +106 -1
- package/packages/client/dist/runner/scheduler.js +995 -7
- package/packages/client/dist/runner-cli.js +18 -2
- package/packages/client/dist/state/invocation-shape.js +13 -1
- package/packages/client/dist/state/project-state-snapshot.d.ts +8 -0
- package/packages/client/dist/state/project-state-snapshot.js +1731 -1
- package/packages/client/dist/state/state-json-repair.js +785 -3
- package/packages/client/dist/state/structural-dag-projection.js +118 -1
- package/packages/client/dist/telemetry/index.js +5 -1
- package/packages/client/dist/telemetry/offline-queue.js +131 -1
- package/packages/client/dist/tier/index.js +5 -1
- package/packages/client/dist/tier/tier-aware-client.js +405 -1
- package/packages/client/dist/types/index.js +38 -1
- package/packages/client/dist/version.js +27 -1
- package/packages/client/dist/yolo/change-evidence-collector.js +126 -1
- package/packages/client/dist/yolo/invoke-envelope-capability.js +62 -1
- package/packages/client/dist/yolo/spec-envelope-collector.js +184 -5
- package/packages/client/dist/yoloop/discovery-hook.js +300 -2
- package/packages/client/dist/yoloop/index.js +8 -1
- package/packages/client/dist/yoloop/invoke-hooks.js +669 -12
- package/packages/client/dist/yoloop/shared-discover-epics.js +1007 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +65 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.js +33 -1
- package/packages/client/dist/yoloop/target-normalization.js +180 -1
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +8 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1920 -1
- package/packages/shared/dist/yolo/spec-envelope.js +3 -6
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +1 -1
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -1 +1,50 @@
|
|
|
1
|
-
import{PROJECT_IDENTITY_MIGRATION_CAPABILITY
|
|
1
|
+
import { PROJECT_IDENTITY_MIGRATION_CAPABILITY } from '../../../shared/dist/invocation/index.js';
|
|
2
|
+
const MAX_CAPABILITY_RESPONSE_BYTES = 16 * 1024;
|
|
3
|
+
/**
|
|
4
|
+
* Fail-closed, non-mutating rollout probe. An older server cannot cause the
|
|
5
|
+
* client to persist a local project ID that has no remote floor authority.
|
|
6
|
+
*/
|
|
7
|
+
export async function probeProjectIdentityMigrationCapability(serverUrl, fetcher = globalThis.fetch) {
|
|
8
|
+
if (!fetcher)
|
|
9
|
+
return { supported: false, reason: 'fetch-unavailable', publicSafe: true };
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
12
|
+
timeout.unref?.();
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetcher(`${serverUrl.replace(/\/+$/, '')}/api/v1/config`, {
|
|
15
|
+
method: 'GET',
|
|
16
|
+
headers: { accept: 'application/json' },
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok)
|
|
20
|
+
return { supported: false, reason: 'http-rejected', publicSafe: true };
|
|
21
|
+
const raw = await response.text();
|
|
22
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_CAPABILITY_RESPONSE_BYTES) {
|
|
23
|
+
return { supported: false, reason: 'response-too-large', publicSafe: true };
|
|
24
|
+
}
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { supported: false, reason: 'response-invalid', publicSafe: true };
|
|
31
|
+
}
|
|
32
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
33
|
+
return { supported: false, reason: 'response-invalid', publicSafe: true };
|
|
34
|
+
}
|
|
35
|
+
const capabilities = parsed.capabilities;
|
|
36
|
+
if (!Array.isArray(capabilities)
|
|
37
|
+
|| capabilities.some((value) => typeof value !== 'string' || value.length > 80)) {
|
|
38
|
+
return { supported: false, reason: 'response-invalid', publicSafe: true };
|
|
39
|
+
}
|
|
40
|
+
return capabilities.includes(PROJECT_IDENTITY_MIGRATION_CAPABILITY)
|
|
41
|
+
? { supported: true, reason: 'supported', publicSafe: true }
|
|
42
|
+
: { supported: false, reason: 'capability-missing', publicSafe: true };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { supported: false, reason: 'request-failed', publicSafe: true };
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,7 +1,493 @@
|
|
|
1
|
-
import{createHash
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, } from 'node:fs';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { assertProjectIdentityMigrationReceiptV1, PROJECT_ID_PATTERN, } from '../../../shared/dist/invocation/index.js';
|
|
5
|
+
import { atomicReplaceUtf8, durableCreateUtf8, fsyncDirectoryBestEffort, recoverAtomicReplaceUtf8, } from '../fs/atomic-state-replace.js';
|
|
6
|
+
const MIGRATION_SCHEMA_VERSION = 1;
|
|
7
|
+
const STATE_RELATIVE_PATH = join('.neocortex', 'state.json');
|
|
8
|
+
const MIGRATION_RELATIVE_PATH = join('.neocortex', 'project-identity-migration.json');
|
|
9
|
+
const LOCK_RELATIVE_PATH = join('.neocortex', 'project-identity-migration.lock');
|
|
10
|
+
const BACKUP_PATH_PATTERN = /^\.neocortex[\\/]state\.json\.project-identity-backup-(?:\d{8}T\d{9}Z-)?mig_[a-f0-9]{32}$/;
|
|
11
|
+
export class ProjectIdentityMigrationError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message = code) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = 'ProjectIdentityMigrationError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
|
20
|
+
function projectId() {
|
|
21
|
+
return `prj_${randomUUID().replaceAll('-', '')}`;
|
|
22
|
+
}
|
|
23
|
+
function operationId() {
|
|
24
|
+
return `mig_${randomUUID().replaceAll('-', '')}`;
|
|
25
|
+
}
|
|
26
|
+
function backupTimestamp(date) {
|
|
27
|
+
return date.toISOString().replace(/[-:.]/g, '');
|
|
28
|
+
}
|
|
29
|
+
function assertSafeProjectRoot(projectRoot) {
|
|
30
|
+
const resolved = resolve(projectRoot);
|
|
31
|
+
let canonical;
|
|
32
|
+
try {
|
|
33
|
+
canonical = realpathSync(resolved);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_ROOT_INVALID');
|
|
37
|
+
}
|
|
38
|
+
if (!statSync(canonical).isDirectory()) {
|
|
39
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_ROOT_INVALID');
|
|
40
|
+
}
|
|
41
|
+
const ncPath = join(canonical, '.neocortex');
|
|
42
|
+
if (existsSync(ncPath) && lstatSync(ncPath).isSymbolicLink()) {
|
|
43
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_SYMLINK_REJECTED');
|
|
44
|
+
}
|
|
45
|
+
const statePath = join(canonical, STATE_RELATIVE_PATH);
|
|
46
|
+
if (existsSync(statePath)) {
|
|
47
|
+
const stateStat = lstatSync(statePath);
|
|
48
|
+
if (stateStat.isSymbolicLink() || !stateStat.isFile()) {
|
|
49
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_SYMLINK_REJECTED');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const migrationPath = join(canonical, MIGRATION_RELATIVE_PATH);
|
|
53
|
+
if (existsSync(migrationPath)) {
|
|
54
|
+
const migrationStat = lstatSync(migrationPath);
|
|
55
|
+
if (migrationStat.isSymbolicLink() || !migrationStat.isFile()) {
|
|
56
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_SYMLINK_REJECTED');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return canonical;
|
|
60
|
+
}
|
|
61
|
+
function withMigrationLock(root, operation) {
|
|
62
|
+
const lockPath = join(root, LOCK_RELATIVE_PATH);
|
|
63
|
+
mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
64
|
+
const claimLock = () => {
|
|
65
|
+
const ownerPath = `${lockPath}.${process.pid}.${randomUUID()}.owner`;
|
|
66
|
+
durableCreateUtf8(ownerPath, `${process.pid}\n`);
|
|
67
|
+
try {
|
|
68
|
+
// The hard link is an atomic no-overwrite claim. Because its source was
|
|
69
|
+
// fsynced first, a canonical lock can never exist with empty metadata.
|
|
70
|
+
linkSync(ownerPath, lockPath);
|
|
71
|
+
fsyncDirectoryBestEffort(dirname(lockPath));
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
try {
|
|
75
|
+
unlinkSync(ownerPath);
|
|
76
|
+
}
|
|
77
|
+
catch { /* orphan owner files do not grant the lock */ }
|
|
78
|
+
fsyncDirectoryBestEffort(dirname(lockPath));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
claimLock();
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code !== 'EEXIST') {
|
|
86
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_LOCKED');
|
|
87
|
+
}
|
|
88
|
+
const lockStat = lstatSync(lockPath);
|
|
89
|
+
if (lockStat.isSymbolicLink() || !lockStat.isFile()) {
|
|
90
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_SYMLINK_REJECTED');
|
|
91
|
+
}
|
|
92
|
+
const rawOwner = readFileSync(lockPath, 'utf8');
|
|
93
|
+
if (!/^[1-9][0-9]*\n$/.test(rawOwner) || rawOwner.length > 24) {
|
|
94
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_LOCK_CORRUPT');
|
|
95
|
+
}
|
|
96
|
+
const ownerPid = Number(rawOwner.trim());
|
|
97
|
+
let ownerAlive = true;
|
|
98
|
+
try {
|
|
99
|
+
process.kill(ownerPid, 0);
|
|
100
|
+
}
|
|
101
|
+
catch (probeError) {
|
|
102
|
+
ownerAlive = probeError.code !== 'ESRCH';
|
|
103
|
+
}
|
|
104
|
+
if (ownerAlive)
|
|
105
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_LOCKED');
|
|
106
|
+
try {
|
|
107
|
+
unlinkSync(lockPath);
|
|
108
|
+
fsyncDirectoryBestEffort(dirname(lockPath));
|
|
109
|
+
claimLock();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_LOCKED');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return operation();
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(lockPath);
|
|
121
|
+
fsyncDirectoryBestEffort(dirname(lockPath));
|
|
122
|
+
}
|
|
123
|
+
catch { /* lock cleanup is best effort */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function parseState(raw) {
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(raw);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_STATE_CORRUPT');
|
|
133
|
+
}
|
|
134
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
135
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_STATE_CORRUPT');
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
function readDocument(root) {
|
|
140
|
+
const path = join(root, MIGRATION_RELATIVE_PATH);
|
|
141
|
+
try {
|
|
142
|
+
recoverAtomicReplaceUtf8(path);
|
|
143
|
+
recoverAtomicReplaceUtf8(join(root, STATE_RELATIVE_PATH));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_ATOMIC_RECOVERY_FAILED');
|
|
147
|
+
}
|
|
148
|
+
if (!existsSync(path))
|
|
149
|
+
return null;
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_CORRUPT');
|
|
156
|
+
}
|
|
157
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
158
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_CORRUPT');
|
|
159
|
+
}
|
|
160
|
+
const document = parsed;
|
|
161
|
+
if (document.schemaVersion !== 1
|
|
162
|
+
|| typeof document.operationId !== 'string'
|
|
163
|
+
|| !/^mig_[a-f0-9]{32}$/.test(document.operationId)
|
|
164
|
+
|| typeof document.projectId !== 'string'
|
|
165
|
+
|| !PROJECT_ID_PATTERN.test(document.projectId)
|
|
166
|
+
|| !['prepared', 'remote-acknowledged', 'finalized'].includes(String(document.phase))
|
|
167
|
+
|| !Number.isSafeInteger(document.legacyEpicFloor)
|
|
168
|
+
|| (document.legacyEpicFloor ?? -1) < 0
|
|
169
|
+
|| typeof document.originalStateExists !== 'boolean'
|
|
170
|
+
|| typeof document.originalStateDigest !== 'string'
|
|
171
|
+
|| !/^[a-f0-9]{64}$/.test(document.originalStateDigest)
|
|
172
|
+
|| typeof document.preparedStateDigest !== 'string'
|
|
173
|
+
|| !/^[a-f0-9]{64}$/.test(document.preparedStateDigest)
|
|
174
|
+
|| (document.backupPath !== null
|
|
175
|
+
&& (typeof document.backupPath !== 'string' || !BACKUP_PATH_PATTERN.test(document.backupPath)))
|
|
176
|
+
|| typeof document.createdAt !== 'string'
|
|
177
|
+
|| typeof document.updatedAt !== 'string') {
|
|
178
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_CORRUPT');
|
|
179
|
+
}
|
|
180
|
+
if (document.receipt !== undefined)
|
|
181
|
+
assertProjectIdentityMigrationReceiptV1(document.receipt);
|
|
182
|
+
const phaseRequiresReceipt = document.phase === 'remote-acknowledged' || document.phase === 'finalized';
|
|
183
|
+
if ((document.phase === 'prepared' && document.receipt !== undefined)
|
|
184
|
+
|| (phaseRequiresReceipt && document.receipt === undefined)
|
|
185
|
+
|| (document.receipt !== undefined
|
|
186
|
+
&& (document.receipt.operationId !== document.operationId
|
|
187
|
+
|| document.receipt.projectId !== document.projectId
|
|
188
|
+
|| document.receipt.acceptedLegacyFloor < document.legacyEpicFloor))) {
|
|
189
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_CORRUPT');
|
|
190
|
+
}
|
|
191
|
+
return document;
|
|
192
|
+
}
|
|
193
|
+
function legacyEpicFloor(state) {
|
|
194
|
+
const candidates = [];
|
|
195
|
+
if (state.epics && typeof state.epics === 'object' && !Array.isArray(state.epics)) {
|
|
196
|
+
candidates.push(...Object.keys(state.epics));
|
|
197
|
+
}
|
|
198
|
+
const ledger = state.epic_id_ledger;
|
|
199
|
+
if (ledger && typeof ledger === 'object' && !Array.isArray(ledger)) {
|
|
200
|
+
const ids = ledger.reserved_ids;
|
|
201
|
+
if (Array.isArray(ids))
|
|
202
|
+
candidates.push(...ids.filter((value) => typeof value === 'string'));
|
|
203
|
+
}
|
|
204
|
+
return candidates.reduce((floor, candidate) => {
|
|
205
|
+
const match = candidate.match(/^P?(\d+)$/i);
|
|
206
|
+
if (!match)
|
|
207
|
+
return floor;
|
|
208
|
+
const value = Number(match[1]);
|
|
209
|
+
return Number.isSafeInteger(value) ? Math.max(floor, value) : floor;
|
|
210
|
+
}, 0);
|
|
211
|
+
}
|
|
212
|
+
function requestFor(document) {
|
|
213
|
+
return {
|
|
214
|
+
version: 1,
|
|
215
|
+
operationId: document.operationId,
|
|
216
|
+
projectId: document.projectId,
|
|
217
|
+
localStateDigest: document.preparedStateDigest,
|
|
218
|
+
legacyEpicFloor: document.legacyEpicFloor,
|
|
219
|
+
phase: 'seed',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function stateWithProjectIdentity(raw, identity) {
|
|
223
|
+
const state = parseState(raw);
|
|
224
|
+
const configKey = projectIdentityContainerKey(state);
|
|
225
|
+
const config = (state[configKey] ?? {});
|
|
226
|
+
state[configKey] = { ...config, project_id: identity };
|
|
227
|
+
if (state.schema_version === undefined)
|
|
228
|
+
state.schema_version = 1;
|
|
229
|
+
if (!state.stories || typeof state.stories !== 'object' || Array.isArray(state.stories))
|
|
230
|
+
state.stories = {};
|
|
231
|
+
if (!state.epics || typeof state.epics !== 'object' || Array.isArray(state.epics))
|
|
232
|
+
state.epics = {};
|
|
233
|
+
return { raw: `${JSON.stringify(state, null, 2)}\n`, state };
|
|
234
|
+
}
|
|
235
|
+
function projectIdentityContainerKey(state) {
|
|
236
|
+
return state.config && typeof state.config === 'object' && !Array.isArray(state.config)
|
|
237
|
+
? 'config'
|
|
238
|
+
: state.project && typeof state.project === 'object' && !Array.isArray(state.project)
|
|
239
|
+
? 'project'
|
|
240
|
+
: 'config';
|
|
241
|
+
}
|
|
242
|
+
function projectIdentityFromState(state) {
|
|
243
|
+
const container = state[projectIdentityContainerKey(state)];
|
|
244
|
+
return container && typeof container === 'object' && !Array.isArray(container)
|
|
245
|
+
? container.project_id
|
|
246
|
+
: undefined;
|
|
247
|
+
}
|
|
248
|
+
function assertFinalizedStateIdentity(statePath, document) {
|
|
249
|
+
if (!existsSync(statePath)) {
|
|
250
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_DRIFT');
|
|
251
|
+
}
|
|
252
|
+
const state = parseState(readFileSync(statePath, 'utf8'));
|
|
253
|
+
if (projectIdentityFromState(state) !== document.projectId) {
|
|
254
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_DRIFT');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function reconcileFinalizedStateIdentity(root, statePath, currentRaw, document) {
|
|
258
|
+
const state = parseState(currentRaw);
|
|
259
|
+
const currentProjectId = projectIdentityFromState(state);
|
|
260
|
+
if (currentProjectId === document.projectId)
|
|
261
|
+
return;
|
|
262
|
+
if (currentProjectId !== undefined) {
|
|
263
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_DRIFT');
|
|
264
|
+
}
|
|
265
|
+
const now = new Date();
|
|
266
|
+
const recoveryBackup = join(root, '.neocortex', `state.json.project-identity-reconcile-backup-${backupTimestamp(now)}-${operationId()}`);
|
|
267
|
+
durableCreateUtf8(recoveryBackup, currentRaw);
|
|
268
|
+
atomicReplaceUtf8(statePath, stateWithProjectIdentity(currentRaw, document.projectId).raw);
|
|
269
|
+
}
|
|
270
|
+
function defaultStateRaw() {
|
|
271
|
+
return `${JSON.stringify({
|
|
272
|
+
schema_version: 1,
|
|
273
|
+
config: { project_name: 'neocortex-project', default_branch: 'main', language: 'pt-BR' },
|
|
274
|
+
stories: {},
|
|
275
|
+
epics: {},
|
|
276
|
+
}, null, 2)}\n`;
|
|
277
|
+
}
|
|
278
|
+
function verifyBackup(root, document) {
|
|
279
|
+
if (!document.originalStateExists)
|
|
280
|
+
return;
|
|
281
|
+
if (!document.backupPath) {
|
|
282
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_BACKUP_MISSING');
|
|
283
|
+
}
|
|
284
|
+
const backup = join(root, document.backupPath);
|
|
285
|
+
if (!existsSync(backup)) {
|
|
286
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_BACKUP_MISSING');
|
|
287
|
+
}
|
|
288
|
+
const backupStat = lstatSync(backup);
|
|
289
|
+
if (backupStat.isSymbolicLink() || !backupStat.isFile()) {
|
|
290
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_SYMLINK_REJECTED');
|
|
291
|
+
}
|
|
292
|
+
if (sha256(readFileSync(backup)) !== document.originalStateDigest) {
|
|
293
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_BACKUP_CORRUPT');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export function prepareProjectIdentityMigration(projectRoot) {
|
|
297
|
+
const root = assertSafeProjectRoot(projectRoot);
|
|
298
|
+
return withMigrationLock(root, () => {
|
|
299
|
+
const existingDocument = readDocument(root);
|
|
300
|
+
if (existingDocument) {
|
|
301
|
+
const statePath = join(root, STATE_RELATIVE_PATH);
|
|
302
|
+
verifyBackup(root, existingDocument);
|
|
303
|
+
const stateExists = existsSync(statePath);
|
|
304
|
+
const currentRaw = stateExists ? readFileSync(statePath, 'utf8') : null;
|
|
305
|
+
const currentDigest = currentRaw === null ? null : sha256(currentRaw);
|
|
306
|
+
if (currentDigest === existingDocument.preparedStateDigest) {
|
|
307
|
+
return { changed: false, document: existingDocument, request: requestFor(existingDocument) };
|
|
308
|
+
}
|
|
309
|
+
if (existingDocument.phase === 'finalized' && currentRaw !== null) {
|
|
310
|
+
const currentProjectId = projectIdentityFromState(parseState(currentRaw));
|
|
311
|
+
if (currentProjectId === existingDocument.projectId) {
|
|
312
|
+
return { changed: false, document: existingDocument, request: requestFor(existingDocument) };
|
|
313
|
+
}
|
|
314
|
+
reconcileFinalizedStateIdentity(root, statePath, currentRaw, existingDocument);
|
|
315
|
+
return { changed: true, document: existingDocument, request: requestFor(existingDocument) };
|
|
316
|
+
}
|
|
317
|
+
const mayResumePrepare = existingDocument.phase === 'prepared'
|
|
318
|
+
&& ((stateExists && currentDigest === existingDocument.originalStateDigest)
|
|
319
|
+
|| (!stateExists && !existingDocument.originalStateExists));
|
|
320
|
+
if (!mayResumePrepare) {
|
|
321
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_DRIFT');
|
|
322
|
+
}
|
|
323
|
+
const originalRaw = stateExists ? readFileSync(statePath, 'utf8') : defaultStateRaw();
|
|
324
|
+
const recovered = stateWithProjectIdentity(originalRaw, existingDocument.projectId).raw;
|
|
325
|
+
if (sha256(recovered) !== existingDocument.preparedStateDigest) {
|
|
326
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_DRIFT');
|
|
327
|
+
}
|
|
328
|
+
atomicReplaceUtf8(statePath, recovered);
|
|
329
|
+
return { changed: true, document: existingDocument, request: requestFor(existingDocument) };
|
|
330
|
+
}
|
|
331
|
+
const statePath = join(root, STATE_RELATIVE_PATH);
|
|
332
|
+
const originalStateExists = existsSync(statePath);
|
|
333
|
+
const originalRaw = originalStateExists ? readFileSync(statePath, 'utf8') : defaultStateRaw();
|
|
334
|
+
const state = parseState(originalRaw);
|
|
335
|
+
const configKey = projectIdentityContainerKey(state);
|
|
336
|
+
const config = (state[configKey] ?? {});
|
|
337
|
+
const currentProjectId = config.project_id;
|
|
338
|
+
const nextProjectId = typeof currentProjectId === 'string' && PROJECT_ID_PATTERN.test(currentProjectId)
|
|
339
|
+
? currentProjectId
|
|
340
|
+
: projectId();
|
|
341
|
+
const preparedState = stateWithProjectIdentity(originalRaw, nextProjectId);
|
|
342
|
+
const nextRaw = preparedState.raw;
|
|
343
|
+
const nextOperationId = operationId();
|
|
344
|
+
const nowDate = new Date();
|
|
345
|
+
const backupPath = originalStateExists
|
|
346
|
+
? join('.neocortex', `state.json.project-identity-backup-${backupTimestamp(nowDate)}-${nextOperationId}`)
|
|
347
|
+
: null;
|
|
348
|
+
if (backupPath) {
|
|
349
|
+
durableCreateUtf8(join(root, backupPath), originalRaw);
|
|
350
|
+
}
|
|
351
|
+
const now = nowDate.toISOString();
|
|
352
|
+
const document = {
|
|
353
|
+
schemaVersion: 1,
|
|
354
|
+
operationId: nextOperationId,
|
|
355
|
+
projectId: nextProjectId,
|
|
356
|
+
phase: 'prepared',
|
|
357
|
+
legacyEpicFloor: legacyEpicFloor(preparedState.state),
|
|
358
|
+
originalStateExists,
|
|
359
|
+
originalStateDigest: sha256(originalRaw),
|
|
360
|
+
preparedStateDigest: sha256(nextRaw),
|
|
361
|
+
backupPath,
|
|
362
|
+
createdAt: now,
|
|
363
|
+
updatedAt: now,
|
|
364
|
+
};
|
|
365
|
+
// Persist the recovery manifest before replacing state. If the process dies
|
|
366
|
+
// in this window, the next prepare deterministically applies the same ID.
|
|
367
|
+
atomicReplaceUtf8(join(root, MIGRATION_RELATIVE_PATH), `${JSON.stringify(document, null, 2)}\n`);
|
|
368
|
+
atomicReplaceUtf8(statePath, nextRaw);
|
|
369
|
+
return { changed: nextRaw !== originalRaw, document, request: requestFor(document) };
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
export function finalizeProjectIdentityMigration(projectRoot, receipt) {
|
|
373
|
+
assertProjectIdentityMigrationReceiptV1(receipt);
|
|
374
|
+
const root = assertSafeProjectRoot(projectRoot);
|
|
375
|
+
return withMigrationLock(root, () => {
|
|
376
|
+
const document = readDocument(root);
|
|
377
|
+
if (!document)
|
|
378
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_NOT_PREPARED');
|
|
379
|
+
if (document.operationId !== receipt.operationId || document.projectId !== receipt.projectId) {
|
|
380
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_RECEIPT_CONFLICT');
|
|
381
|
+
}
|
|
382
|
+
const statePath = join(root, STATE_RELATIVE_PATH);
|
|
383
|
+
if (document.phase === 'finalized') {
|
|
384
|
+
if (!document.receipt || document.receipt.receiptDigest !== receipt.receiptDigest) {
|
|
385
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_ACK_REQUIRED');
|
|
386
|
+
}
|
|
387
|
+
assertFinalizedStateIdentity(statePath, document);
|
|
388
|
+
return document;
|
|
389
|
+
}
|
|
390
|
+
if (!existsSync(statePath) || sha256(readFileSync(statePath)) !== document.preparedStateDigest) {
|
|
391
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_DRIFT');
|
|
392
|
+
}
|
|
393
|
+
if (document.phase === 'prepared' || !document.receipt
|
|
394
|
+
|| document.receipt.receiptDigest !== receipt.receiptDigest) {
|
|
395
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_ACK_REQUIRED');
|
|
396
|
+
}
|
|
397
|
+
const next = {
|
|
398
|
+
...document,
|
|
399
|
+
phase: 'finalized',
|
|
400
|
+
updatedAt: new Date().toISOString(),
|
|
401
|
+
receipt,
|
|
402
|
+
};
|
|
403
|
+
atomicReplaceUtf8(join(root, MIGRATION_RELATIVE_PATH), `${JSON.stringify(next, null, 2)}\n`);
|
|
404
|
+
return next;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/** Persist the authenticated ACK before local finalization so a crash is reconcilable. */
|
|
408
|
+
export function acknowledgeProjectIdentityMigration(projectRoot, receipt) {
|
|
409
|
+
assertProjectIdentityMigrationReceiptV1(receipt);
|
|
410
|
+
const root = assertSafeProjectRoot(projectRoot);
|
|
411
|
+
return withMigrationLock(root, () => {
|
|
412
|
+
const document = readDocument(root);
|
|
413
|
+
if (!document)
|
|
414
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_NOT_PREPARED');
|
|
415
|
+
if (document.operationId !== receipt.operationId || document.projectId !== receipt.projectId) {
|
|
416
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_RECEIPT_CONFLICT');
|
|
417
|
+
}
|
|
418
|
+
if (document.receipt && document.receipt.receiptDigest !== receipt.receiptDigest) {
|
|
419
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_RECEIPT_CONFLICT');
|
|
420
|
+
}
|
|
421
|
+
const statePath = join(root, STATE_RELATIVE_PATH);
|
|
422
|
+
if (document.phase === 'finalized') {
|
|
423
|
+
assertFinalizedStateIdentity(statePath, document);
|
|
424
|
+
return document;
|
|
425
|
+
}
|
|
426
|
+
if (!existsSync(statePath) || sha256(readFileSync(statePath)) !== document.preparedStateDigest) {
|
|
427
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_DRIFT');
|
|
428
|
+
}
|
|
429
|
+
if (document.phase === 'remote-acknowledged')
|
|
430
|
+
return document;
|
|
431
|
+
const next = {
|
|
432
|
+
...document,
|
|
433
|
+
phase: 'remote-acknowledged',
|
|
434
|
+
updatedAt: new Date().toISOString(),
|
|
435
|
+
receipt,
|
|
436
|
+
};
|
|
437
|
+
atomicReplaceUtf8(join(root, MIGRATION_RELATIVE_PATH), `${JSON.stringify(next, null, 2)}\n`);
|
|
438
|
+
return next;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
export function rollbackPreparedProjectIdentityMigration(projectRoot) {
|
|
442
|
+
const root = assertSafeProjectRoot(projectRoot);
|
|
443
|
+
withMigrationLock(root, () => {
|
|
444
|
+
const document = readDocument(root);
|
|
445
|
+
if (!document)
|
|
446
|
+
return;
|
|
447
|
+
if (document.phase !== 'prepared' || document.receipt) {
|
|
448
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_ROLLBACK_AFTER_ACK_DENIED');
|
|
449
|
+
}
|
|
450
|
+
const statePath = join(root, STATE_RELATIVE_PATH);
|
|
451
|
+
if (!existsSync(statePath) || sha256(readFileSync(statePath)) !== document.preparedStateDigest) {
|
|
452
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_STATE_DRIFT');
|
|
453
|
+
}
|
|
454
|
+
if (document.originalStateExists) {
|
|
455
|
+
if (!document.backupPath)
|
|
456
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_BACKUP_MISSING');
|
|
457
|
+
const backup = join(root, document.backupPath);
|
|
458
|
+
if (!existsSync(backup))
|
|
459
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_BACKUP_MISSING');
|
|
460
|
+
const backupStat = lstatSync(backup);
|
|
461
|
+
if (backupStat.isSymbolicLink() || !backupStat.isFile()) {
|
|
462
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_SYMLINK_REJECTED');
|
|
463
|
+
}
|
|
464
|
+
const originalRaw = readFileSync(backup, 'utf8');
|
|
465
|
+
if (sha256(originalRaw) !== document.originalStateDigest) {
|
|
466
|
+
throw new ProjectIdentityMigrationError('PROJECT_IDENTITY_MIGRATION_BACKUP_CORRUPT');
|
|
467
|
+
}
|
|
468
|
+
atomicReplaceUtf8(statePath, originalRaw);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
unlinkSync(statePath);
|
|
472
|
+
fsyncDirectoryBestEffort(dirname(statePath));
|
|
473
|
+
}
|
|
474
|
+
const migrationPath = join(root, MIGRATION_RELATIVE_PATH);
|
|
475
|
+
unlinkSync(migrationPath);
|
|
476
|
+
fsyncDirectoryBestEffort(dirname(migrationPath));
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Read-only admission check used to prevent create-epic from observing the
|
|
481
|
+
* locally prepared project ID before its remote floor has been acknowledged.
|
|
482
|
+
*/
|
|
483
|
+
export function readProjectIdentityMigrationPhase(projectRoot) {
|
|
484
|
+
const root = assertSafeProjectRoot(projectRoot);
|
|
485
|
+
return readDocument(root)?.phase ?? null;
|
|
486
|
+
}
|
|
487
|
+
export function projectIdentityMigrationRelativePaths(projectRoot) {
|
|
488
|
+
const root = assertSafeProjectRoot(projectRoot);
|
|
489
|
+
return {
|
|
490
|
+
state: relative(root, join(root, STATE_RELATIVE_PATH)),
|
|
491
|
+
migration: relative(root, join(root, MIGRATION_RELATIVE_PATH)),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
@@ -18,6 +18,13 @@ export interface StateMigrationAuthorityV1 {
|
|
|
18
18
|
readonly plan: Omit<StateMigrationPlanV1, 'targetState'> | null;
|
|
19
19
|
readonly publicSafe: true;
|
|
20
20
|
}
|
|
21
|
+
/** Private, complete source used by the P224 brownfield planner. */
|
|
22
|
+
export interface StateMigrationSourceSnapshotV1 {
|
|
23
|
+
readonly state: Readonly<Record<string, unknown>>;
|
|
24
|
+
readonly raw: string;
|
|
25
|
+
readonly byteDigest: string;
|
|
26
|
+
readonly byteLength: number;
|
|
27
|
+
}
|
|
21
28
|
export declare class StateMigrationApplyError extends Error {
|
|
22
29
|
readonly reasonCode: string;
|
|
23
30
|
readonly receipt?: StateMigrationReceiptV1 | undefined;
|
|
@@ -29,6 +36,21 @@ export interface StateMigrationApplyHooks {
|
|
|
29
36
|
readonly afterReplace?: (statePath: string) => void;
|
|
30
37
|
}
|
|
31
38
|
export declare function collectStateMigrationAuthority(projectRoot: string): StateMigrationAuthorityV1;
|
|
39
|
+
export declare function readStateMigrationSourceSnapshot(projectRoot: string): StateMigrationSourceSnapshotV1;
|
|
40
|
+
/**
|
|
41
|
+
* Restore one migration preimage only while both current postimage and backup
|
|
42
|
+
* bytes remain exact. This is intentionally narrower than a general reset and
|
|
43
|
+
* never removes the backup or migration history.
|
|
44
|
+
*/
|
|
45
|
+
export declare function restoreStateMigrationPreimage(input: {
|
|
46
|
+
readonly projectRoot: string;
|
|
47
|
+
readonly operationId: string;
|
|
48
|
+
readonly expectedCurrentDigest: string;
|
|
49
|
+
readonly expectedBackupDigest: string;
|
|
50
|
+
}): {
|
|
51
|
+
readonly restoredDigest: string;
|
|
52
|
+
readonly backupPreserved: true;
|
|
53
|
+
};
|
|
32
54
|
export declare function previewStateMigration(projectRoot: string): StateMigrationPlanV1 | null;
|
|
33
55
|
/** Apply one shared plan under a client-owned lock, byte CAS and durable backup. */
|
|
34
56
|
export declare function applyStateMigration(projectRoot: string, expectedPlan?: Pick<StateMigrationPlanV1, 'operationId' | 'sourceCanonicalHash' | 'targetCanonicalHash'> & {
|