@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.
- package/README.md +28 -6
- package/SUBTITLES.md +2 -0
- package/dist/adapters/alignment/window.js +46 -5
- package/dist/adapters/alignment/worker.js +2 -2
- 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,194 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { carryCheckpointGates } from "../checkpoints/recovery.js";
|
|
4
|
+
import { narrationRegenerationKey } from "../narration/plan.js";
|
|
5
|
+
import { recipeInputSchema } from "../rebuild/recipe-input-schema.js";
|
|
6
|
+
import { planRevision } from "../rebuild/recipe-save.js";
|
|
7
|
+
import { transitionRevisionWork } from "../rebuild/repo.js";
|
|
8
|
+
import { projectStandings } from "../rebuild/runtime-store.js";
|
|
9
|
+
import { discardPreparedAssets } from "../storage/assets.js";
|
|
10
|
+
import { deleteStagedFile } from "../storage/repo.js";
|
|
11
|
+
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
12
|
+
import { validateAssetReferences, validateReplacementAvailability, validateUploads, } from "./mutation-assets.js";
|
|
13
|
+
import { cuesChanged, inspectCueAudio, measuredOutputs, validateProposedCues, } from "./mutation-cues.js";
|
|
14
|
+
import { prepareEditAssets } from "./mutation-prepare.js";
|
|
15
|
+
import { checkMutation, insertReceipt, requestHash, requiredView } from "./mutation-request.js";
|
|
16
|
+
import { preserveDeferredIntent } from "./mutation-work.js";
|
|
17
|
+
import { cloneManifest, ensureRevisionStages, projectSelected } from "./projection.js";
|
|
18
|
+
import { insertAsset, insertRevision } from "./repo.js";
|
|
19
|
+
import { validateNarrationIntent, validateTemplateIntent } from "./rules.js";
|
|
20
|
+
import { revisionEditSchema } from "./schema.js";
|
|
21
|
+
export { bindUpload } from "./mutation-assets.js";
|
|
22
|
+
export { restoreRevision } from "./restore.js";
|
|
23
|
+
export async function saveRevision(deps, input) {
|
|
24
|
+
const parsed = revisionEditSchema.safeParse(input.edit);
|
|
25
|
+
if (!parsed.success)
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
reason: "invalid-edit",
|
|
29
|
+
currentRevisionId: input.baseRevisionId,
|
|
30
|
+
fields: parsed.error.issues.map((row) => ({
|
|
31
|
+
field: row.path.join("."),
|
|
32
|
+
message: row.message,
|
|
33
|
+
})),
|
|
34
|
+
};
|
|
35
|
+
const edit = parsed.data;
|
|
36
|
+
const identity = {
|
|
37
|
+
...input,
|
|
38
|
+
operation: "save",
|
|
39
|
+
hash: requestHash("save", input.baseRevisionId, edit),
|
|
40
|
+
};
|
|
41
|
+
const checked = checkMutation(deps, identity);
|
|
42
|
+
if (checked !== undefined)
|
|
43
|
+
return checked;
|
|
44
|
+
const base = requiredView(deps, input.projectId, input.baseRevisionId);
|
|
45
|
+
const fields = [
|
|
46
|
+
...validateUploads(deps, edit),
|
|
47
|
+
...validateTemplateIntent(base, edit),
|
|
48
|
+
...validateNarrationIntent(base, edit),
|
|
49
|
+
...validateAssetReferences(deps, input.projectId, edit.content, []),
|
|
50
|
+
];
|
|
51
|
+
if (fields.length > 0)
|
|
52
|
+
return { ok: false, reason: "invalid-edit", currentRevisionId: base.revision.id, fields };
|
|
53
|
+
const unavailable = validateReplacementAvailability(deps, base, edit);
|
|
54
|
+
if (unavailable.length > 0)
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
reason: "invalid-edit",
|
|
58
|
+
currentRevisionId: base.revision.id,
|
|
59
|
+
fields: unavailable,
|
|
60
|
+
};
|
|
61
|
+
const { edit: supplied, assets: prepared } = await prepareEditAssets(deps, base, edit);
|
|
62
|
+
try {
|
|
63
|
+
const durations = await inspectCueAudio(deps, base, supplied, prepared);
|
|
64
|
+
const result = transact(deps.db, () => {
|
|
65
|
+
const checked = checkMutation(deps, identity);
|
|
66
|
+
if (checked !== undefined)
|
|
67
|
+
return checked;
|
|
68
|
+
const fresh = requiredView(deps, input.projectId, input.baseRevisionId);
|
|
69
|
+
const tokens = { ...fresh.revision.content.regenerationTokens };
|
|
70
|
+
for (const key of new Set((edit.regenerate ?? []).map(narrationRegenerationKey)))
|
|
71
|
+
tokens[key] = deps.ids.next();
|
|
72
|
+
const finalEdit = {
|
|
73
|
+
...supplied,
|
|
74
|
+
content: { ...supplied.content, regenerationTokens: tokens },
|
|
75
|
+
};
|
|
76
|
+
const outputAssets = prepared.filter((row) => row.upload?.destination.kind !== "narration");
|
|
77
|
+
const inspected = measuredOutputs(deps, fresh, durations);
|
|
78
|
+
const preparedManifest = {
|
|
79
|
+
outputs: [
|
|
80
|
+
...outputAssets.map((row) => ({
|
|
81
|
+
slot: row.slot,
|
|
82
|
+
workKey: row.workKey,
|
|
83
|
+
assetId: row.asset.id,
|
|
84
|
+
output: row.output,
|
|
85
|
+
fingerprint: "",
|
|
86
|
+
state: "ready",
|
|
87
|
+
})),
|
|
88
|
+
],
|
|
89
|
+
pieces: [],
|
|
90
|
+
};
|
|
91
|
+
let plan = planRevision(fresh, finalEdit, preparedManifest, inspected);
|
|
92
|
+
if (!plan.ok)
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: "invalid-edit",
|
|
96
|
+
currentRevisionId: fresh.revision.id,
|
|
97
|
+
fields: plan.fields,
|
|
98
|
+
};
|
|
99
|
+
const fields = [
|
|
100
|
+
...validateAssetReferences(deps, input.projectId, plan.content, prepared.map((row) => row.asset)),
|
|
101
|
+
...validateProposedCues(fresh, finalEdit, plan, prepared, durations),
|
|
102
|
+
];
|
|
103
|
+
if (fields.length > 0)
|
|
104
|
+
return { ok: false, reason: "invalid-edit", currentRevisionId: fresh.revision.id, fields };
|
|
105
|
+
if (cuesChanged(fresh, finalEdit) && plan.content.subtitleCues !== undefined) {
|
|
106
|
+
const timing = plan.recipes.find((row) => row.key === "subtitles:timing");
|
|
107
|
+
if (timing !== undefined) {
|
|
108
|
+
plan = planRevision(fresh, {
|
|
109
|
+
...finalEdit,
|
|
110
|
+
content: {
|
|
111
|
+
...plan.content,
|
|
112
|
+
subtitleCues: {
|
|
113
|
+
...plan.content.subtitleCues,
|
|
114
|
+
audioFingerprint: timing.logicalFingerprint,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}, preparedManifest, inspected);
|
|
118
|
+
if (!plan.ok)
|
|
119
|
+
throw new Error("Validated captions could not bind to their narration timeline.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
plan = preserveDeferredIntent(deps, fresh, plan);
|
|
123
|
+
const revision = {
|
|
124
|
+
id: deps.ids.next(),
|
|
125
|
+
projectId: input.projectId,
|
|
126
|
+
parentId: fresh.revision.id,
|
|
127
|
+
restoredFromId: null,
|
|
128
|
+
config: plan.config,
|
|
129
|
+
content: plan.content,
|
|
130
|
+
fingerprints: plan.fingerprints,
|
|
131
|
+
createdAt: deps.clock.now().toISOString(),
|
|
132
|
+
};
|
|
133
|
+
insertRevision(deps.db, revision);
|
|
134
|
+
ensureRevisionStages(deps, revision);
|
|
135
|
+
for (const row of prepared)
|
|
136
|
+
insertAsset(deps.db, row.asset);
|
|
137
|
+
cloneManifest(deps, revision, plan.manifest);
|
|
138
|
+
if (!advanceHead(deps.db, input.projectId, input.baseRevisionId, revision))
|
|
139
|
+
throw new Error("Project head changed during the revision transaction.");
|
|
140
|
+
transitionRevisionWork(deps, {
|
|
141
|
+
projectId: input.projectId,
|
|
142
|
+
baseRevisionId: input.baseRevisionId,
|
|
143
|
+
revisionId: revision.id,
|
|
144
|
+
fingerprints: plan.fingerprints,
|
|
145
|
+
baseFingerprints: plan.baseFingerprints,
|
|
146
|
+
logicalKeys: logicalKeys(deps, input.projectId, input.baseRevisionId, plan.fingerprints),
|
|
147
|
+
recipes: plan.recipes,
|
|
148
|
+
});
|
|
149
|
+
carryCheckpointGates(deps, input.baseRevisionId, revision);
|
|
150
|
+
projectSelected(deps.db, revision);
|
|
151
|
+
projectStandings(deps, revision.projectId);
|
|
152
|
+
insertReceipt(deps, identity, revision.id);
|
|
153
|
+
for (const row of prepared)
|
|
154
|
+
if (row.upload !== undefined)
|
|
155
|
+
deleteStagedFile(deps.db, row.upload.stagedFileId);
|
|
156
|
+
return { ok: true, view: requiredView(deps, input.projectId, revision.id), duplicate: false };
|
|
157
|
+
});
|
|
158
|
+
if (result.ok && !result.duplicate)
|
|
159
|
+
for (const row of prepared)
|
|
160
|
+
if (row.upload !== undefined)
|
|
161
|
+
releaseStagedFile({ ...deps, emit: () => undefined }, row.upload.stagedFileId);
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
discardPreparedAssets(deps, prepared
|
|
166
|
+
.map((row) => row.asset)
|
|
167
|
+
.filter((asset) => deps.db.prepare("SELECT 1 FROM project_assets WHERE id=?").get(asset.id) === undefined));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function advanceHead(db, projectId, baseRevisionId, revision) {
|
|
171
|
+
const result = db
|
|
172
|
+
.prepare("UPDATE project_heads SET revision_id = ? WHERE project_id = ? AND revision_id = ?")
|
|
173
|
+
.run(revision.id, projectId, baseRevisionId);
|
|
174
|
+
if (Number(result.changes) !== 1)
|
|
175
|
+
return false;
|
|
176
|
+
db.prepare("UPDATE projects SET title = ?, format = ?, config = ?, updated_at = ? WHERE id = ?").run(revision.config.title, revision.config.format, JSON.stringify(revision.config), revision.createdAt, projectId);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
export function logicalKeys(deps, projectId, revisionId, fingerprints) {
|
|
180
|
+
const values = {};
|
|
181
|
+
const rows = deps.db
|
|
182
|
+
.prepare("SELECT p.work_key,p.input_json,r.logical_key FROM revision_work_reservations r JOIN revision_work_pieces p ON p.id=r.piece_id AND p.work_id=r.work_id WHERE r.project_id=? AND r.revision_id=?")
|
|
183
|
+
.all(projectId, revisionId);
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
const input = recipeInputSchema.parse(JSON.parse(z.string().parse(row.input_json)));
|
|
186
|
+
if (input.kind === "tts" && fingerprints[`${input.logicalKey}:1`] !== undefined)
|
|
187
|
+
values[z.string().parse(row.work_key)] = `${input.logicalKey}:1`;
|
|
188
|
+
else if (typeof row.logical_key === "string")
|
|
189
|
+
values[z.string().parse(row.work_key)] = row.logical_key;
|
|
190
|
+
else if (input.kind === "tts")
|
|
191
|
+
values[z.string().parse(row.work_key)] = `${input.logicalKey}:1`;
|
|
192
|
+
}
|
|
193
|
+
return values;
|
|
194
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { stageKinds } from "../../kernel/pipeline.js";
|
|
3
|
+
import { insertPiece } from "../../kernel/runner/piece-repo.js";
|
|
4
|
+
import { recipeInputSchema } from "../rebuild/recipe-input-schema.js";
|
|
5
|
+
import { insertOutput } from "../storage/repo.js";
|
|
6
|
+
import { insertManifestOutput, insertManifestPiece, outputsForRevision, piecesForRevision, selectOutputRecord, selectPieceRecord, } from "./repo.js";
|
|
7
|
+
export function ensureRevisionStages(deps, revision) {
|
|
8
|
+
for (const kind of stageKinds) {
|
|
9
|
+
const source = revision.config.sources[kind];
|
|
10
|
+
deps.db
|
|
11
|
+
.prepare("INSERT INTO stages(id,project_id,kind,source,state) VALUES (?,?,?,?,?) ON CONFLICT(project_id,kind) DO UPDATE SET source=excluded.source")
|
|
12
|
+
.run(deps.ids.next(), revision.projectId, kind, source, source === "off" ? "skipped" : "pending");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function positionedOutput(revision, row) {
|
|
16
|
+
if (row.output.role !== "image")
|
|
17
|
+
return row;
|
|
18
|
+
const index = revision.content.imageOrder.indexOf(row.workKey.replace(/^image:/, ""));
|
|
19
|
+
return index < 0
|
|
20
|
+
? row
|
|
21
|
+
: { ...row, output: { ...row.output, meta: { ...row.output.meta, index: index + 1 } } };
|
|
22
|
+
}
|
|
23
|
+
export function positionedPiece(revision, row) {
|
|
24
|
+
if (row.piece.kind !== "image")
|
|
25
|
+
return row;
|
|
26
|
+
const index = revision.content.imageOrder.indexOf(row.key.replace(/^image:/, ""));
|
|
27
|
+
return index < 0 ? row : { ...row, piece: { ...row.piece, idx: index + 1 } };
|
|
28
|
+
}
|
|
29
|
+
export function cloneManifest(deps, revision, manifest) {
|
|
30
|
+
for (const row of manifest.outputs) {
|
|
31
|
+
if (row.output.role === "image" &&
|
|
32
|
+
!revision.content.imageOrder.includes(row.workKey.replace(/^image:/, "")))
|
|
33
|
+
continue;
|
|
34
|
+
const recordId = deps.ids.next();
|
|
35
|
+
insertManifestOutput(deps.db, revision, positionedOutput(revision, row), recordId);
|
|
36
|
+
selectOutputRecord(deps.db, revision.id, row.slot, recordId);
|
|
37
|
+
}
|
|
38
|
+
for (const row of manifest.pieces) {
|
|
39
|
+
if (row.piece.kind === "image" &&
|
|
40
|
+
!revision.content.imageOrder.includes(row.key.replace(/^image:/, "")))
|
|
41
|
+
continue;
|
|
42
|
+
const recordId = deps.ids.next();
|
|
43
|
+
insertManifestPiece(deps.db, revision, positionedPiece(revision, row), recordId);
|
|
44
|
+
selectPieceRecord(deps.db, revision.id, row.key, recordId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function projectSelected(db, revision) {
|
|
48
|
+
const outputs = outputsForRevision(db, revision.projectId, revision.id).filter((row) => row.selected);
|
|
49
|
+
const pieces = piecesForRevision(db, revision.projectId, revision.id).filter((row) => row.selected &&
|
|
50
|
+
(revision.fingerprints[row.key] !== undefined ||
|
|
51
|
+
desiredPhysicalPiece(db, revision, row) ||
|
|
52
|
+
db
|
|
53
|
+
.prepare("SELECT 1 FROM revision_work_reservations WHERE revision_id=? AND work_key=?")
|
|
54
|
+
.get(revision.id, row.key) !== undefined));
|
|
55
|
+
db.prepare("DELETE FROM outputs WHERE project_id=?").run(revision.projectId);
|
|
56
|
+
for (const row of outputs)
|
|
57
|
+
insertOutput(db, row.output);
|
|
58
|
+
db.prepare("DELETE FROM stage_pieces WHERE stage_id IN (SELECT id FROM stages WHERE project_id=?)").run(revision.projectId);
|
|
59
|
+
for (const row of pieces) {
|
|
60
|
+
const stage = db
|
|
61
|
+
.prepare("SELECT id FROM stages WHERE project_id=? AND kind=?")
|
|
62
|
+
.get(revision.projectId, row.stageKind);
|
|
63
|
+
if (stage === undefined || z.string().parse(stage.id) !== row.piece.stageId)
|
|
64
|
+
throw new Error("Manifest piece stage does not belong to this project.");
|
|
65
|
+
insertPiece(db, row.piece);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function desiredPhysicalPiece(db, revision, row) {
|
|
69
|
+
const piece = db
|
|
70
|
+
.prepare("SELECT p.input_json FROM revision_work_pieces p JOIN revision_work w ON w.id=p.work_id WHERE p.id=? AND w.project_id=?")
|
|
71
|
+
.get(row.piece.id, revision.projectId);
|
|
72
|
+
if (piece === undefined) {
|
|
73
|
+
if (row.stageKind !== "audio" || (row.piece.kind !== "chunk" && row.piece.kind !== "segment"))
|
|
74
|
+
return false;
|
|
75
|
+
const payload = z
|
|
76
|
+
.object({ logicalKey: z.string() })
|
|
77
|
+
.safeParse(JSON.parse(row.piece.payload ?? "null"));
|
|
78
|
+
return payload.success && revision.fingerprints[`${payload.data.logicalKey}:1`] !== undefined;
|
|
79
|
+
}
|
|
80
|
+
const input = recipeInputSchema.parse(JSON.parse(z.string().parse(piece.input_json)));
|
|
81
|
+
return input.kind === "tts" && revision.fingerprints[`${input.logicalKey}:1`] !== undefined;
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { workExists } from "../../kernel/runner/work-authority.js";
|
|
4
|
+
import { pieceFile } from "../storage/reconcile.js";
|
|
5
|
+
import { outputSchema } from "../storage/schema.js";
|
|
6
|
+
import { outputsForRevision, piecesForRevision, revisionById } from "./repo.js";
|
|
7
|
+
import { stagePieceSchema } from "./schema.js";
|
|
8
|
+
export function publicationAuthority(db, publication) {
|
|
9
|
+
const { work, pieceId, publicationId } = publication;
|
|
10
|
+
if (publicationId !== (pieceId ?? work.workId))
|
|
11
|
+
throw new Error("Publication identity does not match its durable work.");
|
|
12
|
+
if (!workExists(db, work))
|
|
13
|
+
return undefined;
|
|
14
|
+
const row = pieceId === null
|
|
15
|
+
? db
|
|
16
|
+
.prepare("SELECT work_key,fingerprint FROM revision_work_reservations WHERE work_id=? AND piece_id IS NULL")
|
|
17
|
+
.get(work.workId)
|
|
18
|
+
: db
|
|
19
|
+
.prepare("SELECT work_key,fingerprint FROM revision_work_pieces WHERE work_id=? AND id=?")
|
|
20
|
+
.get(work.workId, pieceId);
|
|
21
|
+
if (row === undefined && pieceId === null) {
|
|
22
|
+
const candidates = Object.entries(revisionById(db, work.projectId, work.revisionId)?.fingerprints ?? {}).filter(([, value]) => value === work.fingerprint);
|
|
23
|
+
const candidate = candidates.length === 1 ? candidates[0] : undefined;
|
|
24
|
+
if (candidate !== undefined)
|
|
25
|
+
return { key: candidate[0], fingerprint: candidate[1] };
|
|
26
|
+
}
|
|
27
|
+
if (row === undefined)
|
|
28
|
+
throw new Error("Publication has no unambiguous durable authority.");
|
|
29
|
+
const parsed = z.object({ work_key: z.string(), fingerprint: z.string() }).parse(row);
|
|
30
|
+
return { key: parsed.work_key, fingerprint: parsed.fingerprint };
|
|
31
|
+
}
|
|
32
|
+
export function validatePublication(db, publication, outputs, pieces) {
|
|
33
|
+
if (outputs.length + pieces.length === 0)
|
|
34
|
+
throw new Error("A publication must contain a result.");
|
|
35
|
+
if (new Set(outputs.map((row) => row.slot)).size !== outputs.length ||
|
|
36
|
+
new Set(pieces.map((row) => row.key)).size !== pieces.length)
|
|
37
|
+
throw new Error("Publication destinations must be unique.");
|
|
38
|
+
const authority = publicationAuthority(db, publication);
|
|
39
|
+
const durablePiece = publication.pieceId === null
|
|
40
|
+
? undefined
|
|
41
|
+
: db
|
|
42
|
+
.prepare("SELECT logical_fingerprint FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
43
|
+
.get(publication.pieceId, publication.work.workId);
|
|
44
|
+
const revision = revisionById(db, publication.work.projectId, publication.work.revisionId);
|
|
45
|
+
for (const row of outputs) {
|
|
46
|
+
const retainedMember = ((authority?.key === "subtitles:files" &&
|
|
47
|
+
((row.workKey === "export:wav" && row.output.role === "audio_export") ||
|
|
48
|
+
(row.workKey === "export:video" &&
|
|
49
|
+
row.output.role === "video" &&
|
|
50
|
+
row.output.meta.subtitlesMode !== "burn-in"))) ||
|
|
51
|
+
((authority?.key === "export:wav" || authority?.key === "export:video") &&
|
|
52
|
+
row.workKey === "subtitles:files" &&
|
|
53
|
+
["subtitles_srt", "subtitles_vtt", "subtitle_ass", "subtitle_font"].includes(row.output.role))) &&
|
|
54
|
+
outputsForRevision(db, publication.work.projectId, publication.work.revisionId).some((old) => old.selected &&
|
|
55
|
+
old.state === "ready" &&
|
|
56
|
+
old.workKey === row.workKey &&
|
|
57
|
+
old.output.role === row.output.role &&
|
|
58
|
+
old.assetId === row.asset.id &&
|
|
59
|
+
old.fingerprint === row.fingerprint);
|
|
60
|
+
if (!retainedMember &&
|
|
61
|
+
row.fingerprint !== revision?.fingerprints[row.workKey] &&
|
|
62
|
+
!(row.workKey === authority?.key &&
|
|
63
|
+
(row.fingerprint === authority.fingerprint ||
|
|
64
|
+
row.fingerprint === durablePiece?.logical_fingerprint)))
|
|
65
|
+
throw new Error("Output fingerprint differs from its authorized recipe.");
|
|
66
|
+
outputSchema.parse(row.output);
|
|
67
|
+
if (row.output.projectId !== publication.work.projectId ||
|
|
68
|
+
row.asset.projectId !== publication.work.projectId ||
|
|
69
|
+
row.output.stageKind !== publication.work.kind ||
|
|
70
|
+
row.output.path !== row.asset.path ||
|
|
71
|
+
row.output.bytes !== row.asset.bytes)
|
|
72
|
+
throw new Error("Output descriptor does not match its owned asset and stage.");
|
|
73
|
+
}
|
|
74
|
+
for (const row of pieces) {
|
|
75
|
+
stagePieceSchema.parse(row.piece);
|
|
76
|
+
const owned = db
|
|
77
|
+
.prepare("SELECT work_key,fingerprint FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
78
|
+
.get(row.piece.id, publication.work.workId);
|
|
79
|
+
const file = pieceFile(row.piece.payload);
|
|
80
|
+
if (row.stageKind !== publication.work.kind ||
|
|
81
|
+
row.piece.stageId !== publication.work.stageId ||
|
|
82
|
+
owned === undefined ||
|
|
83
|
+
owned.work_key !== row.key ||
|
|
84
|
+
owned.fingerprint !== row.fingerprint ||
|
|
85
|
+
(publication.pieceId !== null && row.piece.id !== publication.pieceId) ||
|
|
86
|
+
(row.asset !== null &&
|
|
87
|
+
(row.asset.projectId !== publication.work.projectId || file !== row.asset.path)) ||
|
|
88
|
+
(row.asset === null && file !== undefined))
|
|
89
|
+
throw new Error("Piece descriptor does not match its owned work and asset.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function isPublicationReplay(db, publication, outputs, pieces) {
|
|
93
|
+
const oldOutputs = outputsForRevision(db, publication.work.projectId, publication.work.revisionId).filter((row) => row.publicationId === publication.publicationId);
|
|
94
|
+
const oldPieces = piecesForRevision(db, publication.work.projectId, publication.work.revisionId).filter((row) => row.publicationId === publication.publicationId);
|
|
95
|
+
if (oldOutputs.length + oldPieces.length === 0)
|
|
96
|
+
return false;
|
|
97
|
+
const descriptors = outputs.map((row) => ({
|
|
98
|
+
slot: row.slot,
|
|
99
|
+
workKey: row.workKey,
|
|
100
|
+
assetId: row.asset.id,
|
|
101
|
+
output: row.output,
|
|
102
|
+
fingerprint: row.fingerprint,
|
|
103
|
+
state: "ready",
|
|
104
|
+
}));
|
|
105
|
+
const pieceDescriptors = pieces.map((row) => ({
|
|
106
|
+
key: row.key,
|
|
107
|
+
stageKind: row.stageKind,
|
|
108
|
+
assetId: row.asset?.id ?? null,
|
|
109
|
+
piece: row.piece,
|
|
110
|
+
fingerprint: row.fingerprint,
|
|
111
|
+
}));
|
|
112
|
+
const normalize = (rows) => rows
|
|
113
|
+
.map((row) => JSON.parse(JSON.stringify(row)))
|
|
114
|
+
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
115
|
+
if (!isDeepStrictEqual(normalize(oldOutputs.map(outputDescriptor)), normalize(descriptors)) ||
|
|
116
|
+
!isDeepStrictEqual(normalize(oldPieces.map(pieceDescriptor)), normalize(pieceDescriptors)))
|
|
117
|
+
throw new Error("Publication conflicts with its complete retained result.");
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
function outputDescriptor({ recordId: _recordId, publicationId: _publicationId, selected: _selected, ...row }) {
|
|
121
|
+
return row;
|
|
122
|
+
}
|
|
123
|
+
function pieceDescriptor({ recordId: _recordId, publicationId: _publicationId, selected: _selected, ...row }) {
|
|
124
|
+
return row;
|
|
125
|
+
}
|
|
126
|
+
export function replacementSlots(authority, outputs) {
|
|
127
|
+
if (authority === "article:body")
|
|
128
|
+
return ["article:article_md", "article:article_txt", "article:sources", "article:glossary"];
|
|
129
|
+
if (authority === "subtitles:files" || authority === "export:wav" || authority === "export:video")
|
|
130
|
+
return [
|
|
131
|
+
"video:subtitles_srt",
|
|
132
|
+
"video:subtitles_vtt",
|
|
133
|
+
"video:subtitle_ass",
|
|
134
|
+
"video:subtitle_font",
|
|
135
|
+
...(authority === "subtitles:files"
|
|
136
|
+
? []
|
|
137
|
+
: ["video:video", "video:audio_export", "video:render_params"]),
|
|
138
|
+
];
|
|
139
|
+
return outputs.map((row) => row.slot);
|
|
140
|
+
}
|
|
141
|
+
export function validateBundle(authority, outputs) {
|
|
142
|
+
const allowed = (row) => row.workKey === authority ||
|
|
143
|
+
((authority === "export:wav" || authority === "export:video") &&
|
|
144
|
+
["subtitles:files", "subtitles:cues"].includes(row.workKey)) ||
|
|
145
|
+
(authority === "subtitles:files" &&
|
|
146
|
+
((row.workKey === "export:wav" && row.output.role === "audio_export") ||
|
|
147
|
+
(row.workKey === "export:video" &&
|
|
148
|
+
row.output.role === "video" &&
|
|
149
|
+
row.output.meta.subtitlesMode !== "burn-in")));
|
|
150
|
+
if (outputs.some((row) => !allowed(row)))
|
|
151
|
+
throw new Error("Publication member is outside its authorized result bundle.");
|
|
152
|
+
const roles = new Set(outputs.map((row) => row.output.role));
|
|
153
|
+
const media = outputs.find((row) => row.output.role === "video" || row.output.role === "audio_export");
|
|
154
|
+
if ((authority === "export:wav" && (!roles.has("audio_export") || !roles.has("render_params"))) ||
|
|
155
|
+
(authority === "export:video" && (!roles.has("video") || !roles.has("render_params"))))
|
|
156
|
+
throw new Error("Media and render parameters must publish as one complete bundle.");
|
|
157
|
+
if (media !== undefined &&
|
|
158
|
+
media.output.meta.subtitlesMode !== undefined &&
|
|
159
|
+
media.output.meta.subtitlesMode !== "off") {
|
|
160
|
+
if (!roles.has("subtitles_srt") ||
|
|
161
|
+
!roles.has("subtitles_vtt") ||
|
|
162
|
+
(media.output.meta.subtitlesMode === "burn-in" &&
|
|
163
|
+
(!roles.has("subtitle_ass") || !roles.has("subtitle_font"))))
|
|
164
|
+
throw new Error("Declared subtitle files must be committed with their media descriptor.");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
2
|
+
import { publicationTargets } from "../rebuild/repo.js";
|
|
3
|
+
import { discardPreparedAssets } from "../storage/assets.js";
|
|
4
|
+
import { positionedOutput, positionedPiece, projectSelected } from "./projection.js";
|
|
5
|
+
import { isPublicationReplay, publicationAuthority, replacementSlots, validateBundle, validatePublication, } from "./publication-rules.js";
|
|
6
|
+
import { insertAsset, insertManifestOutput, insertManifestPiece, outputsForRevision, revisionById, selectOutputRecord, selectPieceRecord, } from "./repo.js";
|
|
7
|
+
export function commitRevisionOutputs(deps, publication, outputs, pieces) {
|
|
8
|
+
const assets = [
|
|
9
|
+
...outputs.map((row) => row.asset),
|
|
10
|
+
...pieces.flatMap((row) => (row.asset === null ? [] : [row.asset])),
|
|
11
|
+
];
|
|
12
|
+
try {
|
|
13
|
+
return transact(deps.db, () => {
|
|
14
|
+
const result = { originRevisionId: publication.work.revisionId, currentAttached: false };
|
|
15
|
+
const authority = publicationAuthority(deps.db, publication);
|
|
16
|
+
if (authority === undefined)
|
|
17
|
+
return result;
|
|
18
|
+
validatePublication(deps.db, publication, outputs, pieces);
|
|
19
|
+
validateBundle(authority.key, outputs);
|
|
20
|
+
if (isPublicationReplay(deps.db, publication, outputs, pieces))
|
|
21
|
+
return result;
|
|
22
|
+
const targets = publicationTargets(deps.db, publication, authority.key, authority.fingerprint);
|
|
23
|
+
if (targets.length === 0)
|
|
24
|
+
return result;
|
|
25
|
+
for (const asset of assets)
|
|
26
|
+
insertAsset(deps.db, asset);
|
|
27
|
+
let currentAttached = false;
|
|
28
|
+
for (const target of targets) {
|
|
29
|
+
const revision = revisionById(deps.db, publication.work.projectId, target.revisionId);
|
|
30
|
+
if (revision === undefined)
|
|
31
|
+
throw new Error("Publication target disappeared inside its transaction.");
|
|
32
|
+
const selected = target.selected;
|
|
33
|
+
const before = outputsForRevision(deps.db, revision.projectId, revision.id);
|
|
34
|
+
const exportAuthority = authority.key === "export:wav" || authority.key === "export:video";
|
|
35
|
+
const selectsMember = (output) => {
|
|
36
|
+
if (!selected)
|
|
37
|
+
return false;
|
|
38
|
+
if (authority.key === "subtitles:files" &&
|
|
39
|
+
(output.output.role === "audio_export" || output.output.role === "video"))
|
|
40
|
+
return before.some((row) => row.selected &&
|
|
41
|
+
row.state === "ready" &&
|
|
42
|
+
row.workKey === output.workKey &&
|
|
43
|
+
row.assetId === output.asset.id &&
|
|
44
|
+
row.fingerprint === output.fingerprint);
|
|
45
|
+
if (!exportAuthority || output.workKey !== "subtitles:files")
|
|
46
|
+
return true;
|
|
47
|
+
if ((revision.config.subtitles?.mode ?? "off") === "off")
|
|
48
|
+
return false;
|
|
49
|
+
return (revision.fingerprints[output.workKey] === output.fingerprint ||
|
|
50
|
+
before.some((row) => row.selected &&
|
|
51
|
+
row.state === "ready" &&
|
|
52
|
+
row.slot === output.slot &&
|
|
53
|
+
row.assetId === output.asset.id &&
|
|
54
|
+
row.fingerprint === output.fingerprint));
|
|
55
|
+
};
|
|
56
|
+
if (selected)
|
|
57
|
+
for (const slot of replacementSlots(authority.key, outputs)) {
|
|
58
|
+
const captionSlot = [
|
|
59
|
+
"video:subtitles_srt",
|
|
60
|
+
"video:subtitles_vtt",
|
|
61
|
+
"video:subtitle_ass",
|
|
62
|
+
"video:subtitle_font",
|
|
63
|
+
].includes(slot);
|
|
64
|
+
if (exportAuthority &&
|
|
65
|
+
captionSlot &&
|
|
66
|
+
(revision.config.subtitles?.mode ?? "off") !== "off" &&
|
|
67
|
+
!outputs.some((output) => output.slot === slot && selectsMember(output)))
|
|
68
|
+
continue;
|
|
69
|
+
deps.db
|
|
70
|
+
.prepare("UPDATE revision_outputs SET selected=0 WHERE revision_id=? AND slot=?")
|
|
71
|
+
.run(revision.id, slot);
|
|
72
|
+
}
|
|
73
|
+
for (const output of outputs) {
|
|
74
|
+
const row = {
|
|
75
|
+
slot: output.slot,
|
|
76
|
+
workKey: output.workKey,
|
|
77
|
+
assetId: output.asset.id,
|
|
78
|
+
output: output.output,
|
|
79
|
+
fingerprint: output.fingerprint,
|
|
80
|
+
state: "ready",
|
|
81
|
+
};
|
|
82
|
+
const recordId = deps.ids.next();
|
|
83
|
+
insertManifestOutput(deps.db, revision, target.revisionId === publication.work.revisionId
|
|
84
|
+
? row
|
|
85
|
+
: positionedOutput(revision, row), recordId, publication.publicationId);
|
|
86
|
+
if (selectsMember(output))
|
|
87
|
+
selectOutputRecord(deps.db, revision.id, row.slot, recordId);
|
|
88
|
+
}
|
|
89
|
+
for (const piece of pieces) {
|
|
90
|
+
const row = {
|
|
91
|
+
key: piece.key,
|
|
92
|
+
stageKind: piece.stageKind,
|
|
93
|
+
assetId: piece.asset?.id ?? null,
|
|
94
|
+
piece: piece.piece,
|
|
95
|
+
fingerprint: piece.fingerprint,
|
|
96
|
+
};
|
|
97
|
+
const recordId = deps.ids.next();
|
|
98
|
+
insertManifestPiece(deps.db, revision, target.revisionId === publication.work.revisionId
|
|
99
|
+
? row
|
|
100
|
+
: positionedPiece(revision, row), recordId, publication.publicationId);
|
|
101
|
+
if (selected)
|
|
102
|
+
selectPieceRecord(deps.db, revision.id, row.key, recordId);
|
|
103
|
+
}
|
|
104
|
+
if (selected && target.current) {
|
|
105
|
+
projectSelected(deps.db, revision);
|
|
106
|
+
currentAttached = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { ...result, currentAttached };
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
discardPreparedAssets(deps, assets);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { projectRevisionSchema } from "./schema.js";
|
|
3
|
+
export { insertManifestOutput, insertManifestPiece, outputsForRevision, piecesForRevision, selectOutputRecord, selectPieceRecord, } from "./manifest-repo.js";
|
|
4
|
+
const revisionRow = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
project_id: z.string(),
|
|
7
|
+
parent_id: z.string().nullable(),
|
|
8
|
+
restored_from_id: z.string().nullable(),
|
|
9
|
+
config: z.string(),
|
|
10
|
+
content: z.string(),
|
|
11
|
+
fingerprints: z.string(),
|
|
12
|
+
created_at: z.string(),
|
|
13
|
+
});
|
|
14
|
+
const assetRow = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
project_id: z.string(),
|
|
17
|
+
path: z.string(),
|
|
18
|
+
bytes: z.number().nullable(),
|
|
19
|
+
created_at: z.string(),
|
|
20
|
+
});
|
|
21
|
+
export function insertRevision(db, revision) {
|
|
22
|
+
db.prepare(`INSERT INTO project_revisions
|
|
23
|
+
(id,project_id,parent_id,restored_from_id,config,content,fingerprints,created_at)
|
|
24
|
+
VALUES (?,?,?,?,?,?,?,?)`).run(revision.id, revision.projectId, revision.parentId, revision.restoredFromId, JSON.stringify(revision.config), JSON.stringify(revision.content), JSON.stringify(revision.fingerprints), revision.createdAt);
|
|
25
|
+
}
|
|
26
|
+
export function revisionById(db, projectId, revisionId) {
|
|
27
|
+
const row = db
|
|
28
|
+
.prepare("SELECT * FROM project_revisions WHERE project_id=? AND id=?")
|
|
29
|
+
.get(projectId, revisionId);
|
|
30
|
+
return row === undefined ? undefined : toRevision(row);
|
|
31
|
+
}
|
|
32
|
+
export function currentRevisionId(db, projectId) {
|
|
33
|
+
const row = db.prepare("SELECT revision_id FROM project_heads WHERE project_id=?").get(projectId);
|
|
34
|
+
return row === undefined
|
|
35
|
+
? undefined
|
|
36
|
+
: z.object({ revision_id: z.string() }).parse(row).revision_id;
|
|
37
|
+
}
|
|
38
|
+
export function listRevisionHistory(db, projectId) {
|
|
39
|
+
const head = currentRevisionId(db, projectId);
|
|
40
|
+
return db
|
|
41
|
+
.prepare("SELECT * FROM project_revisions WHERE project_id=? ORDER BY created_at DESC,id DESC")
|
|
42
|
+
.all(projectId)
|
|
43
|
+
.map((row) => {
|
|
44
|
+
const revision = toRevision(row);
|
|
45
|
+
return {
|
|
46
|
+
id: revision.id,
|
|
47
|
+
parentId: revision.parentId,
|
|
48
|
+
restoredFromId: revision.restoredFromId,
|
|
49
|
+
title: revision.config.title,
|
|
50
|
+
createdAt: revision.createdAt,
|
|
51
|
+
current: revision.id === head,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function insertAsset(db, asset) {
|
|
56
|
+
const existing = db
|
|
57
|
+
.prepare("SELECT * FROM project_assets WHERE id=? OR (project_id=? AND path=?)")
|
|
58
|
+
.all(asset.id, asset.projectId, asset.path);
|
|
59
|
+
if (existing.length > 0) {
|
|
60
|
+
const row = assetRow.parse(existing[0]);
|
|
61
|
+
if (existing.length !== 1 ||
|
|
62
|
+
row.id !== asset.id ||
|
|
63
|
+
row.project_id !== asset.projectId ||
|
|
64
|
+
row.path !== asset.path ||
|
|
65
|
+
row.bytes !== asset.bytes)
|
|
66
|
+
throw new Error("Asset identity conflicts with a registered file.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
db.prepare("INSERT INTO project_assets(id,project_id,path,bytes,created_at) VALUES (?,?,?,?,?)").run(asset.id, asset.projectId, asset.path, asset.bytes, asset.createdAt);
|
|
70
|
+
}
|
|
71
|
+
function toRevision(value) {
|
|
72
|
+
const row = revisionRow.parse(value);
|
|
73
|
+
return projectRevisionSchema.parse({
|
|
74
|
+
id: row.id,
|
|
75
|
+
projectId: row.project_id,
|
|
76
|
+
parentId: row.parent_id,
|
|
77
|
+
restoredFromId: row.restored_from_id,
|
|
78
|
+
config: JSON.parse(row.config),
|
|
79
|
+
content: JSON.parse(row.content),
|
|
80
|
+
fingerprints: JSON.parse(row.fingerprints),
|
|
81
|
+
createdAt: row.created_at,
|
|
82
|
+
});
|
|
83
|
+
}
|