codex-workflow-v2 2.0.0-beta.13.9 → 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.
Files changed (271) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +89 -29
  3. package/dist/src/alpha6/adoption.d.ts +2 -0
  4. package/dist/src/alpha6/adoption.js +22 -0
  5. package/dist/src/alpha6/adoption.js.map +1 -1
  6. package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
  7. package/dist/src/alpha6/captured-check-evidence.js +152 -0
  8. package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
  9. package/dist/src/alpha6/component-owner.d.ts +15 -1
  10. package/dist/src/alpha6/component-owner.js +44 -2
  11. package/dist/src/alpha6/component-owner.js.map +1 -1
  12. package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
  13. package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
  14. package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
  15. package/dist/src/alpha6/downstream-proof.d.ts +1 -25
  16. package/dist/src/alpha6/downstream-proof.js +0 -171
  17. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  18. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  19. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  20. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  21. package/dist/src/alpha6/milestone.d.ts +2 -0
  22. package/dist/src/alpha6/milestone.js +10 -1
  23. package/dist/src/alpha6/milestone.js.map +1 -1
  24. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  25. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  26. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  27. package/dist/src/alpha6/plan-integrity.js +7 -5
  28. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  29. package/dist/src/alpha6/remediation.d.ts +6 -21
  30. package/dist/src/alpha6/remediation.js +161 -476
  31. package/dist/src/alpha6/remediation.js.map +1 -1
  32. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  33. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  34. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  35. package/dist/src/beta1/project-transaction.d.ts +3 -1
  36. package/dist/src/beta1/project-transaction.js +6 -2
  37. package/dist/src/beta1/project-transaction.js.map +1 -1
  38. package/dist/src/checks/runner.d.ts +26 -0
  39. package/dist/src/checks/runner.js +223 -0
  40. package/dist/src/checks/runner.js.map +1 -0
  41. package/dist/src/checks/task-sync.d.ts +10 -0
  42. package/dist/src/checks/task-sync.js +41 -0
  43. package/dist/src/checks/task-sync.js.map +1 -0
  44. package/dist/src/checks/worker.d.ts +1 -0
  45. package/dist/src/checks/worker.js +154 -0
  46. package/dist/src/checks/worker.js.map +1 -0
  47. package/dist/src/cli-actions.d.ts +2 -2
  48. package/dist/src/cli-actions.js +3 -15
  49. package/dist/src/cli-actions.js.map +1 -1
  50. package/dist/src/cli.js +14 -98
  51. package/dist/src/cli.js.map +1 -1
  52. package/dist/src/contracts.d.ts +35 -32
  53. package/dist/src/dependency-provenance.d.ts +2 -2
  54. package/dist/src/dependency-provenance.js +12 -69
  55. package/dist/src/dependency-provenance.js.map +1 -1
  56. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  57. package/dist/src/domain/base-sync-conflict.js +32 -0
  58. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  59. package/dist/src/domain/step-start-admission.d.ts +4 -0
  60. package/dist/src/domain/step-start-admission.js +9 -0
  61. package/dist/src/domain/step-start-admission.js.map +1 -0
  62. package/dist/src/domain/virgin-registration.d.ts +3 -0
  63. package/dist/src/domain/virgin-registration.js +65 -0
  64. package/dist/src/domain/virgin-registration.js.map +1 -0
  65. package/dist/src/errors.d.ts +1 -1
  66. package/dist/src/errors.js.map +1 -1
  67. package/dist/src/gateway-handshake.js +0 -8
  68. package/dist/src/gateway-handshake.js.map +1 -1
  69. package/dist/src/git.d.ts +3 -1
  70. package/dist/src/git.js +41 -14
  71. package/dist/src/git.js.map +1 -1
  72. package/dist/src/graph.js +25 -3
  73. package/dist/src/graph.js.map +1 -1
  74. package/dist/src/index.d.ts +1 -0
  75. package/dist/src/navigation-actions.d.ts +9 -0
  76. package/dist/src/navigation-actions.js +72 -0
  77. package/dist/src/navigation-actions.js.map +1 -0
  78. package/dist/src/navigation-update-artifact.d.ts +5 -0
  79. package/dist/src/navigation-update-artifact.js +227 -0
  80. package/dist/src/navigation-update-artifact.js.map +1 -0
  81. package/dist/src/navigation-update.d.ts +39 -0
  82. package/dist/src/navigation-update.js +82 -0
  83. package/dist/src/navigation-update.js.map +1 -0
  84. package/dist/src/observation.js +52 -29
  85. package/dist/src/observation.js.map +1 -1
  86. package/dist/src/observed-routes.js +5 -6
  87. package/dist/src/observed-routes.js.map +1 -1
  88. package/dist/src/pending-review-update.d.ts +0 -13
  89. package/dist/src/pending-review-update.js +1 -6
  90. package/dist/src/pending-review-update.js.map +1 -1
  91. package/dist/src/reviewer.d.ts +1 -1
  92. package/dist/src/reviewer.js +20 -17
  93. package/dist/src/reviewer.js.map +1 -1
  94. package/dist/src/runtime.d.ts +3 -0
  95. package/dist/src/runtime.js +6 -0
  96. package/dist/src/runtime.js.map +1 -0
  97. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  98. package/dist/src/state/corrective-replan-executor.js +37 -1
  99. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  100. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  101. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  102. package/dist/src/state/corrective-replan-public.js +1 -1
  103. package/dist/src/state/corrective-replan-public.js.map +1 -1
  104. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  105. package/dist/src/state/corrective-replan-transaction.js +12 -11
  106. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  107. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  108. package/dist/src/state/corrective-yield-transaction.js +11 -10
  109. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  110. package/dist/src/state/lock.d.ts +4 -0
  111. package/dist/src/state/lock.js +21 -0
  112. package/dist/src/state/lock.js.map +1 -1
  113. package/dist/src/version.d.ts +1 -1
  114. package/dist/src/version.js +1 -1
  115. package/dist/src/version.js.map +1 -1
  116. package/dist/src/workflow-blocker-route.d.ts +8 -0
  117. package/dist/src/workflow-blocker-route.js +106 -0
  118. package/dist/src/workflow-blocker-route.js.map +1 -0
  119. package/dist/src/workflow.d.ts +48 -63
  120. package/dist/src/workflow.js +666 -1460
  121. package/dist/src/workflow.js.map +1 -1
  122. package/docs/autonomy-guardrails.md +26 -304
  123. package/docs/decisions.md +14 -112
  124. package/docs/development-flow.md +35 -238
  125. package/docs/project-memory.md +31 -50
  126. package/docs/release-app-evidence.md +190 -0
  127. package/docs/release.md +95 -388
  128. package/docs/updating-existing-project.md +19 -717
  129. package/package.json +11 -13
  130. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  131. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
  132. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  133. package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
  134. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  135. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  136. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  137. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -757
  138. package/references/state-machine.md +4 -4
  139. package/roles/technical-planner.md +1 -1
  140. package/schemas/corrective-decision-event.schema.json +3 -1
  141. package/schemas/project-knowledge-map.schema.json +3 -1
  142. package/schemas/remediation-event.schema.json +10 -1
  143. package/schemas/task.schema.json +46 -2
  144. package/schemas/transition-payloads.schema.json +16 -1
  145. package/src/alpha6/adoption.ts +1123 -0
  146. package/src/alpha6/captured-check-evidence.ts +132 -0
  147. package/src/alpha6/check-support-anchor.ts +128 -0
  148. package/src/alpha6/component-owner.ts +168 -0
  149. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  150. package/src/alpha6/downstream-proof.ts +520 -0
  151. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  152. package/src/alpha6/handoff.ts +1443 -0
  153. package/src/alpha6/journal.ts +473 -0
  154. package/src/alpha6/literal-test-invocation.ts +84 -0
  155. package/src/alpha6/mechanical-feasibility.ts +488 -0
  156. package/src/alpha6/milestone.ts +2192 -0
  157. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  158. package/src/alpha6/npm-check-contract.ts +47 -0
  159. package/src/alpha6/plan-integrity.ts +298 -0
  160. package/src/alpha6/plan-risk.ts +1480 -0
  161. package/src/alpha6/preexecution-replan.ts +187 -0
  162. package/src/alpha6/remediation-cause.ts +98 -0
  163. package/src/alpha6/remediation.ts +2438 -0
  164. package/src/alpha6/review.ts +1198 -0
  165. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  166. package/src/alpha6/store-sidecars.ts +335 -0
  167. package/src/alpha7/autonomy.ts +411 -0
  168. package/src/alpha7/corrective-recovery.ts +1332 -0
  169. package/src/artifacts.ts +130 -0
  170. package/src/beta1/project-transaction.ts +355 -0
  171. package/src/change-explanation.ts +153 -0
  172. package/src/checks/runner.ts +245 -0
  173. package/src/checks/task-sync.ts +41 -0
  174. package/src/checks/worker.ts +149 -0
  175. package/src/cli-actions.ts +107 -0
  176. package/src/cli.ts +1457 -0
  177. package/src/contracts.ts +1494 -0
  178. package/src/credential-output.ts +89 -0
  179. package/src/credential-transport.ts +215 -0
  180. package/src/delegation.ts +190 -0
  181. package/src/dependency-provenance.ts +472 -0
  182. package/src/diagnostics.ts +93 -0
  183. package/src/domain/base-sync-conflict.ts +35 -0
  184. package/src/domain/completed-step-carryover.ts +76 -0
  185. package/src/domain/discovery.ts +27 -0
  186. package/src/domain/plan-semantics.ts +58 -0
  187. package/src/domain/step-start-admission.ts +10 -0
  188. package/src/domain/validation.ts +177 -0
  189. package/src/domain/virgin-registration.ts +46 -0
  190. package/src/errors.ts +24 -0
  191. package/src/fs-utils.ts +61 -0
  192. package/src/gateway-handshake.ts +95 -0
  193. package/src/git.ts +183 -0
  194. package/src/graph.ts +342 -0
  195. package/src/historical-step-provenance.ts +136 -0
  196. package/src/index.ts +23 -0
  197. package/src/lifecycle/canonical-hash.ts +28 -0
  198. package/src/lifecycle/catalog.ts +202 -0
  199. package/src/lifecycle/compiler-inspection.ts +29 -0
  200. package/src/lifecycle/core-static-readiness.ts +132 -0
  201. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  202. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  203. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  204. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  205. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  206. package/src/lifecycle/corrective-replan.ts +843 -0
  207. package/src/lifecycle/evaluator.ts +48 -0
  208. package/src/lifecycle/fingerprint.ts +488 -0
  209. package/src/lifecycle/immutable.ts +8 -0
  210. package/src/lifecycle/implementation-table.ts +118 -0
  211. package/src/lifecycle/index.ts +8 -0
  212. package/src/lifecycle/schema-artifact.ts +263 -0
  213. package/src/lifecycle/semantic-registry.ts +572 -0
  214. package/src/lifecycle/types.ts +838 -0
  215. package/src/memory.ts +273 -0
  216. package/src/migration.ts +161 -0
  217. package/src/navigation-actions.ts +70 -0
  218. package/src/navigation-update-artifact.ts +198 -0
  219. package/src/navigation-update.ts +121 -0
  220. package/src/observation.ts +225 -0
  221. package/src/observed-routes.ts +660 -0
  222. package/src/operational-contract.ts +125 -0
  223. package/src/pending-review-update.ts +175 -0
  224. package/src/repository.ts +99 -0
  225. package/src/reviewer.ts +1879 -0
  226. package/src/runtime.ts +6 -0
  227. package/src/state/corrective-replan-executor.ts +818 -0
  228. package/src/state/corrective-replan-public-schema.ts +83 -0
  229. package/src/state/corrective-replan-public.ts +908 -0
  230. package/src/state/corrective-replan-transaction.ts +949 -0
  231. package/src/state/corrective-yield-executor.ts +327 -0
  232. package/src/state/corrective-yield-transaction.ts +730 -0
  233. package/src/state/lock.ts +902 -0
  234. package/src/state/store.ts +567 -0
  235. package/src/transition-core.ts +330 -0
  236. package/src/ulid.ts +24 -0
  237. package/src/version.ts +2 -0
  238. package/src/workflow-blocker-route.ts +109 -0
  239. package/src/workflow.ts +10172 -0
  240. package/docs/alpha7.1-implementation-brief.md +0 -268
  241. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  242. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  243. package/docs/beta1-stabilization-brief.md +0 -165
  244. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  245. package/docs/beta13.2-signal-review-recovery.md +0 -38
  246. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  247. package/docs/change-model.md +0 -118
  248. package/docs/delegated-approval.md +0 -254
  249. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  250. package/docs/pdf/README.md +0 -24
  251. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  252. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  253. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  254. package/docs/pdf/requirements.txt +0 -1
  255. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  256. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
  257. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  258. package/docs/pending-review-update.md +0 -15
  259. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  260. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  261. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  262. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  263. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  264. package/docs/problem-briefs/README.md +0 -120
  265. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  266. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  267. package/docs/split-required-recovery.md +0 -47
  268. package/docs/stable-release-defect-register.md +0 -730
  269. package/docs/validation-report.md +0 -182
  270. package/scripts/generate-pdf-docs.py +0 -524
  271. package/scripts/run-pdf-docs.mjs +0 -62
