@animastor/orchestration 0.1.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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/package.json +51 -0
  4. package/src/host/host-bindings.js +138 -0
  5. package/src/index.js +99 -0
  6. package/src/orchestration/index.js +40 -0
  7. package/src/orchestration/orchestrator.js +630 -0
  8. package/src/orchestration/runtime-result-consumer.js +54 -0
  9. package/src/orchestration/scene-callbacks.js +542 -0
  10. package/src/orchestration/scene-orchestrator.js +601 -0
  11. package/src/orchestration/scene-restoration.js +129 -0
  12. package/src/orchestration/scene-utils.js +36 -0
  13. package/src/runtime/active-scenes-index.js +164 -0
  14. package/src/runtime/audio-fsm-port.js +187 -0
  15. package/src/runtime/circuit-breaker.js +588 -0
  16. package/src/runtime/counter-reconciliation.js +308 -0
  17. package/src/runtime/dispatch-engine.js +1768 -0
  18. package/src/runtime/failure-taxonomy.js +125 -0
  19. package/src/runtime/hub-cancel-port.js +122 -0
  20. package/src/runtime/layer-config-port.js +136 -0
  21. package/src/runtime/lease-manager.js +607 -0
  22. package/src/runtime/persistence-port.js +162 -0
  23. package/src/runtime/placeholder-audio-port.js +117 -0
  24. package/src/runtime/progress-events-port.js +121 -0
  25. package/src/runtime/reconciliation-engine.js +2405 -0
  26. package/src/runtime/retry-budget-manager.js +229 -0
  27. package/src/runtime/runtime-metrics.js +427 -0
  28. package/src/runtime/runtime-result-emitter.js +123 -0
  29. package/src/runtime/runtime-scheduler.js +629 -0
  30. package/src/runtime/scene-data-port.js +114 -0
  31. package/src/runtime/scene-window.js +827 -0
  32. package/src/runtime/video-fsm-port.js +189 -0
  33. package/src/runtime/worker-health.js +270 -0
  34. package/test/counter-reconciliation.test.js +164 -0
  35. package/test/mocks/redis-mock.js +221 -0
  36. package/test/test-bindings.js +60 -0
