@you-agent-factory/factory-replay 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.
@@ -0,0 +1,255 @@
1
+ import { canonicalizeFactoryEvents } from "./replay.js";
2
+ import { projectFactoryTopologyConnection } from "./topology-connection.js";
3
+ import { FACTORY_TOPOLOGY_RELATIONSHIPS } from "./topology-contract.js";
4
+ import { factoryTopologyEntityId, factoryTopologyNodeId, } from "./topology-identity.js";
5
+ export { projectFactoryTopologyConnection } from "./topology-connection.js";
6
+ export { FACTORY_TOPOLOGY_RELATIONSHIPS } from "./topology-contract.js";
7
+ export { factoryTopologyEntityId, factoryTopologyNodeId, } from "./topology-identity.js";
8
+ function handlesFor(kind) {
9
+ const handles = new Map();
10
+ for (const relationship of Object.values(FACTORY_TOPOLOGY_RELATIONSHIPS)) {
11
+ if (relationship.source.nodeKind === kind) {
12
+ handles.set(relationship.source.handleId, {
13
+ id: relationship.source.handleId,
14
+ role: "source",
15
+ });
16
+ }
17
+ if (relationship.target.nodeKind === kind) {
18
+ handles.set(relationship.target.handleId, {
19
+ id: relationship.target.handleId,
20
+ role: "target",
21
+ });
22
+ }
23
+ }
24
+ return [...handles.values()].sort((left, right) => left.id.localeCompare(right.id));
25
+ }
26
+ function sortedByIdentity(values) {
27
+ return [...(values ?? [])].sort((left, right) => {
28
+ const identityDifference = factoryTopologyEntityId(left.id, left.name).localeCompare(factoryTopologyEntityId(right.id, right.name));
29
+ return identityDifference || left.name.localeCompare(right.name);
30
+ });
31
+ }
32
+ function createContext() {
33
+ return {
34
+ connections: new Map(),
35
+ issues: new Map(),
36
+ nodes: new Map(),
37
+ resourcesByName: new Map(),
38
+ workersByName: new Map(),
39
+ workStatesByName: new Map(),
40
+ workTypesByName: new Map(),
41
+ };
42
+ }
43
+ function addNode(context, node) {
44
+ if (context.nodes.has(node.id)) {
45
+ const id = `duplicate-entity:${node.id}`;
46
+ context.issues.set(id, {
47
+ code: "DUPLICATE_ENTITY_ID",
48
+ id,
49
+ message: `Factory topology contains duplicate node id ${node.id}.`,
50
+ nodeId: node.id,
51
+ });
52
+ return;
53
+ }
54
+ context.nodes.set(node.id, node);
55
+ }
56
+ function addConnection(context, kind, sourceNodeId, targetNodeId, sourceReference, targetReference) {
57
+ const result = projectFactoryTopologyConnection([...context.nodes.values()], {
58
+ kind,
59
+ sourceNodeId,
60
+ sourceReference,
61
+ targetNodeId,
62
+ targetReference,
63
+ });
64
+ if (!result.ok) {
65
+ context.issues.set(result.issue.id, result.issue);
66
+ return;
67
+ }
68
+ context.connections.set(result.connection.id, result.connection);
69
+ }
70
+ function appendNodes(factory, context) {
71
+ for (const resource of sortedByIdentity(factory.resources)) {
72
+ const id = factoryTopologyEntityId(resource.id, resource.name);
73
+ context.resourcesByName.set(resource.name, id);
74
+ addNode(context, {
75
+ capacity: resource.capacity,
76
+ entityId: id,
77
+ handles: handlesFor("resource"),
78
+ id: factoryTopologyNodeId("resource", id),
79
+ kind: "resource",
80
+ label: resource.name,
81
+ });
82
+ }
83
+ for (const worker of sortedByIdentity(factory.workers)) {
84
+ const id = factoryTopologyEntityId(worker.id, worker.name);
85
+ context.workersByName.set(worker.name, id);
86
+ addNode(context, {
87
+ entityId: id,
88
+ handles: handlesFor("worker"),
89
+ id: factoryTopologyNodeId("worker", id),
90
+ kind: "worker",
91
+ label: worker.name,
92
+ });
93
+ }
94
+ for (const workType of sortedByIdentity(factory.workTypes)) {
95
+ const id = factoryTopologyEntityId(workType.id, workType.name);
96
+ context.workTypesByName.set(workType.name, id);
97
+ addNode(context, {
98
+ entityId: id,
99
+ handles: handlesFor("work-type"),
100
+ id: factoryTopologyNodeId("work-type", id),
101
+ kind: "work-type",
102
+ label: workType.name,
103
+ });
104
+ const states = new Map();
105
+ for (const state of sortedByIdentity(workType.states)) {
106
+ const stateId = `${id}:${factoryTopologyEntityId(state.id, state.name)}`;
107
+ states.set(state.name, stateId);
108
+ addNode(context, {
109
+ category: state.type,
110
+ entityId: stateId,
111
+ handles: handlesFor("work-state"),
112
+ id: factoryTopologyNodeId("work-state", stateId),
113
+ kind: "work-state",
114
+ label: state.name,
115
+ workTypeId: id,
116
+ });
117
+ }
118
+ context.workStatesByName.set(workType.name, states);
119
+ }
120
+ for (const workstation of sortedByIdentity(factory.workstations)) {
121
+ const id = factoryTopologyEntityId(workstation.id, workstation.name);
122
+ addNode(context, {
123
+ entityId: id,
124
+ handles: handlesFor("workstation"),
125
+ id: factoryTopologyNodeId("workstation", id),
126
+ kind: "workstation",
127
+ label: workstation.name,
128
+ });
129
+ }
130
+ }
131
+ function appendFoundationConnections(factory, context) {
132
+ for (const worker of sortedByIdentity(factory.workers)) {
133
+ const workerEntityId = context.workersByName.get(worker.name);
134
+ for (const resource of sortedByIdentity(worker.resources)) {
135
+ const resourceEntityId = context.resourcesByName.get(resource.name);
136
+ addConnection(context, "worker-resource", resourceEntityId && factoryTopologyNodeId("resource", resourceEntityId), workerEntityId && factoryTopologyNodeId("worker", workerEntityId), resource.name, worker.name);
137
+ }
138
+ }
139
+ for (const workType of sortedByIdentity(factory.workTypes)) {
140
+ const workTypeEntityId = context.workTypesByName.get(workType.name);
141
+ for (const state of sortedByIdentity(workType.states)) {
142
+ const stateEntityId = context.workStatesByName
143
+ .get(workType.name)
144
+ ?.get(state.name);
145
+ addConnection(context, "work-type-state", workTypeEntityId &&
146
+ factoryTopologyNodeId("work-type", workTypeEntityId), stateEntityId && factoryTopologyNodeId("work-state", stateEntityId), workType.name, `${workType.name}:${state.name}`);
147
+ }
148
+ }
149
+ }
150
+ function sortedRoutes(routes) {
151
+ const normalizedRoutes = !routes
152
+ ? []
153
+ : Array.isArray(routes)
154
+ ? routes
155
+ : [routes];
156
+ return [...normalizedRoutes].sort((left, right) => `${left.workType}:${left.state}`.localeCompare(`${right.workType}:${right.state}`));
157
+ }
158
+ function appendRoutes(context, workstation, workstationNodeId) {
159
+ const routes = [
160
+ ["workstation-input", workstation.inputs],
161
+ ["workstation-output", workstation.outputs],
162
+ ["workstation-on-continue", workstation.onContinue],
163
+ ["workstation-on-failure", workstation.onFailure],
164
+ ["workstation-on-rejection", workstation.onRejection],
165
+ [
166
+ "workstation-output",
167
+ workstation.classificationRoutes?.flatMap((route) => route.outputs),
168
+ ],
169
+ ];
170
+ for (const [kind, routeList] of routes) {
171
+ for (const route of sortedRoutes(routeList)) {
172
+ const stateEntityId = context.workStatesByName
173
+ .get(route.workType)
174
+ ?.get(route.state);
175
+ if (!stateEntityId &&
176
+ route.state === "available" &&
177
+ context.resourcesByName.has(route.workType)) {
178
+ continue;
179
+ }
180
+ const stateNodeId = stateEntityId && factoryTopologyNodeId("work-state", stateEntityId);
181
+ const isInput = kind === "workstation-input";
182
+ addConnection(context, kind, isInput ? stateNodeId : workstationNodeId, isInput ? workstationNodeId : stateNodeId, isInput ? `${route.workType}:${route.state}` : workstation.name, isInput ? workstation.name : `${route.workType}:${route.state}`);
183
+ }
184
+ }
185
+ }
186
+ function appendWorkstationConnections(factory, context) {
187
+ for (const workstation of sortedByIdentity(factory.workstations)) {
188
+ const workstationNodeId = factoryTopologyNodeId("workstation", factoryTopologyEntityId(workstation.id, workstation.name));
189
+ const workerEntityId = context.workersByName.get(workstation.worker);
190
+ if (workstation.worker.trim()) {
191
+ addConnection(context, "worker-assignment", workerEntityId && factoryTopologyNodeId("worker", workerEntityId), workstationNodeId, workstation.worker, workstation.name);
192
+ }
193
+ for (const resource of sortedByIdentity(workstation.resources)) {
194
+ const resourceEntityId = context.resourcesByName.get(resource.name);
195
+ addConnection(context, "workstation-resource", resourceEntityId && factoryTopologyNodeId("resource", resourceEntityId), workstationNodeId, resource.name, workstation.name);
196
+ }
197
+ appendRoutes(context, workstation, workstationNodeId);
198
+ }
199
+ }
200
+ /** Project one public Factory definition without mutating caller-owned data. */
201
+ export function projectFactoryTopology(input) {
202
+ if (!input.factory) {
203
+ return {
204
+ connections: [],
205
+ issues: [
206
+ {
207
+ code: "MISSING_FACTORY",
208
+ id: "missing-factory",
209
+ message: "No Factory topology is available at the selected tick.",
210
+ },
211
+ ],
212
+ nodes: [],
213
+ ok: false,
214
+ selectedTick: input.selectedTick,
215
+ };
216
+ }
217
+ const context = createContext();
218
+ appendNodes(input.factory, context);
219
+ appendFoundationConnections(input.factory, context);
220
+ appendWorkstationConnections(input.factory, context);
221
+ const issues = [...context.issues.values()].sort((left, right) => left.id.localeCompare(right.id));
222
+ if (issues.length > 0) {
223
+ return {
224
+ connections: [],
225
+ issues,
226
+ nodes: [],
227
+ ok: false,
228
+ selectedTick: input.selectedTick,
229
+ };
230
+ }
231
+ return {
232
+ connections: [...context.connections.values()].sort((left, right) => left.id.localeCompare(right.id)),
233
+ issues,
234
+ nodes: [...context.nodes.values()].sort((left, right) => left.id.localeCompare(right.id)),
235
+ ok: true,
236
+ selectedTick: input.selectedTick,
237
+ };
238
+ }
239
+ /** Reconstruct the last canonical Factory topology effective at one tick. */
240
+ export function projectFactoryTopologyAtTick(input) {
241
+ let factory;
242
+ for (const event of canonicalizeFactoryEvents(input.events)) {
243
+ if (event.context.tick > input.tick)
244
+ break;
245
+ if (event.type !== "INITIAL_STRUCTURE_REQUEST" &&
246
+ event.type !== "FACTORY_CHANGE") {
247
+ continue;
248
+ }
249
+ const payload = event.payload;
250
+ if (payload.factory && typeof payload.factory === "object") {
251
+ factory = payload.factory;
252
+ }
253
+ }
254
+ return projectFactoryTopology({ factory, selectedTick: input.tick });
255
+ }
package/package.json CHANGED
@@ -1,26 +1,51 @@
1
1
  {
2
2
  "name": "@you-agent-factory/factory-replay",
3
- "version": "0.0.0",
4
- "description": "Framework-independent deterministic replay primitives for Factory events.",
3
+ "version": "0.0.2",
4
+ "description": "Deterministic, framework-independent Factory event replay.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/portpowered/you-agent-factory.git"
8
+ "url": "git+https://github.com/portpowered/you-agent-factory.git",
9
+ "directory": "ui/packages/factory-replay"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
9
13
  },
