@ornexus/neocortex 4.60.71 → 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 +1211 -1217
- 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,5 +1,462 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/** Root-owned P03 transition after review of an isolated, admitted generation candidate. */
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
+
import { planningPendingSidecarDigest, planningPhysicalPatchDigest } from './planning-diagnosis-checkpoint.js';
|
|
8
|
+
import { acceptedPlanningResearchPredecessor } from './planning-research-checkpoint.js';
|
|
9
|
+
const HEX = /^[a-f0-9]{64}$/;
|
|
10
|
+
const EPIC = /^P[1-9][0-9]{0,11}$/;
|
|
11
|
+
const CLAIM = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
12
|
+
const P01_STEP_DIGEST = 'acd455ed0e39d98b90fb57391db75e366e4c5604db043e03a89a20e33d643c3c';
|
|
13
|
+
const P02_STEP_DIGEST = 'fa2194f89c01f07e225fd6f39e98e679f628e072239b6cd0af3ee527f0722518';
|
|
14
|
+
const P03_STEP_DIGEST = '8636f22ec5950410902448ee1e9cb4947359ac98df518742bddbbb367468e3ae';
|
|
15
|
+
function record(value) {
|
|
16
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function digest(value) {
|
|
19
|
+
return createHash('sha256').update(value).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
function git(root, args, maxBuffer = 128 * 1024) {
|
|
22
|
+
return execFileSync('git', [...args], { cwd: root, encoding: 'utf8',
|
|
23
|
+
stdio: ['ignore', 'pipe', 'ignore'], maxBuffer }).trim();
|
|
24
|
+
}
|
|
25
|
+
function regularBytes(root, path, maxBytes) {
|
|
26
|
+
const entry = lstatSync(join(root, path));
|
|
27
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size < 1 || entry.size > maxBytes) {
|
|
28
|
+
throw new Error('planning-generation-physical-artifact-invalid');
|
|
29
|
+
}
|
|
30
|
+
return readFileSync(join(root, path));
|
|
31
|
+
}
|
|
32
|
+
function committedBytes(root, path, maxBytes, revision = 'HEAD') {
|
|
33
|
+
const result = spawnSync('git', ['show', `${revision}:${path}`], { cwd: root,
|
|
34
|
+
encoding: 'buffer', maxBuffer: maxBytes });
|
|
35
|
+
if (result.status !== 0 || !Buffer.isBuffer(result.stdout)
|
|
36
|
+
|| result.stdout.length < 1 || result.stdout.length > maxBytes) {
|
|
37
|
+
throw new Error('planning-generation-predecessor-uncommitted');
|
|
38
|
+
}
|
|
39
|
+
return result.stdout;
|
|
40
|
+
}
|
|
41
|
+
function safeParents(root, epicId) {
|
|
42
|
+
for (const relativePath of ['.neocortex', '.neocortex/planning',
|
|
43
|
+
`.neocortex/planning/epic-${epicId}`, 'docs', 'docs/epics', 'docs/stories']) {
|
|
44
|
+
const entry = lstatSync(join(root, relativePath));
|
|
45
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()
|
|
46
|
+
|| entry.uid !== process.getuid?.()
|
|
47
|
+
|| relativePath === '.neocortex' && (entry.mode & 0o022) !== 0) {
|
|
48
|
+
throw new Error('planning-generation-directory-boundary-invalid');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function scalar(content, key) {
|
|
53
|
+
const header = /^---\n([\s\S]{1,8192}?)\n---\n/.exec(content.replace(/\r\n/g, '\n'))?.[1];
|
|
54
|
+
const fields = header?.split('\n').filter((line) => line.startsWith(`${key}:`)) ?? [];
|
|
55
|
+
return fields.length === 1 ? fields[0].slice(key.length + 1).trim()
|
|
56
|
+
.replace(/^(?:"(.*)"|'(.*)')$/, '$1$2') : undefined;
|
|
57
|
+
}
|
|
58
|
+
function stepMatches(value, expected) {
|
|
59
|
+
return typeof value === 'string' && value.length <= 128 && digest(value) === expected;
|
|
60
|
+
}
|
|
61
|
+
function storyPaths(epicId, count) {
|
|
62
|
+
return Array.from({ length: count }, (_, index) => `docs/stories/${epicId}.${String(index + 1).padStart(2, '0')}.story.md`);
|
|
63
|
+
}
|
|
64
|
+
function validateReview(value) {
|
|
65
|
+
if (!record(value) || Object.keys(value).sort().join('|') !== [
|
|
66
|
+
'schemaVersion', 'verdict', 'claimId', 'epicId', 'diagnosisReceiptId',
|
|
67
|
+
'researchReceiptId', 'intakeRevision', 'sourceHead', 'sourceBranch',
|
|
68
|
+
'sourceStateSha256', 'admission', 'artifactDigests', 'qaEvidenceId', 'artifactSummary',
|
|
69
|
+
].sort().join('|') || value.schemaVersion !== 'PlanningGenerationRootReview/1'
|
|
70
|
+
|| value.verdict !== 'accepted' || typeof value.claimId !== 'string' || !CLAIM.test(value.claimId)
|
|
71
|
+
|| typeof value.epicId !== 'string' || !EPIC.test(value.epicId)
|
|
72
|
+
|| typeof value.diagnosisReceiptId !== 'string' || !HEX.test(value.diagnosisReceiptId)
|
|
73
|
+
|| typeof value.researchReceiptId !== 'string' || !HEX.test(value.researchReceiptId)
|
|
74
|
+
|| typeof value.intakeRevision !== 'string' || !HEX.test(value.intakeRevision)
|
|
75
|
+
|| typeof value.sourceHead !== 'string' || !/^[a-f0-9]{40}$/.test(value.sourceHead)
|
|
76
|
+
|| typeof value.sourceBranch !== 'string'
|
|
77
|
+
|| !/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(value.sourceBranch)
|
|
78
|
+
|| typeof value.sourceStateSha256 !== 'string' || !HEX.test(value.sourceStateSha256)
|
|
79
|
+
|| !record(value.admission) || Object.keys(value.admission).sort().join('|') !== [
|
|
80
|
+
'schemaVersion', 'outcome', 'baseSha', 'executionBranch', 'changedPaths', 'patchSha256',
|
|
81
|
+
].sort().join('|') || value.admission.schemaVersion !== 1
|
|
82
|
+
|| value.admission.outcome !== 'admitted-unverified'
|
|
83
|
+
|| value.admission.baseSha !== value.sourceHead
|
|
84
|
+
|| value.admission.executionBranch !== value.sourceBranch
|
|
85
|
+
|| typeof value.admission.patchSha256 !== 'string' || !HEX.test(value.admission.patchSha256)
|
|
86
|
+
|| !record(value.artifactDigests)
|
|
87
|
+
|| Object.keys(value.artifactDigests).sort().join('|') !== 'epic|index|stories'
|
|
88
|
+
|| typeof value.artifactDigests.epic !== 'string' || !HEX.test(value.artifactDigests.epic)
|
|
89
|
+
|| typeof value.artifactDigests.index !== 'string' || !HEX.test(value.artifactDigests.index)
|
|
90
|
+
|| !record(value.artifactDigests.stories)
|
|
91
|
+
|| Object.keys(value.artifactDigests.stories).length < 1
|
|
92
|
+
|| Object.keys(value.artifactDigests.stories).length > 24
|
|
93
|
+
|| Object.values(value.artifactDigests.stories).some((entry) => typeof entry !== 'string' || !HEX.test(entry))
|
|
94
|
+
|| typeof value.qaEvidenceId !== 'string' || !/^qa-[a-z0-9-]{8,80}$/.test(value.qaEvidenceId)
|
|
95
|
+
|| typeof value.artifactSummary !== 'string' || value.artifactSummary.length < 16
|
|
96
|
+
|| value.artifactSummary.length > 2048) {
|
|
97
|
+
throw new Error('planning-generation-review-invalid');
|
|
98
|
+
}
|
|
99
|
+
const stories = storyPaths(value.epicId, Object.keys(value.artifactDigests.stories).length);
|
|
100
|
+
if (!isDeepStrictEqual(Object.keys(value.artifactDigests.stories).sort(), [...stories].sort())) {
|
|
101
|
+
throw new Error('planning-generation-review-invalid');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function physicalAcceptance(review) {
|
|
105
|
+
const fields = { schemaVersion: 'PlanningGenerationPhysicalAcceptance/1',
|
|
106
|
+
claimId: review.claimId, epicId: review.epicId,
|
|
107
|
+
diagnosisReceiptId: review.diagnosisReceiptId, researchReceiptId: review.researchReceiptId,
|
|
108
|
+
intakeRevision: review.intakeRevision, qaEvidenceId: review.qaEvidenceId,
|
|
109
|
+
sourceHead: review.sourceHead, sourceStateSha256: review.sourceStateSha256,
|
|
110
|
+
admissionPatchSha256: review.admission.patchSha256,
|
|
111
|
+
artifactDigests: review.artifactDigests };
|
|
112
|
+
return { ...fields, receiptId: digest(JSON.stringify(fields)), publicSafe: true };
|
|
113
|
+
}
|
|
114
|
+
function evidence(root, review, checkPatch, ownTemporaryPath, sourceStateOverride) {
|
|
115
|
+
safeParents(root, review.epicId);
|
|
116
|
+
if (git(root, ['rev-parse', 'HEAD']) !== review.sourceHead
|
|
117
|
+
|| git(root, ['symbolic-ref', '--quiet', '--short', 'HEAD']) !== review.sourceBranch) {
|
|
118
|
+
throw new Error('planning-generation-head-conflict');
|
|
119
|
+
}
|
|
120
|
+
const prefix = `.neocortex/planning/epic-${review.epicId}`;
|
|
121
|
+
const epicPath = `docs/epics/epic-${review.epicId}.md`;
|
|
122
|
+
const indexPath = '.neocortex/planning/planning.json';
|
|
123
|
+
const stories = storyPaths(review.epicId, Object.keys(review.artifactDigests.stories).length);
|
|
124
|
+
const allowed = { index: indexPath, epic: epicPath,
|
|
125
|
+
...Object.fromEntries(stories.map((path, index) => [`story${index}`, path])) };
|
|
126
|
+
if (!Array.isArray(review.admission.changedPaths)
|
|
127
|
+
|| !isDeepStrictEqual([...review.admission.changedPaths].sort(), Object.values(allowed).sort())
|
|
128
|
+
|| checkPatch && planningPhysicalPatchDigest(root, allowed, ownTemporaryPath, true)
|
|
129
|
+
!== review.admission.patchSha256) {
|
|
130
|
+
throw new Error('planning-generation-admission-patch-conflict');
|
|
131
|
+
}
|
|
132
|
+
const indexBytes = regularBytes(root, indexPath, 128 * 1024);
|
|
133
|
+
const epicBytes = regularBytes(root, epicPath, 512 * 1024);
|
|
134
|
+
const storyBytes = stories.map((path) => regularBytes(root, path, 128 * 1024));
|
|
135
|
+
if (digest(indexBytes) !== review.artifactDigests.index
|
|
136
|
+
|| digest(epicBytes) !== review.artifactDigests.epic
|
|
137
|
+
|| stories.some((path, index) => digest(storyBytes[index]) !== review.artifactDigests.stories[path])) {
|
|
138
|
+
throw new Error('planning-generation-artifact-conflict');
|
|
139
|
+
}
|
|
140
|
+
const epicText = epicBytes.toString('utf8');
|
|
141
|
+
const epicTitle = scalar(epicText, 'title');
|
|
142
|
+
if (scalar(epicText, 'id') !== review.epicId
|
|
143
|
+
|| scalar(epicText, 'stories_count') !== String(stories.length)
|
|
144
|
+
|| !epicTitle || epicTitle.length > 200 || !epicText.includes('## Stories')) {
|
|
145
|
+
throw new Error('planning-generation-epic-invalid');
|
|
146
|
+
}
|
|
147
|
+
for (const [index, bytes] of storyBytes.entries()) {
|
|
148
|
+
const text = bytes.toString('utf8');
|
|
149
|
+
const header = /^---\n([\s\S]{1,8192}?)\n---\n/.exec(text.replace(/\r\n/g, '\n'))?.[1] ?? '';
|
|
150
|
+
const canonicalEpic = scalar(text, 'epic_id');
|
|
151
|
+
// Old accepted receipts keep their original bytes on replay. New seals
|
|
152
|
+
// require the canonical field consumed by story-context collection.
|
|
153
|
+
const epicMatches = canonicalEpic === review.epicId && !/^\s*epic\s*:/m.test(header)
|
|
154
|
+
|| !checkPatch && canonicalEpic === undefined && !/^\s*epic_id\s*:/m.test(header)
|
|
155
|
+
&& scalar(text, 'epic') === review.epicId;
|
|
156
|
+
if (scalar(text, 'id') !== `${review.epicId}.${String(index + 1).padStart(2, '0')}`
|
|
157
|
+
|| !epicMatches || scalar(text, 'status') !== 'backlog'
|
|
158
|
+
|| !scalar(text, 'title') || !text.includes('## Acceptance Criteria')
|
|
159
|
+
|| !/\bGiven\b[\s\S]*\bWhen\b[\s\S]*\bThen\b/.test(text)) {
|
|
160
|
+
throw new Error('planning-generation-story-invalid');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const stateRaw = sourceStateOverride ?? regularBytes(root, '.neocortex/state.json', 8 * 1024 * 1024);
|
|
164
|
+
const state = JSON.parse(stateRaw.toString('utf8'));
|
|
165
|
+
if (!record(state) || !record(state.epics) || !record(state.stories)) {
|
|
166
|
+
throw new Error('planning-generation-state-invalid');
|
|
167
|
+
}
|
|
168
|
+
const predecessor = acceptedPlanningResearchPredecessor(state, review.claimId, review.epicId);
|
|
169
|
+
if (predecessor.diagnosisReceiptId !== review.diagnosisReceiptId
|
|
170
|
+
|| predecessor.researchReceiptId !== review.researchReceiptId
|
|
171
|
+
|| predecessor.intakeRevision !== review.intakeRevision
|
|
172
|
+
|| checkPatch && digest(stateRaw) !== review.sourceStateSha256) {
|
|
173
|
+
throw new Error('planning-generation-state-conflict');
|
|
174
|
+
}
|
|
175
|
+
const priorEpic = state.epics[review.epicId];
|
|
176
|
+
if (!record(priorEpic) || stories.some((path, index) => Object.hasOwn(state.stories, `${review.epicId}.${String(index + 1).padStart(2, '0')}`))) {
|
|
177
|
+
throw new Error('planning-generation-story-collision');
|
|
178
|
+
}
|
|
179
|
+
const p01 = priorEpic.createEpicPlanningPhysical;
|
|
180
|
+
const p02 = priorEpic.createEpicPlanningResearchPhysical;
|
|
181
|
+
if (!record(p01) || !record(p02) || !record(p01.artifactDigests) || !record(p02.artifactDigests)) {
|
|
182
|
+
throw new Error('planning-generation-predecessor-invalid');
|
|
183
|
+
}
|
|
184
|
+
for (const [name, path] of Object.entries({ idea: `${prefix}/idea.json`,
|
|
185
|
+
diagnosis: `${prefix}/diagnosis.md`, enrichment: `${prefix}/enriched-prompt.md`,
|
|
186
|
+
research: `${prefix}/research.md` })) {
|
|
187
|
+
const expected = name === 'research' ? p02.artifactDigests.research
|
|
188
|
+
: p01.artifactDigests[name];
|
|
189
|
+
const current = regularBytes(root, path, name === 'idea' ? 32 * 1024 : 256 * 1024);
|
|
190
|
+
if (digest(current) !== expected || !current.equals(committedBytes(root, path, 512 * 1024))) {
|
|
191
|
+
throw new Error('planning-generation-predecessor-artifact-drift');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const baseline = committedBytes(root, indexPath, 128 * 1024);
|
|
195
|
+
if (digest(baseline) !== p02.artifactDigests.index) {
|
|
196
|
+
throw new Error('planning-generation-predecessor-index-drift');
|
|
197
|
+
}
|
|
198
|
+
const prior = JSON.parse(baseline.toString('utf8'));
|
|
199
|
+
const next = JSON.parse(indexBytes.toString('utf8'));
|
|
200
|
+
if (!record(prior) || !record(prior.epics) || !record(next) || !record(next.epics)) {
|
|
201
|
+
throw new Error('planning-generation-index-invalid');
|
|
202
|
+
}
|
|
203
|
+
const previous = prior.epics[review.epicId];
|
|
204
|
+
const generated = next.epics[review.epicId];
|
|
205
|
+
if (!record(previous) || previous.id !== review.epicId || previous.status !== 'researched'
|
|
206
|
+
|| !Array.isArray(previous.steps_completed) || previous.steps_completed.length !== 2
|
|
207
|
+
|| !stepMatches(previous.steps_completed[0], P01_STEP_DIGEST)
|
|
208
|
+
|| !stepMatches(previous.steps_completed[1], P02_STEP_DIGEST)
|
|
209
|
+
|| !isDeepStrictEqual(previous.output_files, { diagnosis: `${prefix}/diagnosis.md`,
|
|
210
|
+
research: `${prefix}/research.md` }) || !record(generated)
|
|
211
|
+
|| !Array.isArray(generated.steps_completed)
|
|
212
|
+
|| !stepMatches(generated.steps_completed[2], P03_STEP_DIGEST)) {
|
|
213
|
+
throw new Error('planning-generation-index-predecessor-invalid');
|
|
214
|
+
}
|
|
215
|
+
const expected = { ...prior, last_scan: next.last_scan, epics: { ...prior.epics,
|
|
216
|
+
[review.epicId]: { ...previous, status: 'generated',
|
|
217
|
+
steps_completed: [...previous.steps_completed, generated.steps_completed[2]],
|
|
218
|
+
completed_at: generated.completed_at,
|
|
219
|
+
output_files: { ...previous.output_files,
|
|
220
|
+
epic: epicPath, stories } } } };
|
|
221
|
+
if (typeof next.last_scan !== 'string' || !Number.isFinite(Date.parse(next.last_scan))
|
|
222
|
+
|| typeof generated.completed_at !== 'string'
|
|
223
|
+
|| !Number.isFinite(Date.parse(generated.completed_at))
|
|
224
|
+
|| !isDeepStrictEqual(next, expected)) {
|
|
225
|
+
throw new Error('planning-generation-index-transition-invalid');
|
|
226
|
+
}
|
|
227
|
+
return { stateRaw, state, title: epicTitle, stories, storyBytes,
|
|
228
|
+
completedAt: generated.completed_at };
|
|
229
|
+
}
|
|
230
|
+
function projectState(before, review, receipt) {
|
|
231
|
+
const state = before.state;
|
|
232
|
+
const storyIds = before.stories.map((path) => path.slice('docs/stories/'.length, -'.story.md'.length));
|
|
233
|
+
const epic = state.epics[review.epicId];
|
|
234
|
+
const stories = { ...state.stories };
|
|
235
|
+
for (const [index, id] of storyIds.entries()) {
|
|
236
|
+
stories[id] = { id, epic_id: review.epicId,
|
|
237
|
+
title: scalar(before.storyBytes[index].toString('utf8'), 'title'), status: 'backlog' };
|
|
238
|
+
}
|
|
239
|
+
return { ...state, epics: { ...state.epics, [review.epicId]: { ...epic,
|
|
240
|
+
title: before.title, status: 'generated', stories: storyIds,
|
|
241
|
+
total_stories: storyIds.length, completed_stories: 0,
|
|
242
|
+
createEpicPlanningGenerationPhysical: receipt } },
|
|
243
|
+
stories, last_updated: before.completedAt };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Reopen an already sealed and committed P03 before root memory work. This is
|
|
247
|
+
* physical consistency verification, not new QA, a claim or terminal authority.
|
|
248
|
+
* The sealer intentionally stays bound to its original pre-commit HEAD.
|
|
249
|
+
*/
|
|
250
|
+
export function verifyCommittedPlanningGeneration(projectRoot, claimId, epicId) {
|
|
251
|
+
const invalid = () => new Error('planning-generation-committed-evidence-invalid');
|
|
252
|
+
const root = resolve(projectRoot);
|
|
253
|
+
if (!CLAIM.test(claimId) || !EPIC.test(epicId) || realpathSync(root) !== root
|
|
254
|
+
|| git(root, ['rev-parse', '--show-toplevel']) !== root)
|
|
255
|
+
throw invalid();
|
|
256
|
+
safeParents(root, epicId);
|
|
257
|
+
const verifiedHead = git(root, ['rev-parse', 'HEAD']);
|
|
258
|
+
const stateRaw = regularBytes(root, '.neocortex/state.json', 8 * 1024 * 1024);
|
|
259
|
+
if (!stateRaw.equals(committedBytes(root, '.neocortex/state.json', 8 * 1024 * 1024)))
|
|
260
|
+
throw invalid();
|
|
261
|
+
const current = JSON.parse(stateRaw.toString('utf8'));
|
|
262
|
+
const epic = record(current) && record(current.epics) ? current.epics[epicId] : undefined;
|
|
263
|
+
const stored = record(epic) ? epic.createEpicPlanningGenerationPhysical : undefined;
|
|
264
|
+
if (!record(stored) || stored.schemaVersion !== 'PlanningGenerationPhysicalAcceptance/1'
|
|
265
|
+
|| stored.claimId !== claimId || stored.epicId !== epicId
|
|
266
|
+
|| typeof stored.sourceHead !== 'string' || !/^[a-f0-9]{40}$/.test(stored.sourceHead)
|
|
267
|
+
|| typeof stored.qaEvidenceId !== 'string' || !/^qa-[a-z0-9-]{8,80}$/.test(stored.qaEvidenceId)
|
|
268
|
+
|| ['diagnosisReceiptId', 'researchReceiptId', 'intakeRevision', 'sourceStateSha256',
|
|
269
|
+
'admissionPatchSha256'].some(key => typeof stored[key] !== 'string' || !HEX.test(stored[key]))
|
|
270
|
+
|| !record(stored.artifactDigests)
|
|
271
|
+
|| Object.keys(stored.artifactDigests).sort().join('|') !== 'epic|index|stories'
|
|
272
|
+
|| typeof stored.artifactDigests.epic !== 'string' || !HEX.test(stored.artifactDigests.epic)
|
|
273
|
+
|| typeof stored.artifactDigests.index !== 'string' || !HEX.test(stored.artifactDigests.index)
|
|
274
|
+
|| !record(stored.artifactDigests.stories))
|
|
275
|
+
throw invalid();
|
|
276
|
+
const count = Object.keys(stored.artifactDigests.stories).length;
|
|
277
|
+
const stories = storyPaths(epicId, count);
|
|
278
|
+
if (count < 1 || count > 24
|
|
279
|
+
|| !isDeepStrictEqual(Object.keys(stored.artifactDigests.stories).sort(), [...stories].sort())
|
|
280
|
+
|| Object.values(stored.artifactDigests.stories).some(value => typeof value !== 'string' || !HEX.test(value))) {
|
|
281
|
+
throw invalid();
|
|
282
|
+
}
|
|
283
|
+
const fields = { schemaVersion: 'PlanningGenerationPhysicalAcceptance/1',
|
|
284
|
+
claimId, epicId, diagnosisReceiptId: stored.diagnosisReceiptId,
|
|
285
|
+
researchReceiptId: stored.researchReceiptId, intakeRevision: stored.intakeRevision,
|
|
286
|
+
qaEvidenceId: stored.qaEvidenceId, sourceHead: stored.sourceHead,
|
|
287
|
+
sourceStateSha256: stored.sourceStateSha256,
|
|
288
|
+
admissionPatchSha256: stored.admissionPatchSha256,
|
|
289
|
+
// The original sealer hashes this closed object's reviewed insertion order.
|
|
290
|
+
artifactDigests: stored.artifactDigests };
|
|
291
|
+
const receipt = { ...fields, receiptId: digest(JSON.stringify(fields)), publicSafe: true };
|
|
292
|
+
if (!isDeepStrictEqual(stored, receipt))
|
|
293
|
+
throw invalid();
|
|
294
|
+
const ancestry = spawnSync('git', ['merge-base', '--is-ancestor', stored.sourceHead, 'HEAD'], { cwd: root, stdio: 'ignore' });
|
|
295
|
+
if (ancestry.status !== 0)
|
|
296
|
+
throw invalid();
|
|
297
|
+
const baselineRaw = committedBytes(root, '.neocortex/state.json', 8 * 1024 * 1024, stored.sourceHead);
|
|
298
|
+
if (digest(baselineRaw) !== stored.sourceStateSha256)
|
|
299
|
+
throw invalid();
|
|
300
|
+
const baseline = JSON.parse(baselineRaw.toString('utf8'));
|
|
301
|
+
const predecessor = acceptedPlanningResearchPredecessor(baseline, claimId, epicId);
|
|
302
|
+
if (predecessor.diagnosisReceiptId !== stored.diagnosisReceiptId
|
|
303
|
+
|| predecessor.researchReceiptId !== stored.researchReceiptId
|
|
304
|
+
|| predecessor.intakeRevision !== stored.intakeRevision || !record(baseline)
|
|
305
|
+
|| !record(baseline.epics) || !record(baseline.epics[epicId]))
|
|
306
|
+
throw invalid();
|
|
307
|
+
const baselineEpic = baseline.epics[epicId];
|
|
308
|
+
const p01 = baselineEpic.createEpicPlanningPhysical;
|
|
309
|
+
const p02 = baselineEpic.createEpicPlanningResearchPhysical;
|
|
310
|
+
if (!record(p01) || !record(p01.artifactDigests) || !record(p02) || !record(p02.artifactDigests))
|
|
311
|
+
throw invalid();
|
|
312
|
+
const prefix = `.neocortex/planning/epic-${epicId}`;
|
|
313
|
+
for (const [name, path, limit] of [['idea', `${prefix}/idea.json`, 32 * 1024],
|
|
314
|
+
['diagnosis', `${prefix}/diagnosis.md`, 256 * 1024],
|
|
315
|
+
['enrichment', `${prefix}/enriched-prompt.md`, 256 * 1024],
|
|
316
|
+
['research', `${prefix}/research.md`, 256 * 1024]]) {
|
|
317
|
+
const bytes = regularBytes(root, path, limit);
|
|
318
|
+
const expected = name === 'research' ? p02.artifactDigests.research : p01.artifactDigests[name];
|
|
319
|
+
if (digest(bytes) !== expected || !bytes.equals(committedBytes(root, path, limit))
|
|
320
|
+
|| !bytes.equals(committedBytes(root, path, limit, stored.sourceHead)))
|
|
321
|
+
throw invalid();
|
|
322
|
+
}
|
|
323
|
+
const readAccepted = (path, expected, limit) => {
|
|
324
|
+
const bytes = regularBytes(root, path, limit);
|
|
325
|
+
if (digest(bytes) !== expected || !bytes.equals(committedBytes(root, path, limit)))
|
|
326
|
+
throw invalid();
|
|
327
|
+
return bytes;
|
|
328
|
+
};
|
|
329
|
+
const epicBytes = readAccepted(`docs/epics/epic-${epicId}.md`, fields.artifactDigests.epic, 512 * 1024);
|
|
330
|
+
const storyBytes = stories.map(path => readAccepted(path, fields.artifactDigests.stories[path], 128 * 1024));
|
|
331
|
+
const indexPath = '.neocortex/planning/planning.json';
|
|
332
|
+
const index = JSON.parse(readAccepted(indexPath, fields.artifactDigests.index, 128 * 1024).toString('utf8'));
|
|
333
|
+
const priorIndexRaw = committedBytes(root, indexPath, 128 * 1024, stored.sourceHead);
|
|
334
|
+
if (digest(priorIndexRaw) !== p02.artifactDigests.index)
|
|
335
|
+
throw invalid();
|
|
336
|
+
const priorIndex = JSON.parse(priorIndexRaw.toString('utf8'));
|
|
337
|
+
if (!record(index) || !record(index.epics) || !record(index.epics[epicId])
|
|
338
|
+
|| !record(priorIndex) || !record(priorIndex.epics) || !record(priorIndex.epics[epicId]))
|
|
339
|
+
throw invalid();
|
|
340
|
+
const entry = index.epics[epicId];
|
|
341
|
+
const priorEntry = priorIndex.epics[epicId];
|
|
342
|
+
if (!Array.isArray(priorEntry.steps_completed) || priorEntry.steps_completed.length !== 2
|
|
343
|
+
|| !stepMatches(priorEntry.steps_completed[0], P01_STEP_DIGEST)
|
|
344
|
+
|| !stepMatches(priorEntry.steps_completed[1], P02_STEP_DIGEST)
|
|
345
|
+
|| !Array.isArray(entry.steps_completed) || !stepMatches(entry.steps_completed[2], P03_STEP_DIGEST)
|
|
346
|
+
|| typeof entry.completed_at !== 'string' || !Number.isFinite(Date.parse(entry.completed_at))
|
|
347
|
+
|| typeof index.last_scan !== 'string' || !Number.isFinite(Date.parse(index.last_scan))
|
|
348
|
+
|| !isDeepStrictEqual(index, { ...priorIndex, last_scan: index.last_scan, epics: { ...priorIndex.epics,
|
|
349
|
+
[epicId]: { ...priorEntry, status: 'generated',
|
|
350
|
+
steps_completed: [...priorEntry.steps_completed, entry.steps_completed[2]], completed_at: entry.completed_at,
|
|
351
|
+
output_files: { ...priorEntry.output_files,
|
|
352
|
+
epic: `docs/epics/epic-${epicId}.md`, stories } } } }))
|
|
353
|
+
throw invalid();
|
|
354
|
+
const title = scalar(epicBytes.toString('utf8'), 'title');
|
|
355
|
+
if (!title || !isDeepStrictEqual(current, projectState({ stateRaw: baselineRaw, state: baseline,
|
|
356
|
+
title, stories, storyBytes, completedAt: entry.completed_at }, { epicId }, receipt)))
|
|
357
|
+
throw invalid();
|
|
358
|
+
if (git(root, ['rev-parse', 'HEAD']) !== verifiedHead
|
|
359
|
+
|| !regularBytes(root, '.neocortex/state.json', 8 * 1024 * 1024).equals(stateRaw))
|
|
360
|
+
throw invalid();
|
|
361
|
+
return { epicId, physicalAcceptanceReceiptId: receipt.receiptId,
|
|
362
|
+
storyIds: stories.map(path => path.slice('docs/stories/'.length, -'.story.md'.length)),
|
|
363
|
+
verifiedHead, stateSha256: digest(stateRaw), publicSafe: true };
|
|
364
|
+
}
|
|
365
|
+
/** The root supplies independent QA; this CAS persists the accepted P03 checkpoint. */
|
|
366
|
+
export function sealPlanningGeneration(projectRoot, reviewInput) {
|
|
367
|
+
validateReview(reviewInput);
|
|
368
|
+
const review = reviewInput;
|
|
369
|
+
const root = resolve(projectRoot);
|
|
370
|
+
if (realpathSync(root) !== root || git(root, ['rev-parse', '--show-toplevel']) !== root) {
|
|
371
|
+
throw new Error('planning-generation-root-invalid');
|
|
372
|
+
}
|
|
373
|
+
safeParents(root, review.epicId);
|
|
374
|
+
const stateDir = join(root, '.neocortex');
|
|
375
|
+
const lockPath = join(stateDir, 'state.json.planning.lock');
|
|
376
|
+
const lock = openSync(lockPath, 'wx', 0o600);
|
|
377
|
+
let temporaryPath;
|
|
378
|
+
try {
|
|
379
|
+
const pendingDigest = planningPendingSidecarDigest(root);
|
|
380
|
+
const receipt = physicalAcceptance(review);
|
|
381
|
+
const storyIds = storyPaths(review.epicId, Object.keys(review.artifactDigests.stories).length)
|
|
382
|
+
.map((path) => path.slice('docs/stories/'.length, -'.story.md'.length));
|
|
383
|
+
const currentRaw = regularBytes(root, '.neocortex/state.json', 8 * 1024 * 1024);
|
|
384
|
+
if (digest(currentRaw) !== review.sourceStateSha256) {
|
|
385
|
+
const sourceState = committedBytes(root, '.neocortex/state.json', 8 * 1024 * 1024);
|
|
386
|
+
if (digest(sourceState) !== review.sourceStateSha256) {
|
|
387
|
+
throw new Error('planning-generation-replay-conflict');
|
|
388
|
+
}
|
|
389
|
+
const baseline = evidence(root, review, false, undefined, sourceState);
|
|
390
|
+
if (!isDeepStrictEqual(JSON.parse(currentRaw.toString('utf8')), projectState(baseline, review, receipt))) {
|
|
391
|
+
throw new Error('planning-generation-replay-conflict');
|
|
392
|
+
}
|
|
393
|
+
return { epicId: review.epicId, physicalAcceptanceReceiptId: receipt.receiptId,
|
|
394
|
+
qaEvidenceId: review.qaEvidenceId, storyIds };
|
|
395
|
+
}
|
|
396
|
+
const before = evidence(root, review, true);
|
|
397
|
+
const next = projectState(before, review, receipt);
|
|
398
|
+
const commonRaw = git(root, ['rev-parse', '--git-common-dir']);
|
|
399
|
+
const commonDir = isAbsolute(commonRaw) ? resolve(commonRaw) : resolve(root, commonRaw);
|
|
400
|
+
if (realpathSync(commonDir) !== commonDir || lstatSync(commonDir).isSymbolicLink()) {
|
|
401
|
+
throw new Error('planning-generation-backup-dir-invalid');
|
|
402
|
+
}
|
|
403
|
+
const backupDir = join(commonDir, 'neocortex-planning');
|
|
404
|
+
mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
405
|
+
const backupEntry = lstatSync(backupDir);
|
|
406
|
+
if (!backupEntry.isDirectory() || backupEntry.isSymbolicLink()
|
|
407
|
+
|| backupEntry.uid !== process.getuid?.() || (backupEntry.mode & 0o077) !== 0) {
|
|
408
|
+
throw new Error('planning-generation-backup-dir-invalid');
|
|
409
|
+
}
|
|
410
|
+
const suffix = randomUUID();
|
|
411
|
+
const backup = openSync(join(backupDir, `state-backup-${suffix}.json`), 'wx', 0o600);
|
|
412
|
+
try {
|
|
413
|
+
writeFileSync(backup, before.stateRaw);
|
|
414
|
+
fsyncSync(backup);
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
closeSync(backup);
|
|
418
|
+
}
|
|
419
|
+
const backupDirectory = openSync(backupDir, 'r');
|
|
420
|
+
try {
|
|
421
|
+
fsyncSync(backupDirectory);
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
closeSync(backupDirectory);
|
|
425
|
+
}
|
|
426
|
+
temporaryPath = join(stateDir, `state.json.planning-tmp-${suffix}`);
|
|
427
|
+
const temporary = openSync(temporaryPath, 'wx', 0o600);
|
|
428
|
+
try {
|
|
429
|
+
writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`);
|
|
430
|
+
fsyncSync(temporary);
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
closeSync(temporary);
|
|
434
|
+
}
|
|
435
|
+
const recheck = evidence(root, review, true, `.neocortex/state.json.planning-tmp-${suffix}`);
|
|
436
|
+
if (!recheck.stateRaw.equals(before.stateRaw))
|
|
437
|
+
throw new Error('planning-generation-state-conflict');
|
|
438
|
+
if (planningPendingSidecarDigest(root) !== pendingDigest) {
|
|
439
|
+
throw new Error('planning-generation-pending-conflict');
|
|
440
|
+
}
|
|
441
|
+
renameSync(temporaryPath, join(stateDir, 'state.json'));
|
|
442
|
+
temporaryPath = undefined;
|
|
443
|
+
const stateDirectory = openSync(stateDir, 'r');
|
|
444
|
+
try {
|
|
445
|
+
fsyncSync(stateDirectory);
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
closeSync(stateDirectory);
|
|
449
|
+
}
|
|
450
|
+
return { epicId: review.epicId, physicalAcceptanceReceiptId: receipt.receiptId,
|
|
451
|
+
qaEvidenceId: review.qaEvidenceId, storyIds };
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
if (temporaryPath)
|
|
455
|
+
try {
|
|
456
|
+
unlinkSync(temporaryPath);
|
|
457
|
+
}
|
|
458
|
+
catch { /* preserve primary failure */ }
|
|
459
|
+
closeSync(lock);
|
|
460
|
+
unlinkSync(lockPath);
|
|
461
|
+
}
|
|
462
|
+
}
|