forge-workflow 0.1.0-beta.6 → 0.1.0-beta.7

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 (156) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/guides/MIGRATION.md +3 -3
  3. package/docs/reference/RELEASE.md +3 -3
  4. package/lib/commands/merge.js +2 -2
  5. package/lib/commands/push.js +13 -4
  6. package/lib/commands/shepherd.js +1 -1
  7. package/lib/commands/validate.js +13 -1
  8. package/lib/kernel/migrations.js +1 -1
  9. package/lib/kernel/sqlite-driver.js +1 -1
  10. package/lib/memory/router.js +1 -1
  11. package/lib/memory/usage-evidence.js +1 -1
  12. package/lib/pr-monitor/flow-monitor.js +2 -2
  13. package/lib/pr-monitor/reconcile-executor.js +1 -1
  14. package/lib/project-memory.js +1 -1
  15. package/lib/validation-receipt.js +190 -0
  16. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  17. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  18. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  19. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  20. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  21. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  22. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  23. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  24. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  25. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  26. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  27. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  28. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  29. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  30. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  31. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  32. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  33. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  34. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  35. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  36. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  37. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  38. package/node_modules/@forge/contracts/index.js +32 -0
  39. package/node_modules/@forge/contracts/package.json +35 -0
  40. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  41. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  42. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  43. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  44. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  45. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  46. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  47. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  48. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  49. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  50. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  51. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  52. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  53. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  54. package/node_modules/@forge/contracts/src/identity.js +42 -0
  55. package/node_modules/@forge/contracts/src/schema.js +72 -0
  56. package/node_modules/@forge/contracts/src/validate.js +305 -0
  57. package/node_modules/@forge/flow/index.js +114 -0
  58. package/node_modules/@forge/flow/package.json +34 -0
  59. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  60. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  61. package/node_modules/@forge/flow/src/executor.js +279 -0
  62. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  63. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  64. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  65. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  66. package/node_modules/@forge/memory/index.js +331 -0
  67. package/node_modules/@forge/memory/package.json +34 -0
  68. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  69. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  70. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  71. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  72. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  73. package/package.json +9 -1
  74. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  75. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  76. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  77. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  78. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  79. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  80. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  81. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  82. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  83. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  84. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  85. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  86. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  87. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  88. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  89. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  90. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  91. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  92. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  93. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  94. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  95. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  96. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  97. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  98. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  99. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  100. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  101. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  102. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  103. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  104. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  105. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  106. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  107. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  108. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  109. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  110. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  111. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  112. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  113. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  114. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  115. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  116. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  117. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  118. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  119. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  120. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  121. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  122. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  123. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  124. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  125. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  126. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  127. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  128. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  129. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  130. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  131. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  132. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  133. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  134. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  135. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  136. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  137. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  138. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  139. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  140. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  141. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  142. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  143. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  144. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  145. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  146. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  147. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  148. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  149. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  150. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  151. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  152. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  153. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  154. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  155. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  156. package/scripts/test.js +1 -1
