@gentbajko/slopify 0.8.3 → 1.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.
Files changed (207) hide show
  1. package/README.md +28 -6
  2. package/SUBTITLES.md +2 -0
  3. package/dist/adapters/alignment/window.js +46 -5
  4. package/dist/adapters/alignment/worker.js +2 -2
  5. package/dist/adapters/llm/claude-code.js +6 -3
  6. package/dist/adapters/llm/codex.js +102 -12
  7. package/dist/adapters/llm/gemini.js +11 -8
  8. package/dist/adapters/llm/run-cli.js +61 -6
  9. package/dist/adapters/tts/inworld-async.js +4 -4
  10. package/dist/adapters/tts/inworld.js +73 -68
  11. package/dist/catalog/registry.js +13 -73
  12. package/dist/edge/cli.js +5 -2
  13. package/dist/edge/events/hub.js +7 -2
  14. package/dist/edge/events/preview-cache.js +7 -4
  15. package/dist/edge/events/visibility.js +33 -0
  16. package/dist/edge/http/actions.js +74 -79
  17. package/dist/edge/http/app.js +39 -6
  18. package/dist/edge/http/audio-preview.js +9 -1
  19. package/dist/edge/http/checkpoints.js +95 -0
  20. package/dist/edge/http/diagnostics.js +31 -0
  21. package/dist/edge/http/draft-files.js +69 -0
  22. package/dist/edge/http/draft-problem.js +15 -0
  23. package/dist/edge/http/drafts.js +95 -0
  24. package/dist/edge/http/multipart.js +86 -0
  25. package/dist/edge/http/mutations.js +49 -0
  26. package/dist/edge/http/planning.js +12 -1
  27. package/dist/edge/http/project-templates.js +74 -0
  28. package/dist/edge/http/projects.js +19 -2
  29. package/dist/edge/http/revision-files.js +84 -0
  30. package/dist/edge/http/revisions.js +103 -0
  31. package/dist/edge/http/schedules.js +100 -0
  32. package/dist/edge/http/staging.js +11 -52
  33. package/dist/edge/http/storage.js +110 -0
  34. package/dist/edge/http/subtitles.js +9 -79
  35. package/dist/edge/http/tutorial.js +26 -0
  36. package/dist/edge/signal-shutdown.js +23 -0
  37. package/dist/kernel/audio-preview.js +11 -5
  38. package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
  39. package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
  40. package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
  41. package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
  42. package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
  43. package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
  44. package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
  45. package/dist/kernel/ports/model.js +5 -0
  46. package/dist/kernel/runner/attempt-repo.js +15 -0
  47. package/dist/kernel/runner/attempt.js +14 -2
  48. package/dist/kernel/runner/checkpoint-authority.js +23 -0
  49. package/dist/kernel/runner/index.js +35 -11
  50. package/dist/kernel/runner/providers.js +18 -8
  51. package/dist/kernel/runner/work-attempt.js +47 -0
  52. package/dist/kernel/runner/work-authority.js +37 -0
  53. package/dist/kernel/runner/work.js +23 -0
  54. package/dist/main.js +227 -69
  55. package/dist/slices/admission/repo.js +5 -56
  56. package/dist/slices/admission/schema.js +64 -0
  57. package/dist/slices/admission/start.js +13 -4
  58. package/dist/slices/article/continuation.js +9 -3
  59. package/dist/slices/article/provided-entries.js +7 -3
  60. package/dist/slices/article/run.js +20 -7
  61. package/dist/slices/article/segments.js +13 -4
  62. package/dist/slices/batch/index.js +8 -11
  63. package/dist/slices/cancel/index.js +78 -34
  64. package/dist/slices/checkpoints/change.js +250 -0
  65. package/dist/slices/checkpoints/fingerprint.js +33 -0
  66. package/dist/slices/checkpoints/index.js +2 -0
  67. package/dist/slices/checkpoints/model.js +1 -0
  68. package/dist/slices/checkpoints/recovery.js +116 -0
  69. package/dist/slices/checkpoints/repo.js +132 -0
  70. package/dist/slices/checkpoints/rules.js +30 -0
  71. package/dist/slices/checkpoints/schema.js +59 -0
  72. package/dist/slices/control/index.js +88 -22
  73. package/dist/slices/control/providers.js +3 -2
  74. package/dist/slices/control/revision-control-schema.js +7 -0
  75. package/dist/slices/control/revision-control.js +59 -0
  76. package/dist/slices/estimate/index.js +77 -59
  77. package/dist/slices/estimate/requests.js +102 -0
  78. package/dist/slices/images/run.js +14 -3
  79. package/dist/slices/library/slots.js +11 -11
  80. package/dist/slices/library/snapshot.js +33 -0
  81. package/dist/slices/narration/legacy-plan.js +58 -0
  82. package/dist/slices/narration/live.js +2 -2
  83. package/dist/slices/narration/plan.js +48 -54
  84. package/dist/slices/narration/run.js +21 -8
  85. package/dist/slices/play-drafts/convert.js +120 -0
  86. package/dist/slices/play-drafts/model.js +1 -0
  87. package/dist/slices/play-drafts/readiness.js +80 -0
  88. package/dist/slices/play-drafts/repo.js +76 -0
  89. package/dist/slices/play-drafts/review-inputs.js +150 -0
  90. package/dist/slices/play-drafts/review.js +109 -0
  91. package/dist/slices/play-drafts/schema.js +218 -0
  92. package/dist/slices/play-drafts/service.js +242 -0
  93. package/dist/slices/play-drafts/start-repo.js +113 -0
  94. package/dist/slices/play-drafts/start.js +148 -0
  95. package/dist/slices/play-drafts/uploads.js +67 -0
  96. package/dist/slices/project-templates/from-project.js +159 -0
  97. package/dist/slices/project-templates/model.js +1 -0
  98. package/dist/slices/project-templates/repo.js +41 -0
  99. package/dist/slices/project-templates/schema.js +37 -0
  100. package/dist/slices/project-templates/service.js +136 -0
  101. package/dist/slices/project-templates/setup.js +55 -0
  102. package/dist/slices/rebuild/admission-repo.js +120 -0
  103. package/dist/slices/rebuild/dependencies.js +33 -0
  104. package/dist/slices/rebuild/model.js +94 -0
  105. package/dist/slices/rebuild/narration-history.js +43 -0
  106. package/dist/slices/rebuild/narration-reuse.js +20 -0
  107. package/dist/slices/rebuild/preview-details.js +144 -0
  108. package/dist/slices/rebuild/preview-plan.js +223 -0
  109. package/dist/slices/rebuild/preview-retained.js +128 -0
  110. package/dist/slices/rebuild/provided-review.js +32 -0
  111. package/dist/slices/rebuild/recipe-audio.js +172 -0
  112. package/dist/slices/rebuild/recipe-build.js +17 -0
  113. package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
  114. package/dist/slices/rebuild/recipe-exports.js +65 -0
  115. package/dist/slices/rebuild/recipe-fixture.js +150 -0
  116. package/dist/slices/rebuild/recipe-input-schema.js +82 -0
  117. package/dist/slices/rebuild/recipe-legacy.js +135 -0
  118. package/dist/slices/rebuild/recipe-model.js +77 -0
  119. package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
  120. package/dist/slices/rebuild/recipe-save.js +221 -0
  121. package/dist/slices/rebuild/recipe-text.js +170 -0
  122. package/dist/slices/rebuild/recipe-validation.js +86 -0
  123. package/dist/slices/rebuild/recipe-visual.js +111 -0
  124. package/dist/slices/rebuild/recipe-work.js +212 -0
  125. package/dist/slices/rebuild/recipes.js +4 -0
  126. package/dist/slices/rebuild/repo.js +64 -0
  127. package/dist/slices/rebuild/runtime-actions.js +76 -0
  128. package/dist/slices/rebuild/runtime-admission.js +71 -0
  129. package/dist/slices/rebuild/runtime-article.js +117 -0
  130. package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
  131. package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
  132. package/dist/slices/rebuild/runtime-export.js +139 -0
  133. package/dist/slices/rebuild/runtime-instructions.js +51 -0
  134. package/dist/slices/rebuild/runtime-local.js +137 -0
  135. package/dist/slices/rebuild/runtime-materialize.js +105 -0
  136. package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
  137. package/dist/slices/rebuild/runtime-plan.js +103 -0
  138. package/dist/slices/rebuild/runtime-provider.js +184 -0
  139. package/dist/slices/rebuild/runtime-publication.js +172 -0
  140. package/dist/slices/rebuild/runtime-run.js +35 -0
  141. package/dist/slices/rebuild/runtime-store.js +166 -0
  142. package/dist/slices/rebuild/runtime-subtitles.js +202 -0
  143. package/dist/slices/rebuild/service-readiness.js +110 -0
  144. package/dist/slices/rebuild/service.js +149 -0
  145. package/dist/slices/rebuild/transition-repo.js +108 -0
  146. package/dist/slices/rebuild/work-records.js +44 -0
  147. package/dist/slices/reruns/index.js +5 -0
  148. package/dist/slices/research/run.js +33 -12
  149. package/dist/slices/revisions/adopt-assets.js +31 -0
  150. package/dist/slices/revisions/adopt-content.js +112 -0
  151. package/dist/slices/revisions/adopt.js +100 -0
  152. package/dist/slices/revisions/downloads.js +80 -0
  153. package/dist/slices/revisions/index.js +5 -0
  154. package/dist/slices/revisions/manifest-repo.js +125 -0
  155. package/dist/slices/revisions/model.js +1 -0
  156. package/dist/slices/revisions/mutation-assets.js +143 -0
  157. package/dist/slices/revisions/mutation-cues.js +95 -0
  158. package/dist/slices/revisions/mutation-prepare.js +182 -0
  159. package/dist/slices/revisions/mutation-request.js +58 -0
  160. package/dist/slices/revisions/mutation-work.js +47 -0
  161. package/dist/slices/revisions/mutations.js +194 -0
  162. package/dist/slices/revisions/projection.js +82 -0
  163. package/dist/slices/revisions/publication-model.js +1 -0
  164. package/dist/slices/revisions/publication-rules.js +166 -0
  165. package/dist/slices/revisions/publish.js +115 -0
  166. package/dist/slices/revisions/repo.js +83 -0
  167. package/dist/slices/revisions/restore.js +60 -0
  168. package/dist/slices/revisions/rules.js +99 -0
  169. package/dist/slices/revisions/schema.js +160 -0
  170. package/dist/slices/revisions/view.js +44 -0
  171. package/dist/slices/schedules/calendar.js +47 -0
  172. package/dist/slices/schedules/model.js +1 -0
  173. package/dist/slices/schedules/repo.js +281 -0
  174. package/dist/slices/schedules/scheduler.js +144 -0
  175. package/dist/slices/schedules/schema.js +80 -0
  176. package/dist/slices/schedules/service.js +178 -0
  177. package/dist/slices/settings/cli-paths.js +1 -1
  178. package/dist/slices/settings/cli-status.js +45 -5
  179. package/dist/slices/settings/tutorial-schema.js +39 -0
  180. package/dist/slices/settings/tutorial.js +66 -0
  181. package/dist/slices/storage/assets.js +108 -0
  182. package/dist/slices/storage/delete-project.js +2 -1
  183. package/dist/slices/storage/downloads.js +4 -1
  184. package/dist/slices/storage/portable.js +871 -0
  185. package/dist/slices/storage/prepare.js +67 -0
  186. package/dist/slices/storage/reconcile.js +25 -5
  187. package/dist/slices/storage/repo.js +2 -15
  188. package/dist/slices/storage/schema.js +35 -0
  189. package/dist/slices/storage/staging-refs.js +26 -0
  190. package/dist/slices/storage/staging.js +16 -11
  191. package/dist/slices/thumbnail/run.js +26 -9
  192. package/dist/slices/video/audio-export-args.js +33 -0
  193. package/dist/slices/video/audio-export.js +13 -36
  194. package/dist/slices/video/audio-inputs.js +4 -3
  195. package/dist/slices/video/ffmpeg.js +6 -0
  196. package/dist/slices/video/reuse-audio.js +11 -4
  197. package/dist/updater/install-flow.js +27 -2
  198. package/dist/updater/plan.js +28 -2
  199. package/dist/updater/service.js +27 -2
  200. package/dist/updater/worker.js +2 -1
  201. package/dist/web/assets/index-CO7DnVX9.js +137 -0
  202. package/dist/web/assets/index-DX1_chKs.css +1 -0
  203. package/dist/web/index.html +2 -2
  204. package/package.json +2 -2
  205. package/dist/adapters/tts/inworld-text.js +0 -32
  206. package/dist/web/assets/index-Ce2HApus.css +0 -1
  207. package/dist/web/assets/index-xbD0MZOY.js +0 -130