@@ -0,0 +1,335 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import type { SidecarEventRecord } from '../contracts.js';
5
+ import { WorkflowError } from '../errors.js';
6
+
7
+ const HASH_64_PATTERN = /^[a-f0-9]{64}$/;
8
+ const SIDECAR_NAME_PATTERN = /^[a-z][a-z0-9-]*\.jsonl$/;
9
+
10
+ type CanonicalJsonValue =
11
+ | null
12
+ | boolean
13
+ | number
14
+ | string
15
+ | CanonicalJsonValue[]
16
+ | { [key: string]: CanonicalJsonValue };
17
+
18
+ export interface PreparedSidecarAppend<T extends SidecarEventRecord> {
19
+ event: T;
20
+ content: string;
21
+ contentHash: string;
22
+ }
23
+
24
+ export interface SidecarEventValidationContext {
25
+ file: string;
26
+ line: number;
27
+ eventId: string;
28
+ }
29
+
30
+ export interface SidecarReadOptions<T extends SidecarEventRecord> {
31
+ validateEvent?: SidecarEventValidator<T>;
32
+ trustedRoot?: string;
33
+ }
34
+
35
+ export type SidecarAppendInput<T extends SidecarEventRecord> = Omit<T, 'previousEventHash' | 'eventHash'> & {
36
+ previousEventHash?: string | null;
37
+ eventHash?: string;
38
+ };
39
+
40
+ export type SidecarEventValidator<T extends SidecarEventRecord> = (
41
+ event: T,
42
+ context: SidecarEventValidationContext,
43
+ ) => void;
44
+
45
+ export function sha256Hex(content: string | Buffer): string {
46
+ return createHash('sha256').update(content).digest('hex');
47
+ }
48
+
49
+ export function canonicalJsonStringify(value: unknown): string {
50
+ return JSON.stringify(canonicalizeJson(value));
51
+ }
52
+
53
+ export function assertHash64(value: string, label: string): void {
54
+ if (!HASH_64_PATTERN.test(value)) {
55
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a sha256 hex digest.`, { value });
56
+ }
57
+ }
58
+
59
+ export function assertSafeSidecarName(name: string): void {
60
+ if (!name.trim()) {
61
+ throw new WorkflowError('INVALID_ARGUMENT', 'Sidecar name is required.');
62
+ }
63
+ if (path.isAbsolute(name) || name.includes('/') || name.includes('\\') || !SIDECAR_NAME_PATTERN.test(name)) {
64
+ throw new WorkflowError('INVALID_ARGUMENT', `Invalid sidecar path: ${name}`, {
65
+ expectedPattern: SIDECAR_NAME_PATTERN.source,
66
+ });
67
+ }
68
+ }
69
+
70
+ export function resolveSidecarPath(entityRoot: string, sidecarName: string, trustedRoot?: string): string {
71
+ assertSafeSidecarName(sidecarName);
72
+ assertNoSymlinkPathComponents(entityRoot, {
73
+ label: 'Sidecar entity root',
74
+ requireLeaf: true,
75
+ ...(trustedRoot ? { trustedRoot } : {}),
76
+ });
77
+ return path.join(entityRoot, sidecarName);
78
+ }
79
+
80
+ export function computeSidecarEventHash<T extends SidecarEventRecord>(event: Omit<T, 'eventHash'> | T): string {
81
+ const { eventHash: _eventHash, ...payload } = event as Record<string, unknown>;
82
+ return sha256Hex(canonicalJsonStringify(payload));
83
+ }
84
+
85
+ export function renderSidecarEvent<T extends SidecarEventRecord>(event: T): string {
86
+ return `${canonicalJsonStringify(event)}\n`;
87
+ }
88
+
89
+ export function renderSidecarEvents<T extends SidecarEventRecord>(events: readonly T[]): string {
90
+ return events.map((event) => renderSidecarEvent(event)).join('');
91
+ }
92
+
93
+ export function readSidecarEvents<T extends SidecarEventRecord>(file: string, options: SidecarReadOptions<T> = {}): T[] {
94
+ assertNoSymlinkPathComponents(file, {
95
+ label: 'Sidecar path',
96
+ ...(options.trustedRoot ? { trustedRoot: options.trustedRoot } : {}),
97
+ });
98
+ if (!existsSync(file)) return [];
99
+ const content = readFileSync(file, 'utf8');
100
+ if (content.length === 0) {
101
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar log must not be empty: ${file}`);
102
+ }
103
+ const rawLines = content.split('\n');
104
+ if (rawLines.at(-1) === '') rawLines.pop();
105
+ if (rawLines.some((line) => line.length === 0)) {
106
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar log contains a blank line: ${file}`);
107
+ }
108
+ const events: T[] = [];
109
+ const eventIds = new Set<string>();
110
+ let previousEventHash: string | null = null;
111
+ for (const [index, rawLine] of rawLines.entries()) {
112
+ let parsed: unknown;
113
+ try {
114
+ parsed = JSON.parse(rawLine);
115
+ } catch (error) {
116
+ throw new WorkflowError('STATE_CORRUPT', `Invalid sidecar JSONL: ${file}`, {
117
+ line: index + 1,
118
+ cause: error instanceof Error ? error.message : String(error),
119
+ });
120
+ }
121
+ const event = parsed as T;
122
+ validateSidecarEventShape(event, file, index + 1);
123
+ runSidecarEventValidator(event, options.validateEvent, {
124
+ file,
125
+ line: index + 1,
126
+ eventId: event.eventId,
127
+ });
128
+ if (eventIds.has(event.eventId)) {
129
+ throw new WorkflowError('STATE_CORRUPT', `Duplicate sidecar event id in ${file}`, {
130
+ eventId: event.eventId,
131
+ line: index + 1,
132
+ });
133
+ }
134
+ eventIds.add(event.eventId);
135
+ if (event.previousEventHash !== previousEventHash) {
136
+ throw new WorkflowError('STATE_CORRUPT', `Broken sidecar event chain in ${file}`, {
137
+ line: index + 1,
138
+ expectedPreviousEventHash: previousEventHash,
139
+ actualPreviousEventHash: event.previousEventHash,
140
+ });
141
+ }
142
+ const expectedEventHash = computeSidecarEventHash(event);
143
+ if (event.eventHash !== expectedEventHash) {
144
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar event hash mismatch in ${file}`, {
145
+ line: index + 1,
146
+ eventId: event.eventId,
147
+ expectedEventHash,
148
+ actualEventHash: event.eventHash,
149
+ });
150
+ }
151
+ previousEventHash = event.eventHash;
152
+ events.push(event);
153
+ }
154
+ return events;
155
+ }
156
+
157
+ export function prepareSidecarAppend<T extends SidecarEventRecord>(
158
+ existingEvents: readonly T[],
159
+ input: SidecarAppendInput<T>,
160
+ options: SidecarReadOptions<T> = {},
161
+ ): PreparedSidecarAppend<T> {
162
+ const { previousEventHash: providedPreviousEventHash, eventHash: providedEventHash, ...payload } = input;
163
+ const nextPreviousEventHash = existingEvents.length > 0 ? existingEvents[existingEvents.length - 1]!.eventHash : null;
164
+ if (providedPreviousEventHash !== undefined && providedPreviousEventHash !== nextPreviousEventHash) {
165
+ throw new WorkflowError('STATE_CONFLICT', 'Sidecar append previousEventHash does not match the current chain.', {
166
+ expectedPreviousEventHash: nextPreviousEventHash,
167
+ actualPreviousEventHash: providedPreviousEventHash,
168
+ });
169
+ }
170
+ const event = {
171
+ ...payload,
172
+ previousEventHash: nextPreviousEventHash,
173
+ } as Omit<T, 'eventHash'>;
174
+ validateSidecarEventShape({ ...event, eventHash: computeSidecarEventHash(event) } as T, '<pending-sidecar>', 1);
175
+ const duplicate = existingEvents.find((candidate) => candidate.eventId === event.eventId);
176
+ if (duplicate) {
177
+ throw new WorkflowError('INVALID_ARGUMENT', `Sidecar event id already exists: ${event.eventId}`);
178
+ }
179
+ const computedEventHash = computeSidecarEventHash(event);
180
+ if (providedEventHash !== undefined && providedEventHash !== computedEventHash) {
181
+ throw new WorkflowError('STATE_CONFLICT', 'Sidecar append eventHash does not match the canonical payload.', {
182
+ expectedEventHash: computedEventHash,
183
+ actualEventHash: providedEventHash,
184
+ });
185
+ }
186
+ const appendedEvent = { ...event, eventHash: computedEventHash } as T;
187
+ runSidecarEventValidator(appendedEvent, options.validateEvent, {
188
+ file: '<pending-sidecar>',
189
+ line: existingEvents.length + 1,
190
+ eventId: appendedEvent.eventId,
191
+ });
192
+ const content = renderSidecarEvents([...existingEvents, appendedEvent]);
193
+ return {
194
+ event: appendedEvent,
195
+ content,
196
+ contentHash: sha256Hex(content),
197
+ };
198
+ }
199
+
200
+ function canonicalizeJson(value: unknown): CanonicalJsonValue {
201
+ if (value === null) return null;
202
+ if (typeof value === 'string' || typeof value === 'boolean') return value;
203
+ if (typeof value === 'number') {
204
+ if (!Number.isFinite(value)) {
205
+ throw new WorkflowError('INVALID_ARGUMENT', 'Canonical JSON cannot represent non-finite numbers.');
206
+ }
207
+ return value;
208
+ }
209
+ if (Array.isArray(value)) {
210
+ return value.map((item) => canonicalizeJson(item));
211
+ }
212
+ if (typeof value === 'object') {
213
+ const record = value as Record<string, unknown>;
214
+ return Object.fromEntries(
215
+ Object.keys(record)
216
+ .sort()
217
+ .filter((key) => record[key] !== undefined)
218
+ .map((key) => [key, canonicalizeJson(record[key])]),
219
+ );
220
+ }
221
+ throw new WorkflowError('INVALID_ARGUMENT', `Canonical JSON does not support value type: ${typeof value}`);
222
+ }
223
+
224
+ function validateSidecarEventShape(event: SidecarEventRecord, file: string, line: number): void {
225
+ if (!event || typeof event !== 'object' || Array.isArray(event)) {
226
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar event must be an object: ${file}`, { line });
227
+ }
228
+ if (typeof event.eventId !== 'string' || !event.eventId.trim()) {
229
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar event is missing eventId: ${file}`, { line });
230
+ }
231
+ if (event.previousEventHash !== null) {
232
+ if (typeof event.previousEventHash !== 'string') {
233
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar event previousEventHash must be null or string: ${file}`, {
234
+ line,
235
+ eventId: event.eventId,
236
+ });
237
+ }
238
+ assertHash64(event.previousEventHash, `Sidecar event previousEventHash at ${file}:${line}`);
239
+ }
240
+ if (typeof event.recordedAt !== 'string' || !event.recordedAt.trim()) {
241
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar event is missing recordedAt: ${file}`, {
242
+ line,
243
+ eventId: event.eventId,
244
+ });
245
+ }
246
+ if (typeof event.eventHash !== 'string') {
247
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar event is missing eventHash: ${file}`, {
248
+ line,
249
+ eventId: event.eventId,
250
+ });
251
+ }
252
+ assertHash64(event.eventHash, `Sidecar event eventHash at ${file}:${line}`);
253
+ }
254
+
255
+ export function assertNoSymlinkPathComponents(
256
+ candidatePath: string,
257
+ options: { label?: string; requireLeaf?: boolean; trustedRoot?: string } = {},
258
+ ): void {
259
+ const label = options.label ?? 'Path';
260
+ const resolved = path.resolve(candidatePath);
261
+ if (options.trustedRoot) {
262
+ const resolvedTrustedRoot = path.resolve(options.trustedRoot);
263
+ if (resolved !== resolvedTrustedRoot && !resolved.startsWith(`${resolvedTrustedRoot}${path.sep}`)) {
264
+ throw new WorkflowError('INVALID_ARGUMENT', `${label} is outside of the trusted root: ${candidatePath}`, {
265
+ trustedRoot: resolvedTrustedRoot,
266
+ });
267
+ }
268
+ if (!existsSync(resolvedTrustedRoot)) {
269
+ throw new WorkflowError('STATE_CORRUPT', `${label} trusted root does not exist: ${candidatePath}`, {
270
+ trustedRoot: resolvedTrustedRoot,
271
+ });
272
+ }
273
+ let current = resolvedTrustedRoot;
274
+ const relativeSegments = path.relative(resolvedTrustedRoot, resolved).split(path.sep).filter(Boolean);
275
+ for (const [index, segment] of relativeSegments.entries()) {
276
+ current = path.join(current, segment);
277
+ if (!existsSync(current)) {
278
+ if (index === relativeSegments.length - 1 && options.requireLeaf !== true) return;
279
+ return;
280
+ }
281
+ if (lstatSync(current).isSymbolicLink()) {
282
+ throw new WorkflowError('STATE_CORRUPT', `${label} must not traverse a symlink: ${candidatePath}`, {
283
+ symlinkPath: current,
284
+ });
285
+ }
286
+ }
287
+ return;
288
+ }
289
+ const root = path.parse(resolved).root;
290
+ let current = root;
291
+ const segments = resolved.slice(root.length).split(path.sep).filter(Boolean);
292
+ if (!existsSync(root)) {
293
+ throw new WorkflowError('STATE_CORRUPT', `${label} root does not exist: ${candidatePath}`);
294
+ }
295
+ if (lstatSync(root).isSymbolicLink()) {
296
+ throw new WorkflowError('STATE_CORRUPT', `${label} root must not be a symlink: ${candidatePath}`);
297
+ }
298
+ for (const [index, segment] of segments.entries()) {
299
+ current = path.join(current, segment);
300
+ if (!existsSync(current)) {
301
+ if (index === segments.length - 1 && options.requireLeaf !== true) return;
302
+ return;
303
+ }
304
+ if (lstatSync(current).isSymbolicLink()) {
305
+ throw new WorkflowError('STATE_CORRUPT', `${label} must not traverse a symlink: ${candidatePath}`, {
306
+ symlinkPath: current,
307
+ });
308
+ }
309
+ }
310
+ }
311
+
312
+ function runSidecarEventValidator<T extends SidecarEventRecord>(
313
+ event: T,
314
+ validateEvent: SidecarEventValidator<T> | undefined,
315
+ context: SidecarEventValidationContext,
316
+ ): void {
317
+ if (!validateEvent) return;
318
+ try {
319
+ validateEvent(event, context);
320
+ } catch (error) {
321
+ if (error instanceof WorkflowError && error.code === 'STATE_CORRUPT') {
322
+ throw new WorkflowError(error.code, error.message, {
323
+ ...error.details,
324
+ file: context.file,
325
+ line: context.line,
326
+ eventId: context.eventId,
327
+ });
328
+ }
329
+ throw new WorkflowError('STATE_CORRUPT', `Sidecar event validation failed: ${context.file}`, {
330
+ line: context.line,
331
+ eventId: context.eventId,
332
+ cause: error instanceof Error ? error.message : String(error),
333
+ });
334
+ }
335
+ }