@@ -0,0 +1,469 @@
1
+ "use strict";
2
+
3
+ const { createHash } = require("node:crypto");
4
+
5
+ const MAX_ATTEMPTS = 128;
6
+ const MAX_ELAPSED_MS = 86_400_000;
7
+ const MAX_EVENTS = 256;
8
+ const MAX_HISTORY = 256;
9
+ const MAX_GRACE_MS = 3_600_000;
10
+ const EVENT_TYPES = new Set([
11
+ "start",
12
+ "attempt",
13
+ "exit",
14
+ "cancel-requested",
15
+ "cancel-acknowledged",
16
+ "grace-expired",
17
+ "forced-kill-acknowledged",
18
+ "termination-acknowledged",
19
+ "orphan-detected",
20
+ "reap",
21
+ "tick",
22
+ "deadline",
23
+ ]);
24
+ const STATE_CLONE_LIMITS = Object.freeze({ maxBytes: 4_194_304, maxDepth: 8, maxNodes: 32_768 });
25
+ const MAX_EVENT_ID_LENGTH = 128;
26
+ const EVENT_DIGEST_BYTES = 64;
27
+ const STATE_OVERHEAD_RESERVE_BYTES = 32_768;
28
+ const SEEN_EVENT_ENTRY_OVERHEAD_BYTES = (MAX_EVENT_ID_LENGTH * 6) + 2 + EVENT_DIGEST_BYTES + 2 + 3;
29
+ const EVENT_BYTE_BUDGET = Math.floor((STATE_CLONE_LIMITS.maxBytes - STATE_OVERHEAD_RESERVE_BYTES) / MAX_EVENTS)
30
+ - SEEN_EVENT_ENTRY_OVERHEAD_BYTES;
31
+ const EVENT_CLONE_LIMITS = Object.freeze({ maxBytes: EVENT_BYTE_BUDGET, maxDepth: 8, maxNodes: 256 });
32
+
33
+ class ProcessLifecycleError extends Error {
34
+ constructor(code, message, details = {}) {
35
+ super(message);
36
+ this.name = "ProcessLifecycleError";
37
+ this.code = code;
38
+ this.details = details;
39
+ }
40
+ }
41
+
42
+ function fail(code, message, details) {
43
+ throw new ProcessLifecycleError(code, message, details);
44
+ }
45
+
46
+ function ownData(value, key, name) {
47
+ let descriptor;
48
+ try {
49
+ descriptor = Object.getOwnPropertyDescriptor(value, key);
50
+ } catch {
51
+ fail("INVALID_INPUT", `${name || key} cannot be inspected`);
52
+ }
53
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) fail("INVALID_INPUT", `${name || key} must be a data property`);
54
+ return descriptor.value;
55
+ }
56
+
57
+ function readDescriptor(value, key, name = key) {
58
+ try {
59
+ return Object.getOwnPropertyDescriptor(value, key);
60
+ } catch {
61
+ fail("INVALID_INPUT", `${name} cannot be inspected`);
62
+ }
63
+ }
64
+
65
+ function assertDataProperties(value, name) {
66
+ let keys;
67
+ try {
68
+ keys = Reflect.ownKeys(value);
69
+ } catch {
70
+ fail("INVALID_OPTIONS", `${name} cannot be inspected`);
71
+ }
72
+ for (const key of keys) {
73
+ if (typeof key !== "string") fail("INVALID_OPTIONS", `${name} contains a symbol key`);
74
+ const descriptor = readDescriptor(value, key, `${name}.${key}`);
75
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) fail("INVALID_OPTIONS", `${name}.${key} must be a data property`);
76
+ }
77
+ }
78
+
79
+ function optionalData(value, key) {
80
+ const descriptor = readDescriptor(value, key, key);
81
+ if (!descriptor) return { present: false, value: undefined };
82
+ if (!Object.hasOwn(descriptor, "value")) fail("INVALID_OPTIONS", `${key} must be a data property`);
83
+ return { present: true, value: descriptor.value };
84
+ }
85
+
86
+ function safeClone(value, limits = {}, depth = 0, seen = { nodes: 0 }) {
87
+ const maxDepth = limits.maxDepth ?? 8;
88
+ const maxNodes = limits.maxNodes ?? 256;
89
+ const maxBytes = limits.maxBytes ?? 16_384;
90
+ if (depth > maxDepth) fail("BOUNDED_INPUT", "value exceeds maximum depth");
91
+ seen.nodes += 1;
92
+ if (seen.nodes > maxNodes) fail("BOUNDED_INPUT", "value exceeds maximum nodes");
93
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
94
+ if (typeof value === "number") {
95
+ if (!Number.isFinite(value)) fail("INVALID_INPUT", "non-finite numbers are not accepted");
96
+ return value;
97
+ }
98
+ if (typeof value !== "object") fail("INVALID_INPUT", "functions, symbols, bigint, and undefined are not accepted");
99
+
100
+ let prototype;
101
+ let keys;
102
+ try {
103
+ prototype = Object.getPrototypeOf(value);
104
+ keys = Reflect.ownKeys(value);
105
+ } catch {
106
+ fail("INVALID_INPUT", "value cannot be inspected");
107
+ }
108
+ if (prototype !== Object.prototype && prototype !== null && !Array.isArray(value)) fail("INVALID_INPUT", "value must be plain data");
109
+ if (keys.some((key) => typeof key !== "string")) fail("INVALID_INPUT", "value contains a non-string key");
110
+
111
+ let clone;
112
+ if (Array.isArray(value)) {
113
+ const lengthDescriptor = readDescriptor(value, "length", "array length");
114
+ if (!lengthDescriptor || !Object.hasOwn(lengthDescriptor, "value") || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value > maxNodes) {
115
+ fail("INVALID_INPUT", "array length is invalid");
116
+ }
117
+ clone = [];
118
+ for (let index = 0; index < lengthDescriptor.value; index += 1) {
119
+ const descriptor = readDescriptor(value, String(index), `array index ${index}`);
120
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) fail("INVALID_INPUT", "array contains a hole or accessor");
121
+ clone.push(safeClone(descriptor.value, limits, depth + 1, seen));
122
+ }
123
+ if (keys.length !== lengthDescriptor.value + 1) fail("INVALID_INPUT", "array contains unsupported properties");
124
+ } else {
125
+ clone = {};
126
+ for (const key of keys) {
127
+ const descriptor = readDescriptor(value, key);
128
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) fail("INVALID_INPUT", "value contains an accessor");
129
+ Object.defineProperty(clone, key, {
130
+ configurable: true,
131
+ enumerable: true,
132
+ value: safeClone(descriptor.value, limits, depth + 1, seen),
133
+ writable: true,
134
+ });
135
+ }
136
+ }
137
+ try {
138
+ if (Buffer.byteLength(JSON.stringify(clone), "utf8") > maxBytes) fail("BOUNDED_INPUT", "value exceeds maximum bytes");
139
+ } catch (error) {
140
+ if (error instanceof ProcessLifecycleError) throw error;
141
+ fail("INVALID_INPUT", "value cannot be serialized");
142
+ }
143
+ return clone;
144
+ }
145
+
146
+ function stableStringify(value) {
147
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
148
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
149
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
150
+ }
151
+
152
+ function eventDigest(event) {
153
+ return createHash("sha256").update(stableStringify(event), "utf8").digest("hex");
154
+ }
155
+
156
+ function deepFreeze(value, seen = new Set()) {
157
+ if (!value || typeof value !== "object" || seen.has(value)) return value;
158
+ seen.add(value);
159
+ for (const key of Reflect.ownKeys(value)) {
160
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
161
+ if (descriptor && Object.hasOwn(descriptor, "value")) deepFreeze(descriptor.value, seen);
162
+ }
163
+ return Object.freeze(value);
164
+ }
165
+
166
+ function assertPlainRecord(value, name) {
167
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail("INVALID_INPUT", `${name} must be an object`);
168
+ try {
169
+ const prototype = Object.getPrototypeOf(value);
170
+ if (prototype !== Object.prototype && prototype !== null) fail("INVALID_INPUT", `${name} must be a plain object`);
171
+ } catch {
172
+ fail("INVALID_INPUT", `${name} cannot be inspected`);
173
+ }
174
+ }
175
+
176
+ function assertLimit(value, name, ceiling) {
177
+ if (!Number.isSafeInteger(value) || value < 1 || value > ceiling) fail("INVALID_LIMIT", `${name} must be a safe integer from 1 to ${ceiling}`);
178
+ }
179
+
180
+ function validateOptions(rawOptions) {
181
+ assertPlainRecord(rawOptions, "options");
182
+ assertDataProperties(rawOptions, "options");
183
+ const processId = ownData(rawOptions, "processId", "processId");
184
+ if (typeof processId !== "string" || processId.length === 0 || processId.length > 128) fail("INVALID_OPTIONS", "processId must be a bounded string");
185
+ const clock = ownData(rawOptions, "clock", "clock");
186
+ if (typeof clock !== "function") fail("INVALID_OPTIONS", "clock must be an injected function");
187
+ const graceMs = ownData(rawOptions, "graceMs", "graceMs");
188
+ assertLimit(graceMs, "graceMs", MAX_GRACE_MS);
189
+ const maxAttempts = ownData(rawOptions, "maxAttempts", "maxAttempts");
190
+ const maxElapsedMs = ownData(rawOptions, "maxElapsedMs", "maxElapsedMs");
191
+ const maxEvents = ownData(rawOptions, "maxEvents", "maxEvents");
192
+ const maxHistory = ownData(rawOptions, "maxHistory", "maxHistory");
193
+ assertLimit(maxAttempts, "maxAttempts", MAX_ATTEMPTS);
194
+ assertLimit(maxElapsedMs, "maxElapsedMs", MAX_ELAPSED_MS);
195
+ assertLimit(maxEvents, "maxEvents", MAX_EVENTS);
196
+ assertLimit(maxHistory, "maxHistory", MAX_HISTORY);
197
+ if (maxHistory > maxEvents) fail("INVALID_LIMIT", "maxHistory cannot exceed maxEvents");
198
+ return { processId, clock, graceMs, maxAttempts, maxElapsedMs, maxEvents, maxHistory };
199
+ }
200
+
201
+ function readNow(options) {
202
+ let now;
203
+ try {
204
+ now = options.now === undefined ? options.clock() : options.now;
205
+ } catch {
206
+ fail("CLOCK_FAILURE", "injected clock failed");
207
+ }
208
+ if (!Number.isSafeInteger(now) || now < 0) fail("CLOCK_INVALID", "injected clock must return a non-negative safe integer");
209
+ return now;
210
+ }
211
+
212
+ function normalizeEvent(rawEvent) {
213
+ const event = safeClone(rawEvent, EVENT_CLONE_LIMITS);
214
+ assertPlainRecord(event, "event");
215
+ if (typeof event.id !== "string" || event.id.length === 0 || event.id.length > 128) fail("INVALID_EVENT", "event id must be a bounded string");
216
+ if (!EVENT_TYPES.has(event.type)) fail("INVALID_EVENT", `unsupported process lifecycle event: ${String(event.type)}`);
217
+ if (event.code !== undefined && (!Number.isSafeInteger(event.code) || event.code < 0)) fail("INVALID_EVENT", "exit code must be a non-negative safe integer");
218
+ if (event.exitCode !== undefined && (!Number.isSafeInteger(event.exitCode) || event.exitCode < 0)) fail("INVALID_EVENT", "exitCode must be a non-negative safe integer");
219
+ if (event.code !== undefined && event.exitCode !== undefined && event.code !== event.exitCode) fail("INVALID_EVENT", "code and exitCode conflict");
220
+ if (event.signal !== undefined && event.signal !== null && (typeof event.signal !== "string" || event.signal.length > 32)) fail("INVALID_EVENT", "signal is invalid");
221
+ if (event.amount !== undefined && (!Number.isSafeInteger(event.amount) || event.amount < 1)) fail("INVALID_EVENT", "event amount must be a positive safe integer");
222
+ if (event.childReaped !== undefined && typeof event.childReaped !== "boolean") fail("INVALID_EVENT", "childReaped must be boolean");
223
+ if (event.at !== undefined && (!Number.isSafeInteger(event.at) || event.at < 0)) fail("INVALID_EVENT", "event timestamp is invalid");
224
+ return event;
225
+ }
226
+
227
+ function cloneResult(state, effects) {
228
+ return {
229
+ state: deepFreeze(safeClone(state, STATE_CLONE_LIMITS)),
230
+ effects: deepFreeze(safeClone(effects)),
231
+ };
232
+ }
233
+
234
+ function createState(options, now) {
235
+ return {
236
+ processId: options.processId,
237
+ phase: "READY",
238
+ status: "PENDING",
239
+ terminal: false,
240
+ terminalReason: null,
241
+ attempts: 0,
242
+ elapsedMs: 0,
243
+ startAt: null,
244
+ lastAt: now,
245
+ cancelRequestedAt: null,
246
+ eventCount: 0,
247
+ history: [],
248
+ seenEvents: {},
249
+ exitCode: null,
250
+ signal: null,
251
+ cancellationRequested: false,
252
+ terminationAcknowledged: false,
253
+ childReaped: false,
254
+ forcedKill: false,
255
+ orphaned: false,
256
+ };
257
+ }
258
+
259
+ function createProcessState(rawOptions, now = 0) {
260
+ const options = validateOptions(rawOptions);
261
+ if (!Number.isSafeInteger(now) || now < 0) fail("CLOCK_INVALID", "initial time is invalid");
262
+ return createState(options, now);
263
+ }
264
+
265
+ function isResourceLimitOutcome(reason) {
266
+ return reason === "ATTEMPT_CAP" || reason === "ELAPSED_CAP";
267
+ }
268
+
269
+ function terminalStatus(state) {
270
+ if (isResourceLimitOutcome(state.terminalReason)) return "INCOMPLETE";
271
+ if (state.cancellationRequested || state.forcedKill) return "CANCELLED";
272
+ if (state.orphaned) return "INCOMPLETE";
273
+ if (state.signal !== null) return "FAIL";
274
+ if (state.exitCode === null) return "INCOMPLETE";
275
+ return state.exitCode === 0 ? "PASS" : "FAIL";
276
+ }
277
+
278
+ function reduceProcessLifecycle(rawState, rawEvent, rawOptions = {}) {
279
+ assertPlainRecord(rawState, "state");
280
+ const event = normalizeEvent(rawEvent);
281
+ const state = safeClone(rawState, STATE_CLONE_LIMITS);
282
+ assertPlainRecord(rawOptions, "options");
283
+ assertDataProperties(rawOptions, "options");
284
+ const options = validateOptions(rawOptions);
285
+ const suppliedNow = optionalData(rawOptions, "now");
286
+ if (state.phase === "TERMINATION_ACKNOWLEDGED" && state.terminationAcknowledged !== true) {
287
+ fail("INVALID_STATE", "termination acknowledgement phase requires truthful acknowledgement");
288
+ }
289
+ const seen = state.seenEvents || {};
290
+ const digest = eventDigest(event);
291
+ if (Object.hasOwn(seen, event.id)) {
292
+ if (seen[event.id] !== digest) fail("IDENTITY_CONFLICT", "event identity conflict");
293
+ return cloneResult(state, []);
294
+ }
295
+ if (state.terminal || state.phase === "TERMINAL") fail("POST_TERMINAL_EVENT", "post-terminal event is rejected");
296
+ if (!Number.isSafeInteger(state.eventCount) || state.eventCount < 0) fail("INVALID_STATE", "event count is invalid");
297
+ if (state.eventCount >= options.maxEvents) fail("EVENT_CAP_EXCEEDED", "process lifecycle event cap exceeded");
298
+
299
+ const now = suppliedNow.value === undefined ? (event.at ?? state.lastAt) : suppliedNow.value;
300
+ if (!Number.isSafeInteger(now) || now < 0) fail("CLOCK_INVALID", "reducer time is invalid");
301
+ if (event.at !== undefined && suppliedNow.value !== undefined && event.at !== now) fail("NON_MONOTONIC_INPUT", "event timestamp does not match injected clock");
302
+ if (state.lastAt !== null && (!Number.isSafeInteger(state.lastAt) || now < state.lastAt)) fail("NON_MONOTONIC_CLOCK", "injected clock is non-monotonic");
303
+
304
+ const next = {
305
+ ...state,
306
+ lastAt: now,
307
+ eventCount: state.eventCount + 1,
308
+ history: [...(state.history || []), event].slice(-options.maxHistory),
309
+ seenEvents: { ...seen, [event.id]: digest },
310
+ };
311
+ if (next.startAt !== null) {
312
+ const elapsed = now - next.startAt;
313
+ if (!Number.isSafeInteger(elapsed) || elapsed < 0) fail("ELAPSED_OVERFLOW", "elapsed time overflow");
314
+ next.elapsedMs = elapsed;
315
+ }
316
+
317
+ const invalidPhase = (expected) => fail("INVALID_TRANSITION", `${event.type} requires ${expected}`);
318
+ let result;
319
+ switch (event.type) {
320
+ case "start":
321
+ if (state.phase !== "READY") invalidPhase("READY phase");
322
+ next.phase = "RUNNING";
323
+ next.status = "RUNNING";
324
+ next.startAt = now;
325
+ next.attempts = 1;
326
+ result = { state: next, effects: [{ type: "START_PROCESS", processId: options.processId }] };
327
+ break;
328
+ case "attempt":
329
+ if (state.phase !== "RUNNING") invalidPhase("RUNNING phase");
330
+ next.attempts += event.amount ?? 1;
331
+ if (!Number.isSafeInteger(next.attempts) || next.attempts > options.maxAttempts) {
332
+ next.phase = "CANCEL_REQUESTED";
333
+ next.status = "INCOMPLETE";
334
+ next.terminalReason = "ATTEMPT_CAP";
335
+ next.cancellationRequested = true;
336
+ next.cancelRequestedAt = now;
337
+ result = { state: next, effects: [{ type: "ATTEMPT_CAP", attempts: next.attempts }, { type: "REQUEST_TERMINATION" }] };
338
+ } else {
339
+ result = { state: next, effects: [{ type: "ATTEMPT", attempts: next.attempts }] };
340
+ }
341
+ break;
342
+ case "exit":
343
+ if (!["RUNNING", "CANCEL_REQUESTED", "ORPHANED"].includes(state.phase)) invalidPhase("RUNNING, CANCEL_REQUESTED, or ORPHANED phase");
344
+ if (event.code === undefined && event.exitCode === undefined && (event.signal === undefined || event.signal === null)) {
345
+ fail("INVALID_EVENT", "exit requires an exitCode or signal");
346
+ }
347
+ next.exitCode = event.exitCode ?? event.code ?? null;
348
+ next.signal = event.signal ?? null;
349
+ next.phase = "EXITED";
350
+ result = { state: next, effects: [{ type: "ACKNOWLEDGE_TERMINATION", code: next.exitCode, signal: next.signal }] };
351
+ break;
352
+ case "cancel-requested":
353
+ if (state.phase !== "RUNNING") invalidPhase("RUNNING phase");
354
+ next.phase = "CANCEL_REQUESTED";
355
+ next.status = "CANCELLING";
356
+ next.cancellationRequested = true;
357
+ next.cancelRequestedAt = now;
358
+ result = { state: next, effects: [{ type: "REQUEST_TERMINATION" }] };
359
+ break;
360
+ case "cancel-acknowledged":
361
+ if (state.phase !== "CANCEL_REQUESTED") invalidPhase("CANCEL_REQUESTED phase");
362
+ next.phase = "TERMINATION_ACKNOWLEDGED";
363
+ next.status = isResourceLimitOutcome(next.terminalReason) ? "INCOMPLETE" : "CANCELLED";
364
+ next.terminationAcknowledged = true;
365
+ result = { state: next, effects: [{ type: "REAP_CHILD" }] };
366
+ break;
367
+ case "grace-expired": {
368
+ if (state.phase !== "CANCEL_REQUESTED") invalidPhase("CANCEL_REQUESTED phase");
369
+ if (state.cancelRequestedAt === null || now - state.cancelRequestedAt < options.graceMs) fail("GRACE_NOT_EXPIRED", "termination grace period has not expired");
370
+ next.phase = "FORCE_KILL_REQUESTED";
371
+ next.status = "INCOMPLETE";
372
+ next.forcedKill = true;
373
+ next.terminalReason = "GRACE_EXPIRED";
374
+ result = { state: next, effects: [{ type: "FORCE_KILL" }] };
375
+ break;
376
+ }
377
+ case "forced-kill-acknowledged":
378
+ if (state.phase !== "FORCE_KILL_REQUESTED") invalidPhase("FORCE_KILL_REQUESTED phase");
379
+ next.phase = "TERMINATION_ACKNOWLEDGED";
380
+ next.status = "CANCELLED";
381
+ next.signal = event.signal ?? "SIGKILL";
382
+ next.terminationAcknowledged = true;
383
+ result = { state: next, effects: [{ type: "REAP_CHILD" }] };
384
+ break;
385
+ case "termination-acknowledged":
386
+ if (!["EXITED", "ORPHANED"].includes(state.phase)) invalidPhase("EXITED or ORPHANED phase");
387
+ next.phase = "TERMINATION_ACKNOWLEDGED";
388
+ next.terminationAcknowledged = true;
389
+ if (state.orphaned) next.status = "INCOMPLETE";
390
+ result = { state: next, effects: [{ type: "REAP_CHILD" }] };
391
+ break;
392
+ case "orphan-detected":
393
+ if (!["RUNNING", "EXITED"].includes(state.phase)) invalidPhase("RUNNING or EXITED phase");
394
+ next.phase = "ORPHANED";
395
+ next.status = "INCOMPLETE";
396
+ next.orphaned = true;
397
+ next.terminalReason = "ORPHANED";
398
+ result = { state: next, effects: [{ type: "REAP_ORPHAN" }] };
399
+ break;
400
+ case "reap":
401
+ if (state.phase !== "TERMINATION_ACKNOWLEDGED" || state.terminationAcknowledged !== true) {
402
+ invalidPhase("truthful termination acknowledgement and reaping");
403
+ }
404
+ if (event.childReaped !== true) fail("REAP_NOT_ACKNOWLEDGED", "child reaping acknowledgement is required");
405
+ next.childReaped = true;
406
+ next.phase = "TERMINAL";
407
+ next.terminal = true;
408
+ next.status = terminalStatus(next);
409
+ next.terminalReason = next.terminalReason || "REAPED";
410
+ result = { state: next, effects: [{ type: "REAP_COMPLETE" }] };
411
+ break;
412
+ case "tick":
413
+ case "deadline":
414
+ if (!["RUNNING", "CANCEL_REQUESTED"].includes(state.phase)) invalidPhase("RUNNING or CANCEL_REQUESTED phase");
415
+ if (next.elapsedMs >= options.maxElapsedMs) {
416
+ next.phase = "CANCEL_REQUESTED";
417
+ next.status = "INCOMPLETE";
418
+ next.terminalReason = "ELAPSED_CAP";
419
+ next.cancellationRequested = true;
420
+ next.cancelRequestedAt = state.cancelRequestedAt ?? now;
421
+ result = { state: next, effects: [{ type: "REQUEST_TERMINATION", reason: "ELAPSED_CAP" }] };
422
+ } else {
423
+ result = { state: next, effects: [] };
424
+ }
425
+ break;
426
+ default:
427
+ fail("INVALID_EVENT", `unsupported process lifecycle event: ${event.type}`);
428
+ }
429
+ return cloneResult(result.state, result.effects);
430
+ }
431
+
432
+ function createProcessLifecycle(rawOptions) {
433
+ const options = validateOptions(rawOptions);
434
+ // The first injected clock read belongs to the first transition. Construction is inert.
435
+ let current = createState(options, 0);
436
+ return Object.freeze({
437
+ dispatch(rawEvent) {
438
+ const event = normalizeEvent(rawEvent);
439
+ const digest = eventDigest(event);
440
+ if (Object.hasOwn(current.seenEvents, event.id)) {
441
+ const existing = current.seenEvents[event.id];
442
+ if (existing !== digest) fail("IDENTITY_CONFLICT", "event identity conflict");
443
+ return cloneResult(current, []);
444
+ }
445
+ const result = reduceProcessLifecycle(current, event, { ...options, now: readNow(options) });
446
+ current = result.state;
447
+ return result;
448
+ },
449
+ snapshot() {
450
+ return deepFreeze(safeClone(current, STATE_CLONE_LIMITS));
451
+ },
452
+ serialize() {
453
+ return stableStringify(safeClone(current, STATE_CLONE_LIMITS));
454
+ },
455
+ });
456
+ }
457
+
458
+ module.exports = {
459
+ MAX_ATTEMPTS,
460
+ EVENT_BYTE_BUDGET,
461
+ MAX_ELAPSED_MS,
462
+ MAX_EVENTS,
463
+ MAX_GRACE_MS,
464
+ MAX_HISTORY,
465
+ ProcessLifecycleError,
466
+ createProcessLifecycle,
467
+ createProcessState,
468
+ reduceProcessLifecycle,
469
+ };