@@ -0,0 +1,212 @@
1
+ import { z } from "zod";
2
+ import { estimateRequests } from "../estimate/index.js";
3
+ import { planDependencies } from "./dependencies.js";
4
+ import { matchingNarrationPiece } from "./narration-reuse.js";
5
+ import { buildRecipes } from "./recipe-build.js";
6
+ import { manualCuesNeedReview } from "./recipe-exports.js";
7
+ import { selectedReference, } from "./recipe-model.js";
8
+ export function planRevisionWork(revision, manifest, catalogue, availableAssetIds, resolved, retainedNarration = []) {
9
+ const selected = {
10
+ outputs: manifest.outputs.filter(selectedReference),
11
+ pieces: manifest.pieces.filter(selectedReference),
12
+ };
13
+ const available = {
14
+ outputs: selected.outputs.filter((row) => availableAssetIds.has(row.assetId)),
15
+ pieces: selected.pieces.filter((row) => row.assetId === null || availableAssetIds.has(row.assetId)),
16
+ };
17
+ const usableArticle = revision.config.sources.article !== "generate" ||
18
+ revision.content.articleEdited === true ||
19
+ selected.outputs.some((row) => row.workKey === "article:body" &&
20
+ row.state === "ready" &&
21
+ availableAssetIds.has(row.assetId));
22
+ const logicalContext = {
23
+ config: revision.config,
24
+ content: revision.content,
25
+ manifest: available,
26
+ resolved: { ...resolved, articleMarkdown: usableArticle ? resolved.articleMarkdown : null },
27
+ };
28
+ const logical = buildRecipes(logicalContext);
29
+ const context = { ...logicalContext, catalogue };
30
+ const recipes = buildRecipes(context).map((row) => ({
31
+ ...row,
32
+ logicalFingerprint: logical.find((one) => one.key === (row.input.kind === "tts" ? `${row.input.logicalKey}:1` : row.key))?.fingerprint ?? row.fingerprint,
33
+ unresolved: row.unresolved ||
34
+ (row.input.kind === "provided" &&
35
+ (row.input.assetId === null || !availableAssetIds.has(row.input.assetId))),
36
+ }));
37
+ const retained = recipes.flatMap((row) => retainedFor(row, logical, row.input.kind === "tts"
38
+ ? { ...selected, pieces: [...selected.pieces, ...retainedNarration] }
39
+ : selected, availableAssetIds));
40
+ const rawWork = planDependencies(recipes.map(comparisonRecipe), retained);
41
+ const manualReview = manualCuesNeedReview(logicalContext, logical);
42
+ const work = rawWork
43
+ .filter((row) => revision.content.subtitleCues === undefined || row.key !== "subtitles:timing")
44
+ .map((row) => manualReview && row.key === "subtitles:cues"
45
+ ? {
46
+ ...row,
47
+ disposition: "review",
48
+ reason: "Narration or transcript changed. Review the saved manual cues before reusing them.",
49
+ }
50
+ : row);
51
+ const changedInputs = work
52
+ .filter((row) => row.disposition !== "reuse")
53
+ .map((row) => ({
54
+ path: row.key,
55
+ before: selected.outputs.find((one) => one.workKey === row.key)?.fingerprint ??
56
+ selected.pieces.find((one) => one.key === row.key)?.fingerprint ??
57
+ null,
58
+ after: row.fingerprint,
59
+ }));
60
+ const priced = work.map((row) => priceRecipe(row, recipes.find((one) => one.key === row.key)));
61
+ return {
62
+ recipes,
63
+ work,
64
+ changedInputs,
65
+ providedReuseRequired: work.filter((row) => row.disposition === "review").map((row) => row.key),
66
+ wholeRequestNotice: revision.config.sources.audio === "generate" &&
67
+ (revision.config.chunking?.mode ?? "whole") === "whole"
68
+ ? "Whole-text narration is one logical request. Editing its text rebuilds every provider part of that request."
69
+ : null,
70
+ costs: estimateRequests(priced, catalogue),
71
+ };
72
+ }
73
+ function comparisonRecipe(row) {
74
+ return {
75
+ key: row.key,
76
+ stage: row.stage,
77
+ kind: row.kind,
78
+ requestFingerprint: row.requestFingerprint,
79
+ fingerprint: row.fingerprint,
80
+ dependsOn: row.dependsOn,
81
+ unresolved: row.unresolved,
82
+ };
83
+ }
84
+ function retainedFor(row, logical, manifest, available) {
85
+ if (row.input.kind === "provided" &&
86
+ row.key.startsWith("audio:") &&
87
+ row.key !== "audio:provided") {
88
+ const assetId = row.input.assetId;
89
+ const previous = manifest.pieces.find((one) => one.key === row.key &&
90
+ one.assetId === assetId &&
91
+ one.piece.state === "done" &&
92
+ one.fingerprint !== row.fingerprint &&
93
+ z.object({ provided: z.literal(true) }).safeParse(JSON.parse(one.piece.payload ?? "null"))
94
+ .success);
95
+ if (previous !== undefined)
96
+ return [
97
+ {
98
+ key: row.key,
99
+ requestFingerprint: "",
100
+ fingerprint: previous.fingerprint,
101
+ available: assetId !== null && available.has(assetId),
102
+ inflight: false,
103
+ pieceIds: [previous.piece.id],
104
+ },
105
+ ];
106
+ }
107
+ const narration = matchingNarrationPiece(row, manifest.pieces, available);
108
+ if (narration !== undefined)
109
+ return [
110
+ {
111
+ key: row.key,
112
+ requestFingerprint: row.requestFingerprint,
113
+ fingerprint: row.fingerprint,
114
+ available: true,
115
+ inflight: false,
116
+ pieceIds: [narration.piece.id],
117
+ },
118
+ ];
119
+ const bundle = manifest.outputs.filter((one) => one.workKey === row.key && one.fingerprint === row.logicalFingerprint);
120
+ const complete = bundle.every((one) => one.state === "ready" && available.has(one.assetId));
121
+ const current = bundle.length > 0 && complete;
122
+ const pieces = manifest.pieces.filter((one) => one.key === row.key);
123
+ const matched = pieces.filter((one) => {
124
+ const request = one.piece.payload === null
125
+ ? undefined
126
+ : z
127
+ .object({ requestFingerprint: z.string().optional() })
128
+ .parse(JSON.parse(one.piece.payload)).requestFingerprint;
129
+ return (one.fingerprint === row.fingerprint &&
130
+ request === row.requestFingerprint &&
131
+ (one.assetId === null || available.has(one.assetId)));
132
+ });
133
+ const concat = row.key.startsWith("audio:body:") && row.key !== "audio:body:concat"
134
+ ? logical.find((one) => one.key === "audio:body:concat")
135
+ : undefined;
136
+ const retainedConcat = concat !== undefined &&
137
+ manifest.outputs.some((one) => one.workKey === concat.key &&
138
+ one.fingerprint === concat.fingerprint &&
139
+ one.state === "ready" &&
140
+ available.has(one.assetId));
141
+ if (current ||
142
+ matched.length > 0 ||
143
+ retainedConcat ||
144
+ (row.input.kind === "provided" &&
145
+ row.input.assetId !== null &&
146
+ available.has(row.input.assetId) &&
147
+ !manifest.outputs.some((one) => one.workKey === row.key)))
148
+ return [
149
+ {
150
+ key: row.key,
151
+ requestFingerprint: row.requestFingerprint,
152
+ fingerprint: row.fingerprint,
153
+ available: current ||
154
+ (complete && matched.some((one) => one.piece.state === "done")) ||
155
+ retainedConcat ||
156
+ row.input.kind === "provided",
157
+ inflight: matched.some((one) => one.piece.state === "running"),
158
+ pieceIds: matched.map((one) => one.piece.id),
159
+ },
160
+ ];
161
+ return manifest.outputs
162
+ .filter((one) => one.workKey === row.key)
163
+ .map((one) => ({
164
+ key: row.key,
165
+ requestFingerprint: "",
166
+ fingerprint: one.fingerprint,
167
+ available: manifest.outputs
168
+ .filter((sibling) => sibling.workKey === one.workKey && sibling.fingerprint === one.fingerprint)
169
+ .every((sibling) => sibling.state === "ready" && available.has(sibling.assetId)),
170
+ inflight: false,
171
+ pieceIds: [],
172
+ }));
173
+ }
174
+ export function priceRecipe(work, value) {
175
+ if (value === undefined || work.disposition !== "generate")
176
+ return {
177
+ kind: "local",
178
+ stage: work.key,
179
+ detail: work.disposition === "reuse"
180
+ ? "Retained work; no generation charge."
181
+ : "Local, provided, or blocked work; no admitted generation charge.",
182
+ };
183
+ const input = value.input;
184
+ if (input.kind === "llm")
185
+ return {
186
+ kind: "llm",
187
+ stage: work.key,
188
+ provider: input.provider,
189
+ model: input.model,
190
+ inputCharacters: input.messages.reduce((sum, message) => sum + message.content.length, 0),
191
+ outputCharacters: work.key === "article:body" ? 9000 : 2400,
192
+ detail: "Actual saved request input; output length is estimated. Retries and tools are excluded.",
193
+ };
194
+ if (input.kind === "tts")
195
+ return {
196
+ kind: "tts",
197
+ stage: work.key,
198
+ provider: input.provider,
199
+ model: input.model,
200
+ text: input.text,
201
+ };
202
+ if (input.kind === "image")
203
+ return { kind: "image", stage: work.key, provider: input.provider, model: input.model };
204
+ return {
205
+ kind: "tts-estimate",
206
+ stage: work.key,
207
+ provider: "",
208
+ model: "",
209
+ characters: 9000,
210
+ detail: "Future generated input is unknown. This group materializes under the same admitted revision; its final request count and charge are unknown.",
211
+ };
212
+ }
@@ -0,0 +1,4 @@
1
+ export { baselineFingerprints, legacyOutputSlot, legacyOutputWorkKey, legacyPieceKey, } from "./recipe-legacy.js";
2
+ export { normalizeArticleIntent, planRevision } from "./recipe-save.js";
3
+ export { visualRecipes } from "./recipe-visual.js";
4
+ export { planRevisionWork } from "./recipe-work.js";
@@ -0,0 +1,64 @@
1
+ import { transact } from "../../kernel/db/tx.js";
2
+ import { workExists } from "../../kernel/runner/work-authority.js";
3
+ import { currentRevisionId, revisionById } from "../revisions/repo.js";
4
+ import { rebuildPreviewSchema } from "./model.js";
5
+ export { claimWork, finishWork, maySubmit } from "../../kernel/runner/work-authority.js";
6
+ export { transitionRevisionWork } from "./transition-repo.js";
7
+ export function publicationTargets(db, publication, workKey, workFingerprint) {
8
+ const { work, pieceId, publicationId } = publication;
9
+ if (publicationId !== (pieceId ?? work.workId))
10
+ throw new Error("Publication identity does not match its durable work.");
11
+ if (!workExists(db, work))
12
+ return [];
13
+ if (pieceId !== null) {
14
+ const piece = db
15
+ .prepare("SELECT work_key,fingerprint FROM revision_work_pieces WHERE id=? AND work_id=?")
16
+ .get(pieceId, work.workId);
17
+ if (piece === undefined)
18
+ throw new Error("Publication piece does not belong to its work.");
19
+ if (piece.work_key !== workKey || piece.fingerprint !== workFingerprint)
20
+ throw new Error("Publication authority does not match its durable piece.");
21
+ }
22
+ const currentId = currentRevisionId(db, work.projectId);
23
+ const owns = (revisionId) => {
24
+ const reservation = db
25
+ .prepare("SELECT logical_key,desired_fingerprint FROM revision_work_reservations WHERE project_id=? AND revision_id=? AND work_key=? AND work_id=? AND fingerprint=? AND piece_id IS ?")
26
+ .get(work.projectId, revisionId, workKey, work.workId, workFingerprint, pieceId);
27
+ if (reservation === undefined)
28
+ return false;
29
+ const logicalKey = typeof reservation.logical_key === "string" ? reservation.logical_key : workKey;
30
+ const desired = typeof reservation.desired_fingerprint === "string"
31
+ ? reservation.desired_fingerprint
32
+ : workFingerprint;
33
+ return revisionById(db, work.projectId, revisionId)?.fingerprints[logicalKey] === desired;
34
+ };
35
+ const targets = [
36
+ {
37
+ revisionId: work.revisionId,
38
+ current: currentId === work.revisionId,
39
+ selected: owns(work.revisionId),
40
+ },
41
+ ];
42
+ if (currentId !== undefined && currentId !== work.revisionId && owns(currentId))
43
+ targets.push({ revisionId: currentId, current: true, selected: true });
44
+ return targets;
45
+ }
46
+ export function storePreview(deps, preview, execution) {
47
+ const parsed = rebuildPreviewSchema.parse(preview);
48
+ deps.db
49
+ .prepare("INSERT INTO rebuild_previews(id,project_id,revision_id,plan_fingerprint,body_json,created_at,execution_json) VALUES (?,?,?,?,?,?,?)")
50
+ .run(parsed.id, parsed.projectId, parsed.baseRevisionId, parsed.planFingerprint, JSON.stringify(parsed), deps.clock.now().toISOString(), execution === undefined ? null : JSON.stringify(execution));
51
+ }
52
+ export function previewById(db, projectId, previewId) {
53
+ const row = db
54
+ .prepare("SELECT body_json FROM rebuild_previews WHERE id=? AND project_id=?")
55
+ .get(previewId, projectId);
56
+ return row === undefined
57
+ ? undefined
58
+ : rebuildPreviewSchema.parse(JSON.parse(String(row.body_json)));
59
+ }
60
+ export function recoverWork(db) {
61
+ transact(db, () => db.exec(`UPDATE revision_work_pieces SET state='held',dispatch_state='held' WHERE state!='done' AND work_id IN (SELECT id FROM revision_work WHERE project_id NOT IN (SELECT project_id FROM project_queue WHERE state='queued'));
62
+ UPDATE revision_work SET state='pending',dispatch_state='held' WHERE state!='done' AND project_id NOT IN (SELECT project_id FROM project_queue WHERE state='queued');`));
63
+ }
64
+ export { admissionReceipt, admitPreview } from "./admission-repo.js";
@@ -0,0 +1,76 @@
1
+ import { saveRevision } from "../revisions/mutations.js";
2
+ import { currentRevisionId } from "../revisions/repo.js";
3
+ import { getRevisionView } from "../revisions/view.js";
4
+ import { projectStandings } from "./runtime-store.js";
5
+ export async function revisionAction(deps, projectId, action) {
6
+ const revisionId = currentRevisionId(deps.db, projectId);
7
+ const view = revisionId === undefined ? undefined : getRevisionView(deps, projectId, revisionId);
8
+ if (view === undefined)
9
+ return { ok: false, reason: "no-project" };
10
+ if (action.kind === "retry")
11
+ return { ok: false, reason: "rebuild-required" };
12
+ const config = view.revision.config;
13
+ const content = view.revision.content;
14
+ const image = action.kind === "delete-image" || action.kind === "regenerate-image"
15
+ ? view.outputs.find((row) => row.selected && row.output.id === action.outputId && row.output.role === "image")
16
+ : undefined;
17
+ if ((action.kind === "delete-image" || action.kind === "regenerate-image") && image === undefined)
18
+ return { ok: false, reason: "unknown-image" };
19
+ const imageKey = image?.workKey.slice(6);
20
+ if (action.kind === "article" && action.markdown.trim() === "")
21
+ return { ok: false, reason: "empty-article" };
22
+ const regenerate = action.kind === "rerun"
23
+ ? Object.keys(view.revision.fingerprints).filter((key) => keyStage(key) === action.stage)
24
+ : action.kind === "regenerate-image" && image !== undefined
25
+ ? [image.workKey]
26
+ : undefined;
27
+ const nextContent = action.kind === "article"
28
+ ? { ...content, articleMarkdown: action.markdown, articleEdited: true }
29
+ : action.kind === "delete-image"
30
+ ? {
31
+ ...content,
32
+ imageOrder: content.imageOrder.filter((key) => key !== imageKey),
33
+ imageDefinitions: Object.fromEntries(Object.entries(content.imageDefinitions).filter(([key]) => key !== imageKey)),
34
+ }
35
+ : content;
36
+ const nextConfig = action.kind === "delete-image" && nextContent.imageOrder.length === 0
37
+ ? {
38
+ ...config,
39
+ sources: { ...config.sources, images: "off", video: "off" },
40
+ ...(config.subtitles?.mode === "burn-in"
41
+ ? { subtitles: { ...config.subtitles, mode: "files" } }
42
+ : {}),
43
+ }
44
+ : config;
45
+ const saved = await saveRevision(deps, {
46
+ projectId,
47
+ baseRevisionId: view.revision.id,
48
+ idempotencyKey: deps.ids.next(),
49
+ edit: {
50
+ config: nextConfig,
51
+ content: nextContent,
52
+ ...(regenerate === undefined ? {} : { regenerate }),
53
+ },
54
+ });
55
+ if (!saved.ok)
56
+ return { ok: false, reason: "not-rerunnable" };
57
+ projectStandings(deps, projectId);
58
+ return {
59
+ ok: true,
60
+ redone: [],
61
+ };
62
+ }
63
+ function keyStage(key) {
64
+ const prefix = key.split(":")[0];
65
+ if (prefix === "entry" || prefix === "article")
66
+ return "article";
67
+ if (prefix === "image")
68
+ return "images";
69
+ if (prefix === "audio")
70
+ return "audio";
71
+ if (prefix === "thumbnail")
72
+ return "thumbnail";
73
+ if (prefix === "research")
74
+ return "research";
75
+ return "video";
76
+ }
@@ -0,0 +1,71 @@
1
+ import { transact } from "../../kernel/db/tx.js";
2
+ import { narrationRegenerationToken } from "../narration/plan.js";
3
+ import { currentRevisionId } from "../revisions/repo.js";
4
+ import { bindNarrationReuse, narrationOrdinal } from "./runtime-narration-reuse.js";
5
+ import { executionCatalogue, executionPlan } from "./runtime-plan.js";
6
+ import { insertWorkPiece } from "./work-records.js";
7
+ export function admitInitialRevision(deps, view, catalogue) {
8
+ transact(deps.db, () => {
9
+ if (currentRevisionId(deps.db, view.revision.projectId) !== view.revision.id)
10
+ throw new Error("Initial admission requires the selected revision.");
11
+ if (deps.db
12
+ .prepare("SELECT 1 FROM revision_work WHERE project_id=? LIMIT 1")
13
+ .get(view.revision.projectId) !== undefined)
14
+ throw new Error("An existing project requires explicit rebuild admission.");
15
+ const snapshot = executionCatalogue(catalogue, view.revision.config);
16
+ const plan = executionPlan(deps, view, snapshot);
17
+ for (const recipe of plan.recipes) {
18
+ if (!plan.work.some((row) => row.key === recipe.key))
19
+ continue;
20
+ const desired = view.revision.fingerprints[recipe.key];
21
+ const logicalKey = recipe.input.kind === "tts" ? `${recipe.input.logicalKey}:1` : recipe.key;
22
+ const anchor = view.revision.fingerprints[logicalKey] ?? desired;
23
+ if (anchor === undefined)
24
+ throw new Error(`Initial work ${recipe.key} has no desired anchor.`);
25
+ const reuse = plan.work.find((row) => row.key === recipe.key)?.disposition === "reuse";
26
+ if (reuse)
27
+ bindNarrationReuse(deps, view, recipe, narrationOrdinal(plan.recipes, recipe.key));
28
+ insertInvocation(deps, view, recipe, snapshot, { key: logicalKey, fingerprint: anchor }, reuse);
29
+ }
30
+ });
31
+ }
32
+ export function insertInvocation(deps, view, recipe, catalogue, anchor, completed, reservationRevisionId = view.revision.id, admissionId = null) {
33
+ const { projectId, id: revisionId } = view.revision;
34
+ const stage = deps.db
35
+ .prepare("SELECT id FROM stages WHERE project_id=? AND kind=?")
36
+ .get(projectId, recipe.stage);
37
+ if (typeof stage?.id !== "string")
38
+ throw new Error("Revision work has no owned stage.");
39
+ const work = {
40
+ projectId,
41
+ revisionId,
42
+ workId: deps.ids.next(),
43
+ stageId: stage.id,
44
+ kind: recipe.stage,
45
+ fingerprint: recipe.fingerprint,
46
+ };
47
+ deps.db
48
+ .prepare(`INSERT INTO revision_work(id,project_id,revision_id,stage_id,kind,fingerprint,state,dispatch_state,recipe_context,created_at,admission_id) VALUES (?,?,?,?,?,?,?,'allowed',?,?,?)`)
49
+ .run(work.workId, projectId, revisionId, stage.id, recipe.stage, recipe.fingerprint, completed ? "done" : "pending", JSON.stringify(catalogue), deps.clock.now().toISOString(), admissionId);
50
+ const pieceId = deps.ids.next();
51
+ insertWorkPiece(deps.db, {
52
+ id: pieceId,
53
+ workId: work.workId,
54
+ key: recipe.key,
55
+ fingerprint: recipe.fingerprint,
56
+ requestFingerprint: recipe.requestFingerprint,
57
+ logicalFingerprint: recipe.logicalFingerprint,
58
+ input: recipe.input,
59
+ continuation: null,
60
+ generationToken: recipe.input.kind === "tts"
61
+ ? narrationRegenerationToken(view.revision.content.regenerationTokens, recipe.input.logicalKey, recipe.input.segment)
62
+ : (view.revision.content.regenerationTokens[recipe.key] ?? null),
63
+ state: completed ? "done" : "pending",
64
+ dispatchState: "allowed",
65
+ submittedAt: null,
66
+ });
67
+ deps.db
68
+ .prepare(`INSERT INTO revision_work_reservations(project_id,revision_id,work_key,work_id,piece_id,fingerprint,logical_key,desired_fingerprint) VALUES (?,?,?,?,?,?,?,?)`)
69
+ .run(projectId, reservationRevisionId, recipe.key, work.workId, pieceId, recipe.fingerprint, anchor.key, anchor.fingerprint);
70
+ return work;
71
+ }
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ import { transact } from "../../kernel/db/tx.js";
3
+ import { continuationLimit, continuationMessages } from "../article/continuation.js";
4
+ import { currentRevisionId, revisionById } from "../revisions/repo.js";
5
+ import { recipe } from "./recipe-model.js";
6
+ import { retainPartialArticle } from "./runtime-publication.js";
7
+ import { insertWorkPiece, workPieces } from "./work-records.js";
8
+ const answerSchema = z.object({
9
+ text: z.string(),
10
+ finishReason: z.string().nullable(),
11
+ usage: z.object({ inputTokens: z.number(), outputTokens: z.number() }).nullable(),
12
+ });
13
+ export async function executeArticleRequests(deps, context, providers, initial) {
14
+ if (initial.input.kind !== "llm")
15
+ throw new Error("Article generation needs a pinned LLM request.");
16
+ let piece = initial;
17
+ let text = "";
18
+ let inputTokens = 0;
19
+ let outputTokens = 0;
20
+ for (let part = 0; part <= continuationLimit; part += 1) {
21
+ if (piece.input.kind !== "llm")
22
+ throw new Error("An article continuation must contain an exact LLM request.");
23
+ const saved = deps.db
24
+ .prepare("SELECT result_json FROM revision_work_pieces WHERE id=? AND work_id=?")
25
+ .get(piece.id, context.work.workId);
26
+ let answer;
27
+ if (typeof saved?.result_json === "string")
28
+ answer = answerSchema.parse(JSON.parse(saved.result_json));
29
+ else {
30
+ const input = piece.input;
31
+ const result = await providers.forPiece(piece.id).llm({
32
+ provider: input.provider,
33
+ model: input.model,
34
+ messages: input.messages,
35
+ thinkingConfig: input.thinkingConfig,
36
+ ...(input.thinking === null ? {} : { thinking: input.thinking }),
37
+ webSearch: input.webSearch,
38
+ previewLabel: part === 0 ? "Article" : `Continuation ${part}`,
39
+ check: (value) => value.text.trim() === ""
40
+ ? "The article answered with nothing."
41
+ : part === continuationLimit && value.finishReason === "length"
42
+ ? `The article was still unfinished after ${continuationLimit} continuations.`
43
+ : undefined,
44
+ }, (event) => {
45
+ if (event.type === "delta")
46
+ context.emit({
47
+ type: "article.delta",
48
+ projectId: context.work.projectId,
49
+ workPieceId: piece.id,
50
+ text: event.text,
51
+ });
52
+ });
53
+ if (!result.ok)
54
+ return result;
55
+ answer = result.value;
56
+ deps.db
57
+ .prepare("UPDATE revision_work_pieces SET result_json=? WHERE id=? AND work_id=? AND result_json IS NULL")
58
+ .run(JSON.stringify(answer), piece.id, context.work.workId);
59
+ }
60
+ text += answer.text;
61
+ inputTokens += answer.usage?.inputTokens ?? 0;
62
+ outputTokens += answer.usage?.outputTokens ?? 0;
63
+ if (answer.finishReason !== "length")
64
+ return {
65
+ ok: true,
66
+ value: { text, finishReason: answer.finishReason, usage: { inputTokens, outputTokens } },
67
+ };
68
+ retainPartialArticle(deps, context, piece, text);
69
+ const next = continuationPiece(deps, context, initial, part + 1, text);
70
+ if (next === undefined)
71
+ return { ok: false, reason: "held" };
72
+ piece = next;
73
+ }
74
+ throw new Error("The article exceeded its continuation limit.");
75
+ }
76
+ function continuationPiece(deps, context, initial, part, text) {
77
+ return transact(deps.db, () => {
78
+ const key = `article:continuation:${part}`;
79
+ const existing = workPieces(deps.db, context.work.workId).find((piece) => piece.key === key);
80
+ if (existing !== undefined)
81
+ return existing;
82
+ if (!context.maySubmit(initial.id) || initial.input.kind !== "llm")
83
+ return undefined;
84
+ const revision = revisionById(deps.db, context.work.projectId, context.work.revisionId);
85
+ const head = currentRevisionId(deps.db, context.work.projectId);
86
+ if (revision === undefined || head === undefined)
87
+ return undefined;
88
+ const anchor = deps.db
89
+ .prepare("SELECT logical_key,desired_fingerprint,work_key,fingerprint FROM revision_work_reservations WHERE revision_id=? AND work_id=? AND piece_id=?")
90
+ .get(head, context.work.workId, initial.id);
91
+ if (anchor === undefined)
92
+ return undefined;
93
+ const value = recipe({ content: revision.content }, key, "article", {
94
+ ...initial.input,
95
+ messages: continuationMessages(initial.input.messages, text),
96
+ }, [initial.key], { tokenKey: initial.key });
97
+ const piece = {
98
+ id: deps.ids.next(),
99
+ workId: context.work.workId,
100
+ key,
101
+ fingerprint: value.fingerprint,
102
+ requestFingerprint: value.requestFingerprint,
103
+ logicalFingerprint: value.logicalFingerprint,
104
+ input: value.input,
105
+ continuation: null,
106
+ generationToken: initial.generationToken,
107
+ state: "pending",
108
+ dispatchState: "allowed",
109
+ submittedAt: null,
110
+ };
111
+ insertWorkPiece(deps.db, piece);
112
+ deps.db
113
+ .prepare("INSERT INTO revision_work_reservations(project_id,revision_id,work_key,work_id,piece_id,fingerprint,logical_key,desired_fingerprint) VALUES (?,?,?,?,?,?,?,?)")
114
+ .run(context.work.projectId, head, key, context.work.workId, piece.id, piece.fingerprint, z.string().parse(anchor.logical_key ?? anchor.work_key), z.string().parse(anchor.desired_fingerprint ?? anchor.fingerprint));
115
+ return piece;
116
+ });
117
+ }
@@ -0,0 +1,45 @@
1
+ import { checkpointClosure, checkpointFingerprint } from "../checkpoints/fingerprint.js";
2
+ import { saveCheckpointSet } from "../checkpoints/repo.js";
3
+ import { currentRevisionId } from "../revisions/repo.js";
4
+ import { getRevisionView } from "../revisions/view.js";
5
+ import { executionCatalogue, executionPlan } from "./runtime-plan.js";
6
+ export function admitReviewedCheckpoints(deps, projectIds, reviewed, catalogue) {
7
+ return projectIds.flatMap((projectId, runIndex) => {
8
+ const gates = reviewed.filter((gate) => gate.runIndex === runIndex);
9
+ if (!gates.length)
10
+ return [];
11
+ const revisionId = currentRevisionId(deps.db, projectId);
12
+ const view = revisionId === undefined ? undefined : getRevisionView(deps, projectId, revisionId);
13
+ if (!view)
14
+ throw new Error("Checkpoint admission has no retained revision");
15
+ const plan = executionPlan(deps, view, executionCatalogue(catalogue, view.revision.config));
16
+ const checkpoints = gates.map((gate) => {
17
+ const work = deps.db
18
+ .prepare("SELECT id FROM revision_work WHERE project_id=? AND revision_id=? AND kind=? ORDER BY id LIMIT 1")
19
+ .get(projectId, view.revision.id, gate.stage);
20
+ if (typeof work?.id !== "string")
21
+ throw new Error("Checkpoint admission has no stage work");
22
+ return {
23
+ checkpointId: gate.checkpointId,
24
+ stage: gate.stage,
25
+ workId: work.id,
26
+ fingerprint: checkpointFingerprint(view.revision, checkpointClosure(gate.stage, plan.recipes)),
27
+ state: "held",
28
+ };
29
+ });
30
+ const saved = saveCheckpointSet(deps.db, {
31
+ projectId,
32
+ revisionId: view.revision.id,
33
+ checkpoints,
34
+ createdAt: deps.clock.now().toISOString(),
35
+ });
36
+ if (!saved.ok)
37
+ throw new Error("Checkpoint admission could not persist reviewed gates");
38
+ return saved.value.map((row) => {
39
+ const gate = gates.find((gate) => gate.checkpointId === row.checkpointId);
40
+ if (!gate)
41
+ throw new Error("Checkpoint admission lost its reviewed identity");
42
+ return { ...row, reviewedFingerprint: gate.fingerprint };
43
+ });
44
+ });
45
+ }