@you-agent-factory/factory-emulator 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) you-agent-factory contributors
package/README.md ADDED
@@ -0,0 +1,286 @@
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.
@@ -0,0 +1,191 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://you-agent-factory.dev/schemas/factory-emulator/scenario/v1",
4
+ "title": "Factory Emulator Scenario v1",
5
+ "description": "A deterministic, authored scenario for a Factory emulator. activityLabel is transient emulator metadata and is never canonical Factory event content.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["version", "id", "seed", "startAt", "rules", "unmatchedBehavior"],
9
+ "properties": {
10
+ "version": { "const": "you-agent-factory.emulator.scenario.v1" },
11
+ "id": { "type": "string", "minLength": 1, "maxLength": 128 },
12
+ "seed": { "type": "string", "minLength": 1, "maxLength": 256 },
13
+ "startAt": {
14
+ "type": "string",
15
+ "format": "date-time",
16
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?Z$"
17
+ },
18
+ "initialSubmissions": {
19
+ "type": "array",
20
+ "items": { "$ref": "#/$defs/initialSubmission" }
21
+ },
22
+ "rules": {
23
+ "type": "array",
24
+ "items": { "$ref": "#/$defs/rule" }
25
+ },
26
+ "unmatchedBehavior": { "$ref": "#/$defs/unmatchedBehavior" },
27
+ "activityLabel": {
28
+ "type": "string",
29
+ "minLength": 1,
30
+ "maxLength": 120,
31
+ "description": "Bounded transient emulator activity metadata. It is never a canonical Factory event field."
32
+ }
33
+ },
34
+ "$defs": {
35
+ "initialSubmission": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["id", "workType"],
39
+ "properties": {
40
+ "id": { "type": "string", "minLength": 1, "maxLength": 128 },
41
+ "workType": { "type": "string", "minLength": 1, "maxLength": 128 },
42
+ "input": { "type": "object", "additionalProperties": true }
43
+ }
44
+ },
45
+ "rule": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "required": ["id", "match", "outcomes", "exhaustionBehavior"],
49
+ "properties": {
50
+ "id": { "type": "string", "minLength": 1, "maxLength": 128 },
51
+ "match": { "$ref": "#/$defs/matcher" },
52
+ "outcomes": {
53
+ "type": "array",
54
+ "minItems": 1,
55
+ "items": { "$ref": "#/$defs/outcome" }
56
+ },
57
+ "exhaustionBehavior": { "$ref": "#/$defs/exhaustionBehavior" }
58
+ }
59
+ },
60
+ "matcher": {
61
+ "oneOf": [
62
+ {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "required": ["kind"],
66
+ "properties": { "kind": { "const": "all" } }
67
+ },
68
+ {
69
+ "type": "object",
70
+ "additionalProperties": false,
71
+ "required": ["kind", "workType"],
72
+ "properties": {
73
+ "kind": { "const": "workType" },
74
+ "workType": { "type": "string", "minLength": 1, "maxLength": 128 }
75
+ }
76
+ },
77
+ {
78
+ "type": "object",
79
+ "additionalProperties": false,
80
+ "required": ["kind", "submissionId"],
81
+ "properties": {
82
+ "kind": { "const": "submissionId" },
83
+ "submissionId": { "type": "string", "minLength": 1, "maxLength": 128 }
84
+ }
85
+ }
86
+ ]
87
+ },
88
+ "outcome": {
89
+ "oneOf": [
90
+ {
91
+ "type": "object",
92
+ "additionalProperties": false,
93
+ "required": ["kind"],
94
+ "properties": {
95
+ "kind": { "const": "complete" },
96
+ "durationMs": {
97
+ "type": "integer",
98
+ "minimum": 0,
99
+ "maximum": 9007199254740991,
100
+ "description": "Deterministic virtual execution duration in milliseconds. Defaults to zero."
101
+ },
102
+ "output": { "type": "object", "additionalProperties": true },
103
+ "lineageCursor": { "$ref": "#/$defs/lineageCursor" }
104
+ }
105
+ },
106
+ {
107
+ "type": "object",
108
+ "additionalProperties": false,
109
+ "required": ["kind", "reason"],
110
+ "properties": {
111
+ "kind": { "const": "reject" },
112
+ "durationMs": {
113
+ "type": "integer",
114
+ "minimum": 0,
115
+ "maximum": 9007199254740991,
116
+ "description": "Deterministic virtual execution duration in milliseconds. Defaults to zero."
117
+ },
118
+ "reason": { "type": "string", "minLength": 1, "maxLength": 512 }
119
+ }
120
+ }
121
+ ]
122
+ },
123
+ "lineageCursor": {
124
+ "oneOf": [
125
+ {
126
+ "type": "object",
127
+ "additionalProperties": false,
128
+ "required": ["kind", "submissionId"],
129
+ "properties": {
130
+ "kind": { "const": "initialSubmission" },
131
+ "submissionId": { "type": "string", "minLength": 1, "maxLength": 128 }
132
+ }
133
+ },
134
+ {
135
+ "type": "object",
136
+ "additionalProperties": false,
137
+ "required": ["kind", "ruleId", "outcomeIndex"],
138
+ "properties": {
139
+ "kind": { "const": "scriptedOutcome" },
140
+ "ruleId": { "type": "string", "minLength": 1, "maxLength": 128 },
141
+ "outcomeIndex": { "type": "integer", "minimum": 0 }
142
+ }
143
+ }
144
+ ]
145
+ },
146
+ "exhaustionBehavior": {
147
+ "oneOf": [
148
+ {
149
+ "type": "object",
150
+ "additionalProperties": false,
151
+ "required": ["kind"],
152
+ "properties": { "kind": { "const": "repeatLast" } }
153
+ },
154
+ {
155
+ "type": "object",
156
+ "additionalProperties": false,
157
+ "required": ["kind"],
158
+ "properties": { "kind": { "const": "useUnmatchedBehavior" } }
159
+ },
160
+ {
161
+ "type": "object",
162
+ "additionalProperties": false,
163
+ "required": ["kind", "reason"],
164
+ "properties": {
165
+ "kind": { "const": "reject" },
166
+ "reason": { "type": "string", "minLength": 1, "maxLength": 512 }
167
+ }
168
+ }
169
+ ]
170
+ },
171
+ "unmatchedBehavior": {
172
+ "oneOf": [
173
+ {
174
+ "type": "object",
175
+ "additionalProperties": false,
176
+ "required": ["kind"],
177
+ "properties": { "kind": { "const": "ignore" } }
178
+ },
179
+ {
180
+ "type": "object",
181
+ "additionalProperties": false,
182
+ "required": ["kind", "reason"],
183
+ "properties": {
184
+ "kind": { "const": "reject" },
185
+ "reason": { "type": "string", "minLength": 1, "maxLength": 512 }
186
+ }
187
+ }
188
+ ]
189
+ }
190
+ }
191
+ }