@gentbajko/slopify 0.8.4 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-DX1_chKs.css +1 -0
- package/dist/web/assets/index-DwM-Y6nw.js +137 -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,159 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { projectById } from "../admission/repo.js";
|
|
4
|
+
import { detectSlots } from "../admission/substitute.js";
|
|
5
|
+
import { listCheckpoints } from "../checkpoints/repo.js";
|
|
6
|
+
import { requestHash } from "../play-drafts/repo.js";
|
|
7
|
+
import { currentRevisionId, revisionById } from "../revisions/repo.js";
|
|
8
|
+
import { createTemplate, readTemplate } from "./service.js";
|
|
9
|
+
export const projectTemplateCreateSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
id: z.uuid(),
|
|
12
|
+
name: z.string().trim().min(1).max(200),
|
|
13
|
+
projectId: z.string().min(1).max(64),
|
|
14
|
+
revisionId: z.string().min(1).max(64),
|
|
15
|
+
})
|
|
16
|
+
.strict()
|
|
17
|
+
.readonly();
|
|
18
|
+
export const templateFromProjectSchema = projectTemplateCreateSchema;
|
|
19
|
+
export function createTemplateFromProject(deps, input) {
|
|
20
|
+
const parsed = projectTemplateCreateSchema.safeParse(input);
|
|
21
|
+
if (!parsed.success)
|
|
22
|
+
return { ok: false, reason: "invalid-input" };
|
|
23
|
+
return transact(deps.db, () => {
|
|
24
|
+
const hash = requestHash({ operation: "template-from-project", ...parsed.data });
|
|
25
|
+
const old = deps.db
|
|
26
|
+
.prepare("SELECT creation_hash FROM project_templates WHERE id=?")
|
|
27
|
+
.get(parsed.data.id);
|
|
28
|
+
if (old)
|
|
29
|
+
return old.creation_hash === hash
|
|
30
|
+
? readTemplate(deps, parsed.data.id, 1)
|
|
31
|
+
: { ok: false, reason: "conflict" };
|
|
32
|
+
const project = projectById(deps.db, parsed.data.projectId);
|
|
33
|
+
if (project === undefined)
|
|
34
|
+
return { ok: false, reason: "not-found" };
|
|
35
|
+
const current = currentRevisionId(deps.db, project.id);
|
|
36
|
+
if (current === undefined || current !== parsed.data.revisionId)
|
|
37
|
+
return { ok: false, reason: "conflict" };
|
|
38
|
+
const revision = revisionById(deps.db, project.id, current);
|
|
39
|
+
if (revision === undefined)
|
|
40
|
+
return { ok: false, reason: "conflict" };
|
|
41
|
+
if (revision.config.sources.images === "generate" &&
|
|
42
|
+
revision.content.imageOrder.some((key) => revision.content.imageDefinitions[key]?.source === "provide"))
|
|
43
|
+
return { ok: false, reason: "invalid-input" };
|
|
44
|
+
const result = createTemplate(deps, {
|
|
45
|
+
id: parsed.data.id,
|
|
46
|
+
name: parsed.data.name,
|
|
47
|
+
document: documentFromProject(deps, revision),
|
|
48
|
+
});
|
|
49
|
+
if (result.ok)
|
|
50
|
+
deps.db
|
|
51
|
+
.prepare("UPDATE project_templates SET creation_hash=? WHERE id=?")
|
|
52
|
+
.run(hash, parsed.data.id);
|
|
53
|
+
return result;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function documentFromProject(deps, revision) {
|
|
57
|
+
const config = revision.config;
|
|
58
|
+
const prompts = [];
|
|
59
|
+
const entries = [];
|
|
60
|
+
const addPrompt = (kind, name, key, literal) => {
|
|
61
|
+
if (!name)
|
|
62
|
+
return;
|
|
63
|
+
const body = literal ?? revision.content.promptTemplates[key] ?? config.rendered[key];
|
|
64
|
+
if (body === null || body === undefined)
|
|
65
|
+
return;
|
|
66
|
+
prompts.push({
|
|
67
|
+
id: deps.uuid(),
|
|
68
|
+
kind,
|
|
69
|
+
name,
|
|
70
|
+
body,
|
|
71
|
+
slots: [...detectSlots(body).names],
|
|
72
|
+
updatedAt: revision.createdAt,
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
addPrompt("article", config.articlePrompt, "article");
|
|
76
|
+
const definitions = revision.content.imageOrder.flatMap((key) => {
|
|
77
|
+
const definition = revision.content.imageDefinitions[key];
|
|
78
|
+
return definition === undefined ? [] : [definition];
|
|
79
|
+
});
|
|
80
|
+
const imagePrompts = config.sources.images === "generate" && definitions.length > 0
|
|
81
|
+
? definitions.map((definition, index) => {
|
|
82
|
+
const key = definition.templateKey ?? "";
|
|
83
|
+
const original = revision.content.promptTemplates[key];
|
|
84
|
+
const body = original && definition.prompt === config.rendered[key]
|
|
85
|
+
? original
|
|
86
|
+
: (definition.prompt ?? original ?? "");
|
|
87
|
+
const name = `Project image ${index + 1}`;
|
|
88
|
+
addPrompt("image", name, key, body);
|
|
89
|
+
return { name, number: "1" };
|
|
90
|
+
})
|
|
91
|
+
: config.imagePrompts.map((prompt, index) => {
|
|
92
|
+
addPrompt("image", prompt.name, `imagePrompts.${index}`);
|
|
93
|
+
return { name: prompt.name, number: String(prompt.number) };
|
|
94
|
+
});
|
|
95
|
+
addPrompt("thumbnail", config.thumbnailPrompt, "thumbnailPrompt");
|
|
96
|
+
for (const category of ["intro", "outro"]) {
|
|
97
|
+
const choice = config[category];
|
|
98
|
+
const body = revision.content.promptTemplates[category] ?? config.rendered[category];
|
|
99
|
+
if (choice !== undefined && body !== null && body !== undefined)
|
|
100
|
+
entries.push({
|
|
101
|
+
id: deps.uuid(),
|
|
102
|
+
category,
|
|
103
|
+
mode: choice.mode,
|
|
104
|
+
name: choice.name,
|
|
105
|
+
body,
|
|
106
|
+
slots: [...detectSlots(body).names],
|
|
107
|
+
updatedAt: revision.createdAt,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const attachment = (name) => ({ attachmentId: deps.uuid(), name });
|
|
111
|
+
const provided = {
|
|
112
|
+
research: config.sources.research === "provide" ? (config.provided.research ?? "") : "",
|
|
113
|
+
article: config.sources.article === "provide"
|
|
114
|
+
? (revision.content.articleMarkdown ?? config.provided.article ?? "")
|
|
115
|
+
: "",
|
|
116
|
+
audio: config.sources.audio === "provide" ? attachment("Audio from project") : null,
|
|
117
|
+
thumbnail: config.sources.thumbnail === "provide" ? attachment("Thumbnail from project") : null,
|
|
118
|
+
images: config.sources.images === "provide"
|
|
119
|
+
? (definitions.length ? definitions : (config.provided.images ?? [])).map((_, index) => attachment(`Image ${index + 1}`))
|
|
120
|
+
: [],
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
librarySnapshot: { prompts, entries },
|
|
125
|
+
form: {
|
|
126
|
+
title: config.title,
|
|
127
|
+
format: config.format,
|
|
128
|
+
sources: config.sources,
|
|
129
|
+
checkpoints: listCheckpoints(deps.db, revision.projectId, revision.id).map((gate) => gate.stage),
|
|
130
|
+
llm: config.llm ?? { provider: "", model: "" },
|
|
131
|
+
audio: config.audio ?? { provider: "", model: "", voice: "" },
|
|
132
|
+
images: config.images ?? { provider: "", model: "" },
|
|
133
|
+
articlePrompt: config.articlePrompt ?? "",
|
|
134
|
+
imagePrompts,
|
|
135
|
+
thumbnailPrompt: config.thumbnailPrompt ?? "",
|
|
136
|
+
intro: config.intro?.name ?? "",
|
|
137
|
+
outro: config.outro?.name ?? "",
|
|
138
|
+
chunking: {
|
|
139
|
+
mode: config.chunking?.mode ?? "whole",
|
|
140
|
+
words: config.chunking?.words === undefined ? "500" : String(config.chunking.words),
|
|
141
|
+
characters: config.chunking?.characters === undefined ? "3000" : String(config.chunking.characters),
|
|
142
|
+
},
|
|
143
|
+
subtitles: {
|
|
144
|
+
mode: config.subtitles?.mode ?? "off",
|
|
145
|
+
language: "en",
|
|
146
|
+
fontId: config.subtitles?.fontId ?? "default",
|
|
147
|
+
fontSize: config.subtitles?.fontSize === undefined ? "48" : String(config.subtitles.fontSize),
|
|
148
|
+
position: config.subtitles?.position ?? "bottom",
|
|
149
|
+
},
|
|
150
|
+
values: config.values,
|
|
151
|
+
provided,
|
|
152
|
+
},
|
|
153
|
+
section: "content",
|
|
154
|
+
variants: [],
|
|
155
|
+
expectedWords: "1500",
|
|
156
|
+
previewText: "Every story begins with a word.",
|
|
157
|
+
fontUpload: null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { projectTemplateSchema } from "./schema.js";
|
|
3
|
+
const select = `SELECT t.id,r.version,r.name,t.created_at AS createdAt,r.created_at AS updatedAt,r.document_json AS document FROM project_templates t JOIN project_template_revisions r ON r.template_id=t.id`;
|
|
4
|
+
const templateSummarySchema = z
|
|
5
|
+
.object({
|
|
6
|
+
id: z.uuid(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
version: z.number().int().positive(),
|
|
9
|
+
createdAt: z.string(),
|
|
10
|
+
updatedAt: z.string(),
|
|
11
|
+
})
|
|
12
|
+
.strict()
|
|
13
|
+
.readonly();
|
|
14
|
+
export function templateById(db, id, version) {
|
|
15
|
+
const row = db
|
|
16
|
+
.prepare(`${select} WHERE t.id=? AND r.version=${version === undefined ? "t.head_version" : "?"}`)
|
|
17
|
+
.get(...(version === undefined ? [id] : [id, version]));
|
|
18
|
+
if (row === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (typeof row.document !== "string")
|
|
21
|
+
throw new Error("Template document is missing");
|
|
22
|
+
return projectTemplateSchema.parse({ ...row, document: JSON.parse(row.document) });
|
|
23
|
+
}
|
|
24
|
+
export function templateSummaries(db) {
|
|
25
|
+
return db
|
|
26
|
+
.prepare(`${select} WHERE r.version=t.head_version ORDER BY lower(r.name),t.id`)
|
|
27
|
+
.all()
|
|
28
|
+
.map((row) => {
|
|
29
|
+
const parsed = templateSummarySchema.parse({
|
|
30
|
+
id: row.id,
|
|
31
|
+
name: row.name,
|
|
32
|
+
version: row.version,
|
|
33
|
+
createdAt: row.createdAt,
|
|
34
|
+
updatedAt: row.updatedAt,
|
|
35
|
+
});
|
|
36
|
+
return parsed;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function insertTemplateRevision(db, template) {
|
|
40
|
+
db.prepare("INSERT INTO project_template_revisions(template_id,version,name,document_json,created_at) VALUES (?,?,?,?,?)").run(template.id, template.version, template.name, JSON.stringify(template.document), template.updatedAt);
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { playDraftDocumentSchema } from "../play-drafts/schema.js";
|
|
3
|
+
export const templateCreateSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
id: z.uuid(),
|
|
6
|
+
name: z.string().trim().min(1).max(200),
|
|
7
|
+
document: playDraftDocumentSchema,
|
|
8
|
+
})
|
|
9
|
+
.strict()
|
|
10
|
+
.readonly();
|
|
11
|
+
export const templateUpdateSchema = templateCreateSchema
|
|
12
|
+
.unwrap()
|
|
13
|
+
.extend({
|
|
14
|
+
baseVersion: z.number().int().positive(),
|
|
15
|
+
mutationId: z.uuid(),
|
|
16
|
+
})
|
|
17
|
+
.strict()
|
|
18
|
+
.readonly();
|
|
19
|
+
export const templateDeleteSchema = z
|
|
20
|
+
.object({ id: z.uuid(), baseVersion: z.number().int().positive() })
|
|
21
|
+
.strict()
|
|
22
|
+
.readonly();
|
|
23
|
+
export const templateInstantiateSchema = z
|
|
24
|
+
.object({ templateId: z.uuid(), id: z.uuid(), version: z.number().int().positive() })
|
|
25
|
+
.strict()
|
|
26
|
+
.readonly();
|
|
27
|
+
export const projectTemplateSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
id: z.uuid(),
|
|
30
|
+
name: z.string(),
|
|
31
|
+
version: z.number().int().positive(),
|
|
32
|
+
createdAt: z.string(),
|
|
33
|
+
updatedAt: z.string(),
|
|
34
|
+
document: playDraftDocumentSchema,
|
|
35
|
+
})
|
|
36
|
+
.strict()
|
|
37
|
+
.readonly();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { requestHash } from "../play-drafts/repo.js";
|
|
4
|
+
import { createDraft, readDraft } from "../play-drafts/service.js";
|
|
5
|
+
import { insertTemplateRevision, templateById, templateSummaries } from "./repo.js";
|
|
6
|
+
import { templateCreateSchema, templateDeleteSchema, templateInstantiateSchema, templateUpdateSchema, } from "./schema.js";
|
|
7
|
+
import { freshTemplateDraft, templateSetup } from "./setup.js";
|
|
8
|
+
export function listTemplates(deps) {
|
|
9
|
+
return templateSummaries(deps.db);
|
|
10
|
+
}
|
|
11
|
+
export function readTemplate(deps, id, version) {
|
|
12
|
+
if (!z.uuid().safeParse(id).success ||
|
|
13
|
+
(version !== undefined && !z.number().int().positive().safeParse(version).success))
|
|
14
|
+
return { ok: false, reason: "invalid-input" };
|
|
15
|
+
const value = templateById(deps.db, id, version);
|
|
16
|
+
return value ? { ok: true, value } : { ok: false, reason: "not-found" };
|
|
17
|
+
}
|
|
18
|
+
export function createTemplate(deps, input) {
|
|
19
|
+
const parsed = templateCreateSchema.safeParse(input);
|
|
20
|
+
if (!parsed.success)
|
|
21
|
+
return { ok: false, reason: "invalid-input" };
|
|
22
|
+
return transact(deps.db, () => {
|
|
23
|
+
const { id, name, document } = parsed.data;
|
|
24
|
+
const hash = requestHash(parsed.data);
|
|
25
|
+
const old = deps.db.prepare("SELECT creation_hash FROM project_templates WHERE id=?").get(id);
|
|
26
|
+
if (old)
|
|
27
|
+
return old.creation_hash === hash
|
|
28
|
+
? readTemplate(deps, id, 1)
|
|
29
|
+
: { ok: false, reason: "conflict" };
|
|
30
|
+
const setup = templateSetup(deps, document);
|
|
31
|
+
if (!setup.ok)
|
|
32
|
+
return setup;
|
|
33
|
+
const at = deps.clock.now().toISOString();
|
|
34
|
+
const value = {
|
|
35
|
+
id,
|
|
36
|
+
name,
|
|
37
|
+
document: setup.value,
|
|
38
|
+
version: 1,
|
|
39
|
+
createdAt: at,
|
|
40
|
+
updatedAt: at,
|
|
41
|
+
};
|
|
42
|
+
deps.db
|
|
43
|
+
.prepare("INSERT INTO project_templates(id,head_version,creation_hash,created_at) VALUES (?,1,?,?)")
|
|
44
|
+
.run(id, hash, at);
|
|
45
|
+
insertTemplateRevision(deps.db, value);
|
|
46
|
+
return { ok: true, value };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function updateTemplate(deps, input) {
|
|
50
|
+
const parsed = templateUpdateSchema.safeParse(input);
|
|
51
|
+
if (!parsed.success)
|
|
52
|
+
return { ok: false, reason: "invalid-input" };
|
|
53
|
+
return transact(deps.db, () => {
|
|
54
|
+
const { id, baseVersion, mutationId, name, document } = parsed.data;
|
|
55
|
+
const previous = templateById(deps.db, id);
|
|
56
|
+
if (!previous)
|
|
57
|
+
return { ok: false, reason: "not-found" };
|
|
58
|
+
const row = deps.db
|
|
59
|
+
.prepare("SELECT mutation_id,mutation_hash FROM project_templates WHERE id=?")
|
|
60
|
+
.get(id);
|
|
61
|
+
const hash = requestHash(parsed.data);
|
|
62
|
+
if (row?.mutation_id === mutationId)
|
|
63
|
+
return row.mutation_hash === hash
|
|
64
|
+
? { ok: true, value: previous }
|
|
65
|
+
: { ok: false, reason: "conflict" };
|
|
66
|
+
if (previous.version !== baseVersion)
|
|
67
|
+
return { ok: false, reason: "conflict" };
|
|
68
|
+
const setup = templateSetup(deps, document);
|
|
69
|
+
if (!setup.ok)
|
|
70
|
+
return setup;
|
|
71
|
+
const value = {
|
|
72
|
+
...previous,
|
|
73
|
+
name,
|
|
74
|
+
document: setup.value,
|
|
75
|
+
version: baseVersion + 1,
|
|
76
|
+
updatedAt: deps.clock.now().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
insertTemplateRevision(deps.db, value);
|
|
79
|
+
deps.db
|
|
80
|
+
.prepare("UPDATE project_templates SET head_version=?,mutation_id=?,mutation_hash=? WHERE id=? AND head_version=?")
|
|
81
|
+
.run(value.version, mutationId, hash, id, baseVersion);
|
|
82
|
+
return { ok: true, value };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function deleteTemplate(deps, input) {
|
|
86
|
+
const parsed = templateDeleteSchema.safeParse(input);
|
|
87
|
+
if (!parsed.success)
|
|
88
|
+
return { ok: false, reason: "invalid-input" };
|
|
89
|
+
return transact(deps.db, () => {
|
|
90
|
+
const previous = templateById(deps.db, parsed.data.id);
|
|
91
|
+
if (!previous)
|
|
92
|
+
return { ok: false, reason: "not-found" };
|
|
93
|
+
if (previous.version !== parsed.data.baseVersion)
|
|
94
|
+
return { ok: false, reason: "conflict" };
|
|
95
|
+
if (deps.db
|
|
96
|
+
.prepare("SELECT 1 FROM schedules WHERE template_id=? AND deleted_at IS NULL LIMIT 1")
|
|
97
|
+
.get(parsed.data.id))
|
|
98
|
+
return { ok: false, reason: "referenced-by-schedule" };
|
|
99
|
+
deps.db.prepare("DELETE FROM project_templates WHERE id=?").run(parsed.data.id);
|
|
100
|
+
return { ok: true, value: { deleted: true } };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function instantiateTemplate(deps, input) {
|
|
104
|
+
const parsed = templateInstantiateSchema.safeParse(input);
|
|
105
|
+
if (!parsed.success)
|
|
106
|
+
return { ok: false, reason: "invalid-input" };
|
|
107
|
+
return transact(deps.db, () => {
|
|
108
|
+
const { templateId, id, version } = parsed.data;
|
|
109
|
+
const receipt = deps.db
|
|
110
|
+
.prepare("SELECT template_id,template_version FROM project_template_instantiations WHERE draft_id=?")
|
|
111
|
+
.get(id);
|
|
112
|
+
if (receipt) {
|
|
113
|
+
if (receipt.template_id !== templateId || receipt.template_version !== version)
|
|
114
|
+
return { ok: false, reason: "conflict" };
|
|
115
|
+
const draft = readDraft(deps, id);
|
|
116
|
+
return draft.ok ? draft : { ok: false, reason: "conflict" };
|
|
117
|
+
}
|
|
118
|
+
const template = templateById(deps.db, templateId, version);
|
|
119
|
+
if (!template)
|
|
120
|
+
return { ok: false, reason: "not-found" };
|
|
121
|
+
const result = createDraft(deps, {
|
|
122
|
+
id,
|
|
123
|
+
document: freshTemplateDraft(deps, template.document, { id: templateId, version }),
|
|
124
|
+
});
|
|
125
|
+
if (!result.ok)
|
|
126
|
+
return { ok: false, reason: "conflict" };
|
|
127
|
+
deps.db.prepare("UPDATE play_draft_attachments SET status='reattach' WHERE draft_id=?").run(id);
|
|
128
|
+
deps.db
|
|
129
|
+
.prepare("INSERT INTO project_template_instantiations(draft_id,template_id,template_version) VALUES (?,?,?)")
|
|
130
|
+
.run(id, templateId, version);
|
|
131
|
+
const draft = readDraft(deps, id);
|
|
132
|
+
if (!draft.ok)
|
|
133
|
+
throw new Error("Created template draft could not be read");
|
|
134
|
+
return draft;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { snapshotEntry, snapshotPrompt } from "../library/snapshot.js";
|
|
2
|
+
export function templateSetup(deps, input) {
|
|
3
|
+
const prompts = [];
|
|
4
|
+
const entries = [];
|
|
5
|
+
const choices = [
|
|
6
|
+
{ kind: "article", name: input.form.articlePrompt },
|
|
7
|
+
...input.form.imagePrompts.map((row) => ({ kind: "image", name: row.name })),
|
|
8
|
+
{ kind: "thumbnail", name: input.form.thumbnailPrompt },
|
|
9
|
+
];
|
|
10
|
+
for (const choice of choices) {
|
|
11
|
+
if (choice.name === "")
|
|
12
|
+
continue;
|
|
13
|
+
const row = snapshotPrompt(deps.db, input.librarySnapshot, choice.kind, choice.name);
|
|
14
|
+
if (!row)
|
|
15
|
+
return { ok: false, reason: "missing-prompt" };
|
|
16
|
+
if (!prompts.some((saved) => saved.kind === row.kind && saved.name === row.name))
|
|
17
|
+
prompts.push(row);
|
|
18
|
+
}
|
|
19
|
+
for (const category of ["intro", "outro"]) {
|
|
20
|
+
if (input.form[category] === "")
|
|
21
|
+
continue;
|
|
22
|
+
const row = snapshotEntry(deps.db, input.librarySnapshot, category, input.form[category]);
|
|
23
|
+
if (!row)
|
|
24
|
+
return { ok: false, reason: "missing-prompt" };
|
|
25
|
+
entries.push(row);
|
|
26
|
+
}
|
|
27
|
+
const { templateSource: _source, ...document } = input;
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
value: { ...document, fontUpload: null, librarySnapshot: { prompts, entries } },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function freshTemplateDraft(deps, document, source) {
|
|
34
|
+
const provided = document.form.provided;
|
|
35
|
+
const fresh = (file) => ({
|
|
36
|
+
attachmentId: deps.uuid(),
|
|
37
|
+
name: file.name,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
...document,
|
|
41
|
+
section: "content",
|
|
42
|
+
fontUpload: null,
|
|
43
|
+
templateSource: source,
|
|
44
|
+
variants: document.variants.map((variant) => ({ ...variant, id: deps.uuid() })),
|
|
45
|
+
form: {
|
|
46
|
+
...document.form,
|
|
47
|
+
provided: {
|
|
48
|
+
...provided,
|
|
49
|
+
audio: provided.audio === null ? null : fresh(provided.audio),
|
|
50
|
+
thumbnail: provided.thumbnail === null ? null : fresh(provided.thumbnail),
|
|
51
|
+
images: provided.images.map(fresh),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
2
|
+
import { setProjectPaused } from "../admission/repo.js";
|
|
3
|
+
import { requestHash as hash } from "../revisions/mutation-request.js";
|
|
4
|
+
import { currentRevisionId } from "../revisions/repo.js";
|
|
5
|
+
import { getRevisionView } from "../revisions/view.js";
|
|
6
|
+
import { rebuildAdmissionSchema, } from "./model.js";
|
|
7
|
+
import { executionSnapshotSchema, planPreview, reviewStillCovers } from "./preview-plan.js";
|
|
8
|
+
import { previewById } from "./repo.js";
|
|
9
|
+
import { insertInvocation } from "./runtime-admission.js";
|
|
10
|
+
import { bindNarrationReuse } from "./runtime-narration-reuse.js";
|
|
11
|
+
import { projectStandings } from "./runtime-store.js";
|
|
12
|
+
export function admissionReceipt(db, projectId, idempotencyKey, requestHash) {
|
|
13
|
+
const row = db
|
|
14
|
+
.prepare("SELECT request_hash,response_json FROM rebuild_admissions WHERE project_id=? AND idempotency_key=?")
|
|
15
|
+
.get(projectId, idempotencyKey);
|
|
16
|
+
if (row !== undefined)
|
|
17
|
+
return row.request_hash !== requestHash
|
|
18
|
+
? { ok: false, reason: "conflict" }
|
|
19
|
+
: {
|
|
20
|
+
ok: true,
|
|
21
|
+
value: {
|
|
22
|
+
...rebuildAdmissionSchema.parse(JSON.parse(String(row.response_json))),
|
|
23
|
+
replayed: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
if (db
|
|
27
|
+
.prepare("SELECT 1 FROM revision_mutations WHERE project_id=? AND idempotency_key=? UNION ALL SELECT 1 FROM project_control_receipts WHERE project_id=? AND idempotency_key=?")
|
|
28
|
+
.get(projectId, idempotencyKey, projectId, idempotencyKey) !== undefined)
|
|
29
|
+
return { ok: false, reason: "conflict" };
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
export function admitPreview(deps, input) {
|
|
33
|
+
return transact(deps.db, () => {
|
|
34
|
+
const { preview } = input;
|
|
35
|
+
const replay = admissionReceipt(deps.db, preview.projectId, input.idempotencyKey, input.requestHash);
|
|
36
|
+
if (replay !== undefined)
|
|
37
|
+
return replay;
|
|
38
|
+
if (currentRevisionId(deps.db, preview.projectId) !== preview.baseRevisionId)
|
|
39
|
+
return { ok: false, reason: "conflict" };
|
|
40
|
+
const persisted = previewById(deps.db, preview.projectId, preview.id);
|
|
41
|
+
const row = deps.db
|
|
42
|
+
.prepare("SELECT execution_json FROM rebuild_previews WHERE id=? AND project_id=?")
|
|
43
|
+
.get(preview.id, preview.projectId);
|
|
44
|
+
if (persisted === undefined ||
|
|
45
|
+
typeof row?.execution_json !== "string" ||
|
|
46
|
+
hash("body", "", persisted) !== hash("body", "", preview))
|
|
47
|
+
return { ok: false, reason: "stale-preview" };
|
|
48
|
+
const snapshot = executionSnapshotSchema.parse(JSON.parse(row.execution_json));
|
|
49
|
+
const view = getRevisionView(deps, preview.projectId, preview.baseRevisionId);
|
|
50
|
+
if (view === undefined)
|
|
51
|
+
return { ok: false, reason: "no-project" };
|
|
52
|
+
const fresh = planPreview(deps, view, snapshot.catalogue, preview.selection, preview.id);
|
|
53
|
+
if (!fresh.ok || !reviewStillCovers(preview, snapshot, fresh.value))
|
|
54
|
+
return { ok: false, reason: "stale-preview" };
|
|
55
|
+
const admissionId = deps.ids.next();
|
|
56
|
+
const workIds = [];
|
|
57
|
+
for (const recipe of snapshot.recipes) {
|
|
58
|
+
const disposition = snapshot.dispositions.find((row) => row.key === recipe.key)?.disposition;
|
|
59
|
+
const existing = deps.db
|
|
60
|
+
.prepare(`SELECT w.id,w.state,w.recipe_context,r.piece_id,r.fingerprint FROM revision_work_reservations r JOIN revision_work w ON w.id=r.work_id WHERE r.revision_id=? AND r.work_key=?`)
|
|
61
|
+
.get(view.revision.id, recipe.key);
|
|
62
|
+
if (disposition === "reuse") {
|
|
63
|
+
bindNarrationReuse(deps, view, recipe, snapshot.ordinals[recipe.key] ?? 0);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (existing?.fingerprint === recipe.fingerprint &&
|
|
67
|
+
typeof existing.recipe_context === "string" &&
|
|
68
|
+
existing.state !== "done") {
|
|
69
|
+
workIds.push(String(existing.id));
|
|
70
|
+
if (existing.state !== "running") {
|
|
71
|
+
deps.db
|
|
72
|
+
.prepare("UPDATE revision_work SET state='pending',dispatch_state='allowed',failure_reason=NULL WHERE id=?")
|
|
73
|
+
.run(String(existing.id));
|
|
74
|
+
deps.db
|
|
75
|
+
.prepare(`UPDATE revision_work_pieces SET state='pending',dispatch_state='allowed'
|
|
76
|
+
WHERE work_id=? AND state!='done' AND id IN (
|
|
77
|
+
SELECT p.id FROM revision_work_pieces p
|
|
78
|
+
JOIN revision_work_reservations r ON r.piece_id=p.id AND r.work_id=p.work_id
|
|
79
|
+
AND r.work_key=p.work_key AND r.fingerprint=p.fingerprint
|
|
80
|
+
JOIN project_revisions v ON v.id=r.revision_id
|
|
81
|
+
JOIN json_each(v.fingerprints) f ON f.key=COALESCE(r.logical_key,r.work_key)
|
|
82
|
+
AND f.value=COALESCE(r.desired_fingerprint,r.fingerprint)
|
|
83
|
+
WHERE r.revision_id=? AND (p.id=? OR
|
|
84
|
+
(?='article:body' AND p.work_key LIKE 'article:continuation:%'
|
|
85
|
+
AND COALESCE(r.logical_key,r.work_key)='article:body'))
|
|
86
|
+
)`)
|
|
87
|
+
.run(String(existing.id), view.revision.id, String(existing.piece_id), recipe.key);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (existing?.state === "running")
|
|
92
|
+
throw new Error("Selected work has a mismatched running reservation.");
|
|
93
|
+
deps.db
|
|
94
|
+
.prepare("DELETE FROM revision_work_reservations WHERE revision_id=? AND work_key=?")
|
|
95
|
+
.run(view.revision.id, recipe.key);
|
|
96
|
+
const key = recipe.input.kind === "tts" ? `${recipe.input.logicalKey}:1` : recipe.key;
|
|
97
|
+
const fingerprint = snapshot.anchors[key];
|
|
98
|
+
if (fingerprint === undefined)
|
|
99
|
+
throw new Error("Selected work has no reviewed desired anchor.");
|
|
100
|
+
const work = insertInvocation(deps, view, recipe, snapshot.catalogue, { key, fingerprint }, false, view.revision.id, admissionId);
|
|
101
|
+
workIds.push(work.workId);
|
|
102
|
+
}
|
|
103
|
+
const receipt = {
|
|
104
|
+
revisionId: view.revision.id,
|
|
105
|
+
admissionId,
|
|
106
|
+
workIds: [...new Set(workIds)],
|
|
107
|
+
replayed: false,
|
|
108
|
+
};
|
|
109
|
+
deps.db
|
|
110
|
+
.prepare("INSERT INTO rebuild_admissions(id,project_id,revision_id,idempotency_key,request_hash,preview_id,response_json,created_at) VALUES (?,?,?,?,?,?,?,?)")
|
|
111
|
+
.run(admissionId, preview.projectId, view.revision.id, input.idempotencyKey, input.requestHash, preview.id, JSON.stringify(receipt), deps.clock.now().toISOString());
|
|
112
|
+
for (const review of snapshot.reviews)
|
|
113
|
+
deps.db
|
|
114
|
+
.prepare("INSERT INTO revision_provided_reviews(project_id,revision_id,work_key,dependency_fingerprint,admission_id) VALUES (?,?,?,?,?) ON CONFLICT(revision_id,work_key) DO UPDATE SET dependency_fingerprint=excluded.dependency_fingerprint,admission_id=excluded.admission_id")
|
|
115
|
+
.run(preview.projectId, view.revision.id, review.key, review.fingerprint, admissionId);
|
|
116
|
+
setProjectPaused(deps.db, preview.projectId, false, deps.clock.now().toISOString());
|
|
117
|
+
projectStandings(deps, preview.projectId);
|
|
118
|
+
return { ok: true, value: receipt };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function planDependencies(desired, retained) {
|
|
2
|
+
return desired.map((recipe) => {
|
|
3
|
+
const old = retained.find((one) => one.key === recipe.key &&
|
|
4
|
+
one.fingerprint === recipe.fingerprint &&
|
|
5
|
+
(one.available || one.inflight));
|
|
6
|
+
const changedProvided = recipe.kind === "provided" &&
|
|
7
|
+
retained.some((one) => one.key === recipe.key && one.fingerprint !== recipe.fingerprint);
|
|
8
|
+
const disposition = recipe.unresolved
|
|
9
|
+
? "blocked"
|
|
10
|
+
: old
|
|
11
|
+
? "reuse"
|
|
12
|
+
: changedProvided
|
|
13
|
+
? "review"
|
|
14
|
+
: recipe.kind === "provided"
|
|
15
|
+
? "blocked"
|
|
16
|
+
: recipe.kind === "local"
|
|
17
|
+
? "local"
|
|
18
|
+
: "generate";
|
|
19
|
+
return {
|
|
20
|
+
...recipe,
|
|
21
|
+
disposition,
|
|
22
|
+
inflight: old?.inflight ?? false,
|
|
23
|
+
pieceIds: old?.pieceIds ?? [],
|
|
24
|
+
reason: disposition === "reuse"
|
|
25
|
+
? "Inputs match retained work."
|
|
26
|
+
: disposition === "review"
|
|
27
|
+
? "Confirm the provided content against changed inputs."
|
|
28
|
+
: disposition === "blocked"
|
|
29
|
+
? "An input is unavailable or needs a fresh preview."
|
|
30
|
+
: "Inputs changed or a required output is missing.",
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|