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

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
package/README.md CHANGED
@@ -1,286 +1,305 @@
1
- # `@you-agent-factory/factory-emulator`
2
-
3
- This package publishes the stable v1 JSON Schema and generated TypeScript
4
- contract for deterministic Factory-emulator scenarios. The schema is available
5
- as raw JSON from `@you-agent-factory/factory-emulator/schema`; the root module
6
- exports `scenarioSchema` and `SUPPORTED_SCENARIO_VERSION`.
7
-
8
- Every scenario declares a version, id, deterministic seed, UTC `startAt`,
9
- ordered rules, and explicit unmatched behavior. Rules use finite scripted
10
- outcomes with explicit exhaustion behavior. Initial submissions and lineage
11
- cursors are structurally represented here; the parser validates the scenario
12
- shape and supported Factory execution subset before emulation begins.
13
-
14
- `activityLabel` is optional, limited to 120 characters, and only represents
15
- transient emulator activity. It is never canonical Factory event content.
16
-
17
- The schema under `contracts/factory-emulator/` is authored source. Run
18
- `npm run generate` from this package to regenerate its committed schema and
19
- TypeScript artifacts. The TypeScript declarations are derived from the schema's
20
- properties, required fields, references, variants, and documented runtime
21
- constraints rather than a separately authored type template. Run
22
- `make generate-emulator` to verify they are current; when it reports drift,
23
- regenerate them and commit the resulting artifacts.
24
-
25
- ## Parsing a scenario for browser emulation
26
-
27
- `parseEmulatorScenario(scenario, factory)` is a pure preflight boundary. It
28
- validates the complete canonical Factory JSON Schema before applying the
29
- emulator's narrower capability policy, so accepted definitions can be emitted
30
- in canonical bootstrap events without contract drift. The package carries a
31
- generated byte-for-byte copy of the canonical Factory schema for this runtime
32
- validation; `make generate-emulator` checks that copy for freshness.
33
- The parser returns either the typed authored scenario and supported Factory
34
- definition, or structured diagnostics with a stable JSON Pointer `path`,
35
- `code`, `message`, and `expectation`. It never starts emulator activity or
36
- creates Factory events.
37
-
38
- The v1 emulator accepts static Petri Factory topology only. It rejects
39
- JavaScript orchestration, configured Factory resources or guards, and cron,
40
- repeater, or poller workstation scheduling before emulation starts. This keeps
41
- scripted browser behavior deterministic while the runtime support subset grows.
42
-
43
- ## Rule semantics
44
-
45
- Rules are evaluated in authored order; `selectEmulatorRule` returns the first
46
- matching rule and no later rule can change that result. Parsing rejects a later
47
- rule only when an earlier rule provably covers its supported domain, including a
48
- known initial-submission id covered by an earlier work-type rule. It deliberately
49
- does not report uncertain overlap as shadowing.
50
-
51
- `resolveEmulatorScenarioResult(scenario, submission, invocationIndex)` is a
52
- pure helper for a zero-based invocation count of the selected rule. It returns a
53
- scripted outcome while one is available, repeats the final outcome only for
54
- `repeatLast`, delegates only `useUnmatchedBehavior` exhaustion to the explicit
55
- unmatched behavior, and otherwise returns the explicit exhaustion rejection.
56
-
57
- Initial submission ids and rule ids must be unique. Initial submissions and
58
- work-type matchers must name Factory work types. Lineage cursors may target one
59
- known initial submission or one earlier `complete` scripted outcome; missing,
60
- forward, cyclic, and incompatible targets are rejected with diagnostics before
61
- emulation begins.
62
-
63
- ## Inspecting support and using examples
64
-
65
- `inspectEmulatorSupport()` returns the stable machine-readable v1 capability
66
- report. It covers the accepted static Factory subset, explicitly unsupported
67
- Factory behavior, matchers, outcomes, lineage cursors, exhaustion and unmatched
68
- options, initial-submission constraints, and `activityLabel` limits. The parser
69
- uses that same Factory support policy, so the report cannot advertise execution
70
- capabilities that parsing rejects.
71
-
72
- `emulatorScenarioExamples` exports a minimal scenario and a multi-rule scenario.
73
- The latter shows ordered priority matching, initial submissions, a scripted
74
- lineage cursor, finite exhaustion, explicit unmatched rejection, and transient
75
- `activityLabel` metadata. Both examples are validated through the public parser.
76
-
77
- ## Event sink and logical tick runtime
78
-
79
- `@you-agent-factory/factory-emulator` provides the transport-neutral,
80
- caller-owned `FactoryEventSink` contract used by Factory emulator hosts.
81
-
82
- `write` accepts one ordered canonical `FactoryEventBatch` asynchronously. Its
83
- only successful receipt means every event in the supplied order was accepted;
84
- partial success must reject. `close` is a separate asynchronous operation and
85
- does not implicitly accept a pending write. The values crossing the contract
86
- are data-only, structured-cloneable Factory event and receipt shapes, making
87
- the contract suitable for a future worker or process boundary.
88
-
89
- `createMemoryFactoryEventSink({ maxEvents })` retains only the newest complete
90
- batches that fit its event limit. `createFactoryRecordingSink({ sessionId,
91
- maxEvents })` produces one finite recording for that session. Both helpers
92
- structured-clone accepted batches and returned history, reject writes after
93
- `close`, and preserve each batch atomically when a recording would exceed its
94
- event bound.
95
-
96
- This package intentionally does not own replay history or depend on browser
97
- timers, React, Zustand, dashboard state, or transport code.
98
-
99
- `@you-agent-factory/client` is a peer dependency because it supplies the
100
- canonical Factory event and recording types. Consumers install both published
101
- packages; the emulator package does not embed a checkout-relative dependency.
102
- The peer range remains unconstrained while this dependency is type-only and no
103
- minimum compatible client release has been established.
104
-
105
- `createFactoryEmulator({ initialState, calculateTick, sink })` provides the
106
- small logical-tick boundary used by an emulator host. It calculates a complete
107
- batch from a detached committed-state snapshot, waits for `sink.write`, and
108
- only then commits the calculated next state. A concurrent `advance` rejects
109
- while the write is unresolved, ensuring no later tick is calculated or
110
- committed ahead of an unaccepted batch.
111
-
112
- If a sink rejects a tick, the emulator retains that detached batch and its
113
- calculated state as the only pending transaction. The next `advance` retries it
114
- with the same IDs, timestamps, contents, and order; `reset` explicitly
115
- discards it. `pending` and `status` expose detached recovery state and the last
116
- write or close error. An idle emulator remains open. When configured with
117
- `calculateClose`, `close` writes that caller-supplied terminal lifecycle batch,
118
- waits for it to be accepted, and then closes the sink. A pending rejected tick
119
- blocks close so canonical history cannot be skipped. A rejected terminal batch
120
- is likewise retained unchanged for the next close attempt; after its acceptance
121
- a failed sink close retries only `sink.close`, never the terminal write.
122
-
123
- ## Deterministic session start and reset
124
-
125
- `createFactoryEmulatorSession({ factory, scenario, sink })` owns one long-lived
126
- emulator lifecycle. `start()` revalidates the supported Factory and scenario
127
- before activity, normalizes `startAt` to a UTC instant, then writes an ordered
128
- topology/run bootstrap batch followed by one normalized initial-submission
129
- batch. The returned state becomes visible only after those writes are accepted.
130
-
131
- Session, request, trace, Work, token, dispatch, completion, and event identities are derived from canonical
132
- Factory/scenario inputs, the scenario seed, authored submission coordinates,
133
- authored lineage cursors, and logical sequence. Submitted token identities are
134
- recorded in `WORK_REQUEST` Work tags. Dispatch completion metadata records both
135
- the resolved lineage-token anchor and the resulting token identity, while the
136
- bounded Work snapshot retains the current token identity. The derivation reads
137
- no ambient time, randomness, locale, or process-global counter. Every startup
138
- event is stamped at virtual elapsed time zero (`startAt` after UTC normalization).
139
-
140
- `reset()` is available after a successful start. It clears runtime Work,
141
- virtual elapsed time, counters, and rule cursors and returns the session to
142
- `pre-start` without writing an event. Starting again with the same immutable
143
- configuration reproduces the original event bytes and committed snapshot;
144
- changing the seed creates a different deterministic identity stream.
145
-
146
- While started, `submit(work)` and `submit([work, ...])` accept the same
147
- `id`, `workType`, and optional data-only `input` shape as scenario initial
148
- submissions. The complete submission is detached and validated before the sink
149
- is called. One accepted batch adds all of its Work in caller order; any invalid
150
- member rejects the command without events, counter changes, or committed Work.
151
- Request, trace, Work, and event identities include stable command coordinates,
152
- so repeated sessions reproduce the same submission stream without relying on
153
- ambient counters.
154
-
155
- Scripted `complete` and `reject` outcomes may set a non-negative integer
156
- `durationMs`; omission means zero virtual milliseconds. `advanceToNext()`
157
- commits exactly one scheduler batch: ready Work starts at the current virtual
158
- instant, while the next step jumps to the earliest deadline and completes all
159
- dispatches due there in stable Work order. `advanceBy(durationMs)` processes
160
- each eligible batch through its inclusive target and, when Work remains open,
161
- ends exactly at that virtual instant. Waiting intervals move the virtual clock
162
- without synthetic events, while a fully idle session returns an event-free
163
- no-op receipt and stays open.
164
-
165
- All advancement timestamps are derived from normalized scenario `startAt` plus
166
- integer virtual elapsed milliseconds. The session creates no timers and owns no
167
- playback speed, pause, visibility, or replay-selection state.
168
-
169
- `status()` derives execution state from session facts. A started session with
170
- ready Work reports `ready`; one with no unfinished Work remains open and reports
171
- `idle`; unmatched unfinished Work configured to be ignored reports `waiting`.
172
- During an accepted submission write it reports `active`, while `state()`
173
- continues to expose only the prior committed snapshot. Once the sink accepts
174
- the batch, the new Work becomes visible and the session reports `ready` or
175
- `waiting` from its executable scenario facts.
176
-
177
- Every status value also reports the current virtual instant, cumulative
178
- completed-dispatch, event, and virtual-time usage with configured limits, and a
179
- bounded summary of any rejected sink transaction. Error details are plain data,
180
- including sink operation/message or execution-pause diagnostics. Status never
181
- contains canonical event history, playback controls, timers, visibility state,
182
- or framework-owned objects.
183
-
184
- Configuration and command inputs are validated before runtime ownership and
185
- must contain only plain objects, dense arrays, `null`, booleans, strings, and
186
- finite numbers. Structured-cloneable class instances such as `Date`, `Map`,
187
- `Set`, typed arrays, and regular expressions are rejected as configuration or
188
- submission errors instead of entering scripted outputs, state, retries, or
189
- events. Every receipt, status, pending batch, and state snapshot is a fresh
190
- structured-cloneable data value. Mutating any returned value cannot alter
191
- committed state or a later retry. The session retains current execution facts
192
- and at most one bounded recovery transaction; accepted canonical batch history
193
- belongs exclusively to the caller-owned sink.
194
-
195
- Every session command follows the same calculate/write/commit boundary. If a
196
- logical batch is rejected, `state()` and virtual time remain at the last
197
- accepted transition, `status()` reports `error`, and `pending()` returns a
198
- detached copy of the rejected batch. Only the same command with the same input
199
- may retry that transaction; the retained event values and calculated next
200
- state are reused without recalculation. `reset()` is the sole command that may
201
- discard an open rejected transaction.
202
-
203
- `close()` is explicit and terminal. It writes one deterministic `RUN_RESPONSE`
204
- batch at the current virtual instant, commits the closed lifecycle only after
205
- that batch is accepted, and then awaits `sink.close()`. A terminal write
206
- rejection retries the same event. If only `sink.close()` rejects, the next
207
- `close()` retries that operation without writing the terminal event again.
208
- Non-close commands cannot bypass either recovery state, and all state-changing
209
- commands reject after a successful close.
210
-
211
- ## Deterministic execution limits
212
-
213
- Sessions enforce cumulative execution limits before a calculated batch reaches
214
- the sink. Defaults are 1,000 completed dispatches, 10,000 canonical events, and
215
- 3,600,000 virtual milliseconds (one virtual hour). At one virtual instant, at
216
- most 1,000 scheduler batches may run without virtual-time progress; crossing
217
- that deterministic threshold is reported as a zero-duration cycle. Finite
218
- zero-duration chains at or below the threshold still complete normally.
219
-
220
- Callers may pass `limits` to `createFactoryEmulatorSession`. The limits object
221
- must satisfy the same plain-data ownership boundary as Factory and scenario
222
- configuration: class instances, custom or null prototypes, accessors, and
223
- cycles are rejected before any property is read or sink activity occurs. The
224
- accepted input is detached at construction, so later caller mutation cannot
225
- change execution policy. Every supplied value must be a positive safe integer
226
- and no greater than the exported
227
- `FACTORY_EMULATOR_LIMIT_HARD_CAPS`. `maxEvents` has a minimum of 2 because the
228
- mandatory bootstrap atomically emits `INITIAL_STRUCTURE_REQUEST` and
229
- `RUN_REQUEST`; smaller configurations fail before sink activity. The defaults
230
- and hard caps are exported as
231
- `DEFAULT_FACTORY_EMULATOR_LIMITS` and `FACTORY_EMULATOR_LIMIT_HARD_CAPS`:
232
-
233
- | Limit | Minimum | Default | Hard cap |
234
- | --- | ---: | ---: | ---: |
235
- | `maxCompletedDispatches` | 1 | 1,000 | 100,000 |
236
- | `maxEvents` | 2 | 10,000 | 1,000,000 |
237
- | `maxVirtualElapsedMs` | 1 | 3,600,000 | 31,536,000,000 |
238
- | `maxZeroDurationBatches` | 1 | 1,000 | 100,000 |
239
- | `maxSynchronousBatches` | 1 | 100 | 10,000 |
240
- | `maxSynchronousWorkItems` | 1 | 100 | 10,000 |
241
-
242
- Invalid policy fails at session construction, before bootstrap activity. All
243
- kernel-produced command errors are detached plain-data values with `name`,
244
- `message`, `code`, and family-specific diagnostic fields, so their complete
245
- contract survives `structuredClone` across worker and process boundaries.
246
- Caller-owned sink failures are propagated unchanged and are also projected as
247
- plain data through `status()`, with non-string or unprintable rejection messages
248
- normalized to safe strings. A batch that would exceed a cumulative limit is
249
- not written or committed.
250
- Instead, the command rejects with `FactoryEmulatorExecutionPausedError`, and
251
- `status()` reports `error` with a detached `budget-exceeded` or
252
- `zero-duration-cycle` diagnostic containing the limit name, configured and
253
- observed values, and current virtual instant. Existing Work remains in its last
254
- valid phase; the kernel never fabricates failed Work to represent a pause.
255
-
256
- Before an atomic scheduler batch is materialized, the session examines at most
257
- `maxSynchronousWorkItems` retained Work values and then yields with an explicit
258
- plain-data inspection continuation. That inspection returns the exact candidate
259
- indexes and lineage lookup consumed by pure calculation, so calculation does
260
- not rescan retained Work. Replacement materialization and detached command-state
261
- construction traverse retained Work with the same yield cadence instead of
262
- performing whole-state clones. The inspection counts at most one eligible Work
263
- beyond the limit. An oversized ready or due set pauses with a
264
- `synchronous-work-limit` diagnostic before dispatch/completion events or
265
- calculated next state are built. Initial and interactive submission batches use
266
- the same ceiling. Their array cardinality is checked before any member is
267
- validated or copied; oversized initial input fails session construction and an
268
- oversized interactive command pauses without sink, state, or pending-transaction
269
- residue. Cardinality inspection reads the array's own data descriptor inside an
270
- exception-safe boundary, so throwing, revoked, or otherwise uninspectable proxy
271
- inputs reject as detached command errors instead of leaking host exceptions.
272
- Configuration and command detachment failures are normalized through the same
273
- plain-data error boundary. The shared data-only validator uses an iterative
274
- traversal capped at 10,000 nodes and 100 levels before cloning, so a single
275
- deeply nested Work input cannot overflow the host stack or introduce an
276
- unbounded calculation batch.
277
-
278
- After each `maxSynchronousBatches` scheduler calculations, a long command awaits
279
- a host task boundary. The default uses the runtime task queue without a wall
280
- timer; hosts may inject `yieldControl` to own worker/process scheduling. Event
281
- order is unchanged, and the original command stays serialized until it finishes
282
- or pauses.
283
- `reset()` clears the pause and all usage counters, reproducing the same boundary
284
- for the same inputs. `close()` remains available after a safety pause and may
285
- write its single terminal lifecycle event even when the execution event budget
286
- is exhausted, ensuring the caller-owned sink can always be released.
1
+ # Factory emulator
2
+
3
+ `@you-agent-factory/factory-emulator` is a transport-neutral, non-React package
4
+ for deterministic Factory emulator contracts. Scenario parsing validates both
5
+ the package-local schema and references to a caller-supplied UI client
6
+ `FactoryDefinition`.
7
+
8
+ The hosted Go runtime remains authoritative for Factory execution. This package
9
+ implements only the deterministic browser subset documented in the
10
+ [public package family guide](https://github.com/portpowered/you-agent-factory/blob/main/ui/packages/README.md);
11
+ canonical-event-compatible output
12
+ does not make it a replacement for hosted execution.
13
+
14
+ ```ts
15
+ import factory from "./factory.json" with { type: "json" };
16
+ import scenario from "./scenario.json" with { type: "json" };
17
+ import { parseFactoryEmulatorScenario } from "@you-agent-factory/factory-emulator";
18
+
19
+ const parsed = parseFactoryEmulatorScenario(scenario, factory);
20
+ ```
21
+
22
+ The parser returns a detached scenario value. `safeParseFactoryEmulatorScenario`
23
+ returns all structure and semantic diagnostics without partially accepting an
24
+ invalid scenario.
25
+
26
+ Factories can be preflighted against the deterministic v1 execution subset
27
+ before any event history is emitted:
28
+
29
+ ```ts
30
+ import {
31
+ inspectFactoryEmulatorCompatibility,
32
+ writeFactoryEventsIfCompatible,
33
+ } from "@you-agent-factory/factory-emulator";
34
+
35
+ const compatibility = inspectFactoryEmulatorCompatibility(factory);
36
+ if (!compatibility.supported) {
37
+ console.error(compatibility.diagnostics);
38
+ }
39
+
40
+ // Incompatible Factories return every diagnostic without calling sink.write.
41
+ await writeFactoryEventsIfCompatible(factory, eventBatch, sink);
42
+ ```
43
+
44
+ The inspector treats an omitted orchestrator as the documented Petri default,
45
+ does not mutate or retain the Factory, and reports stable codes with paths into
46
+ the caller-supplied UI client `FactoryDefinition`.
47
+
48
+ ## Long-lived session lifecycle
49
+
50
+ Create a framework-independent session from a compatible Factory, a parsed
51
+ scenario, and a caller-owned event sink. Construction revalidates the inputs
52
+ and optional safety limits before any event is written.
53
+
54
+ ```ts
55
+ import {
56
+ createFactoryEmulatorSession,
57
+ MemoryFactoryEventSink,
58
+ } from "@you-agent-factory/factory-emulator";
59
+
60
+ const sink = new MemoryFactoryEventSink({ maxEvents: 10_000 });
61
+ const session = createFactoryEmulatorSession({
62
+ factory,
63
+ scenario: parsed,
64
+ sink,
65
+ });
66
+
67
+ const before = session.status();
68
+ const started = await session.start();
69
+ await session.submit({
70
+ name: "follow-up",
71
+ workType: "ticket",
72
+ state: "ready",
73
+ input: "Customer reply",
74
+ });
75
+
76
+ await session.submit({
77
+ works: [
78
+ { name: "reply", workType: "ticket", state: "ready" },
79
+ { name: "approval", workType: "ticket", state: "ready" },
80
+ ],
81
+ relations: [
82
+ {
83
+ type: "DEPENDS_ON",
84
+ sourceWorkName: "reply",
85
+ targetWorkName: "approval",
86
+ // Omit only when the target Work Type declares the default "complete".
87
+ requiredState: "classified",
88
+ },
89
+ ],
90
+ });
91
+ const dispatched = await session.advanceToNext();
92
+ const advanced = await session.advanceBy(250);
93
+ const current = session.state();
94
+ const closed = await session.close();
95
+ ```
96
+
97
+ `start()` writes one atomic canonical bootstrap batch in `RUN_REQUEST`,
98
+ `INITIAL_STRUCTURE_REQUEST`, `SESSION_STARTED` order. The started state becomes
99
+ visible only after the sink accepts the complete batch. `state()` and
100
+ `status()` return detached, structured-cloneable snapshots; the kernel does not
101
+ retain playback or event history. An idle session remains open for later Work.
102
+
103
+ `submit()` accepts one validated scenario Work value, a relationship-free Work
104
+ array, or a `{ works, relations }` batch. `DEPENDS_ON` is the only supported
105
+ emulator relationship; both endpoints must be names from that batch. The
106
+ complete request is validated before its atomic sink write, so an invalid item
107
+ or graph cannot partially create Work. Submissions remain available while
108
+ other Work is active and after the session returns to idle.
109
+
110
+ Accepted relationship batches emit one canonical `WORK_REQUEST` carrying the
111
+ resolved relations, followed by one `RELATIONSHIP_CHANGE_REQUEST` per relation
112
+ in declared order. The complete event sequence is one sink batch, and state and
113
+ identity counters commit only after that batch succeeds. Use
114
+ `replayFactoryEmulatorSubmissions(events)` to reconstruct detached Work identity,
115
+ initial state, payload, and outbound relationships from canonical submission
116
+ events.
117
+
118
+ Virtual time advances only when the host calls `advanceBy(durationMs)` or
119
+ `advanceToNext()`. Ready Work starts in one deterministic scheduler batch;
120
+ `advanceToNext()` then jumps to the earliest due instant and completes every
121
+ dispatch due there in stable Work order. `advanceBy()` processes every due
122
+ outcome through its requested instant. Event timestamps are always the scenario
123
+ `startAt` plus committed virtual elapsed time; these commands use no wall-clock
124
+ or browser timers. Receipts, state, status, and validation errors are detached
125
+ structured-cloneable values.
126
+
127
+ Each scheduler batch considers at most 50 eligible bindings in deterministic
128
+ Work-in-queue order. Multi-input workstations require a stable binding for every
129
+ input, and selected candidates claim all consumable Work and declared top-level
130
+ resource capacity atomically. Independent candidates can start together when
131
+ capacity permits; active dispatches retain their claims until completion, and
132
+ released capacity is available to waiting Work in the following scheduler
133
+ batch. Work with `DEPENDS_ON` relations remains ready but cannot enter a
134
+ scheduler candidate until every target Work currently occupies its normalized
135
+ required state. Dependency eligibility is recalculated after each accepted
136
+ completion batch, so newly unblocked Work can dispatch in the same `advanceBy()`
137
+ command. Worker-only resource metadata does not change mocked execution
138
+ capacity.
139
+
140
+ When a target enters a failed state, every non-terminal dependent moves to its
141
+ own Work Type's failed state through a deterministic breadth-first closure in
142
+ that same completion tick. Each move emits one canonical `WORK_STATE_CHANGE`
143
+ with source `cascading-failure`; terminal and already-failed Work is unchanged.
144
+
145
+ The package's frozen runtime-reference corpus includes both a `DEPENDS_ON`
146
+ prerequisite release and a terminal-failure cascade. Conformance compares
147
+ dependency eligibility, dispatch selection, consumed Work, routes, terminal
148
+ states, and submission replay projection at every logical tick.
149
+
150
+ Accepted, continued, rejected, and failed outcomes preserve Work lineage while
151
+ routing to explicit destinations in declared order. Accepted fan-out supports
152
+ multiple outputs and both `OUTPUT_AS_PAYLOAD` and `PRESERVE_INPUT` propagation.
153
+ Missing routes use the supported Factory defaults: failures enter the input Work
154
+ Type's failed state, STANDARD rejections use the effective failure route, and
155
+ REPEATER rejections return to their inputs.
156
+
157
+ Supported workerless `LOGICAL_MOVE` workstations do not need scenario rules.
158
+ Their `VISIT_COUNT` guards read the inclusive transition counts carried by the
159
+ first authored input lineage, and an eligible move routes synchronously at the
160
+ current virtual instant without creating active worker-dispatch state. The
161
+ resulting canonical `DISPATCH_RESPONSE` preserves lineage, propagation mode,
162
+ and declared output order; session Work snapshots expose the carried `visits`
163
+ map for deterministic inspection. Zero-duration logical cycles share the
164
+ session's configured cycle and cooperative-yield boundaries.
165
+
166
+ Canonical identities and event ordering are derived from the Factory identity,
167
+ the complete validated scenario (including its seed), normalized command
168
+ inputs, command order, and virtual elapsed time. Object key insertion order,
169
+ wall-clock time, and host playback speed do not participate. `reset()` restores
170
+ the pre-start counters, rule cursors, identities, initial submissions, and
171
+ virtual-time origin while retaining the caller-owned sink, so the host can
172
+ clear or replace its own recording destination and reproduce the same supported
173
+ history byte for byte.
174
+
175
+ Every state-changing command retains its complete detached candidate state and
176
+ canonical event batch until the caller-owned sink accepts the batch. While a
177
+ write is pending, other state-changing commands fail with
178
+ `FactoryEmulatorPendingCommandError`; read-only snapshots remain available.
179
+ After rejection, status exposes the structured sink error and pending phase.
180
+ Retry the same command with the same arguments to write the byte-identical
181
+ batch before execution continues, or call `reset()` to explicitly discard the
182
+ rejected transaction and restore the pre-start state.
183
+
184
+ `close()` writes one canonical `SESSION_COMPLETED` terminal batch, then awaits
185
+ the sink's optional `close()` boundary before exposing the terminal closed
186
+ state. A terminal write or sink-close rejection remains retryable. Sink-close
187
+ retries never duplicate an already accepted terminal event, and successful
188
+ close rejects all later state-changing commands.
189
+
190
+ ## Execution safety and cooperative scheduling
191
+
192
+ The session enforces deterministic safety budgets before a calculated batch is
193
+ sent to the sink. Defaults allow 1,000 completed dispatches, 10,000 canonical
194
+ events, one virtual hour, 1,000 consecutive zero-duration scheduler batches,
195
+ 100 synchronous scheduler batches, and 1,000 Work items in one synchronous
196
+ batch. Overrides must be positive safe integers and cannot exceed the exported
197
+ `FACTORY_EMULATOR_LIMIT_HARD_CAPS`.
198
+
199
+ Crossing an event, completed-dispatch, or virtual-time budget throws
200
+ `FactoryEmulatorExecutionPausedError` and exposes a detached
201
+ `budget-exceeded` diagnostic through `status().error`. The diagnostic identifies
202
+ the limit, configured and observed values, and virtual-time context. The
203
+ calculated over-limit batch is not written or committed, and the kernel does
204
+ not fabricate failed Work. A consecutive zero-time scheduler chain uses the
205
+ distinct `zero-duration-cycle` diagnostic. Initial and runtime Work sets larger
206
+ than `maxSynchronousWorkItems` fail atomically with
207
+ `bounded-work-exceeded` before partial Work or events become visible.
208
+ After any execution pause, `reset()` clears the structured failure and restores
209
+ the deterministic pre-start state. The caller remains responsible for clearing
210
+ or replacing its owned event history before rerunning the same scenario; that
211
+ rerun emits the same canonical history and stops at the same configured
212
+ boundary.
213
+
214
+ Hosts can provide `yieldControl` to choose their own cooperative task boundary.
215
+ Long advancement commands await it after every `maxSynchronousBatches`
216
+ accepted scheduler batches and then resume the same serialized deterministic
217
+ command. The kernel itself does not create timers or Web Workers.
218
+
219
+ ## Caller-owned event history
220
+
221
+ `MemoryFactoryEventSink` retains ordered Factory Events up to a required
222
+ caller-selected bound. Each non-empty batch is atomic: history changes only
223
+ after the asynchronous write resolves, and rejection or capacity overflow
224
+ preserves the prior snapshot. Concurrent calls are serialized in call order.
225
+
226
+ ```ts
227
+ import { MemoryFactoryEventSink } from "@you-agent-factory/factory-emulator";
228
+
229
+ const sink = new MemoryFactoryEventSink({ maxEvents: 1_000 });
230
+ await sink.write(eventBatch);
231
+ const detachedHistory = sink.snapshot();
232
+ await sink.close();
233
+ ```
234
+
235
+ Input batches and returned snapshots are mutation-isolated. `close()` is
236
+ idempotent, waits for writes accepted before close, and rejects later writes
237
+ with a `FactoryEventSinkError` whose code is `closed`. Empty batches and
238
+ all-or-nothing capacity failures use the `empty_batch` and `capacity_exceeded`
239
+ codes. The optional `beforeWrite` hook provides caller-controlled backpressure
240
+ and failure injection without transferring ownership of retained history.
241
+
242
+ `RecordingFactoryEventSink` applies the same asynchronous, bounded, atomic
243
+ lifecycle to a canonical UI-client `FactoryRecording`. It rejects duplicate
244
+ event IDs, non-canonical ordering, mixed Factory or session identities, and any
245
+ batch that would make the full recording invalid, without changing the prior
246
+ snapshot.
247
+
248
+ ```ts
249
+ import { RecordingFactoryEventSink } from "@you-agent-factory/factory-emulator";
250
+
251
+ const recordingSink = new RecordingFactoryEventSink({
252
+ maxEvents: 1_000,
253
+ recording: {
254
+ schemaVersion: "factory-recording/v1",
255
+ id: "customer-support-example",
256
+ title: "Customer support example",
257
+ factory,
258
+ },
259
+ });
260
+ await recordingSink.write(eventBatch);
261
+ const detachedRecording = recordingSink.snapshot();
262
+ ```
263
+
264
+ ## Package verification
265
+
266
+ The package is built and verified independently from the dashboard. Run
267
+ `bun run verify` from this directory to check the generated schema module,
268
+ types, formatting and lint rules, focused tests, compiled frozen-reference
269
+ conformance and determinism, dependency boundary, packed inventory, and a
270
+ clean installed consumer. The installed consumer reruns every frozen reference
271
+ from the packed artifact, so fixture freshness is proven without a backend or
272
+ fixture generator. `bun run generate` refreshes the committed runtime schema module after editing
273
+ `schema/scenario.schema.json`.
274
+
275
+ `@you-agent-factory/client` is an explicit peer contract because the emulator
276
+ uses its canonical Factory, Factory Event, and recording APIs without bundling
277
+ or duplicating them. Consumers install both packages at the same version.
278
+
279
+ ## Frozen runtime references
280
+
281
+ `loadFactoryEmulatorRuntimeReferences()` returns detached package-local
282
+ references for the documented supported subset. The loader validates each
283
+ fixture's public provenance, Factory and scenario, strictly ordered logical
284
+ ticks, and flattened Factory-event kind sequence before a conformance consumer
285
+ can compare emulator semantics. These references remain frontend-only and do
286
+ not invoke a backend, Go, or WASM fixture generator.
287
+
288
+ The corpus includes resource contention, parallel dispatch, and simultaneous
289
+ completion cases. Their frozen ticks preserve the selected Work, consumed Work,
290
+ completion outcomes, terminal routes, and replay projection so capacity and
291
+ same-deadline behavior remain reproducible between runs.
292
+
293
+ Each fixture records its specific public documentation provenance. In particular,
294
+ the repeater fixture observes a `CONTINUE` route, the routing fixture observes a
295
+ `REJECTED` route, the propagation fixture compares the routed payload under
296
+ `PRESERVE_INPUT`, and the logical-move fixture first makes its `VISIT_COUNT`
297
+ guard eligible before recording the workerless move's synchronous route.
298
+
299
+ `compareFactoryEmulatorRuntimeReference(reference)` runs a reference through a
300
+ fresh deterministic session and compares each logical tick independently. It
301
+ compares ordered event kinds plus normalized dispatch choices, consumed Work,
302
+ outcomes, routes (including routed payload), terminal states, and submission replay projection. Event
303
+ identities and virtual timestamps are deliberately excluded; all other compared
304
+ values are semantic evidence. A mismatch returns the fixture ID, first divergent
305
+ logical tick, comparison surface, expected value, and actual value.