@you-agent-factory/factory-emulator 0.0.0 → 0.0.2

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 (69) hide show
  1. package/LICENSE.md +19 -1
  2. package/README.md +305 -286
  3. package/dist/compatibility.d.ts +19 -0
  4. package/dist/compatibility.js +156 -0
  5. package/dist/event-sink.d.ts +29 -0
  6. package/dist/event-sink.js +58 -0
  7. package/dist/generated/scenario-schema.d.ts +268 -0
  8. package/dist/generated/scenario-schema.js +319 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.js +8 -0
  11. package/dist/logical-move.d.ts +10 -0
  12. package/dist/logical-move.js +18 -0
  13. package/dist/recording-sink.d.ts +22 -0
  14. package/dist/recording-sink.js +122 -0
  15. package/dist/runtime-reference-conformance.d.ts +20 -0
  16. package/dist/runtime-reference-conformance.js +138 -0
  17. package/dist/runtime-reference-evidence.d.ts +9 -0
  18. package/dist/runtime-reference-evidence.js +193 -0
  19. package/dist/runtime-reference-fixtures.d.ts +7 -0
  20. package/dist/runtime-reference-fixtures.js +308 -0
  21. package/dist/runtime-reference.d.ts +48 -0
  22. package/dist/runtime-reference.js +143 -0
  23. package/dist/scenario-contracts.d.ts +81 -0
  24. package/dist/scenario-contracts.js +11 -0
  25. package/dist/scenario.d.ts +271 -0
  26. package/dist/scenario.js +333 -0
  27. package/dist/scheduler.d.ts +24 -0
  28. package/dist/scheduler.js +104 -0
  29. package/dist/session-contracts.d.ts +262 -0
  30. package/dist/session-contracts.js +74 -0
  31. package/dist/session.d.ts +4 -0
  32. package/dist/session.js +1490 -0
  33. package/dist/submission-replay.d.ts +14 -0
  34. package/dist/submission-replay.js +96 -0
  35. package/dist/submission-validation.d.ts +3 -0
  36. package/dist/submission-validation.js +113 -0
  37. package/examples/customer-support.scenario.v1.json +45 -0
  38. package/package.json +44 -22
  39. package/schema/scenario.schema.json +171 -0
  40. package/generated/factory-emulator-scenario.schema.json +0 -191
  41. package/generated/factory.schema.json +0 -2606
  42. package/src/contracts.ts +0 -41
  43. package/src/data-error.js +0 -21
  44. package/src/data-only.js +0 -208
  45. package/src/emulator.js +0 -195
  46. package/src/emulator.ts +0 -86
  47. package/src/examples.js +0 -86
  48. package/src/examples.ts +0 -11
  49. package/src/generated/factory-schema.d.ts +0 -3
  50. package/src/generated/factory-schema.js +0 -5
  51. package/src/generated/scenario-schema.d.ts +0 -4
  52. package/src/generated/scenario-schema.js +0 -6
  53. package/src/generated/scenario.ts +0 -103
  54. package/src/identity.js +0 -55
  55. package/src/index.js +0 -34
  56. package/src/index.ts +0 -102
  57. package/src/limits.js +0 -125
  58. package/src/parser.js +0 -113
  59. package/src/parser.ts +0 -56
  60. package/src/scheduler.js +0 -374
  61. package/src/semantics.js +0 -354
  62. package/src/semantics.ts +0 -38
  63. package/src/session.js +0 -1201
  64. package/src/session.ts +0 -324
  65. package/src/sinks.js +0 -118
  66. package/src/sinks.ts +0 -56
  67. package/src/support.js +0 -334
  68. package/src/support.ts +0 -15
  69. package/src/virtual-time.js +0 -36
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Generated by scripts/generate-scenario-schema.mjs from
3
+ * schema/scenario.schema.json. Do not edit this file directly.
4
+ */
5
+ export const generatedScenarioSchema = {
6
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
7
+ "$id": "https://you-agent-factory.dev/schemas/ui/factory-emulator/scenario/v1",
8
+ "title": "Factory Emulator Scenario v1",
9
+ "type": "object",
10
+ "additionalProperties": false,
11
+ "required": [
12
+ "schemaVersion",
13
+ "id",
14
+ "factory",
15
+ "seed",
16
+ "startAt",
17
+ "rules",
18
+ "unmatched"
19
+ ],
20
+ "properties": {
21
+ "schemaVersion": {
22
+ "const": "factory-emulator-scenario/v1"
23
+ },
24
+ "id": {
25
+ "$ref": "#/$defs/stableIdentity"
26
+ },
27
+ "factory": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "required": [
31
+ "name"
32
+ ],
33
+ "properties": {
34
+ "name": {
35
+ "$ref": "#/$defs/nonEmptyText128"
36
+ }
37
+ }
38
+ },
39
+ "seed": {
40
+ "type": "string",
41
+ "minLength": 1,
42
+ "maxLength": 256
43
+ },
44
+ "startAt": {
45
+ "type": "string",
46
+ "format": "date-time",
47
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$"
48
+ },
49
+ "rules": {
50
+ "type": "array",
51
+ "items": {
52
+ "$ref": "#/$defs/rule"
53
+ }
54
+ },
55
+ "unmatched": {
56
+ "$ref": "#/$defs/unmatched"
57
+ },
58
+ "initialSubmissions": {
59
+ "oneOf": [
60
+ {
61
+ "type": "array",
62
+ "items": {
63
+ "$ref": "#/$defs/initialSubmission"
64
+ }
65
+ },
66
+ {
67
+ "$ref": "#/$defs/submissionBatch"
68
+ }
69
+ ]
70
+ }
71
+ },
72
+ "$defs": {
73
+ "stableIdentity": {
74
+ "type": "string",
75
+ "minLength": 1,
76
+ "maxLength": 128,
77
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
78
+ },
79
+ "nonEmptyText128": {
80
+ "type": "string",
81
+ "minLength": 1,
82
+ "maxLength": 128
83
+ },
84
+ "plainText": {
85
+ "type": "string",
86
+ "maxLength": 65536
87
+ },
88
+ "feedbackText": {
89
+ "type": "string",
90
+ "maxLength": 4096
91
+ },
92
+ "activityLabel": {
93
+ "type": "string",
94
+ "minLength": 1,
95
+ "maxLength": 120
96
+ },
97
+ "inputSelector": {
98
+ "type": "object",
99
+ "additionalProperties": false,
100
+ "properties": {
101
+ "workType": {
102
+ "$ref": "#/$defs/nonEmptyText128"
103
+ },
104
+ "state": {
105
+ "$ref": "#/$defs/nonEmptyText128"
106
+ },
107
+ "name": {
108
+ "$ref": "#/$defs/nonEmptyText128"
109
+ }
110
+ }
111
+ },
112
+ "selector": {
113
+ "type": "object",
114
+ "additionalProperties": false,
115
+ "properties": {
116
+ "workstation": {
117
+ "$ref": "#/$defs/nonEmptyText128"
118
+ },
119
+ "worker": {
120
+ "$ref": "#/$defs/nonEmptyText128"
121
+ },
122
+ "input": {
123
+ "$ref": "#/$defs/inputSelector"
124
+ }
125
+ }
126
+ },
127
+ "cursor": {
128
+ "type": "object",
129
+ "additionalProperties": false,
130
+ "required": [
131
+ "scope",
132
+ "input"
133
+ ],
134
+ "properties": {
135
+ "scope": {
136
+ "const": "lineage"
137
+ },
138
+ "input": {
139
+ "const": "rootWorkId"
140
+ }
141
+ }
142
+ },
143
+ "outcome": {
144
+ "type": "object",
145
+ "additionalProperties": false,
146
+ "required": [
147
+ "result",
148
+ "durationMs"
149
+ ],
150
+ "properties": {
151
+ "result": {
152
+ "enum": [
153
+ "accepted",
154
+ "continued",
155
+ "rejected",
156
+ "failed"
157
+ ]
158
+ },
159
+ "durationMs": {
160
+ "type": "number",
161
+ "minimum": 0
162
+ },
163
+ "output": {
164
+ "$ref": "#/$defs/plainText"
165
+ },
166
+ "feedback": {
167
+ "$ref": "#/$defs/feedbackText"
168
+ },
169
+ "activityLabel": {
170
+ "$ref": "#/$defs/activityLabel"
171
+ },
172
+ "error": {
173
+ "$ref": "#/$defs/feedbackText"
174
+ }
175
+ }
176
+ },
177
+ "rule": {
178
+ "type": "object",
179
+ "additionalProperties": false,
180
+ "required": [
181
+ "id",
182
+ "selector",
183
+ "cursor",
184
+ "outcomes",
185
+ "exhaustion"
186
+ ],
187
+ "properties": {
188
+ "id": {
189
+ "$ref": "#/$defs/stableIdentity"
190
+ },
191
+ "selector": {
192
+ "$ref": "#/$defs/selector"
193
+ },
194
+ "cursor": {
195
+ "$ref": "#/$defs/cursor"
196
+ },
197
+ "outcomes": {
198
+ "type": "array",
199
+ "minItems": 1,
200
+ "items": {
201
+ "$ref": "#/$defs/outcome"
202
+ }
203
+ },
204
+ "exhaustion": {
205
+ "enum": [
206
+ "repeat-last",
207
+ "fail"
208
+ ]
209
+ }
210
+ }
211
+ },
212
+ "unmatched": {
213
+ "oneOf": [
214
+ {
215
+ "type": "object",
216
+ "additionalProperties": false,
217
+ "required": [
218
+ "behavior"
219
+ ],
220
+ "properties": {
221
+ "behavior": {
222
+ "const": "error"
223
+ }
224
+ }
225
+ },
226
+ {
227
+ "type": "object",
228
+ "additionalProperties": false,
229
+ "required": [
230
+ "behavior",
231
+ "outcome"
232
+ ],
233
+ "properties": {
234
+ "behavior": {
235
+ "const": "outcome"
236
+ },
237
+ "outcome": {
238
+ "$ref": "#/$defs/outcome"
239
+ }
240
+ }
241
+ }
242
+ ]
243
+ },
244
+ "initialSubmission": {
245
+ "type": "object",
246
+ "additionalProperties": false,
247
+ "required": [
248
+ "name",
249
+ "workType",
250
+ "state"
251
+ ],
252
+ "properties": {
253
+ "name": {
254
+ "$ref": "#/$defs/stableIdentity"
255
+ },
256
+ "workType": {
257
+ "$ref": "#/$defs/nonEmptyText128"
258
+ },
259
+ "state": {
260
+ "$ref": "#/$defs/nonEmptyText128"
261
+ },
262
+ "input": {
263
+ "$ref": "#/$defs/plainText"
264
+ },
265
+ "parent": {
266
+ "$ref": "#/$defs/stableIdentity"
267
+ }
268
+ }
269
+ },
270
+ "submissionRelation": {
271
+ "type": "object",
272
+ "additionalProperties": false,
273
+ "required": [
274
+ "type",
275
+ "sourceWorkName",
276
+ "targetWorkName"
277
+ ],
278
+ "properties": {
279
+ "type": {
280
+ "enum": [
281
+ "DEPENDS_ON",
282
+ "PARENT_CHILD",
283
+ "SPAWNED_BY"
284
+ ]
285
+ },
286
+ "sourceWorkName": {
287
+ "$ref": "#/$defs/stableIdentity"
288
+ },
289
+ "targetWorkName": {
290
+ "$ref": "#/$defs/stableIdentity"
291
+ },
292
+ "requiredState": {
293
+ "$ref": "#/$defs/nonEmptyText128"
294
+ }
295
+ }
296
+ },
297
+ "submissionBatch": {
298
+ "type": "object",
299
+ "additionalProperties": false,
300
+ "required": [
301
+ "works"
302
+ ],
303
+ "properties": {
304
+ "works": {
305
+ "type": "array",
306
+ "items": {
307
+ "$ref": "#/$defs/initialSubmission"
308
+ }
309
+ },
310
+ "relations": {
311
+ "type": "array",
312
+ "items": {
313
+ "$ref": "#/$defs/submissionRelation"
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ };
@@ -0,0 +1,8 @@
1
+ export { type FactoryEmulatorCompatibilityIssue, type FactoryEmulatorCompatibilityIssueCode, type FactoryEmulatorCompatibilityResult, inspectFactoryEmulatorCompatibility, writeFactoryEventsIfCompatible, } from "./compatibility.js";
2
+ export { type FactoryEventSink, FactoryEventSinkError, type FactoryEventSinkErrorCode, MemoryFactoryEventSink, type MemoryFactoryEventSinkOptions, } from "./event-sink.js";
3
+ export { RecordingFactoryEventSink, type RecordingFactoryEventSinkOptions, } from "./recording-sink.js";
4
+ export { FACTORY_EMULATOR_RUNTIME_REFERENCE_SCHEMA_VERSION, type FactoryEmulatorRuntimeReference, type FactoryEmulatorRuntimeReferenceIssue, type FactoryEmulatorRuntimeReferenceIssueCode, type FactoryEmulatorRuntimeReferenceProvenance, type FactoryEmulatorRuntimeReferenceSemantics, type FactoryEmulatorRuntimeReferenceTick, loadFactoryEmulatorRuntimeReferences, type SafeParseFactoryEmulatorRuntimeReferenceResult, safeParseFactoryEmulatorRuntimeReference, } from "./runtime-reference.js";
5
+ export { compareFactoryEmulatorRuntimeReference, type FactoryEmulatorRuntimeReferenceComparisonSurface, type FactoryEmulatorRuntimeReferenceConformanceResult, type FactoryEmulatorRuntimeReferenceDivergence, } from "./runtime-reference-conformance.js";
6
+ export { FACTORY_EMULATOR_SCENARIO_SCHEMA_VERSION, type FactoryEmulatorInitialSubmission, type FactoryEmulatorInitialSubmissions, type FactoryEmulatorOutcome, type FactoryEmulatorRule, type FactoryEmulatorRuleSelector, type FactoryEmulatorScenario, type FactoryEmulatorScenarioIssue, type FactoryEmulatorScenarioIssueCode, FactoryEmulatorScenarioValidationError, type FactoryEmulatorSubmissionBatch, type FactoryEmulatorSubmissionRelation, parseFactoryEmulatorScenario, type SafeParseFactoryEmulatorScenarioResult, safeParseFactoryEmulatorScenario, scenarioSchema, } from "./scenario.js";
7
+ export { createFactoryEmulatorSession, DEFAULT_FACTORY_EMULATOR_LIMITS, FACTORY_EMULATOR_LIMIT_HARD_CAPS, type FactoryEmulatorAdvanceReceipt, type FactoryEmulatorBudgetUsage, type FactoryEmulatorCloseReceipt, type FactoryEmulatorCommand, type FactoryEmulatorConfigurationDiagnostic, FactoryEmulatorConfigurationError, FactoryEmulatorDurationError, type FactoryEmulatorExecutionDiagnostic, FactoryEmulatorExecutionPausedError, FactoryEmulatorLifecycleError, type FactoryEmulatorLimits, type FactoryEmulatorNormalizedRelation, FactoryEmulatorPendingCommandError, type FactoryEmulatorPendingTransactionStatus, type FactoryEmulatorResetReceipt, type FactoryEmulatorSession, type FactoryEmulatorSessionCounters, type FactoryEmulatorSessionError, type FactoryEmulatorSessionOptions, type FactoryEmulatorSessionState, type FactoryEmulatorSessionStatus, type FactoryEmulatorSessionWork, type FactoryEmulatorStartReceipt, FactoryEmulatorSubmissionError, type FactoryEmulatorSubmitReceipt, type ResolvedFactoryEmulatorLimits, } from "./session.js";
8
+ export { type FactoryEmulatorReplayedWork, replayFactoryEmulatorSubmissions, } from "./submission-replay.js";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { inspectFactoryEmulatorCompatibility, writeFactoryEventsIfCompatible, } from "./compatibility.js";
2
+ export { FactoryEventSinkError, MemoryFactoryEventSink, } from "./event-sink.js";
3
+ export { RecordingFactoryEventSink, } from "./recording-sink.js";
4
+ export { FACTORY_EMULATOR_RUNTIME_REFERENCE_SCHEMA_VERSION, loadFactoryEmulatorRuntimeReferences, safeParseFactoryEmulatorRuntimeReference, } from "./runtime-reference.js";
5
+ export { compareFactoryEmulatorRuntimeReference, } from "./runtime-reference-conformance.js";
6
+ export { FACTORY_EMULATOR_SCENARIO_SCHEMA_VERSION, FactoryEmulatorScenarioValidationError, parseFactoryEmulatorScenario, safeParseFactoryEmulatorScenario, scenarioSchema, } from "./scenario.js";
7
+ export { createFactoryEmulatorSession, DEFAULT_FACTORY_EMULATOR_LIMITS, FACTORY_EMULATOR_LIMIT_HARD_CAPS, FactoryEmulatorConfigurationError, FactoryEmulatorDurationError, FactoryEmulatorExecutionPausedError, FactoryEmulatorLifecycleError, FactoryEmulatorPendingCommandError, FactoryEmulatorSubmissionError, } from "./session.js";
8
+ export { replayFactoryEmulatorSubmissions, } from "./submission-replay.js";
@@ -0,0 +1,10 @@
1
+ interface VisitCountGuard {
2
+ readonly type: string;
3
+ readonly workstation?: string;
4
+ readonly maxVisits?: number;
5
+ }
6
+ /** Evaluates the supported inclusive VISIT_COUNT guard set for one lineage. */
7
+ export declare function visitCountGuardsAllow(guards: readonly VisitCountGuard[], visits: Readonly<Record<string, number>>): boolean;
8
+ /** Carries the greatest contributing lineage counts and records this visit. */
9
+ export declare function visitsAfterTransition(inputs: readonly Readonly<Record<string, number>>[], workstation: string): Readonly<Record<string, number>>;
10
+ export {};
@@ -0,0 +1,18 @@
1
+ /** Evaluates the supported inclusive VISIT_COUNT guard set for one lineage. */
2
+ export function visitCountGuardsAllow(guards, visits) {
3
+ return guards.every(({ type, workstation, maxVisits }) => type === "VISIT_COUNT" &&
4
+ workstation !== undefined &&
5
+ maxVisits !== undefined &&
6
+ (visits[workstation] ?? 0) >= maxVisits);
7
+ }
8
+ /** Carries the greatest contributing lineage counts and records this visit. */
9
+ export function visitsAfterTransition(inputs, workstation) {
10
+ const visits = {};
11
+ for (const input of inputs) {
12
+ for (const [name, count] of Object.entries(input)) {
13
+ visits[name] = Math.max(visits[name] ?? 0, count);
14
+ }
15
+ }
16
+ visits[workstation] = (visits[workstation] ?? 0) + 1;
17
+ return visits;
18
+ }
@@ -0,0 +1,22 @@
1
+ import { type FactoryEvent, type FactoryRecording } from "@you-agent-factory/client";
2
+ import { type FactoryEventSink } from "./event-sink.js";
3
+ export interface RecordingFactoryEventSinkOptions {
4
+ /** Caller-owned recording metadata. The events collection is owned by the sink. */
5
+ readonly recording: Omit<FactoryRecording, "events">;
6
+ /** Maximum number of events retained. A batch that crosses this bound fails whole. */
7
+ readonly maxEvents: number;
8
+ /** Optional asynchronous boundary for backpressure or caller-controlled failure injection. */
9
+ readonly beforeWrite?: (events: readonly FactoryEvent[]) => Promise<void>;
10
+ }
11
+ /**
12
+ * Bounded, caller-owned canonical Factory recording. Concurrent writes are
13
+ * processed in call order and each proposed full recording is validated before
14
+ * the incoming batch becomes visible.
15
+ */
16
+ export declare class RecordingFactoryEventSink implements FactoryEventSink {
17
+ #private;
18
+ constructor(options: RecordingFactoryEventSinkOptions);
19
+ write(events: readonly FactoryEvent[]): Promise<void>;
20
+ snapshot(): FactoryRecording;
21
+ close(): Promise<void>;
22
+ }
@@ -0,0 +1,122 @@
1
+ import { compareFactoryEvents, safeParseFactoryRecording, } from "@you-agent-factory/client";
2
+ import { FactoryEventSinkError } from "./event-sink.js";
3
+ function cloneValue(value) {
4
+ return JSON.parse(JSON.stringify(value));
5
+ }
6
+ function eventFactoryName(event) {
7
+ const payload = event.payload;
8
+ if (typeof payload !== "object" || payload === null)
9
+ return undefined;
10
+ const factory = payload.factory;
11
+ if (typeof factory !== "object" || factory === null)
12
+ return undefined;
13
+ const name = factory.name;
14
+ return typeof name === "string" ? name : undefined;
15
+ }
16
+ function assertUniqueEventIds(events) {
17
+ const seen = new Set();
18
+ for (const event of events) {
19
+ if (seen.has(event.id)) {
20
+ throw new FactoryEventSinkError("duplicate_event_id", `Factory Event ID ${event.id} is already present in the recording.`);
21
+ }
22
+ seen.add(event.id);
23
+ }
24
+ }
25
+ function assertCanonicalOrder(events) {
26
+ for (let index = 1; index < events.length; index += 1) {
27
+ const previous = events[index - 1];
28
+ const current = events[index];
29
+ if (previous !== undefined &&
30
+ current !== undefined &&
31
+ compareFactoryEvents(previous, current) > 0) {
32
+ throw new FactoryEventSinkError("invalid_event_order", `Factory Event ${current.id} is not in canonical recording order.`);
33
+ }
34
+ }
35
+ }
36
+ function assertConsistentSessionIdentity(events) {
37
+ const sessionIds = new Set(events
38
+ .map((event) => event.context.sessionId)
39
+ .filter((sessionId) => sessionId !== undefined));
40
+ const hasMissingIdentity = events.some((event) => event.context.sessionId === undefined);
41
+ if (sessionIds.size > 1 || (sessionIds.size === 1 && hasMissingIdentity)) {
42
+ throw new FactoryEventSinkError("mixed_session_identity", "Factory recording events must use one consistent Factory Session identity.");
43
+ }
44
+ }
45
+ function assertConsistentFactoryIdentity(factory, events) {
46
+ const factoryNames = new Set();
47
+ if (factory?.name !== undefined)
48
+ factoryNames.add(factory.name);
49
+ for (const event of events) {
50
+ const name = eventFactoryName(event);
51
+ if (name !== undefined)
52
+ factoryNames.add(name);
53
+ }
54
+ if (factoryNames.size > 1) {
55
+ throw new FactoryEventSinkError("mixed_factory_identity", "Factory recording metadata and topology events must use one Factory identity.");
56
+ }
57
+ }
58
+ function validateRecording(recording) {
59
+ assertUniqueEventIds(recording.events);
60
+ assertCanonicalOrder(recording.events);
61
+ assertConsistentFactoryIdentity(recording.factory, recording.events);
62
+ assertConsistentSessionIdentity(recording.events);
63
+ const result = safeParseFactoryRecording(recording);
64
+ if (!result.success) {
65
+ throw new FactoryEventSinkError("invalid_recording", `Factory recording validation failed: ${result.issues[0]?.message ?? "unknown error"}`);
66
+ }
67
+ }
68
+ /**
69
+ * Bounded, caller-owned canonical Factory recording. Concurrent writes are
70
+ * processed in call order and each proposed full recording is validated before
71
+ * the incoming batch becomes visible.
72
+ */
73
+ export class RecordingFactoryEventSink {
74
+ #metadata;
75
+ #maxEvents;
76
+ #beforeWrite;
77
+ #events = [];
78
+ #acceptingWrites = true;
79
+ #tail = Promise.resolve();
80
+ #closePromise;
81
+ constructor(options) {
82
+ if (!Number.isSafeInteger(options.maxEvents) || options.maxEvents < 1) {
83
+ throw new FactoryEventSinkError("invalid_capacity", "Recording sink maxEvents must be a positive safe integer.");
84
+ }
85
+ this.#metadata = cloneValue(options.recording);
86
+ this.#maxEvents = options.maxEvents;
87
+ this.#beforeWrite = options.beforeWrite;
88
+ }
89
+ write(events) {
90
+ if (!this.#acceptingWrites) {
91
+ return Promise.reject(new FactoryEventSinkError("closed", "The event sink is closed."));
92
+ }
93
+ if (events.length === 0) {
94
+ return Promise.reject(new FactoryEventSinkError("empty_batch", "Factory Event batches must be non-empty."));
95
+ }
96
+ const proposedBatch = cloneValue(events);
97
+ const write = this.#tail.then(async () => {
98
+ await this.#beforeWrite?.(cloneValue(proposedBatch));
99
+ if (this.#events.length + proposedBatch.length > this.#maxEvents) {
100
+ throw new FactoryEventSinkError("capacity_exceeded", `The batch would exceed the configured ${this.#maxEvents}-event recording capacity.`);
101
+ }
102
+ const proposedRecording = {
103
+ ...this.#metadata,
104
+ events: [...this.#events, ...proposedBatch],
105
+ };
106
+ validateRecording(proposedRecording);
107
+ this.#events = proposedRecording.events;
108
+ });
109
+ this.#tail = write.catch(() => undefined);
110
+ return write;
111
+ }
112
+ snapshot() {
113
+ return cloneValue({ ...this.#metadata, events: this.#events });
114
+ }
115
+ close() {
116
+ if (this.#closePromise !== undefined)
117
+ return this.#closePromise;
118
+ this.#acceptingWrites = false;
119
+ this.#closePromise = this.#tail;
120
+ return this.#closePromise;
121
+ }
122
+ }
@@ -0,0 +1,20 @@
1
+ import type { FactoryEmulatorRuntimeReferenceIssue, FactoryEmulatorRuntimeReferenceSemantics } from "./runtime-reference.js";
2
+ export type FactoryEmulatorRuntimeReferenceComparisonSurface = keyof FactoryEmulatorRuntimeReferenceSemantics | "eventKinds";
3
+ export interface FactoryEmulatorRuntimeReferenceDivergence {
4
+ readonly fixture: string;
5
+ readonly logicalTick: number;
6
+ readonly surface: FactoryEmulatorRuntimeReferenceComparisonSurface;
7
+ readonly expected: unknown;
8
+ readonly actual: unknown;
9
+ }
10
+ export type FactoryEmulatorRuntimeReferenceConformanceResult = {
11
+ readonly matches: true;
12
+ } | {
13
+ readonly matches: false;
14
+ readonly validationIssues: readonly FactoryEmulatorRuntimeReferenceIssue[];
15
+ } | {
16
+ readonly matches: false;
17
+ readonly divergence: FactoryEmulatorRuntimeReferenceDivergence;
18
+ };
19
+ /** Runs the supported emulator subset and reports its first frozen-reference divergence. */
20
+ export declare function compareFactoryEmulatorRuntimeReference(input: unknown): Promise<FactoryEmulatorRuntimeReferenceConformanceResult>;
@@ -0,0 +1,138 @@
1
+ import { MemoryFactoryEventSink } from "./event-sink.js";
2
+ import { safeParseFactoryEmulatorRuntimeReference } from "./runtime-reference.js";
3
+ import { createFactoryEmulatorSession } from "./session.js";
4
+ import { replayFactoryEmulatorSubmissions } from "./submission-replay.js";
5
+ function normalizedSemantics(reference, events, batch, state) {
6
+ const terminalStates = new Set((reference.factory.workTypes ?? []).flatMap(({ name, states }) => (states ?? [])
7
+ .filter(({ type }) => type === "TERMINAL" || type === "FAILED")
8
+ .map(({ name: state }) => `${name}:${state}`)));
9
+ const works = "works" in state ? state.works : [];
10
+ const workByToken = new Map(works.map((work) => [work.tokenId, work]));
11
+ const dispatched = works.flatMap(({ phase, dispatch }) => phase === "active" && dispatch !== undefined
12
+ ? [
13
+ `${dispatch.workstation}:${dispatch.inputTokenIds.map((id) => workByToken.get(id)?.submissionId ?? id).join(",")}`,
14
+ ]
15
+ : []);
16
+ const responses = batch.filter(({ type }) => type === "DISPATCH_RESPONSE");
17
+ const routed = responses.flatMap(({ payload }) => {
18
+ const outputWork = payload.outputWork;
19
+ return Array.isArray(outputWork)
20
+ ? outputWork.flatMap((work) => {
21
+ const value = work;
22
+ return typeof value.workTypeName === "string" &&
23
+ typeof value.state?.name === "string"
24
+ ? [
25
+ `${value.workTypeName}:${value.state.name}:${JSON.stringify(value.payload)}`,
26
+ ]
27
+ : [];
28
+ })
29
+ : [];
30
+ });
31
+ return {
32
+ dispatchChoices: dispatched.sort(),
33
+ consumedWork: responses
34
+ .flatMap(({ context }) => context.workIds ?? [])
35
+ .map((workId) => works.find((work) => work.workId === workId)?.submissionId ?? workId)
36
+ .sort(),
37
+ outcomes: responses
38
+ .map(({ payload }) => String(payload.outcome))
39
+ .sort(),
40
+ routes: routed.sort(),
41
+ terminalStates: works
42
+ .filter((work) => terminalStates.has(`${work.workType}:${work.state}`))
43
+ .map((work) => `${work.workType}:${work.state}`)
44
+ .sort(),
45
+ replayProjection: replayFactoryEmulatorSubmissions(events)
46
+ .map((work) => `${work.submissionId}:${work.workType ?? ""}:${work.state ?? ""}`)
47
+ .sort(),
48
+ };
49
+ }
50
+ function firstDifference(reference, tick, actual) {
51
+ for (const surface of [
52
+ "eventKinds",
53
+ "dispatchChoices",
54
+ "consumedWork",
55
+ "outcomes",
56
+ "routes",
57
+ "terminalStates",
58
+ "replayProjection",
59
+ ]) {
60
+ const expectedValue = surface === "eventKinds" ? tick.eventKinds : tick.semantics[surface];
61
+ const actualValue = surface === "eventKinds" ? actual.eventKinds : actual.semantics[surface];
62
+ if (JSON.stringify(expectedValue) !== JSON.stringify(actualValue)) {
63
+ return {
64
+ fixture: reference.id,
65
+ logicalTick: tick.logicalTick,
66
+ surface,
67
+ expected: expectedValue,
68
+ actual: actualValue,
69
+ };
70
+ }
71
+ }
72
+ return undefined;
73
+ }
74
+ /** Runs the supported emulator subset and reports its first frozen-reference divergence. */
75
+ export async function compareFactoryEmulatorRuntimeReference(input) {
76
+ const parsed = safeParseFactoryEmulatorRuntimeReference(input);
77
+ if (!parsed.success) {
78
+ return { matches: false, validationIssues: parsed.issues };
79
+ }
80
+ const reference = parsed.data;
81
+ const sink = new MemoryFactoryEventSink({ maxEvents: 10_000 });
82
+ const emulator = createFactoryEmulatorSession({
83
+ factory: reference.factory,
84
+ scenario: reference.scenario,
85
+ sink,
86
+ });
87
+ const actual = [];
88
+ let events = [];
89
+ const append = (batch, state) => {
90
+ events = [...events, ...batch];
91
+ actual.push({
92
+ logicalTick: actual.length,
93
+ eventKinds: batch.map(({ type }) => type),
94
+ semantics: normalizedSemantics(reference, events, batch, state),
95
+ });
96
+ };
97
+ const started = await emulator.start();
98
+ for (const batch of started.batches)
99
+ append(batch, started.state);
100
+ while (true) {
101
+ const advanced = await emulator.advanceToNext();
102
+ for (const batch of advanced.batches)
103
+ append(batch, advanced.state);
104
+ if (advanced.status === "idle")
105
+ break;
106
+ }
107
+ for (const [index, tick] of reference.ticks.entries()) {
108
+ const observed = actual[index];
109
+ if (observed === undefined) {
110
+ return {
111
+ matches: false,
112
+ divergence: {
113
+ fixture: reference.id,
114
+ logicalTick: tick.logicalTick,
115
+ surface: "eventKinds",
116
+ expected: tick.eventKinds,
117
+ actual: undefined,
118
+ },
119
+ };
120
+ }
121
+ const divergence = firstDifference(reference, tick, observed);
122
+ if (divergence !== undefined)
123
+ return { matches: false, divergence };
124
+ }
125
+ if (actual.length !== reference.ticks.length) {
126
+ return {
127
+ matches: false,
128
+ divergence: {
129
+ fixture: reference.id,
130
+ logicalTick: reference.ticks.length,
131
+ surface: "eventKinds",
132
+ expected: undefined,
133
+ actual: actual[reference.ticks.length]?.eventKinds,
134
+ },
135
+ };
136
+ }
137
+ return { matches: true };
138
+ }