@gentbajko/slopify 0.8.4 → 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.
- package/README.md +28 -6
- package/dist/adapters/llm/claude-code.js +6 -3
- package/dist/adapters/llm/codex.js +102 -12
- package/dist/adapters/llm/gemini.js +11 -8
- package/dist/adapters/llm/run-cli.js +61 -6
- package/dist/adapters/tts/inworld-async.js +4 -4
- package/dist/adapters/tts/inworld.js +73 -68
- package/dist/catalog/registry.js +13 -73
- package/dist/edge/cli.js +5 -2
- package/dist/edge/events/hub.js +7 -2
- package/dist/edge/events/preview-cache.js +7 -4
- package/dist/edge/events/visibility.js +33 -0
- package/dist/edge/http/actions.js +74 -79
- package/dist/edge/http/app.js +39 -6
- package/dist/edge/http/audio-preview.js +9 -1
- package/dist/edge/http/checkpoints.js +95 -0
- package/dist/edge/http/diagnostics.js +31 -0
- package/dist/edge/http/draft-files.js +69 -0
- package/dist/edge/http/draft-problem.js +15 -0
- package/dist/edge/http/drafts.js +95 -0
- package/dist/edge/http/multipart.js +86 -0
- package/dist/edge/http/mutations.js +49 -0
- package/dist/edge/http/planning.js +12 -1
- package/dist/edge/http/project-templates.js +74 -0
- package/dist/edge/http/projects.js +19 -2
- package/dist/edge/http/revision-files.js +84 -0
- package/dist/edge/http/revisions.js +103 -0
- package/dist/edge/http/schedules.js +100 -0
- package/dist/edge/http/staging.js +11 -52
- package/dist/edge/http/storage.js +110 -0
- package/dist/edge/http/subtitles.js +9 -79
- package/dist/edge/http/tutorial.js +26 -0
- package/dist/edge/signal-shutdown.js +23 -0
- package/dist/kernel/audio-preview.js +11 -5
- package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
- package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
- package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
- package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
- package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
- package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
- package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
- package/dist/kernel/ports/model.js +5 -0
- package/dist/kernel/runner/attempt-repo.js +15 -0
- package/dist/kernel/runner/attempt.js +14 -2
- package/dist/kernel/runner/checkpoint-authority.js +23 -0
- package/dist/kernel/runner/index.js +35 -11
- package/dist/kernel/runner/providers.js +18 -8
- package/dist/kernel/runner/work-attempt.js +47 -0
- package/dist/kernel/runner/work-authority.js +37 -0
- package/dist/kernel/runner/work.js +23 -0
- package/dist/main.js +227 -69
- package/dist/slices/admission/repo.js +5 -56
- package/dist/slices/admission/schema.js +64 -0
- package/dist/slices/admission/start.js +13 -4
- package/dist/slices/article/continuation.js +9 -3
- package/dist/slices/article/provided-entries.js +7 -3
- package/dist/slices/article/run.js +20 -7
- package/dist/slices/article/segments.js +13 -4
- package/dist/slices/batch/index.js +8 -11
- package/dist/slices/cancel/index.js +78 -34
- package/dist/slices/checkpoints/change.js +250 -0
- package/dist/slices/checkpoints/fingerprint.js +33 -0
- package/dist/slices/checkpoints/index.js +2 -0
- package/dist/slices/checkpoints/model.js +1 -0
- package/dist/slices/checkpoints/recovery.js +116 -0
- package/dist/slices/checkpoints/repo.js +132 -0
- package/dist/slices/checkpoints/rules.js +30 -0
- package/dist/slices/checkpoints/schema.js +59 -0
- package/dist/slices/control/index.js +88 -22
- package/dist/slices/control/providers.js +3 -2
- package/dist/slices/control/revision-control-schema.js +7 -0
- package/dist/slices/control/revision-control.js +59 -0
- package/dist/slices/estimate/index.js +77 -59
- package/dist/slices/estimate/requests.js +102 -0
- package/dist/slices/images/run.js +14 -3
- package/dist/slices/library/slots.js +11 -11
- package/dist/slices/library/snapshot.js +33 -0
- package/dist/slices/narration/legacy-plan.js +58 -0
- package/dist/slices/narration/live.js +2 -2
- package/dist/slices/narration/plan.js +48 -54
- package/dist/slices/narration/run.js +21 -8
- package/dist/slices/play-drafts/convert.js +120 -0
- package/dist/slices/play-drafts/model.js +1 -0
- package/dist/slices/play-drafts/readiness.js +80 -0
- package/dist/slices/play-drafts/repo.js +76 -0
- package/dist/slices/play-drafts/review-inputs.js +150 -0
- package/dist/slices/play-drafts/review.js +109 -0
- package/dist/slices/play-drafts/schema.js +218 -0
- package/dist/slices/play-drafts/service.js +242 -0
- package/dist/slices/play-drafts/start-repo.js +113 -0
- package/dist/slices/play-drafts/start.js +148 -0
- package/dist/slices/play-drafts/uploads.js +67 -0
- package/dist/slices/project-templates/from-project.js +159 -0
- package/dist/slices/project-templates/model.js +1 -0
- package/dist/slices/project-templates/repo.js +41 -0
- package/dist/slices/project-templates/schema.js +37 -0
- package/dist/slices/project-templates/service.js +136 -0
- package/dist/slices/project-templates/setup.js +55 -0
- package/dist/slices/rebuild/admission-repo.js +120 -0
- package/dist/slices/rebuild/dependencies.js +33 -0
- package/dist/slices/rebuild/model.js +94 -0
- package/dist/slices/rebuild/narration-history.js +43 -0
- package/dist/slices/rebuild/narration-reuse.js +20 -0
- package/dist/slices/rebuild/preview-details.js +144 -0
- package/dist/slices/rebuild/preview-plan.js +223 -0
- package/dist/slices/rebuild/preview-retained.js +128 -0
- package/dist/slices/rebuild/provided-review.js +32 -0
- package/dist/slices/rebuild/recipe-audio.js +172 -0
- package/dist/slices/rebuild/recipe-build.js +17 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
- package/dist/slices/rebuild/recipe-exports.js +65 -0
- package/dist/slices/rebuild/recipe-fixture.js +150 -0
- package/dist/slices/rebuild/recipe-input-schema.js +82 -0
- package/dist/slices/rebuild/recipe-legacy.js +135 -0
- package/dist/slices/rebuild/recipe-model.js +77 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
- package/dist/slices/rebuild/recipe-save.js +221 -0
- package/dist/slices/rebuild/recipe-text.js +170 -0
- package/dist/slices/rebuild/recipe-validation.js +86 -0
- package/dist/slices/rebuild/recipe-visual.js +111 -0
- package/dist/slices/rebuild/recipe-work.js +212 -0
- package/dist/slices/rebuild/recipes.js +4 -0
- package/dist/slices/rebuild/repo.js +64 -0
- package/dist/slices/rebuild/runtime-actions.js +76 -0
- package/dist/slices/rebuild/runtime-admission.js +71 -0
- package/dist/slices/rebuild/runtime-article.js +117 -0
- package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
- package/dist/slices/rebuild/runtime-export.js +139 -0
- package/dist/slices/rebuild/runtime-instructions.js +51 -0
- package/dist/slices/rebuild/runtime-local.js +137 -0
- package/dist/slices/rebuild/runtime-materialize.js +105 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
- package/dist/slices/rebuild/runtime-plan.js +103 -0
- package/dist/slices/rebuild/runtime-provider.js +184 -0
- package/dist/slices/rebuild/runtime-publication.js +172 -0
- package/dist/slices/rebuild/runtime-run.js +35 -0
- package/dist/slices/rebuild/runtime-store.js +166 -0
- package/dist/slices/rebuild/runtime-subtitles.js +202 -0
- package/dist/slices/rebuild/service-readiness.js +110 -0
- package/dist/slices/rebuild/service.js +149 -0
- package/dist/slices/rebuild/transition-repo.js +108 -0
- package/dist/slices/rebuild/work-records.js +44 -0
- package/dist/slices/reruns/index.js +5 -0
- package/dist/slices/research/run.js +33 -12
- package/dist/slices/revisions/adopt-assets.js +31 -0
- package/dist/slices/revisions/adopt-content.js +112 -0
- package/dist/slices/revisions/adopt.js +100 -0
- package/dist/slices/revisions/downloads.js +80 -0
- package/dist/slices/revisions/index.js +5 -0
- package/dist/slices/revisions/manifest-repo.js +125 -0
- package/dist/slices/revisions/model.js +1 -0
- package/dist/slices/revisions/mutation-assets.js +143 -0
- package/dist/slices/revisions/mutation-cues.js +95 -0
- package/dist/slices/revisions/mutation-prepare.js +182 -0
- package/dist/slices/revisions/mutation-request.js +58 -0
- package/dist/slices/revisions/mutation-work.js +47 -0
- package/dist/slices/revisions/mutations.js +194 -0
- package/dist/slices/revisions/projection.js +82 -0
- package/dist/slices/revisions/publication-model.js +1 -0
- package/dist/slices/revisions/publication-rules.js +166 -0
- package/dist/slices/revisions/publish.js +115 -0
- package/dist/slices/revisions/repo.js +83 -0
- package/dist/slices/revisions/restore.js +60 -0
- package/dist/slices/revisions/rules.js +99 -0
- package/dist/slices/revisions/schema.js +160 -0
- package/dist/slices/revisions/view.js +44 -0
- package/dist/slices/schedules/calendar.js +47 -0
- package/dist/slices/schedules/model.js +1 -0
- package/dist/slices/schedules/repo.js +281 -0
- package/dist/slices/schedules/scheduler.js +144 -0
- package/dist/slices/schedules/schema.js +80 -0
- package/dist/slices/schedules/service.js +178 -0
- package/dist/slices/settings/cli-paths.js +1 -1
- package/dist/slices/settings/cli-status.js +45 -5
- package/dist/slices/settings/tutorial-schema.js +39 -0
- package/dist/slices/settings/tutorial.js +66 -0
- package/dist/slices/storage/assets.js +108 -0
- package/dist/slices/storage/delete-project.js +2 -1
- package/dist/slices/storage/downloads.js +4 -1
- package/dist/slices/storage/portable.js +871 -0
- package/dist/slices/storage/prepare.js +67 -0
- package/dist/slices/storage/reconcile.js +25 -5
- package/dist/slices/storage/repo.js +2 -15
- package/dist/slices/storage/schema.js +35 -0
- package/dist/slices/storage/staging-refs.js +26 -0
- package/dist/slices/storage/staging.js +16 -11
- package/dist/slices/thumbnail/run.js +26 -9
- package/dist/slices/video/audio-export-args.js +33 -0
- package/dist/slices/video/audio-export.js +13 -36
- package/dist/slices/video/audio-inputs.js +4 -3
- package/dist/slices/video/ffmpeg.js +6 -0
- package/dist/slices/video/reuse-audio.js +11 -4
- package/dist/updater/install-flow.js +27 -2
- package/dist/updater/plan.js +28 -2
- package/dist/updater/service.js +27 -2
- package/dist/updater/worker.js +2 -1
- package/dist/web/assets/index-CO7DnVX9.js +137 -0
- package/dist/web/assets/index-DX1_chKs.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/adapters/tts/inworld-text.js +0 -32
- package/dist/web/assets/index-Ce2HApus.css +0 -1
- package/dist/web/assets/index-xbD0MZOY.js +0 -130
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { startRun } from "../admission/start.js";
|
|
4
|
+
import { enqueueBatch, pumpQueue } from "../batch/index.js";
|
|
5
|
+
import { admitReviewedCheckpoints } from "../rebuild/runtime-checkpoints.js";
|
|
6
|
+
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
7
|
+
import { checkDraftReadiness, localDraftReadiness } from "./readiness.js";
|
|
8
|
+
import { attachmentRows, draftRow } from "./repo.js";
|
|
9
|
+
import { readDraft } from "./service.js";
|
|
10
|
+
import { executionSchema, insertStartReceipt, markDraftStartedAndReleaseRefs, readStartReceipt, releaseStartClaim, replayResult, requireStartingIdentity, } from "./start-repo.js";
|
|
11
|
+
const inputSchema = z
|
|
12
|
+
.object({ draftId: z.uuid(), baseVersion: z.number().int().positive(), reviewId: z.uuid() })
|
|
13
|
+
.strict();
|
|
14
|
+
export async function startPlayDraft(deps, input) {
|
|
15
|
+
const parsed = inputSchema.safeParse(input);
|
|
16
|
+
if (!parsed.success)
|
|
17
|
+
return { ok: false, reason: "invalid-edit", currentVersion: null, fields: [] };
|
|
18
|
+
input = parsed.data;
|
|
19
|
+
const claimed = transact(deps.db, () => {
|
|
20
|
+
const receipt = readStartReceipt(deps.db, input.reviewId);
|
|
21
|
+
if (receipt)
|
|
22
|
+
return replayResult(receipt, input);
|
|
23
|
+
const row = draftRow(deps.db, input.draftId);
|
|
24
|
+
if (!row)
|
|
25
|
+
return { ok: false, reason: "not-found", currentVersion: null, fields: [] };
|
|
26
|
+
const view = readDraft(deps, input.draftId);
|
|
27
|
+
if (!view.ok)
|
|
28
|
+
return view;
|
|
29
|
+
if (row.state === "started")
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: "already-started",
|
|
33
|
+
currentVersion: row.version,
|
|
34
|
+
fields: [],
|
|
35
|
+
};
|
|
36
|
+
if (row.state === "starting" && row.start_id !== input.reviewId)
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: "pending-start",
|
|
40
|
+
currentVersion: row.version,
|
|
41
|
+
reviewId: row.start_id ?? input.reviewId,
|
|
42
|
+
fields: [],
|
|
43
|
+
};
|
|
44
|
+
if (row.version !== input.baseVersion || view.value.review?.id !== input.reviewId)
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
reason: "stale-review",
|
|
48
|
+
currentVersion: row.version,
|
|
49
|
+
fields: [],
|
|
50
|
+
};
|
|
51
|
+
if (row.state === "active") {
|
|
52
|
+
deps.db
|
|
53
|
+
.prepare("UPDATE play_drafts SET state='starting',start_id=? WHERE id=? AND version=? AND state='active'")
|
|
54
|
+
.run(input.reviewId, input.draftId, input.baseVersion);
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, value: view.value.review };
|
|
57
|
+
});
|
|
58
|
+
if (!claimed.ok) {
|
|
59
|
+
releaseStartClaim(deps, input, claimed.reason === "stale-review");
|
|
60
|
+
return claimed;
|
|
61
|
+
}
|
|
62
|
+
if ("requestId" in claimed.value)
|
|
63
|
+
return { ok: true, value: claimed.value };
|
|
64
|
+
const identity = requireStartingIdentity(deps, input);
|
|
65
|
+
if (!identity.ok) {
|
|
66
|
+
releaseStartClaim(deps, input, identity.reason === "stale-review");
|
|
67
|
+
return identity;
|
|
68
|
+
}
|
|
69
|
+
const row = draftRow(deps.db, input.draftId);
|
|
70
|
+
if (row?.review_json == null)
|
|
71
|
+
throw new Error("Pending Start lost its review");
|
|
72
|
+
const { catalogue } = executionSchema.parse(JSON.parse(row.review_json)).execution;
|
|
73
|
+
const captured = {
|
|
74
|
+
...deps,
|
|
75
|
+
catalogue: {
|
|
76
|
+
...deps.catalogue,
|
|
77
|
+
read: () => catalogue,
|
|
78
|
+
models: (provider, family) => catalogue[family].filter((m) => m.provider === provider && m.enabled && !m.deprecated),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const checked = await checkDraftReadiness(captured, claimed.value.runs);
|
|
82
|
+
const oldFiles = attachmentRows(deps.db, input.draftId).flatMap((a) => a.staged_file_id === null ? [] : [a.staged_file_id]);
|
|
83
|
+
const result = transact(deps.db, () => {
|
|
84
|
+
const receipt = readStartReceipt(deps.db, input.reviewId);
|
|
85
|
+
if (receipt)
|
|
86
|
+
return replayResult(receipt, input);
|
|
87
|
+
const starting = requireStartingIdentity(deps, input);
|
|
88
|
+
if (!starting.ok)
|
|
89
|
+
return starting;
|
|
90
|
+
const fields = [
|
|
91
|
+
...checked.fields,
|
|
92
|
+
...localDraftReadiness(captured, starting.value.runs, checked.providers),
|
|
93
|
+
];
|
|
94
|
+
if (fields.length)
|
|
95
|
+
return { ok: false, reason: "readiness", currentVersion: input.baseVersion, fields };
|
|
96
|
+
const runs = starting.value.runs;
|
|
97
|
+
const first = runs[0];
|
|
98
|
+
if (!first)
|
|
99
|
+
throw new Error("Reviewed run list was empty");
|
|
100
|
+
const queue = runs.length === 1 ? [] : enqueueBatch(captured, input.reviewId, runs, true);
|
|
101
|
+
const projectIds = runs.length === 1
|
|
102
|
+
? [startRun(captured, first.draft, first.rendered, true, first.templates).project.id]
|
|
103
|
+
: queue.map((e) => e.projectId);
|
|
104
|
+
const checkpointSet = admitReviewedCheckpoints(captured, projectIds, starting.value.checkpointSet ?? [], catalogue);
|
|
105
|
+
const value = {
|
|
106
|
+
requestId: input.reviewId,
|
|
107
|
+
projectIds,
|
|
108
|
+
queue,
|
|
109
|
+
replayed: false,
|
|
110
|
+
...(checkpointSet.length ? { checkpointSet } : {}),
|
|
111
|
+
};
|
|
112
|
+
insertStartReceipt(deps, input, value);
|
|
113
|
+
markDraftStartedAndReleaseRefs(deps.db, input);
|
|
114
|
+
return { ok: true, value };
|
|
115
|
+
});
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
releaseStartClaim(deps, input, result.reason === "stale-review");
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
if (!result.value.replayed) {
|
|
121
|
+
afterCommit(deps, () => deps.recordStarted(result.value.projectIds));
|
|
122
|
+
for (const id of oldFiles)
|
|
123
|
+
afterCommit(deps, () => releaseStagedFile(deps, id));
|
|
124
|
+
afterCommit(deps, () => {
|
|
125
|
+
if (result.value.queue.length)
|
|
126
|
+
pumpQueue(deps.db, deps.runner);
|
|
127
|
+
else
|
|
128
|
+
for (const id of result.value.projectIds)
|
|
129
|
+
deps.runner.tick(id);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
function afterCommit(deps, run) {
|
|
135
|
+
try {
|
|
136
|
+
run();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
try {
|
|
140
|
+
deps.log.write("warn", "play.start.after-commit", {
|
|
141
|
+
detail: "Start committed; a post-commit action failed.",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
/* The durable receipt remains authoritative even if logging fails. */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { stageUpload } from "../storage/staging.js";
|
|
3
|
+
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
4
|
+
import { attachmentRow, draftRow } from "./repo.js";
|
|
5
|
+
import { draftAttachmentSchema } from "./schema.js";
|
|
6
|
+
export async function uploadDraftAttachment(deps, input) {
|
|
7
|
+
const row = z.uuid().safeParse(input.draftId).success
|
|
8
|
+
? draftRow(deps.db, input.draftId)
|
|
9
|
+
: undefined;
|
|
10
|
+
const refuse = (reason) => ({
|
|
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
|
+
if (!z.uuid().safeParse(input.draftId).success || !z.uuid().safeParse(input.attachmentId).success)
|
|
18
|
+
return refuse("invalid-edit");
|
|
19
|
+
if (row === undefined)
|
|
20
|
+
return refuse("not-found");
|
|
21
|
+
if (row.state === "starting")
|
|
22
|
+
return refuse("pending-start");
|
|
23
|
+
if (row.state === "started")
|
|
24
|
+
return refuse("already-started");
|
|
25
|
+
const attachment = attachmentRow(deps.db, input.attachmentId);
|
|
26
|
+
if (attachment === undefined || attachment.draft_id !== input.draftId)
|
|
27
|
+
return refuse("not-found");
|
|
28
|
+
if (attachment.staged_file_id !== null || attachment.status !== "pending")
|
|
29
|
+
return refuse("conflict");
|
|
30
|
+
const storage = { ...deps, emit: () => undefined };
|
|
31
|
+
const result = await stageUpload(storage, {
|
|
32
|
+
stageKind: attachment.kind,
|
|
33
|
+
originalFilename: attachment.original_filename,
|
|
34
|
+
content: input.content,
|
|
35
|
+
onAllocated(file) {
|
|
36
|
+
const bound = deps.db
|
|
37
|
+
.prepare("UPDATE play_draft_attachments SET staged_file_id=?,error=NULL WHERE id=? AND draft_id=? AND staged_file_id IS NULL AND status='pending' AND EXISTS (SELECT 1 FROM play_drafts WHERE id=? AND state='active')")
|
|
38
|
+
.run(file.id, input.attachmentId, input.draftId, input.draftId);
|
|
39
|
+
if (Number(bound.changes) !== 1)
|
|
40
|
+
throw new Error("Upload attachment changed before allocation.");
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (!result.ok) {
|
|
44
|
+
deps.db
|
|
45
|
+
.prepare("UPDATE play_draft_attachments SET status='reattach',error=? WHERE id=? AND draft_id=? AND staged_file_id IS NULL")
|
|
46
|
+
.run("Upload failed. Reattach the file.", input.attachmentId, input.draftId);
|
|
47
|
+
return refuse("invalid-edit");
|
|
48
|
+
}
|
|
49
|
+
const updated = deps.db
|
|
50
|
+
.prepare("UPDATE play_draft_attachments SET status='ready',error=NULL WHERE id=? AND draft_id=? AND staged_file_id=?")
|
|
51
|
+
.run(input.attachmentId, input.draftId, result.file.id);
|
|
52
|
+
releaseStagedFile(storage, result.file.id);
|
|
53
|
+
if (Number(updated.changes) !== 1)
|
|
54
|
+
return refuse("not-found");
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
value: draftAttachmentSchema.parse({
|
|
58
|
+
id: attachment.id,
|
|
59
|
+
kind: attachment.kind,
|
|
60
|
+
name: attachment.original_filename,
|
|
61
|
+
state: "ready",
|
|
62
|
+
stagedFileId: result.file.id,
|
|
63
|
+
bytes: result.file.bytes,
|
|
64
|
+
error: null,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
}
|