@@ -0,0 +1,162 @@
1
+ // ======================================================
2
+ // O-2 PORT: PersistencePort — runtime/orchestration persistence contract
3
+ // ======================================================
4
+ // The runtime and orchestration tiers must not know the persistence
5
+ // implementation. Every PG table, SQL string, repository module and the
6
+ // storage barrel stay HOST-side (backend/src/storage/**); the tiers consume
7
+ // persistence ONLY through this port, wired by the composition root
8
+ // (backend.cjs) at startup.
9
+ //
10
+ // runtime/** + orchestration/** (tier consumers)
11
+ // ↓ consume state operations through
12
+ // runtime/persistence-port ← THIS PORT (tier-owned contract)
13
+ // ↑ wired by the host
14
+ // storage/runtime-persistence-adapter (host PG/Redis adapter)
15
+ //
16
+ // This is the S-6 port convention (ports/generation-config.js): a
17
+ // zero-require contract module, a fail-fast call-time resolver, one
18
+ // set/wired/reset surface. No pg types, no SQL, no repository names and no
19
+ // "future CRUD" cross the boundary — the contract below is EXACTLY the set
20
+ // of persistence operations the two tiers use today (measured at §32.9):
21
+ //
22
+ // tasks — task-status completion + orphan-task checks
23
+ // (runtime-scheduler, reconciliation-engine); createTask
24
+ // is a routes-side concern and stays there
25
+ // sceneAssets — per-asset status truth + version gates + dirty
26
+ // units (orchestrator completeStage version gate,
27
+ // scene-orchestrator dirty reads, scene-window
28
+ // staleness, scene-callbacks markReady/dirty clears,
29
+ // scene-restoration, reconciliation rebuildWorkList);
30
+ // markStale is consumed by state/scene-state-ops
31
+ // (outside the two tiers) and stays a direct repo call
32
+ // iu — image-unit rows (scene-callbacks audio timing
33
+ // recalculation)
34
+ // cancel — generation cancellation tombstones
35
+ // (reconciliation-engine orphan/repair legs)
36
+ // sceneVersions — the two scenes-table version reads every tier does
37
+ // (scene-window, scene-restoration, orchestrator
38
+ // version gate, reconciliation rebuildWorkList)
39
+ // registry — Redis asset-registry reads/writes
40
+ // (reconciliation repair legs, scene-callbacks
41
+ // completion handlers)
42
+ // filesystem — canonical scene audio path composition
43
+ // (reconciliation orphan check, scene-callbacks
44
+ // audio handler)
45
+ // listBooks — distinct book identities from persistent truth
46
+ // (reconciliation rebuildWorkList)
47
+ // (getDirtyMarkers is the two-row-set read the worklist rebuild issues:
48
+ // unit-dirty scenes + per-asset stale/failed/pending markers)
49
+ //
50
+ // Deliberately NOT in the port: transactions, the pool, query handles,
51
+ // CRUD "for later". The host adapter owns every SQL string and every table.
52
+ //
53
+ // Docs: docs/architecture/generation-module-extraction-reconnaissance.md §32.9
54
+ // ======================================================
55
+
56
+ const OPS = [
57
+ 'tasks.updateTaskStatus',
58
+ 'tasks.hasActiveTaskForScene',
59
+
60
+ 'sceneAssets.getAsset',
61
+ 'sceneAssets.markReady',
62
+ 'sceneAssets.getDirtyUnitIds',
63
+ 'sceneAssets.setDirtyUnitIds',
64
+ 'sceneAssets.clearDirtyUnitIds',
65
+ 'sceneAssets.clearDirtyFlag',
66
+ 'sceneAssets.getDirtyScenesByVersion',
67
+ 'sceneAssets.getDirtyMarkers',
68
+
69
+ 'iu.getImageUnitsForScene',
70
+ 'iu.upsertImageUnit',
71
+
72
+ 'cancel.isCancelled',
73
+ 'cancel.getAllCancelled',
74
+
75
+ 'sceneVersions.getSceneVersions',
76
+ 'sceneVersions.getAudioConfigVersions',
77
+ 'sceneVersions.getVersionStalenessRows',
78
+
79
+ 'registry.getSceneAssetsRedis',
80
+ 'registry.registerSceneAudioRedis',
81
+ 'registry.registerSceneImageRedis',
82
+ 'registry.registerSceneVideoRedis',
83
+
84
+ 'filesystem.getSceneAudioPath',
85
+
86
+ 'listBooks',
87
+ ];
88
+
89
+ let impl = null;
90
+
91
+ /**
92
+ * Wire the host persistence adapter. Called once by the composition root
93
+ * (backend.cjs) BEFORE any runtime/orchestration persistence consumer runs.
94
+ * @param {object} adapter — nested namespaces (tasks/sceneAssets/iu/cancel/
95
+ * sceneVersions/registry/filesystem) + the top-level listBooks function;
96
+ * every dotted op in OPS must resolve to a function.
97
+ */
98
+ function setPersistencePort(adapter) {
99
+ if (!adapter) {
100
+ throw new Error('persistence-port: adapter is required');
101
+ }
102
+ for (const op of OPS) {
103
+ if (typeof resolveOp(adapter, op) !== 'function') {
104
+ throw new Error(`persistence-port: adapter must implement ${op}()`);
105
+ }
106
+ }
107
+ impl = adapter;
108
+ }
109
+
110
+ /** Resolve the port at CALL time — fail-fast when unwired. */
111
+ function persistence() {
112
+ if (!impl) {
113
+ throw new Error(
114
+ 'persistence-port: not wired — the composition root must call ' +
115
+ 'setPersistencePort(adapter) (storage/runtime-persistence-adapter) ' +
116
+ 'before runtime/orchestration persistence consumers run (O-2)'
117
+ );
118
+ }
119
+ return impl;
120
+ }
121
+
122
+ /** Dotted-path resolver over the nested adapter namespaces. */
123
+ function resolveOp(adapter, op) {
124
+ let node = adapter;
125
+ for (const part of op.split('.')) {
126
+ node = node?.[part];
127
+ if (node === undefined) return undefined;
128
+ }
129
+ return node;
130
+ }
131
+
132
+ /**
133
+ * Shorthand op resolver: `persist('sceneAssets.getAsset')(...)`.
134
+ * Fails fast when the port or the single op is missing — an op silently
135
+ * degrading to a no-op would corrupt persistence semantics.
136
+ * @param {string} op — dotted op name from OPS
137
+ */
138
+ function persist(op) {
139
+ const fn = resolveOp(persistence(), op);
140
+ if (typeof fn !== 'function') {
141
+ throw new Error(`persistence-port: op '${op}' is not wired (O-2)`);
142
+ }
143
+ return fn;
144
+ }
145
+
146
+ function isPersistencePortWired() {
147
+ return impl !== null;
148
+ }
149
+
150
+ /** Test hygiene: drop the wiring (mocha hooks). */
151
+ function _resetPersistencePort() {
152
+ impl = null;
153
+ }
154
+
155
+ module.exports = {
156
+ OPS,
157
+ setPersistencePort,
158
+ persistence,
159
+ persist,
160
+ isPersistencePortWired,
161
+ _resetPersistencePort,
162
+ };
@@ -0,0 +1,117 @@
1
+ // ======================================================
2
+ // O-4 PORT: PlaceholderAudioPort — runtime/orchestration
3
+ // placeholder-audio contract
4
+ // ======================================================
5
+ // The runtime and orchestration tiers must not know the placeholder-audio
6
+ // host service. Every ffmpeg/child-process detail, scene_assets upsert,
7
+ // OUTPUT_DIR path composition and book fallback the service performs stays
8
+ // HOST-side (backend/src/services/placeholder-audio.js); the tiers consume
9
+ // the three placeholder-audio operations they actually use ONLY through
10
+ // this port, wired by the composition root (backend.cjs) at startup.
11
+ //
12
+ // runtime/** + orchestration/** (tier consumers)
13
+ // ↓ consume placeholder-audio ops through
14
+ // runtime/placeholder-audio-port.js ← THIS PORT (tier-owned contract)
15
+ // ↑ wired by the host
16
+ // storage/placeholder-audio-adapter (host service adapter)
17
+ //
18
+ // This is the O-2/O-3 port convention (runtime/persistence-port.js,
19
+ // runtime/scene-data-port.js): a zero-require contract module, a fail-fast
20
+ // call-time resolver, one set/wired/reset surface. No ffmpeg, no fs, no
21
+ // SQL, no scene_assets knowledge and no "future ops" cross the boundary —
22
+ // the contract below is EXACTLY the set of operations the two tiers use
23
+ // today (measured at O-4 reconnaissance, §32.7 O-P7):
24
+ //
25
+ // hasRealAudio — distinguish real TTS from a placeholder on
26
+ // completion/restore/rebuild reads
27
+ // (scene-window ×3 call sites,
28
+ // scene-restoration restore probe,
29
+ // reconciliation rebuildWorkList)
30
+ // ensurePlaceholderAudio — synchronously materialize the silent-MP3
31
+ // placeholder before a scene enters the
32
+ // scheduler (scene-window addToWindow)
33
+ // replacePlaceholderWithRealAudio — register real audio over the
34
+ // placeholder when the audio stage completes
35
+ // (scene-callbacks handleAudioCompleted)
36
+ //
37
+ // Deliberately NOT in the port: estimateSpeechDurationSec (pure text
38
+ // heuristic with its own canonical owner utils/speech-estimation —
39
+ // consumed by the VBook agent pipeline, not by these tiers),
40
+ // ensureAllPlaceholderAudio/getScenesNeedingPlaceholder (routes/import
41
+ // workflow), markPlaceholderStale (state-layer FSM writer),
42
+ // recoverMissingPlaceholders (host recovery routes), and any CRUD "for
43
+ // later". The host adapter owns every ffmpeg call, every path and every
44
+ // scene_assets write.
45
+ //
46
+ // Docs: docs/architecture/generation-module-extraction-reconnaissance.md §32.7 (O-P7, O-4 DONE)
47
+ // ======================================================
48
+
49
+ const OPS = [
50
+ 'hasRealAudio',
51
+ 'ensurePlaceholderAudio',
52
+ 'replacePlaceholderWithRealAudio',
53
+ ];
54
+
55
+ let impl = null;
56
+
57
+ /**
58
+ * Wire the host placeholder-audio adapter. Called once by the composition
59
+ * root (backend.cjs) BEFORE any runtime/orchestration placeholder-audio
60
+ * consumer runs.
61
+ * @param {object} adapter — flat map with every op in OPS as a function.
62
+ */
63
+ function setPlaceholderAudioPort(adapter) {
64
+ if (!adapter) {
65
+ throw new Error('placeholder-audio-port: adapter is required');
66
+ }
67
+ for (const op of OPS) {
68
+ if (typeof adapter[op] !== 'function') {
69
+ throw new Error(`placeholder-audio-port: adapter must implement ${op}()`);
70
+ }
71
+ }
72
+ impl = adapter;
73
+ }
74
+
75
+ /** Resolve the port at CALL time — fail-fast when unwired. */
76
+ function placeholderAudio() {
77
+ if (!impl) {
78
+ throw new Error(
79
+ 'placeholder-audio-port: not wired — the composition root must call ' +
80
+ 'setPlaceholderAudioPort(adapter) (storage/placeholder-audio-adapter) ' +
81
+ 'before runtime/orchestration placeholder-audio consumers run (O-4)'
82
+ );
83
+ }
84
+ return impl;
85
+ }
86
+
87
+ /**
88
+ * Shorthand op resolver: `placeholderAudioOp('hasRealAudio')(...)`.
89
+ * Fails fast when the port or the single op is missing — a missing
90
+ * placeholder probe must never silently degrade into `false`.
91
+ * @param {string} op — op name from OPS
92
+ */
93
+ function placeholderAudioOp(op) {
94
+ const fn = impl?.[op];
95
+ if (typeof fn !== 'function') {
96
+ throw new Error(`placeholder-audio-port: op '${op}' is not wired (O-4)`);
97
+ }
98
+ return fn;
99
+ }
100
+
101
+ function isPlaceholderAudioPortWired() {
102
+ return impl !== null;
103
+ }
104
+
105
+ /** Test hygiene: drop the wiring (mocha hooks). */
106
+ function _resetPlaceholderAudioPort() {
107
+ impl = null;
108
+ }
109
+
110
+ module.exports = {
111
+ OPS,
112
+ setPlaceholderAudioPort,
113
+ placeholderAudio,
114
+ placeholderAudioOp,
115
+ isPlaceholderAudioPortWired,
116
+ _resetPlaceholderAudioPort,
117
+ };
@@ -0,0 +1,121 @@
1
+ // ======================================================
2
+ // O-5 PORT: ProgressEventsPort — runtime/orchestration
3
+ // progress-observer contract
4
+ // ======================================================
5
+ // The runtime and orchestration tiers must not know the progress host
6
+ // services. The Redis pub/sub channel bytes (`animastor:progress:*`), the
7
+ // task-registry hash key namespace (`animastor:generation-progress:*`),
8
+ // their TTLs and their retention rules stay HOST-side
9
+ // (backend/src/services/progress-pubsub.cjs, backend/src/services/
10
+ // generation-progress.js); the tiers consume progress ONLY through this
11
+ // port, wired by the composition root (backend.cjs) at startup.
12
+ //
13
+ // runtime/** + orchestration/** (tier consumers)
14
+ // ↓ consume progress ops through
15
+ // runtime/progress-events-port.js ← THIS PORT (tier-owned contract)
16
+ // ↑ wired by the host
17
+ // storage/progress-events-adapter (host progress adapter)
18
+ //
19
+ // This is the O-2/O-3/O-4 port convention (runtime/persistence-port.js,
20
+ // runtime/scene-data-port.js, runtime/placeholder-audio-port.js): a
21
+ // zero-require contract module, a fail-fast call-time resolver, one
22
+ // set/wired/reset surface. No Redis key strings, no pub/sub transport, no
23
+ // TTL arithmetic, no SSE knowledge and no "future ops" cross the boundary
24
+ // — the contract below is EXACTLY the set of progress operations the two
25
+ // tiers use today (measured at O-5 reconnaissance, §32.7 O-P4):
26
+ //
27
+ // publishProgress — best-effort per-book progress event (layer
28
+ // advance / generation_complete) consumed by the
29
+ // SSE poller (scene-callbacks ×5 call sites)
30
+ // getSceneTaskState — per-scene selective-task read: which worker
31
+ // types a task-managed scene must run
32
+ // (runtime-scheduler shouldScheduleAssets +
33
+ // markVersionStaleDirty)
34
+ // hasActiveTasks — does the book still have an active
35
+ // generation task (scene-window
36
+ // trySlideWindowOnComplete task_managed gate)
37
+ // reconcileCompletedTasks — mark task records completed once every
38
+ // target asset is ready (runtime-scheduler tick
39
+ // pre-pass; the getAssetStates callback arrives
40
+ // as an argument exactly as pre-O-5)
41
+ //
42
+ // Deliberately NOT in the port: createTasks/listTasks/getTask/updateTask/
43
+ // markCompleted/markCancelled/removeTask/clear (routes-side task CRUD —
44
+ // the HTTP contour owns task lifecycle), getActiveTasksByType,
45
+ // reconcileCompletedTasks' siblings and any CRUD "for later". The host
46
+ // adapter owns every Redis key, every TTL and the pub/sub channel.
47
+ //
48
+ // Docs: docs/architecture/generation-module-extraction-reconnaissance.md §32.7 (O-P4, O-5 DONE)
49
+ // ======================================================
50
+
51
+ const OPS = [
52
+ 'publishProgress',
53
+ 'getSceneTaskState',
54
+ 'hasActiveTasks',
55
+ 'reconcileCompletedTasks',
56
+ ];
57
+
58
+ let impl = null;
59
+
60
+ /**
61
+ * Wire the host progress-events adapter. Called once by the composition
62
+ * root (backend.cjs) BEFORE any runtime/orchestration progress consumer
63
+ * runs.
64
+ * @param {object} adapter — flat map with every op in OPS as a function.
65
+ */
66
+ function setProgressEventsPort(adapter) {
67
+ if (!adapter) {
68
+ throw new Error('progress-events-port: adapter is required');
69
+ }
70
+ for (const op of OPS) {
71
+ if (typeof adapter[op] !== 'function') {
72
+ throw new Error(`progress-events-port: adapter must implement ${op}()`);
73
+ }
74
+ }
75
+ impl = adapter;
76
+ }
77
+
78
+ /** Resolve the port at CALL time — fail-fast when unwired. */
79
+ function progressEvents() {
80
+ if (!impl) {
81
+ throw new Error(
82
+ 'progress-events-port: not wired — the composition root must call ' +
83
+ 'setProgressEventsPort(adapter) (storage/progress-events-adapter) ' +
84
+ 'before runtime/orchestration progress consumers run (O-5)'
85
+ );
86
+ }
87
+ return impl;
88
+ }
89
+
90
+ /**
91
+ * Shorthand op resolver: `progressEventsOp('publishProgress')(...)`.
92
+ * Fails fast when the port or the single op is missing — a progress event
93
+ * or task read silently degrading to a no-op would corrupt window-slide
94
+ * and dispatch decisions.
95
+ * @param {string} op — op name from OPS
96
+ */
97
+ function progressEventsOp(op) {
98
+ const fn = impl?.[op];
99
+ if (typeof fn !== 'function') {
100
+ throw new Error(`progress-events-port: op '${op}' is not wired (O-5)`);
101
+ }
102
+ return fn;
103
+ }
104
+
105
+ function isProgressEventsPortWired() {
106
+ return impl !== null;
107
+ }
108
+
109
+ /** Test hygiene: drop the wiring (mocha hooks). */
110
+ function _resetProgressEventsPort() {
111
+ impl = null;
112
+ }
113
+
114
+ module.exports = {
115
+ OPS,
116
+ setProgressEventsPort,
117
+ progressEvents,
118
+ progressEventsOp,
119
+ isProgressEventsPortWired,
120
+ _resetProgressEventsPort,
121
+ };