@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,218 @@
1
+ import { z } from "zod";
2
+ import { formats } from "../../kernel/pipeline.js";
3
+ import { thinkingModes } from "../../kernel/ports/llm.js";
4
+ import { stageSources } from "../admission/model.js";
5
+ import { runDraftSchema } from "../admission/schema.js";
6
+ import { checkpointRowSchema, checkpointStageSchema } from "../checkpoints/schema.js";
7
+ import { librarySnapshotSchema } from "../library/snapshot.js";
8
+ import { chunkModes } from "../narration/chunk.js";
9
+ import { subtitleModes, subtitlePositions } from "../subtitles/model.js";
10
+ const id = z.uuid();
11
+ const text = z.string();
12
+ const values = z.record(text, text).readonly();
13
+ const provider = z
14
+ .object({ provider: text, model: text, thinking: z.enum(thinkingModes).optional() })
15
+ .strict();
16
+ const file = z.object({ attachmentId: id, name: text }).strict().readonly();
17
+ export const playDraftFormSchema = z
18
+ .object({
19
+ title: text,
20
+ checkpoints: runDraftSchema.shape.checkpoints,
21
+ format: z.enum(formats),
22
+ sources: z
23
+ .object({
24
+ research: z.enum(stageSources),
25
+ article: z.enum(stageSources),
26
+ audio: z.enum(stageSources),
27
+ images: z.enum(stageSources),
28
+ thumbnail: z.enum(stageSources),
29
+ video: z.enum(stageSources),
30
+ })
31
+ .strict()
32
+ .readonly(),
33
+ llm: provider.readonly(),
34
+ audio: provider.extend({ voice: text }).readonly(),
35
+ images: provider.readonly(),
36
+ articlePrompt: text,
37
+ imagePrompts: z.array(z.object({ name: text, number: text }).strict().readonly()).readonly(),
38
+ thumbnailPrompt: text,
39
+ intro: text,
40
+ outro: text,
41
+ chunking: z
42
+ .object({ mode: z.enum(chunkModes), words: text, characters: text })
43
+ .strict()
44
+ .readonly(),
45
+ subtitles: z
46
+ .object({
47
+ mode: z.enum(subtitleModes),
48
+ language: z.literal("en"),
49
+ fontId: text,
50
+ fontSize: text,
51
+ position: z.enum(subtitlePositions),
52
+ })
53
+ .strict()
54
+ .readonly(),
55
+ values,
56
+ provided: z
57
+ .object({
58
+ research: text,
59
+ article: text,
60
+ audio: file.nullable(),
61
+ thumbnail: file.nullable(),
62
+ images: z.array(file).readonly(),
63
+ })
64
+ .strict()
65
+ .readonly(),
66
+ })
67
+ .strict()
68
+ .readonly();
69
+ export const playDraftDocumentSchema = z
70
+ .object({
71
+ schemaVersion: z.literal(1),
72
+ librarySnapshot: librarySnapshotSchema.optional(),
73
+ templateSource: z
74
+ .object({ id, version: z.number().int().positive() })
75
+ .strict()
76
+ .readonly()
77
+ .optional(),
78
+ form: playDraftFormSchema,
79
+ section: z.enum(["content", "outputs", "style", "review"]),
80
+ variants: z.array(z.object({ id, title: text, values }).strict().readonly()).readonly(),
81
+ expectedWords: text,
82
+ previewText: text,
83
+ fontUpload: z.object({ operationId: id, name: text }).strict().readonly().nullable(),
84
+ })
85
+ .strict()
86
+ .readonly();
87
+ export const createDraftInputSchema = z
88
+ .object({ id, document: playDraftDocumentSchema })
89
+ .strict()
90
+ .readonly();
91
+ export const saveDraftInputSchema = createDraftInputSchema
92
+ .unwrap()
93
+ .extend({ baseVersion: z.number().int().positive(), mutationId: id })
94
+ .strict()
95
+ .readonly();
96
+ export const forkDraftInputSchema = createDraftInputSchema
97
+ .unwrap()
98
+ .extend({ sourceId: id })
99
+ .strict()
100
+ .readonly();
101
+ export const discardDraftInputSchema = z
102
+ .object({ id, baseVersion: z.number().int().positive() })
103
+ .strict()
104
+ .readonly();
105
+ export const draftAttachmentSchema = z
106
+ .object({
107
+ id,
108
+ kind: z.enum(["audio", "images", "thumbnail"]),
109
+ name: text,
110
+ state: z.enum(["pending", "copying", "ready", "reattach"]),
111
+ stagedFileId: text.nullable(),
112
+ bytes: z.number().nonnegative(),
113
+ error: text.nullable(),
114
+ })
115
+ .strict()
116
+ .readonly();
117
+ export const playDraftSchema = z
118
+ .object({
119
+ id,
120
+ version: z.number().int().positive(),
121
+ createdAt: text,
122
+ updatedAt: text,
123
+ document: playDraftDocumentSchema,
124
+ })
125
+ .strict()
126
+ .readonly();
127
+ export const playStartResultSchema = z
128
+ .object({
129
+ checkpointSet: z
130
+ .array(checkpointRowSchema.unwrap().extend({ reviewedFingerprint: text }).strict().readonly())
131
+ .readonly()
132
+ .optional(),
133
+ requestId: id,
134
+ projectIds: z.array(text).readonly(),
135
+ queue: z
136
+ .array(z
137
+ .object({
138
+ projectId: text,
139
+ batchId: text,
140
+ position: z.number(),
141
+ state: z.enum(["queued", "active", "finished"]),
142
+ })
143
+ .strict()
144
+ .readonly())
145
+ .readonly(),
146
+ replayed: z.boolean(),
147
+ })
148
+ .strict()
149
+ .readonly();
150
+ const costEstimateSchema = z
151
+ .object({
152
+ currency: z.literal("USD"),
153
+ rows: z
154
+ .array(z
155
+ .object({
156
+ stage: text,
157
+ low: z.number().nullable(),
158
+ high: z.number().nullable(),
159
+ detail: text,
160
+ })
161
+ .strict()
162
+ .readonly())
163
+ .readonly(),
164
+ low: z.number(),
165
+ high: z.number(),
166
+ unknown: z.number(),
167
+ expectedWords: z.number(),
168
+ catalogueDate: text.nullable(),
169
+ assumptions: z.array(text).readonly(),
170
+ })
171
+ .strict()
172
+ .readonly();
173
+ export const playReviewSchema = z
174
+ .object({
175
+ checkpointSet: z
176
+ .array(z
177
+ .object({
178
+ runIndex: z.number().int().nonnegative(),
179
+ checkpointId: text,
180
+ stage: checkpointStageSchema,
181
+ fingerprint: text,
182
+ workKeys: z.array(text).readonly(),
183
+ dependents: z
184
+ .array(z.enum(["research", "article", "audio", "images", "thumbnail", "video"]))
185
+ .readonly(),
186
+ })
187
+ .strict()
188
+ .readonly())
189
+ .readonly()
190
+ .optional(),
191
+ id,
192
+ draftId: id,
193
+ draftVersion: z.number().int().positive(),
194
+ fingerprint: text,
195
+ runs: z
196
+ .array(z
197
+ .object({ draft: runDraftSchema, rendered: values, templates: values })
198
+ .strict()
199
+ .readonly())
200
+ .readonly(),
201
+ estimates: z.array(costEstimateSchema).readonly(),
202
+ })
203
+ .strict()
204
+ .readonly();
205
+ export const draftViewSchema = z
206
+ .object({
207
+ draft: playDraftSchema,
208
+ attachments: z.array(draftAttachmentSchema).readonly(),
209
+ review: playReviewSchema.nullable(),
210
+ pendingStart: z
211
+ .object({ reviewId: id, draftVersion: z.number().int().positive() })
212
+ .strict()
213
+ .readonly()
214
+ .nullable(),
215
+ start: playStartResultSchema.nullable(),
216
+ })
217
+ .strict()
218
+ .readonly();
@@ -0,0 +1,242 @@
1
+ import { statSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { transact } from "../../kernel/db/tx.js";
4
+ import { stagingPath } from "../storage/layout.js";
5
+ import { stagedFileById } from "../storage/repo.js";
6
+ import { releaseStagedFile } from "../storage/staging-refs.js";
7
+ import { attachmentRefs, attachmentRow, attachmentRows, draftRow, draftRows, insertAttachment, insertDraft, requestHash, syncAttachments, } from "./repo.js";
8
+ import { createDraftInputSchema, discardDraftInputSchema, draftViewSchema, forkDraftInputSchema, playDraftDocumentSchema, playReviewSchema, playStartResultSchema, saveDraftInputSchema, } from "./schema.js";
9
+ function refusal(reason, row) {
10
+ return {
11
+ ok: false,
12
+ reason,
13
+ currentVersion: row?.version ?? null,
14
+ fields: [],
15
+ ...(row?.state === "starting" && row.review_id !== null ? { reviewId: row.review_id } : {}),
16
+ };
17
+ }
18
+ function writable(row) {
19
+ if (row === undefined)
20
+ return refusal("not-found");
21
+ if (row.state === "starting")
22
+ return refusal("pending-start", row);
23
+ if (row.state === "started")
24
+ return refusal("already-started", row);
25
+ return null;
26
+ }
27
+ function validRefs(deps, id, document) {
28
+ const refs = attachmentRefs(document);
29
+ if (new Set(refs.map((ref) => ref.attachmentId)).size !== refs.length)
30
+ return false;
31
+ return refs.every((ref) => {
32
+ const old = attachmentRow(deps.db, ref.attachmentId);
33
+ return (old === undefined ||
34
+ (old.draft_id === id && old.kind === ref.kind && old.original_filename === ref.name));
35
+ });
36
+ }
37
+ function parseStoredJson(value) {
38
+ try {
39
+ return JSON.parse(value);
40
+ }
41
+ catch (error) {
42
+ if (error instanceof SyntaxError)
43
+ return undefined;
44
+ throw error;
45
+ }
46
+ }
47
+ function documentOf(row) {
48
+ if (row.schema_version !== 1)
49
+ return null;
50
+ const parsed = playDraftDocumentSchema.safeParse(parseStoredJson(row.document_json));
51
+ return parsed.success ? parsed.data : null;
52
+ }
53
+ function attachmentsOf(deps, id) {
54
+ return attachmentRows(deps.db, id).map((row) => {
55
+ const staged = row.staged_file_id === null ? undefined : stagedFileById(deps.db, row.staged_file_id);
56
+ const file = staged === undefined
57
+ ? undefined
58
+ : statSync(stagingPath(deps.paths, staged.path), { throwIfNoEntry: false });
59
+ const invalid = (row.status === "ready" && row.staged_file_id === null) ||
60
+ (row.staged_file_id !== null &&
61
+ (staged === undefined ||
62
+ (staged.state === "staged" && (!file?.isFile() || file.size !== staged.bytes))));
63
+ const state = invalid ? "reattach" : staged?.state === "copying" ? "copying" : row.status;
64
+ return {
65
+ id: row.id,
66
+ kind: row.kind,
67
+ name: row.original_filename,
68
+ state,
69
+ stagedFileId: invalid ? null : row.staged_file_id,
70
+ bytes: invalid ? 0 : (staged?.bytes ?? 0),
71
+ error: invalid ? "Upload is missing or incomplete. Reattach the file." : row.error,
72
+ };
73
+ });
74
+ }
75
+ export function readDraft(deps, id) {
76
+ if (!z.uuid().safeParse(id).success)
77
+ return refusal("invalid-edit");
78
+ const row = draftRow(deps.db, id);
79
+ if (row === undefined)
80
+ return refusal("not-found");
81
+ const document = documentOf(row);
82
+ if (document === null)
83
+ return refusal("invalid-draft", row);
84
+ const receipt = row.start_id === null
85
+ ? undefined
86
+ : deps.db.prepare("SELECT result_json FROM play_start_receipts WHERE id=?").get(row.start_id);
87
+ const start = receipt === undefined
88
+ ? null
89
+ : playStartResultSchema.parse(JSON.parse(z.object({ result_json: z.string() }).parse(receipt).result_json));
90
+ const parsedReview = row.review_json === null
91
+ ? null
92
+ : z.object({ review: playReviewSchema }).safeParse(parseStoredJson(row.review_json));
93
+ if (parsedReview !== null && !parsedReview.success)
94
+ return refusal("invalid-draft", row);
95
+ const review = parsedReview?.data?.review ?? null;
96
+ return {
97
+ ok: true,
98
+ value: draftViewSchema.parse({
99
+ draft: {
100
+ id: row.id,
101
+ version: row.version,
102
+ createdAt: row.created_at,
103
+ updatedAt: row.updated_at,
104
+ document,
105
+ },
106
+ attachments: attachmentsOf(deps, id),
107
+ review,
108
+ pendingStart: row.state === "starting" && row.review_id !== null
109
+ ? { reviewId: row.review_id, draftVersion: row.version }
110
+ : null,
111
+ start,
112
+ }),
113
+ };
114
+ }
115
+ export function listDrafts(deps) {
116
+ return draftRows(deps.db).map((row) => ({
117
+ id: row.id,
118
+ title: row.title,
119
+ version: row.version,
120
+ updatedAt: row.updated_at,
121
+ readable: documentOf(row) !== null,
122
+ }));
123
+ }
124
+ export function createDraft(deps, input) {
125
+ const parsed = createDraftInputSchema.safeParse(input);
126
+ if (!parsed.success)
127
+ return refusal("invalid-edit");
128
+ return transact(deps.db, () => {
129
+ const value = parsed.data;
130
+ const hash = requestHash(value);
131
+ const old = draftRow(deps.db, value.id);
132
+ if (old !== undefined)
133
+ return old.creation_hash === hash && old.version === 1 && old.state === "active"
134
+ ? readDraft(deps, value.id)
135
+ : refusal("conflict", old);
136
+ if (!validRefs(deps, value.id, value.document))
137
+ return refusal("invalid-edit");
138
+ insertDraft(deps.db, value.id, value.document, hash, deps.clock.now().toISOString());
139
+ syncAttachments(deps.db, value.id, value.document);
140
+ return readDraft(deps, value.id);
141
+ });
142
+ }
143
+ export function saveDraft(deps, input) {
144
+ const parsed = saveDraftInputSchema.safeParse(input);
145
+ if (!parsed.success)
146
+ return refusal("invalid-edit");
147
+ const previous = attachmentRows(deps.db, input.id);
148
+ const result = transact(deps.db, () => {
149
+ const value = parsed.data;
150
+ const row = draftRow(deps.db, value.id);
151
+ const denied = writable(row);
152
+ if (denied !== null || row === undefined)
153
+ return denied ?? refusal("not-found");
154
+ const hash = requestHash(value);
155
+ if (row.save_mutation_id === value.mutationId)
156
+ return row.save_request_hash === hash ? readDraft(deps, value.id) : refusal("conflict", row);
157
+ if (row.version !== value.baseVersion)
158
+ return refusal("conflict", row);
159
+ if (!validRefs(deps, value.id, value.document))
160
+ return refusal("invalid-edit", row);
161
+ const changed = deps.db
162
+ .prepare("UPDATE play_drafts SET version=version+1,document_json=?,title=?,updated_at=?,save_mutation_id=?,save_request_hash=?,review_id=NULL,review_json=NULL,review_fingerprint=NULL WHERE id=? AND version=? AND state='active'")
163
+ .run(JSON.stringify(value.document), value.document.form.title, deps.clock.now().toISOString(), value.mutationId, hash, value.id, value.baseVersion);
164
+ if (Number(changed.changes) !== 1)
165
+ return refusal("conflict", draftRow(deps.db, value.id));
166
+ syncAttachments(deps.db, value.id, value.document);
167
+ return readDraft(deps, value.id);
168
+ });
169
+ if (result.ok)
170
+ for (const attachment of previous)
171
+ if (attachment.staged_file_id !== null)
172
+ releaseStagedFile({ ...deps, emit: () => undefined }, attachment.staged_file_id);
173
+ return result;
174
+ }
175
+ export function forkDraft(deps, input) {
176
+ const parsed = forkDraftInputSchema.safeParse(input);
177
+ if (!parsed.success)
178
+ return refusal("invalid-edit");
179
+ return transact(deps.db, () => {
180
+ const value = parsed.data;
181
+ const hash = requestHash(value);
182
+ const old = draftRow(deps.db, value.id);
183
+ if (old !== undefined)
184
+ return old.creation_hash === hash && old.version === 1 && old.state === "active"
185
+ ? readDraft(deps, value.id)
186
+ : refusal("conflict", old);
187
+ const source = draftRow(deps.db, value.sourceId);
188
+ const denied = writable(source);
189
+ if (denied !== null)
190
+ return denied;
191
+ if (!validRefs(deps, value.sourceId, value.document))
192
+ return refusal("invalid-edit", source);
193
+ const states = new Map(attachmentsOf(deps, value.sourceId).map((file) => [file.id, file]));
194
+ const replacements = new Map(attachmentRefs(value.document).map((ref) => [ref.attachmentId, z.uuid().parse(deps.uuid())]));
195
+ const rewrite = (ref) => ({
196
+ attachmentId: z.uuid().parse(replacements.get(ref.attachmentId)),
197
+ name: ref.name,
198
+ });
199
+ const p = value.document.form.provided;
200
+ const document = {
201
+ ...value.document,
202
+ form: {
203
+ ...value.document.form,
204
+ provided: {
205
+ ...p,
206
+ audio: p.audio === null ? null : rewrite(p.audio),
207
+ thumbnail: p.thumbnail === null ? null : rewrite(p.thumbnail),
208
+ images: p.images.map(rewrite),
209
+ },
210
+ },
211
+ };
212
+ insertDraft(deps.db, value.id, document, hash, deps.clock.now().toISOString());
213
+ for (const ref of attachmentRefs(value.document)) {
214
+ const original = states.get(ref.attachmentId);
215
+ const ready = original?.state === "ready" && original.stagedFileId !== null;
216
+ insertAttachment(deps.db, value.id, { ...ref, ...rewrite(ref) }, ready ? "ready" : "reattach", ready ? original.stagedFileId : null);
217
+ }
218
+ return readDraft(deps, value.id);
219
+ });
220
+ }
221
+ export function discardDraft(deps, input) {
222
+ const parsed = discardDraftInputSchema.safeParse(input);
223
+ if (!parsed.success)
224
+ return refusal("invalid-edit");
225
+ const previous = attachmentRows(deps.db, input.id);
226
+ const result = transact(deps.db, () => {
227
+ const row = draftRow(deps.db, input.id);
228
+ if (row === undefined)
229
+ return refusal("not-found");
230
+ if (row.state === "starting")
231
+ return refusal("pending-start", row);
232
+ if (row.version !== input.baseVersion)
233
+ return refusal("conflict", row);
234
+ deps.db.prepare("DELETE FROM play_drafts WHERE id=?").run(input.id);
235
+ return { ok: true, value: { discarded: true } };
236
+ });
237
+ if (result.ok)
238
+ for (const attachment of previous)
239
+ if (attachment.staged_file_id !== null)
240
+ releaseStagedFile({ ...deps, emit: () => undefined }, attachment.staged_file_id);
241
+ return result;
242
+ }
@@ -0,0 +1,113 @@
1
+ import { z } from "zod";
2
+ import { catalogueSchema } from "../../catalog/schema.js";
3
+ import { draftRow, requestHash } from "./repo.js";
4
+ import { resolveReviewInputs } from "./review.js";
5
+ import { playReviewSchema, playStartResultSchema } from "./schema.js";
6
+ import { readDraft } from "./service.js";
7
+ const receiptSchema = z.object({
8
+ id: z.uuid(),
9
+ draft_id: z.uuid(),
10
+ draft_version: z.number().int().positive(),
11
+ request_hash: z.string(),
12
+ result_json: z.string(),
13
+ });
14
+ export const executionSchema = z.object({
15
+ review: playReviewSchema,
16
+ execution: z.object({
17
+ catalogue: catalogueSchema,
18
+ attachmentIdentity: z.array(z.object({ id: z.uuid(), stagedFileId: z.string(), bytes: z.number() })),
19
+ font: z
20
+ .object({
21
+ id: z.string(),
22
+ name: z.string(),
23
+ family: z.string(),
24
+ source: z.enum(["bundled", "system", "uploaded"]),
25
+ path: z.string(),
26
+ extension: z.enum([".ttf", ".otf", ".ttc"]),
27
+ assName: z.string(),
28
+ faceIndex: z.number(),
29
+ })
30
+ .nullable(),
31
+ }),
32
+ });
33
+ export function readStartReceipt(db, reviewId) {
34
+ const raw = db.prepare("SELECT * FROM play_start_receipts WHERE id=?").get(reviewId);
35
+ if (!raw)
36
+ return undefined;
37
+ const row = receiptSchema.parse(raw);
38
+ return {
39
+ id: row.id,
40
+ draftId: row.draft_id,
41
+ draftVersion: row.draft_version,
42
+ requestHash: row.request_hash,
43
+ result: playStartResultSchema.parse(JSON.parse(row.result_json)),
44
+ };
45
+ }
46
+ export function replayResult(receipt, input) {
47
+ if (receipt.id !== input.reviewId ||
48
+ receipt.draftId !== input.draftId ||
49
+ receipt.draftVersion !== input.baseVersion ||
50
+ receipt.requestHash !== requestHash(input) ||
51
+ receipt.result.requestId !== input.reviewId)
52
+ return { ok: false, reason: "conflict", currentVersion: receipt.draftVersion, fields: [] };
53
+ return { ok: true, value: { ...receipt.result, replayed: true } };
54
+ }
55
+ export function requireStartingIdentity(deps, input) {
56
+ const row = draftRow(deps.db, input.draftId);
57
+ const denied = (reason) => ({
58
+ ok: false,
59
+ reason,
60
+ currentVersion: row?.version ?? null,
61
+ fields: [],
62
+ ...(row?.start_id ? { reviewId: row.start_id } : {}),
63
+ });
64
+ if (!row)
65
+ return denied("not-found");
66
+ if (row.state !== "starting" || row.start_id !== input.reviewId)
67
+ return denied("pending-start");
68
+ if (row.version !== input.baseVersion)
69
+ return denied("conflict");
70
+ if (row.review_id !== input.reviewId || row.review_json === null)
71
+ return denied("stale-review");
72
+ const stored = executionSchema.parse(JSON.parse(row.review_json));
73
+ const view = readDraft(deps, input.draftId);
74
+ if (!view.ok)
75
+ return view;
76
+ const resolved = resolveReviewInputs(deps, view.value, stored.execution.font);
77
+ if (!resolved.ok)
78
+ return { ...resolved, reason: "stale-review" };
79
+ const { fingerprint: _fingerprint, ...current } = resolved.value;
80
+ if (requestHash({
81
+ ...(stored.review.checkpointSet === undefined
82
+ ? {}
83
+ : { checkpointSet: stored.review.checkpointSet }),
84
+ runs: stored.review.runs,
85
+ estimates: stored.review.estimates,
86
+ ...stored.execution,
87
+ }) !== requestHash(current) ||
88
+ stored.review.draftId !== input.draftId ||
89
+ stored.review.draftVersion !== input.baseVersion ||
90
+ stored.review.id !== input.reviewId ||
91
+ resolved.value.fingerprint !== stored.review.fingerprint ||
92
+ row.review_fingerprint !== stored.review.fingerprint)
93
+ return denied("stale-review");
94
+ return { ok: true, value: stored.review };
95
+ }
96
+ export function insertStartReceipt(deps, input, value) {
97
+ deps.db
98
+ .prepare("INSERT INTO play_start_receipts(id,draft_id,draft_version,request_hash,result_json,created_at) VALUES(?,?,?,?,?,?)")
99
+ .run(input.reviewId, input.draftId, input.baseVersion, requestHash(input), JSON.stringify(playStartResultSchema.parse(value)), deps.clock.now().toISOString());
100
+ }
101
+ export function markDraftStartedAndReleaseRefs(db, input) {
102
+ const changed = db
103
+ .prepare("UPDATE play_drafts SET state='started' WHERE id=? AND version=? AND state='starting' AND start_id=?")
104
+ .run(input.draftId, input.baseVersion, input.reviewId);
105
+ if (Number(changed.changes) !== 1)
106
+ throw new Error("The pending Start identity changed");
107
+ db.prepare("DELETE FROM play_draft_attachments WHERE draft_id=?").run(input.draftId);
108
+ }
109
+ export function releaseStartClaim(deps, input, stale) {
110
+ deps.db
111
+ .prepare(`UPDATE play_drafts SET state='active',start_id=NULL${stale ? ",review_id=NULL,review_json=NULL,review_fingerprint=NULL" : ""} WHERE id=? AND version=? AND state='starting' AND start_id=?`)
112
+ .run(input.draftId, input.baseVersion, input.reviewId);
113
+ }