codex-workflow-v2 2.0.0-beta.13.8 → 2.0.0-beta.14
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/README.md +27 -388
- package/dist/reviewer-runtime-build.json +96 -32
- package/dist/src/alpha6/adoption.d.ts +2 -0
- package/dist/src/alpha6/adoption.js +22 -0
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
- package/dist/src/alpha6/captured-check-evidence.js +152 -0
- package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
- package/dist/src/alpha6/component-owner.d.ts +15 -1
- package/dist/src/alpha6/component-owner.js +44 -2
- package/dist/src/alpha6/component-owner.js.map +1 -1
- package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
- package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
- package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
- package/dist/src/alpha6/downstream-proof.d.ts +1 -25
- package/dist/src/alpha6/downstream-proof.js +0 -171
- package/dist/src/alpha6/downstream-proof.js.map +1 -1
- package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
- package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
- package/dist/src/alpha6/literal-test-invocation.js +106 -0
- package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +2 -0
- package/dist/src/alpha6/milestone.js +10 -1
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
- package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
- package/dist/src/alpha6/plan-integrity.js +7 -5
- package/dist/src/alpha6/plan-integrity.js.map +1 -1
- package/dist/src/alpha6/plan-risk.d.ts +2 -0
- package/dist/src/alpha6/plan-risk.js +10 -3
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +6 -21
- package/dist/src/alpha6/remediation.js +161 -476
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
- package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
- package/dist/src/beta1/project-transaction.d.ts +3 -1
- package/dist/src/beta1/project-transaction.js +6 -2
- package/dist/src/beta1/project-transaction.js.map +1 -1
- package/dist/src/checks/runner.d.ts +26 -0
- package/dist/src/checks/runner.js +223 -0
- package/dist/src/checks/runner.js.map +1 -0
- package/dist/src/checks/task-sync.d.ts +10 -0
- package/dist/src/checks/task-sync.js +41 -0
- package/dist/src/checks/task-sync.js.map +1 -0
- package/dist/src/checks/worker.d.ts +1 -0
- package/dist/src/checks/worker.js +154 -0
- package/dist/src/checks/worker.js.map +1 -0
- package/dist/src/cli-actions.d.ts +2 -2
- package/dist/src/cli-actions.js +3 -15
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +14 -98
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +35 -32
- package/dist/src/dependency-provenance.d.ts +2 -2
- package/dist/src/dependency-provenance.js +12 -69
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/base-sync-conflict.d.ts +13 -0
- package/dist/src/domain/base-sync-conflict.js +32 -0
- package/dist/src/domain/base-sync-conflict.js.map +1 -0
- package/dist/src/domain/step-start-admission.d.ts +4 -0
- package/dist/src/domain/step-start-admission.js +9 -0
- package/dist/src/domain/step-start-admission.js.map +1 -0
- package/dist/src/domain/virgin-registration.d.ts +3 -0
- package/dist/src/domain/virgin-registration.js +65 -0
- package/dist/src/domain/virgin-registration.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/gateway-handshake.js +0 -8
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/git.d.ts +3 -1
- package/dist/src/git.js +41 -14
- package/dist/src/git.js.map +1 -1
- package/dist/src/graph.js +25 -3
- package/dist/src/graph.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/lifecycle/corrective-replan.js +3 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/navigation-actions.d.ts +9 -0
- package/dist/src/navigation-actions.js +72 -0
- package/dist/src/navigation-actions.js.map +1 -0
- package/dist/src/navigation-update-artifact.d.ts +5 -0
- package/dist/src/navigation-update-artifact.js +227 -0
- package/dist/src/navigation-update-artifact.js.map +1 -0
- package/dist/src/navigation-update.d.ts +39 -0
- package/dist/src/navigation-update.js +82 -0
- package/dist/src/navigation-update.js.map +1 -0
- package/dist/src/observation.js +52 -29
- package/dist/src/observation.js.map +1 -1
- package/dist/src/observed-routes.js +5 -6
- package/dist/src/observed-routes.js.map +1 -1
- package/dist/src/pending-review-update.d.ts +0 -13
- package/dist/src/pending-review-update.js +1 -6
- package/dist/src/pending-review-update.js.map +1 -1
- package/dist/src/reviewer.d.ts +1 -1
- package/dist/src/reviewer.js +20 -17
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +6 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/state/corrective-replan-executor.d.ts +10 -0
- package/dist/src/state/corrective-replan-executor.js +37 -1
- package/dist/src/state/corrective-replan-executor.js.map +1 -1
- package/dist/src/state/corrective-replan-public-schema.js +15 -2
- package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +6 -5
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
- package/dist/src/state/corrective-replan-transaction.js +12 -11
- package/dist/src/state/corrective-replan-transaction.js.map +1 -1
- package/dist/src/state/corrective-yield-executor.js +2 -2
- package/dist/src/state/corrective-yield-executor.js.map +1 -1
- package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
- package/dist/src/state/corrective-yield-transaction.js +11 -10
- package/dist/src/state/corrective-yield-transaction.js.map +1 -1
- package/dist/src/state/lock.d.ts +4 -0
- package/dist/src/state/lock.js +21 -0
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow-blocker-route.d.ts +8 -0
- package/dist/src/workflow-blocker-route.js +106 -0
- package/dist/src/workflow-blocker-route.js.map +1 -0
- package/dist/src/workflow.d.ts +48 -63
- package/dist/src/workflow.js +698 -1470
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +26 -304
- package/docs/decisions.md +14 -112
- package/docs/development-flow.md +35 -238
- package/docs/project-memory.md +31 -50
- package/docs/release-app-evidence.md +190 -0
- package/docs/release.md +95 -363
- package/docs/updating-existing-project.md +19 -717
- package/package.json +11 -13
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
- package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -740
- package/references/state-machine.md +4 -4
- package/roles/technical-planner.md +1 -1
- package/schemas/corrective-decision-event.schema.json +3 -1
- package/schemas/project-knowledge-map.schema.json +3 -1
- package/schemas/remediation-event.schema.json +10 -1
- package/schemas/task.schema.json +46 -2
- package/schemas/transition-payloads.schema.json +16 -1
- package/src/alpha6/adoption.ts +1123 -0
- package/src/alpha6/captured-check-evidence.ts +132 -0
- package/src/alpha6/check-support-anchor.ts +128 -0
- package/src/alpha6/component-owner.ts +168 -0
- package/src/alpha6/corrective-decision-boundary.ts +39 -0
- package/src/alpha6/downstream-proof.ts +520 -0
- package/src/alpha6/failed-step-planning-recovery.ts +88 -0
- package/src/alpha6/handoff.ts +1443 -0
- package/src/alpha6/journal.ts +473 -0
- package/src/alpha6/literal-test-invocation.ts +84 -0
- package/src/alpha6/mechanical-feasibility.ts +488 -0
- package/src/alpha6/milestone.ts +2192 -0
- package/src/alpha6/navigation-dirty-carryover.ts +154 -0
- package/src/alpha6/npm-check-contract.ts +47 -0
- package/src/alpha6/plan-integrity.ts +298 -0
- package/src/alpha6/plan-risk.ts +1480 -0
- package/src/alpha6/preexecution-replan.ts +187 -0
- package/src/alpha6/remediation-cause.ts +98 -0
- package/src/alpha6/remediation.ts +2438 -0
- package/src/alpha6/review.ts +1198 -0
- package/src/alpha6/root-cause-replan-carryover.ts +491 -0
- package/src/alpha6/store-sidecars.ts +335 -0
- package/src/alpha7/autonomy.ts +411 -0
- package/src/alpha7/corrective-recovery.ts +1332 -0
- package/src/artifacts.ts +130 -0
- package/src/beta1/project-transaction.ts +355 -0
- package/src/change-explanation.ts +153 -0
- package/src/checks/runner.ts +245 -0
- package/src/checks/task-sync.ts +41 -0
- package/src/checks/worker.ts +149 -0
- package/src/cli-actions.ts +107 -0
- package/src/cli.ts +1457 -0
- package/src/contracts.ts +1494 -0
- package/src/credential-output.ts +89 -0
- package/src/credential-transport.ts +215 -0
- package/src/delegation.ts +190 -0
- package/src/dependency-provenance.ts +472 -0
- package/src/diagnostics.ts +93 -0
- package/src/domain/base-sync-conflict.ts +35 -0
- package/src/domain/completed-step-carryover.ts +76 -0
- package/src/domain/discovery.ts +27 -0
- package/src/domain/plan-semantics.ts +58 -0
- package/src/domain/step-start-admission.ts +10 -0
- package/src/domain/validation.ts +177 -0
- package/src/domain/virgin-registration.ts +46 -0
- package/src/errors.ts +24 -0
- package/src/fs-utils.ts +61 -0
- package/src/gateway-handshake.ts +95 -0
- package/src/git.ts +183 -0
- package/src/graph.ts +342 -0
- package/src/historical-step-provenance.ts +136 -0
- package/src/index.ts +23 -0
- package/src/lifecycle/canonical-hash.ts +28 -0
- package/src/lifecycle/catalog.ts +202 -0
- package/src/lifecycle/compiler-inspection.ts +29 -0
- package/src/lifecycle/core-static-readiness.ts +132 -0
- package/src/lifecycle/corrective-replan-authority.ts +136 -0
- package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
- package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
- package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
- package/src/lifecycle/corrective-replan-credentials.ts +48 -0
- package/src/lifecycle/corrective-replan.ts +843 -0
- package/src/lifecycle/evaluator.ts +48 -0
- package/src/lifecycle/fingerprint.ts +488 -0
- package/src/lifecycle/immutable.ts +8 -0
- package/src/lifecycle/implementation-table.ts +118 -0
- package/src/lifecycle/index.ts +8 -0
- package/src/lifecycle/schema-artifact.ts +263 -0
- package/src/lifecycle/semantic-registry.ts +572 -0
- package/src/lifecycle/types.ts +838 -0
- package/src/memory.ts +273 -0
- package/src/migration.ts +161 -0
- package/src/navigation-actions.ts +70 -0
- package/src/navigation-update-artifact.ts +198 -0
- package/src/navigation-update.ts +121 -0
- package/src/observation.ts +225 -0
- package/src/observed-routes.ts +660 -0
- package/src/operational-contract.ts +125 -0
- package/src/pending-review-update.ts +175 -0
- package/src/repository.ts +99 -0
- package/src/reviewer.ts +1879 -0
- package/src/runtime.ts +6 -0
- package/src/state/corrective-replan-executor.ts +818 -0
- package/src/state/corrective-replan-public-schema.ts +83 -0
- package/src/state/corrective-replan-public.ts +908 -0
- package/src/state/corrective-replan-transaction.ts +949 -0
- package/src/state/corrective-yield-executor.ts +327 -0
- package/src/state/corrective-yield-transaction.ts +730 -0
- package/src/state/lock.ts +902 -0
- package/src/state/store.ts +567 -0
- package/src/transition-core.ts +330 -0
- package/src/ulid.ts +24 -0
- package/src/version.ts +2 -0
- package/src/workflow-blocker-route.ts +109 -0
- package/src/workflow.ts +10172 -0
- package/docs/alpha7.1-implementation-brief.md +0 -268
- package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
- package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
- package/docs/beta1-stabilization-brief.md +0 -165
- package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
- package/docs/beta13.2-signal-review-recovery.md +0 -38
- package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
- package/docs/change-model.md +0 -118
- package/docs/delegated-approval.md +0 -254
- package/docs/lifecycle/state-machine-stabilization.md +0 -641
- package/docs/pdf/README.md +0 -24
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/requirements.txt +0 -1
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -508
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
- package/docs/pending-review-update.md +0 -15
- package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
- package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
- package/docs/problem-briefs/README.md +0 -120
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
- package/docs/split-required-recovery.md +0 -47
- package/docs/stable-release-defect-register.md +0 -730
- package/docs/validation-report.md +0 -182
- package/scripts/generate-pdf-docs.py +0 -524
- package/scripts/run-pdf-docs.mjs +0 -62
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fsyncSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
writeSync,
|
|
15
|
+
} from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import type {
|
|
18
|
+
MilestoneTransactionJournal,
|
|
19
|
+
MilestoneTransactionJournalFileSnapshot,
|
|
20
|
+
MilestoneTransactionPhase,
|
|
21
|
+
} from '../contracts.js';
|
|
22
|
+
import { WorkflowError } from '../errors.js';
|
|
23
|
+
import { ensureDirectory, readJson } from '../fs-utils.js';
|
|
24
|
+
import { assertHash64, assertNoSymlinkPathComponents, sha256Hex } from './store-sidecars.js';
|
|
25
|
+
|
|
26
|
+
const TRANSACTION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
27
|
+
const JOURNAL_PHASES = new Set<MilestoneTransactionPhase>(['prepared', 'applying', 'applied', 'rolled_back']);
|
|
28
|
+
|
|
29
|
+
export type JournalSnapshotMode = 'inline' | 'path';
|
|
30
|
+
|
|
31
|
+
interface ValidateJournalOptions {
|
|
32
|
+
expectedMilestoneId?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveMilestoneTransactionDirectory(milestoneRoot: string): string {
|
|
36
|
+
assertNoSymlinkPathComponents(milestoneRoot, { label: 'Milestone root', requireLeaf: true });
|
|
37
|
+
return path.join(milestoneRoot, '.transactions');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveMilestoneTransactionJournalPath(milestoneRoot: string, transactionId: string): string {
|
|
41
|
+
assertSafeTransactionId(transactionId);
|
|
42
|
+
return path.join(resolveMilestoneTransactionDirectory(milestoneRoot), `${transactionId}.json`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveMilestoneTransactionTargetPath(milestoneRoot: string, relativePath: string): string {
|
|
46
|
+
const normalized = normalizeRelativePath(relativePath, 'Milestone transaction target path');
|
|
47
|
+
const resolved = resolveWithinRoot(milestoneRoot, normalized, 'Milestone transaction target path');
|
|
48
|
+
const transactionsRoot = path.resolve(resolveMilestoneTransactionDirectory(milestoneRoot));
|
|
49
|
+
if (resolved === transactionsRoot || resolved.startsWith(`${transactionsRoot}${path.sep}`)) {
|
|
50
|
+
throw new WorkflowError('INVALID_ARGUMENT', `Transaction target path must not point into .transactions: ${relativePath}`);
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createAbsentJournalSnapshot(): MilestoneTransactionJournalFileSnapshot {
|
|
56
|
+
return {
|
|
57
|
+
exists: false,
|
|
58
|
+
contentHash: null,
|
|
59
|
+
contentBase64: null,
|
|
60
|
+
contentPath: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function captureJournalTargetSnapshot(
|
|
65
|
+
milestoneRoot: string,
|
|
66
|
+
relativePath: string,
|
|
67
|
+
options: { mode?: JournalSnapshotMode; transactionId?: string; snapshotPath?: string } = {},
|
|
68
|
+
): MilestoneTransactionJournalFileSnapshot {
|
|
69
|
+
const targetFile = resolveMilestoneTransactionTargetPath(milestoneRoot, relativePath);
|
|
70
|
+
if (!existsSync(targetFile)) return createAbsentJournalSnapshot();
|
|
71
|
+
if (lstatSync(targetFile).isSymbolicLink()) {
|
|
72
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction target must not be a symlink: ${targetFile}`);
|
|
73
|
+
}
|
|
74
|
+
const content = readFileSync(targetFile);
|
|
75
|
+
const snapshotOptions: { mode?: JournalSnapshotMode; transactionId?: string; snapshotPath?: string } = {
|
|
76
|
+
snapshotPath: options.snapshotPath ?? relativePath,
|
|
77
|
+
};
|
|
78
|
+
if (options.mode !== undefined) snapshotOptions.mode = options.mode;
|
|
79
|
+
if (options.transactionId !== undefined) snapshotOptions.transactionId = options.transactionId;
|
|
80
|
+
return createJournalSnapshotFromContent(milestoneRoot, content, snapshotOptions);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createJournalSnapshotFromContent(
|
|
84
|
+
milestoneRoot: string,
|
|
85
|
+
content: string | Buffer,
|
|
86
|
+
options: { mode?: JournalSnapshotMode; transactionId?: string; snapshotPath?: string } = {},
|
|
87
|
+
): MilestoneTransactionJournalFileSnapshot {
|
|
88
|
+
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8');
|
|
89
|
+
if ((options.mode ?? 'inline') === 'path') {
|
|
90
|
+
if (!options.transactionId) {
|
|
91
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Path-backed journal snapshots require a transaction id.');
|
|
92
|
+
}
|
|
93
|
+
if (!options.snapshotPath) {
|
|
94
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Path-backed journal snapshots require a snapshot path.');
|
|
95
|
+
}
|
|
96
|
+
const snapshotFile = resolveMilestoneTransactionSnapshotFile(
|
|
97
|
+
milestoneRoot,
|
|
98
|
+
options.transactionId,
|
|
99
|
+
options.snapshotPath,
|
|
100
|
+
);
|
|
101
|
+
writeBufferAtomic(snapshotFile, buffer);
|
|
102
|
+
const transactionsRoot = resolveMilestoneTransactionDirectory(milestoneRoot);
|
|
103
|
+
return {
|
|
104
|
+
exists: true,
|
|
105
|
+
contentHash: sha256Hex(buffer),
|
|
106
|
+
contentBase64: null,
|
|
107
|
+
contentPath: path.relative(transactionsRoot, snapshotFile).split(path.sep).join('/'),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
exists: true,
|
|
112
|
+
contentHash: sha256Hex(buffer),
|
|
113
|
+
contentBase64: buffer.toString('base64'),
|
|
114
|
+
contentPath: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function readJournalSnapshotContent(
|
|
119
|
+
milestoneRoot: string,
|
|
120
|
+
snapshot: MilestoneTransactionJournalFileSnapshot,
|
|
121
|
+
options: ValidateJournalOptions & { transactionId?: string } = {},
|
|
122
|
+
): Buffer | null {
|
|
123
|
+
validateJournalSnapshot(snapshot, milestoneRoot, options);
|
|
124
|
+
if (!snapshot.exists) return null;
|
|
125
|
+
if (snapshot.contentBase64 !== null) {
|
|
126
|
+
const buffer = decodeCanonicalBase64(snapshot.contentBase64);
|
|
127
|
+
assertSnapshotHash(snapshot, buffer);
|
|
128
|
+
return buffer;
|
|
129
|
+
}
|
|
130
|
+
if (snapshot.contentPath === null) {
|
|
131
|
+
throw new WorkflowError('STATE_CORRUPT', 'Existing journal snapshot must provide contentBase64 or contentPath.');
|
|
132
|
+
}
|
|
133
|
+
const snapshotFile = resolveJournalSnapshotReference(milestoneRoot, snapshot.contentPath, options.transactionId);
|
|
134
|
+
if (!existsSync(snapshotFile)) {
|
|
135
|
+
throw new WorkflowError('STATE_CORRUPT', `Journal snapshot content file not found: ${snapshot.contentPath}`);
|
|
136
|
+
}
|
|
137
|
+
const buffer = readFileSync(snapshotFile);
|
|
138
|
+
assertSnapshotHash(snapshot, buffer);
|
|
139
|
+
return buffer;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function writeMilestoneTransactionJournal(
|
|
143
|
+
milestoneRoot: string,
|
|
144
|
+
journal: MilestoneTransactionJournal,
|
|
145
|
+
): MilestoneTransactionJournal {
|
|
146
|
+
validateMilestoneTransactionJournal(journal, milestoneRoot);
|
|
147
|
+
const file = resolveMilestoneTransactionJournalPath(milestoneRoot, journal.transactionId);
|
|
148
|
+
writeJsonAtomicDurable(file, journal);
|
|
149
|
+
return readMilestoneTransactionJournal(milestoneRoot, journal.transactionId, {
|
|
150
|
+
expectedMilestoneId: journal.milestoneId,
|
|
151
|
+
transactionId: journal.transactionId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function readMilestoneTransactionJournal(
|
|
156
|
+
milestoneRoot: string,
|
|
157
|
+
transactionId: string,
|
|
158
|
+
options: ValidateJournalOptions & { transactionId?: string } = {},
|
|
159
|
+
): MilestoneTransactionJournal {
|
|
160
|
+
const file = resolveMilestoneTransactionJournalPath(milestoneRoot, transactionId);
|
|
161
|
+
if (!existsSync(file)) {
|
|
162
|
+
throw new WorkflowError('NOT_FOUND', `Milestone transaction journal not found: ${transactionId}`);
|
|
163
|
+
}
|
|
164
|
+
const journal = readJson<MilestoneTransactionJournal>(file);
|
|
165
|
+
const validationOptions: ValidateJournalOptions & { transactionId?: string } = { ...options };
|
|
166
|
+
if (!validationOptions.transactionId) validationOptions.transactionId = transactionId;
|
|
167
|
+
validateMilestoneTransactionJournal(journal, milestoneRoot, validationOptions);
|
|
168
|
+
return journal;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function listMilestoneTransactionJournals(
|
|
172
|
+
milestoneRoot: string,
|
|
173
|
+
options: ValidateJournalOptions = {},
|
|
174
|
+
): MilestoneTransactionJournal[] {
|
|
175
|
+
const directory = resolveMilestoneTransactionDirectory(milestoneRoot);
|
|
176
|
+
if (!existsSync(directory)) return [];
|
|
177
|
+
return readdirSync(directory, { withFileTypes: true })
|
|
178
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
179
|
+
.map((entry) => {
|
|
180
|
+
const transactionId = entry.name.slice(0, -'.json'.length);
|
|
181
|
+
return readMilestoneTransactionJournal(milestoneRoot, transactionId, { ...options, transactionId });
|
|
182
|
+
})
|
|
183
|
+
.sort((left, right) => left.preparedAt.localeCompare(right.preparedAt) || left.transactionId.localeCompare(right.transactionId));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function updateMilestoneTransactionJournalPhase(
|
|
187
|
+
milestoneRoot: string,
|
|
188
|
+
transactionId: string,
|
|
189
|
+
phase: MilestoneTransactionPhase,
|
|
190
|
+
updatedAt: string,
|
|
191
|
+
options: ValidateJournalOptions = {},
|
|
192
|
+
): MilestoneTransactionJournal {
|
|
193
|
+
if (!JOURNAL_PHASES.has(phase)) {
|
|
194
|
+
throw new WorkflowError('INVALID_ARGUMENT', `Invalid milestone transaction phase: ${phase}`);
|
|
195
|
+
}
|
|
196
|
+
const journal = readMilestoneTransactionJournal(milestoneRoot, transactionId, options);
|
|
197
|
+
return writeMilestoneTransactionJournal(milestoneRoot, { ...journal, phase, updatedAt });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function removeMilestoneTransactionJournal(milestoneRoot: string, transactionId: string): void {
|
|
201
|
+
const file = resolveMilestoneTransactionJournalPath(milestoneRoot, transactionId);
|
|
202
|
+
rmSync(file, { force: true });
|
|
203
|
+
rmSync(path.join(resolveMilestoneTransactionDirectory(milestoneRoot), transactionId), { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resolveMilestoneTransactionSnapshotFile(
|
|
207
|
+
milestoneRoot: string,
|
|
208
|
+
transactionId: string,
|
|
209
|
+
snapshotPath: string,
|
|
210
|
+
): string {
|
|
211
|
+
assertSafeTransactionId(transactionId);
|
|
212
|
+
const normalized = normalizeRelativePath(snapshotPath, 'Milestone transaction snapshot path');
|
|
213
|
+
return resolveWithinRoot(
|
|
214
|
+
path.join(resolveMilestoneTransactionDirectory(milestoneRoot), transactionId),
|
|
215
|
+
normalized,
|
|
216
|
+
'Milestone transaction snapshot path',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function validateMilestoneTransactionJournal(
|
|
221
|
+
journal: MilestoneTransactionJournal,
|
|
222
|
+
milestoneRoot: string,
|
|
223
|
+
options: ValidateJournalOptions & { transactionId?: string } = {},
|
|
224
|
+
): void {
|
|
225
|
+
if (!journal || typeof journal !== 'object' || Array.isArray(journal)) {
|
|
226
|
+
throw new WorkflowError('STATE_CORRUPT', 'Milestone transaction journal must be an object.');
|
|
227
|
+
}
|
|
228
|
+
if (typeof journal.transactionId !== 'string') {
|
|
229
|
+
throw new WorkflowError('STATE_CORRUPT', 'Milestone transaction journal is missing transactionId.');
|
|
230
|
+
}
|
|
231
|
+
assertSafeTransactionId(journal.transactionId);
|
|
232
|
+
if (typeof journal.milestoneId !== 'string') {
|
|
233
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal is missing milestoneId: ${journal.transactionId}`);
|
|
234
|
+
}
|
|
235
|
+
if (options.expectedMilestoneId && journal.milestoneId !== options.expectedMilestoneId) {
|
|
236
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal milestone mismatch: ${journal.transactionId}`, {
|
|
237
|
+
expectedMilestoneId: options.expectedMilestoneId,
|
|
238
|
+
actualMilestoneId: journal.milestoneId,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (!journal.milestoneId.trim()) {
|
|
242
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal is missing milestoneId: ${journal.transactionId}`);
|
|
243
|
+
}
|
|
244
|
+
if (!Number.isInteger(journal.expectedMilestoneRevision) || journal.expectedMilestoneRevision < 1) {
|
|
245
|
+
throw new WorkflowError(
|
|
246
|
+
'STATE_CORRUPT',
|
|
247
|
+
`Milestone transaction journal expectedMilestoneRevision must be >= 1: ${journal.transactionId}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (typeof journal.actor !== 'string') {
|
|
251
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal is missing actor: ${journal.transactionId}`);
|
|
252
|
+
}
|
|
253
|
+
if (!journal.actor.trim()) {
|
|
254
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal is missing actor: ${journal.transactionId}`);
|
|
255
|
+
}
|
|
256
|
+
if (typeof journal.confirmationCodeBindingHash !== 'string') {
|
|
257
|
+
throw new WorkflowError(
|
|
258
|
+
'STATE_CORRUPT',
|
|
259
|
+
`Milestone transaction journal is missing confirmationCodeBindingHash: ${journal.transactionId}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
assertHash64(
|
|
263
|
+
journal.confirmationCodeBindingHash,
|
|
264
|
+
`Milestone transaction journal confirmationCodeBindingHash for ${journal.transactionId}`,
|
|
265
|
+
);
|
|
266
|
+
if (typeof journal.phase !== 'string') {
|
|
267
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal has invalid phase: ${journal.transactionId}`);
|
|
268
|
+
}
|
|
269
|
+
if (!JOURNAL_PHASES.has(journal.phase)) {
|
|
270
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal has invalid phase: ${journal.transactionId}`, {
|
|
271
|
+
phase: journal.phase,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (typeof journal.preparedAt !== 'string' || !journal.preparedAt.trim()) {
|
|
275
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal is missing preparedAt: ${journal.transactionId}`);
|
|
276
|
+
}
|
|
277
|
+
if (typeof journal.updatedAt !== 'string' || !journal.updatedAt.trim()) {
|
|
278
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal is missing updatedAt: ${journal.transactionId}`);
|
|
279
|
+
}
|
|
280
|
+
if (!Array.isArray(journal.targetFiles)) {
|
|
281
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal targetFiles must be an array: ${journal.transactionId}`);
|
|
282
|
+
}
|
|
283
|
+
if (journal.targetFiles.length === 0) {
|
|
284
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal must declare target files: ${journal.transactionId}`);
|
|
285
|
+
}
|
|
286
|
+
const targetPaths = new Set<string>();
|
|
287
|
+
for (const target of journal.targetFiles) {
|
|
288
|
+
if (!target || typeof target !== 'object' || Array.isArray(target)) {
|
|
289
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal target must be an object: ${journal.transactionId}`);
|
|
290
|
+
}
|
|
291
|
+
if (typeof target.path !== 'string') {
|
|
292
|
+
throw new WorkflowError(
|
|
293
|
+
'STATE_CORRUPT',
|
|
294
|
+
`Milestone transaction journal target path must be a string: ${journal.transactionId}`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const resolvedTarget = resolveMilestoneTransactionTargetPath(milestoneRoot, target.path);
|
|
298
|
+
if (targetPaths.has(resolvedTarget)) {
|
|
299
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction journal target path is duplicated: ${target.path}`, {
|
|
300
|
+
transactionId: journal.transactionId,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
targetPaths.add(resolvedTarget);
|
|
304
|
+
validateJournalSnapshot(target.preimage, milestoneRoot, { transactionId: journal.transactionId });
|
|
305
|
+
validateJournalSnapshot(target.staged, milestoneRoot, { transactionId: journal.transactionId });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function validateJournalSnapshot(
|
|
310
|
+
snapshot: MilestoneTransactionJournalFileSnapshot,
|
|
311
|
+
milestoneRoot: string,
|
|
312
|
+
options: { transactionId?: string } = {},
|
|
313
|
+
): void {
|
|
314
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
315
|
+
throw new WorkflowError('STATE_CORRUPT', 'Journal snapshot must be an object.');
|
|
316
|
+
}
|
|
317
|
+
if (typeof snapshot.exists !== 'boolean') {
|
|
318
|
+
throw new WorkflowError('STATE_CORRUPT', 'Journal snapshot exists must be boolean.');
|
|
319
|
+
}
|
|
320
|
+
if (!snapshot.exists) {
|
|
321
|
+
if (snapshot.contentHash !== null || snapshot.contentBase64 !== null || snapshot.contentPath !== null) {
|
|
322
|
+
throw new WorkflowError('STATE_CORRUPT', 'Absent journal snapshot must not include content.');
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (typeof snapshot.contentHash !== 'string') {
|
|
327
|
+
throw new WorkflowError('STATE_CORRUPT', 'Existing journal snapshot is missing contentHash.');
|
|
328
|
+
}
|
|
329
|
+
assertHash64(snapshot.contentHash, 'Journal snapshot contentHash');
|
|
330
|
+
if (snapshot.contentBase64 !== null && typeof snapshot.contentBase64 !== 'string') {
|
|
331
|
+
throw new WorkflowError('STATE_CORRUPT', 'Journal snapshot contentBase64 must be a string or null.');
|
|
332
|
+
}
|
|
333
|
+
if (snapshot.contentPath !== null && typeof snapshot.contentPath !== 'string') {
|
|
334
|
+
throw new WorkflowError('STATE_CORRUPT', 'Journal snapshot contentPath must be a string or null.');
|
|
335
|
+
}
|
|
336
|
+
const hasInline = typeof snapshot.contentBase64 === 'string';
|
|
337
|
+
const hasPath = typeof snapshot.contentPath === 'string';
|
|
338
|
+
if (hasInline === hasPath) {
|
|
339
|
+
throw new WorkflowError('STATE_CORRUPT', 'Existing journal snapshot must provide exactly one content source.');
|
|
340
|
+
}
|
|
341
|
+
if (hasInline) {
|
|
342
|
+
assertSnapshotHash(snapshot, decodeCanonicalBase64(snapshot.contentBase64 ?? ''));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const snapshotFile = resolveJournalSnapshotReference(milestoneRoot, snapshot.contentPath ?? '', options.transactionId);
|
|
346
|
+
if (!existsSync(snapshotFile)) {
|
|
347
|
+
throw new WorkflowError('STATE_CORRUPT', `Journal snapshot content file not found: ${snapshot.contentPath}`);
|
|
348
|
+
}
|
|
349
|
+
assertNoSymlinkPathComponents(snapshotFile, { label: 'Journal snapshot content file', requireLeaf: true });
|
|
350
|
+
assertSnapshotHash(snapshot, readFileSync(snapshotFile));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function resolveJournalSnapshotReference(milestoneRoot: string, contentPath: string, transactionId?: string): string {
|
|
354
|
+
const normalized = normalizeRelativePath(contentPath, 'Milestone transaction snapshot contentPath');
|
|
355
|
+
const transactionsRoot = resolveMilestoneTransactionDirectory(milestoneRoot);
|
|
356
|
+
const resolved = resolveWithinRoot(transactionsRoot, normalized, 'Milestone transaction snapshot contentPath');
|
|
357
|
+
if (transactionId) {
|
|
358
|
+
const transactionRoot = path.resolve(path.join(transactionsRoot, transactionId));
|
|
359
|
+
if (resolved !== transactionRoot && !resolved.startsWith(`${transactionRoot}${path.sep}`)) {
|
|
360
|
+
throw new WorkflowError(
|
|
361
|
+
'STATE_CORRUPT',
|
|
362
|
+
`Journal snapshot contentPath must stay under the journal transaction root: ${contentPath}`,
|
|
363
|
+
{ transactionId },
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return resolved;
|
|
367
|
+
}
|
|
368
|
+
const relative = path.relative(transactionsRoot, resolved).split(path.sep).filter(Boolean);
|
|
369
|
+
if (relative.length < 2) {
|
|
370
|
+
throw new WorkflowError(
|
|
371
|
+
'STATE_CORRUPT',
|
|
372
|
+
`Journal snapshot contentPath must include a transaction directory: ${contentPath}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
assertSafeTransactionId(relative[0] ?? '');
|
|
376
|
+
return resolved;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function assertSnapshotHash(snapshot: MilestoneTransactionJournalFileSnapshot, buffer: Buffer): void {
|
|
380
|
+
if (snapshot.contentHash === null) {
|
|
381
|
+
throw new WorkflowError('STATE_CORRUPT', 'Journal snapshot is missing contentHash.');
|
|
382
|
+
}
|
|
383
|
+
const actualContentHash = sha256Hex(buffer);
|
|
384
|
+
if (snapshot.contentHash !== actualContentHash) {
|
|
385
|
+
throw new WorkflowError('STATE_CORRUPT', 'Journal snapshot content hash mismatch.', {
|
|
386
|
+
expectedContentHash: snapshot.contentHash,
|
|
387
|
+
actualContentHash,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function assertSafeTransactionId(transactionId: string): void {
|
|
393
|
+
if (!transactionId.trim() || !TRANSACTION_ID_PATTERN.test(transactionId)) {
|
|
394
|
+
throw new WorkflowError('INVALID_ARGUMENT', `Invalid milestone transaction id: ${transactionId}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeRelativePath(candidate: string, label: string): string {
|
|
399
|
+
if (!candidate.trim()) {
|
|
400
|
+
throw new WorkflowError('INVALID_ARGUMENT', `${label} is required.`);
|
|
401
|
+
}
|
|
402
|
+
if (path.isAbsolute(candidate)) {
|
|
403
|
+
throw new WorkflowError('INVALID_ARGUMENT', `${label} must be relative: ${candidate}`);
|
|
404
|
+
}
|
|
405
|
+
const normalized = path.posix.normalize(candidate.replaceAll('\\', '/'));
|
|
406
|
+
if (normalized === '.' || normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) {
|
|
407
|
+
throw new WorkflowError('INVALID_ARGUMENT', `${label} must not traverse directories: ${candidate}`);
|
|
408
|
+
}
|
|
409
|
+
return normalized.split('/').join(path.sep);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function resolveWithinRoot(root: string, relativePath: string, label: string): string {
|
|
413
|
+
const resolvedRoot = path.resolve(root);
|
|
414
|
+
const resolvedCandidate = path.resolve(resolvedRoot, relativePath);
|
|
415
|
+
if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
416
|
+
throw new WorkflowError('INVALID_ARGUMENT', `${label} escapes the milestone root: ${relativePath}`);
|
|
417
|
+
}
|
|
418
|
+
return resolvedCandidate;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function writeBufferAtomic(file: string, content: Buffer): void {
|
|
422
|
+
const directory = path.dirname(file);
|
|
423
|
+
ensureDirectory(directory);
|
|
424
|
+
assertNoSymlinkPathComponents(directory, { label: 'Snapshot directory', requireLeaf: true });
|
|
425
|
+
if (existsSync(file) && lstatSync(file).isSymbolicLink()) {
|
|
426
|
+
throw new WorkflowError('STATE_CORRUPT', `Refusing to replace snapshot symlink: ${file}`);
|
|
427
|
+
}
|
|
428
|
+
const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
|
|
429
|
+
try {
|
|
430
|
+
writeFileSync(temporary, content, { mode: 0o600 });
|
|
431
|
+
renameSync(temporary, file);
|
|
432
|
+
} finally {
|
|
433
|
+
rmSync(temporary, { force: true });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function writeJsonAtomicDurable(file: string, value: unknown): void {
|
|
438
|
+
const directory = path.dirname(file);
|
|
439
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
440
|
+
assertNoSymlinkPathComponents(directory, { label: 'Journal directory', requireLeaf: true });
|
|
441
|
+
if (existsSync(file) && lstatSync(file).isSymbolicLink()) {
|
|
442
|
+
throw new WorkflowError('STATE_CORRUPT', `Refusing to replace transaction journal symlink: ${file}`);
|
|
443
|
+
}
|
|
444
|
+
const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
|
|
445
|
+
let fileDescriptor: number | null = null;
|
|
446
|
+
let directoryDescriptor: number | null = null;
|
|
447
|
+
try {
|
|
448
|
+
fileDescriptor = openSync(temporary, 'wx', 0o600);
|
|
449
|
+
const payload = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
450
|
+
writeSync(fileDescriptor, payload);
|
|
451
|
+
fsyncSync(fileDescriptor);
|
|
452
|
+
closeSync(fileDescriptor);
|
|
453
|
+
fileDescriptor = null;
|
|
454
|
+
renameSync(temporary, file);
|
|
455
|
+
directoryDescriptor = openSync(directory, 'r');
|
|
456
|
+
fsyncSync(directoryDescriptor);
|
|
457
|
+
} finally {
|
|
458
|
+
if (fileDescriptor !== null) closeSync(fileDescriptor);
|
|
459
|
+
if (directoryDescriptor !== null) closeSync(directoryDescriptor);
|
|
460
|
+
rmSync(temporary, { force: true });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function decodeCanonicalBase64(value: string): Buffer {
|
|
465
|
+
if (!value || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
466
|
+
throw new WorkflowError('STATE_CORRUPT', 'Journal snapshot contentBase64 must be canonical base64.');
|
|
467
|
+
}
|
|
468
|
+
const buffer = Buffer.from(value, 'base64');
|
|
469
|
+
if (buffer.toString('base64') !== value) {
|
|
470
|
+
throw new WorkflowError('STATE_CORRUPT', 'Journal snapshot contentBase64 must be canonical base64.');
|
|
471
|
+
}
|
|
472
|
+
return buffer;
|
|
473
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** A deliberately bounded lexical reader, not a JavaScript evaluator. Unknown
|
|
2
|
+
* syntax produces no dependency evidence. Comments/string contents are never code. */
|
|
3
|
+
const SAFE_TEST = /^(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
4
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
5
|
+
type Token = { text: string; string?: string };
|
|
6
|
+
|
|
7
|
+
function tokens(source: string): Token[] {
|
|
8
|
+
const result: Token[] = [];
|
|
9
|
+
const lexer = /\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`|\/(?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\\n])+\/[a-z]*|[A-Za-z_$][\w$]*|\d+|\.\.\.|===|!==|=>|&&|\|\||[^\s]/gy;
|
|
10
|
+
let match: RegExpExecArray | null;
|
|
11
|
+
while ((match = lexer.exec(source))) {
|
|
12
|
+
const raw = match[0];
|
|
13
|
+
if (/^\s|^\/\//.test(raw) || raw.startsWith('/*')) continue;
|
|
14
|
+
if (raw.startsWith('/') && raw.length > 1) { result.push({ text: '<regex>' }); continue; }
|
|
15
|
+
if (raw.startsWith('`')) { result.push({ text: '<template>' }); continue; }
|
|
16
|
+
if (raw.startsWith("'") || raw.startsWith('"')) {
|
|
17
|
+
// Escapes cannot establish executable/path identity in this bounded profile.
|
|
18
|
+
const value = raw.slice(1, -1);
|
|
19
|
+
result.push({ text: JSON.stringify(value), ...(value.includes('\\') ? {} : { string: value }) });
|
|
20
|
+
} else result.push({ text: raw });
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function literalVitestInvocations(source: string): string[][] {
|
|
26
|
+
const ts = tokens(source), code = ts.map(t => t.text).join(' ');
|
|
27
|
+
const functionOffsets = new Set<number>();
|
|
28
|
+
let offset = 0;
|
|
29
|
+
for (const token of ts) {
|
|
30
|
+
if (token.text === 'function' && token.string === undefined) functionOffsets.add(offset);
|
|
31
|
+
offset += token.text.length + 1;
|
|
32
|
+
}
|
|
33
|
+
// Only a local, typed or untyped three-argument wrapper forwarding argv unchanged
|
|
34
|
+
// to imported Node spawnSync at repository cwd, with no shell, is supported.
|
|
35
|
+
const wrappers = new Set<string>();
|
|
36
|
+
const header = /function ([\w$]+) \( ([\w$]+)(?: : string)? , ([\w$]+)(?: : readonly string \[ \])? , ([\w$]+)(?: : NodeJS \. ProcessEnv)?(?: ,)? \) (?:: void )?\{ /g;
|
|
37
|
+
for (const match of code.matchAll(header)) {
|
|
38
|
+
if (!functionOffsets.has(match.index!)) continue;
|
|
39
|
+
const [name, executable, args, env] = match.slice(1);
|
|
40
|
+
if (![name, executable, args, env].every(v => v && IDENTIFIER.test(v))) continue;
|
|
41
|
+
const prefix = `const result = spawnSync ( ${executable} , ${args} , { cwd : process . cwd ( ) , env : ${env} , stdio : "inherit" , } ) ;`;
|
|
42
|
+
const body = code.slice(match.index! + match[0].length);
|
|
43
|
+
// The recognized wrapper has exactly the usual nonzero/error check and throw.
|
|
44
|
+
const ending = /^ if \( result \. error !== undefined \|\| result \. status !== 0 \) \{ throw new Error \( <template> , \{ cause : result \. error , \}(?: ,)? \) ; \} \}/;
|
|
45
|
+
if (body.startsWith(prefix) && ending.test(body.slice(prefix.length))) wrappers.add(name!);
|
|
46
|
+
}
|
|
47
|
+
if (!/import \{ spawnSync \} from "node:child_process" ;/.test(code)) return [];
|
|
48
|
+
// A local shadow/reassignment makes forwarding ambiguous; fail closed.
|
|
49
|
+
for (const name of [...wrappers]) {
|
|
50
|
+
const declarations = ts.filter((t, i) => t.text === name && ts[i - 1]?.text === 'function').length;
|
|
51
|
+
if (declarations !== 1 || ts.some((token, i) => token.text === name
|
|
52
|
+
&& ts[i - 1]?.text !== 'function' && ts[i + 1]?.text !== '(')) wrappers.delete(name);
|
|
53
|
+
}
|
|
54
|
+
if (ts.some((token, i) => token.text === 'spawnSync'
|
|
55
|
+
&& ts[i + 1]?.text !== '('
|
|
56
|
+
&& !(ts[i - 1]?.text === '{' && ts[i - 2]?.text === 'import' && ts[i + 1]?.text === '}'))) return [];
|
|
57
|
+
if (/\bfunction spawnSync\b/.test(code)) return [];
|
|
58
|
+
const found: string[][] = [];
|
|
59
|
+
for (let i = 0; i < ts.length; i++) {
|
|
60
|
+
if (!wrappers.has(ts[i]!.text) || ts[i - 1]?.text === '.' || ts[i + 1]?.text !== '(' || ts[i + 2]?.string !== 'npx' || ts[i + 3]?.text !== ',' || ts[i + 4]?.text !== '[') continue;
|
|
61
|
+
const argv: string[] = []; let j = i + 5;
|
|
62
|
+
while (ts[j]?.string !== undefined) {
|
|
63
|
+
argv.push(ts[j]!.string!); j++;
|
|
64
|
+
if (ts[j]?.text !== ',') break;
|
|
65
|
+
j++;
|
|
66
|
+
}
|
|
67
|
+
if (ts[j]?.text !== ']' || ts[j + 1]?.text !== ',' || argv[0] !== 'vitest' || argv[1] !== 'run') continue;
|
|
68
|
+
const paths = argv.slice(2).filter(arg => !arg.startsWith('--'));
|
|
69
|
+
if (!paths.length || paths.some(arg => !SAFE_TEST.test(arg) || arg.split('/').includes('..'))) continue;
|
|
70
|
+
const options = argv.slice(2 + paths.length);
|
|
71
|
+
if (argv.slice(2, 2 + paths.length).some(arg => arg.startsWith('--')) || options.some(arg => !/^--maxWorkers=[1-9][0-9]*$/.test(arg)) || options.length > 1) continue;
|
|
72
|
+
found.push(argv);
|
|
73
|
+
}
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function literalMigrationCatalogNames(source: string): string[] {
|
|
78
|
+
const ts = tokens(source);
|
|
79
|
+
const key = (t: Token | undefined) => t?.string ?? t?.text;
|
|
80
|
+
if (!ts.some((t, i) => key(t) === 'checksum_sha256' && ts[i + 1]?.text === ':')) return [];
|
|
81
|
+
return [...new Set(ts.flatMap((t, i) => key(t) === 'migration_name' && ts[i + 1]?.text === ':'
|
|
82
|
+
&& ts[i + 2]?.string && /^[0-9]{4}_[a-z0-9_]+\.sql$/.test(ts[i + 2]!.string!)
|
|
83
|
+
? [ts[i + 2]!.string!] : []))].sort();
|
|
84
|
+
}
|