10
- "type": "module",
11
14
  "files": [
12
- "README.md",
15
+ "dist",
13
16
  "LICENSE.md",
14
- "src"
17
+ "README.md"
15
18
  ],
16
- "types": "./src/index.d.ts",
19
+ "type": "module",
20
+ "sideEffects": false,
17
21
  "exports": {
18
22
  ".": {
19
- "types": "./src/index.d.ts",
20
- "import": "./src/index.js"
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
28
+ "scripts": {
29
+ "build": "bun run prepare:client && node scripts/build-package.mjs",
30
+ "check:installed-consumer": "node scripts/verify-installed-consumer.mjs",
31
+ "check:pack": "node scripts/verify-package-pack.mjs",
32
+ "check:package-boundary": "node scripts/check-package-boundary.mjs",
33
+ "prepack": "bun run build",
34
+ "prepare:client": "bun run --cwd ../client build",
35
+ "test": "bun run prepare:client && vitest run --config vitest.config.ts",
36
+ "typecheck": "bun run prepare:client && tsc -p tsconfig.json --noEmit --pretty false",
37
+ "verify": "bun run typecheck && bun run test && bun run build && bun run check:package-boundary && bun run check:pack && bun run check:installed-consumer"
38
+ },
39
+ "peerDependencies": {
40
+ "@you-agent-factory/client": "0.0.2"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@you-agent-factory/client": {
44
+ "optional": true
21
45
  }
22
46
  },
23
- "dependencies": {
24
- "@you-agent-factory/client": "file:../client"
47
+ "devDependencies": {
48
+ "typescript": "~5.9.3",
49
+ "vitest": "4.1.3"
25
50
  }
26
51
  }
package/src/activity.js DELETED
@@ -1,208 +0,0 @@
1
- /** @param {string | undefined} explicitID @param {string} name */
2
- function entityID(explicitID, name) {
3
- return explicitID?.trim() || name;
4
- }
5
-
6
- /** @param {string} id */
7
- function workstationNodeID(id) {
8
- return `workstation:${id}`;
9
- }
10
-
11
- /** @param {string} id */
12
- function resourceNodeID(id) {
13
- return `resource:${id}`;
14
- }
15
-
16
- /**
17
- * Project active Dispatch and resource occupancy evidence from one selected
18
- * canonical replay state without mutating caller-owned data.
19
- *
20
- * @param {import("./index.d.ts").FactoryActivityProjectionInput} input
21
- * @returns {import("./index.d.ts").FactoryActivityProjection}
22
- */
23
- export function projectFactoryActivity(input) {
24
- const issues = new Map();
25
- const { resourcesByName, workstationsByTransition } = indexFactory(
26
- input.factory,
27
- );
28
-
29
- const occupiedByResource = new Map();
30
- const occupancyEvidenceAvailable = input.activeDispatches.every(
31
- (dispatch) => dispatch.resourceNames !== undefined,
32
- );
33
- const activeDispatches = input.activeDispatches.map((evidence) => {
34
- const resolved = workstationsByTransition.get(evidence.transitionId);
35
- const resourceIds = [];
36
- for (const resourceName of evidence.resourceNames ?? []) {
37
- const resource = resourcesByName.get(resourceName);
38
- if (!resource) {
39
- addIssue(
40
- issues,
41
- "UNRESOLVED_RESOURCE",
42
- `dispatch:${evidence.id}:resource:${resourceName}`,
43
- `Dispatch ${evidence.id} refers to unknown resource ${resourceName}.`,
44
- evidence.id,
45
- resourceName,
46
- );
47
- continue;
48
- }
49
- resourceIds.push(resource.id);
50
- occupiedByResource.set(
51
- resource.id,
52
- (occupiedByResource.get(resource.id) ?? 0) + 1,
53
- );
54
- }
55
- if (!resolved) {
56
- addIssue(
57
- issues,
58
- "UNRESOLVED_WORKSTATION",
59
- `dispatch:${evidence.id}:workstation:${evidence.transitionId}`,
60
- `Dispatch ${evidence.id} refers to unknown workstation transition ${evidence.transitionId}.`,
61
- evidence.id,
62
- evidence.transitionId,
63
- );
64
- }
65
- const workerName = resolved?.workstation.worker?.trim();
66
- const worker = workerName
67
- ? (input.factory?.workers ?? []).find(
68
- (candidate) =>
69
- candidate.name === workerName || candidate.id === workerName,
70
- )
71
- : undefined;
72
- if (workerName && !worker) {
73
- addIssue(
74
- issues,
75
- "UNRESOLVED_WORKER",
76
- `dispatch:${evidence.id}:worker:${workerName}`,
77
- `Dispatch ${evidence.id} refers to unknown worker ${workerName}.`,
78
- evidence.id,
79
- workerName,
80
- );
81
- }
82
- return {
83
- id: evidence.id,
84
- resourceIds: [...resourceIds].sort(),
85
- startedTick: evidence.startedTick,
86
- transitionId: evidence.transitionId,
87
- workIds: [...new Set(evidence.workIds)].sort(),
88
- ...(resolved
89
- ? {
90
- workstationId: resolved.id,
91
- workstationNodeId: workstationNodeID(resolved.id),
92
- }
93
- : {}),
94
- ...(worker
95
- ? { workerId: entityID(worker.id, worker.name) }
96
- : {}),
97
- };
98
- });
99
-
100
- const resourceOccupancy = projectResourceOccupancy(
101
- resourcesByName,
102
- occupiedByResource,
103
- occupancyEvidenceAvailable,
104
- issues,
105
- );
106
-
107
- return {
108
- activeDispatches: activeDispatches.sort((left, right) =>
109
- left.id.localeCompare(right.id),
110
- ),
111
- activeWorkstationIds: [
112
- ...new Set(
113
- activeDispatches.flatMap((dispatch) =>
114
- dispatch.workstationId ? [dispatch.workstationId] : [],
115
- ),
116
- ),
117
- ].sort(),
118
- issues: [...issues.values()].sort((left, right) =>
119
- left.id.localeCompare(right.id),
120
- ),
121
- resourceOccupancy: resourceOccupancy.sort((left, right) =>
122
- left.resourceId.localeCompare(right.resourceId),
123
- ),
124
- selectedTick: input.selectedTick,
125
- };
126
- }
127
-
128
- /** @param {import("@you-agent-factory/client").FactoryDefinition | undefined} factory */
129
- function indexFactory(factory) {
130
- const resourcesByName = new Map();
131
- const workstationsByTransition = new Map();
132
- for (const resource of factory?.resources ?? []) {
133
- resourcesByName.set(resource.name, {
134
- capacity: Math.max(0, resource.capacity),
135
- id: entityID(resource.id, resource.name),
136
- });
137
- }
138
- for (const workstation of factory?.workstations ?? []) {
139
- const id = entityID(workstation.id, workstation.name);
140
- workstationsByTransition.set(workstation.name, { id, workstation });
141
- if (workstation.id?.trim()) {
142
- workstationsByTransition.set(workstation.id, { id, workstation });
143
- }
144
- }
145
- return { resourcesByName, workstationsByTransition };
146
- }
147
-
148
- /**
149
- * @param {Map<string, {capacity: number, id: string}>} resourcesByName
150
- * @param {Map<string, number>} occupiedByResource
151
- * @param {boolean} evidenceAvailable
152
- * @param {Map<string, import("./index.d.ts").FactoryActivityProjectionIssue>} issues
153
- * @returns {import("./index.d.ts").FactoryResourceOccupancyProjection[]}
154
- */
155
- function projectResourceOccupancy(
156
- resourcesByName,
157
- occupiedByResource,
158
- evidenceAvailable,
159
- issues,
160
- ) {
161
- return [...resourcesByName.values()].map((resource) => {
162
- if (!evidenceAvailable) {
163
- return {
164
- capacity: resource.capacity,
165
- evidence: "unavailable",
166
- resourceId: resource.id,
167
- resourceNodeId: resourceNodeID(resource.id),
168
- };
169
- }
170
- const occupiedQuantity = occupiedByResource.get(resource.id) ?? 0;
171
- if (occupiedQuantity > resource.capacity) {
172
- addIssue(
173
- issues,
174
- "RESOURCE_CAPACITY_EXCEEDED",
175
- `resource:${resource.id}:capacity-exceeded`,
176
- `Resource ${resource.id} has ${occupiedQuantity} occupied units for capacity ${resource.capacity}.`,
177
- undefined,
178
- resource.id,
179
- );
180
- }
181
- return {
182
- availableQuantity: Math.max(0, resource.capacity - occupiedQuantity),
183
- capacity: resource.capacity,
184
- evidence: "known",
185
- occupiedQuantity,
186
- resourceId: resource.id,
187
- resourceNodeId: resourceNodeID(resource.id),
188
- };
189
- });
190
- }
191
-
192
- /**
193
- * @param {Map<string, import("./index.d.ts").FactoryActivityProjectionIssue>} issues
194
- * @param {import("./index.d.ts").FactoryActivityProjectionIssue["code"]} code
195
- * @param {string} id
196
- * @param {string} message
197
- * @param {string | undefined} dispatchId
198
- * @param {string | undefined} reference
199
- */
200
- function addIssue(issues, code, id, message, dispatchId, reference) {
201
- issues.set(id, {
202
- code,
203
- id,
204
- message,
205
- ...(dispatchId ? { dispatchId } : {}),
206
- ...(reference ? { reference } : {}),
207
- });
208
- }