@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,110 @@
|
|
|
1
|
+
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
2
|
+
import { cliPathStatus } from "../settings/cli-paths.js";
|
|
3
|
+
import { hasKey, listVoices } from "../settings/repo.js";
|
|
4
|
+
import { requiresNewSubmission } from "./preview-retained.js";
|
|
5
|
+
import { recipeProviderChoice } from "./recipe-provider-choice.js";
|
|
6
|
+
export function paidChoices(snapshot, view, deps) {
|
|
7
|
+
const choices = [];
|
|
8
|
+
for (const recipe of snapshot.recipes) {
|
|
9
|
+
if (recipe.kind !== "provider" ||
|
|
10
|
+
snapshot.dispositions.find((row) => row.key === recipe.key)?.disposition === "reuse")
|
|
11
|
+
continue;
|
|
12
|
+
if (!requiresNewSubmission(deps, view.revision.id, recipe.key, recipe.fingerprint))
|
|
13
|
+
continue;
|
|
14
|
+
const choice = recipeProviderChoice(recipe, view.revision.config);
|
|
15
|
+
if (choice !== undefined)
|
|
16
|
+
choices.push(choice);
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
...new Map(choices.map((choice) => [`${choice.family}:${choice.provider}:${choice.model}`, choice])).values(),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
export async function checkReadiness(deps, snapshot, view) {
|
|
23
|
+
const choices = paidChoices(snapshot, view, deps);
|
|
24
|
+
if (choices.length === 0)
|
|
25
|
+
return { fields: [], providers: [] };
|
|
26
|
+
const providers = await deps.providers();
|
|
27
|
+
const fields = [];
|
|
28
|
+
for (const choice of choices) {
|
|
29
|
+
const provider = providers.find((row) => row.id === choice.provider && row.family === choice.family);
|
|
30
|
+
if (provider === undefined || !readinessIsUsable(provider.readiness)) {
|
|
31
|
+
fields.push({
|
|
32
|
+
field: choice.family,
|
|
33
|
+
message: provider?.readiness.kind === "cli" && provider.readiness.issue
|
|
34
|
+
? provider.readiness.issue
|
|
35
|
+
: "Configure this provider before rebuilding.",
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const models = await deps.modelsFor(choice.provider, choice.family);
|
|
40
|
+
if (!models.some((row) => row.id === choice.model))
|
|
41
|
+
fields.push({
|
|
42
|
+
field: `${choice.family}.model`,
|
|
43
|
+
message: "Choose an available model before rebuilding.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return { fields, providers };
|
|
47
|
+
}
|
|
48
|
+
export function localReadiness(deps, snapshot, view, providers, catalogue) {
|
|
49
|
+
const fields = [];
|
|
50
|
+
for (const choice of paidChoices(snapshot, view, deps)) {
|
|
51
|
+
const model = catalogue[choice.family].find((row) => row.provider === choice.provider &&
|
|
52
|
+
row.id === choice.model &&
|
|
53
|
+
row.enabled &&
|
|
54
|
+
!row.deprecated);
|
|
55
|
+
if (model === undefined)
|
|
56
|
+
fields.push({
|
|
57
|
+
field: `${choice.family}.model`,
|
|
58
|
+
message: "The selected model is no longer available.",
|
|
59
|
+
});
|
|
60
|
+
const provider = providers.find((row) => row.id === choice.provider && row.family === choice.family);
|
|
61
|
+
if (provider?.cliPath !== undefined &&
|
|
62
|
+
cliPathStatus(deps.db, provider.id).command !== provider.cliPath.command)
|
|
63
|
+
fields.push({
|
|
64
|
+
field: `${choice.family}.cliPath`,
|
|
65
|
+
message: "The configured CLI command changed. Check readiness again before rebuilding.",
|
|
66
|
+
});
|
|
67
|
+
if (provider?.readiness.kind === "keyed" && !hasKey(deps.db, provider.id))
|
|
68
|
+
fields.push({ field: choice.family, message: "Save the provider key before rebuilding." });
|
|
69
|
+
if (choice.voice !== undefined &&
|
|
70
|
+
!listVoices(deps.db).some((row) => row.provider === choice.provider && row.voiceId === choice.voice))
|
|
71
|
+
fields.push({ field: "audio.voice", message: "Choose a saved voice before rebuilding." });
|
|
72
|
+
}
|
|
73
|
+
for (const recipe of snapshot.recipes) {
|
|
74
|
+
if (snapshot.dispositions.find((row) => row.key === recipe.key)?.disposition === "reuse" ||
|
|
75
|
+
!requiresNewSubmission(deps, view.revision.id, recipe.key, recipe.fingerprint))
|
|
76
|
+
continue;
|
|
77
|
+
const input = recipe.input;
|
|
78
|
+
if (input.kind === "tts") {
|
|
79
|
+
const model = catalogue.tts.find((row) => row.provider === input.provider && row.id === input.model);
|
|
80
|
+
const maximum = model?.tts.maxCharacters;
|
|
81
|
+
if (maximum !== undefined && input.text.length > maximum)
|
|
82
|
+
fields.push({
|
|
83
|
+
field: `work.${recipe.key}.text`,
|
|
84
|
+
message: "This saved physical request exceeds the current model limit. Regenerate this narration group to plan new parts.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else if (input.kind === "llm") {
|
|
88
|
+
const model = catalogue.llm.find((row) => row.provider === input.provider && row.id === input.model);
|
|
89
|
+
if (input.thinking !== null && model?.llm.thinking?.[input.thinking] === undefined)
|
|
90
|
+
fields.push({
|
|
91
|
+
field: `work.${recipe.key}.thinking`,
|
|
92
|
+
message: "The selected thinking setting is no longer supported.",
|
|
93
|
+
});
|
|
94
|
+
if (input.webSearch && model?.llm.webSearch !== true)
|
|
95
|
+
fields.push({
|
|
96
|
+
field: `work.${recipe.key}.webSearch`,
|
|
97
|
+
message: "This model no longer supports the required web search.",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else if (input.kind === "image") {
|
|
101
|
+
const model = catalogue.image.find((row) => row.provider === input.provider && row.id === input.model);
|
|
102
|
+
if (model !== undefined && !model.image.aspectRatios.includes(input.aspect))
|
|
103
|
+
fields.push({
|
|
104
|
+
field: `work.${recipe.key}.aspect`,
|
|
105
|
+
message: "The model does not support this output shape.",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return fields;
|
|
110
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { withProjectControl } from "../control/lock.js";
|
|
2
|
+
import { requestHash } from "../revisions/mutation-request.js";
|
|
3
|
+
import { currentRevisionId } from "../revisions/repo.js";
|
|
4
|
+
import { getRevisionView } from "../revisions/view.js";
|
|
5
|
+
import { previewRebuildSchema, startRebuildSchema, } from "./model.js";
|
|
6
|
+
import { executionSnapshotSchema, planPreview, reviewStillCovers } from "./preview-plan.js";
|
|
7
|
+
import { admissionReceipt, admitPreview, previewById, storePreview } from "./repo.js";
|
|
8
|
+
import { checkReadiness, localReadiness } from "./service-readiness.js";
|
|
9
|
+
export function rebuildConfirmations(required, confirmed, unknownCosts, acknowledged) {
|
|
10
|
+
const given = new Set(confirmed);
|
|
11
|
+
if (given.size !== confirmed.length ||
|
|
12
|
+
required.length !== given.size ||
|
|
13
|
+
required.some((key) => !given.has(key)))
|
|
14
|
+
return "review-required";
|
|
15
|
+
if (unknownCosts > 0 && !acknowledged)
|
|
16
|
+
return "cost-ack-required";
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
export async function previewRebuild(deps, input) {
|
|
20
|
+
const parsed = previewRebuildSchema.safeParse({
|
|
21
|
+
baseRevisionId: input.baseRevisionId,
|
|
22
|
+
request: input.request,
|
|
23
|
+
});
|
|
24
|
+
if (!parsed.success)
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
reason: "invalid-selection",
|
|
28
|
+
fields: parsed.error.issues.map((issue) => ({
|
|
29
|
+
field: issue.path.join("."),
|
|
30
|
+
message: issue.message,
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
33
|
+
const view = getRevisionView(deps, input.projectId, input.baseRevisionId);
|
|
34
|
+
if (view === undefined)
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: currentRevisionId(deps.db, input.projectId) === undefined ? "no-project" : "conflict",
|
|
38
|
+
};
|
|
39
|
+
if (!view.current)
|
|
40
|
+
return { ok: false, reason: "conflict" };
|
|
41
|
+
const plan = planPreview(deps, view, deps.catalogue.read(), input.request, deps.ids.next());
|
|
42
|
+
if (!plan.ok)
|
|
43
|
+
return plan;
|
|
44
|
+
storePreview(deps, plan.value.preview, plan.value.execution);
|
|
45
|
+
return { ok: true, value: plan.value.preview };
|
|
46
|
+
}
|
|
47
|
+
export async function startRebuild(deps, input) {
|
|
48
|
+
const { projectId, ...body } = input;
|
|
49
|
+
const parsed = startRebuildSchema.safeParse(body);
|
|
50
|
+
if (!parsed.success)
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: "invalid-selection",
|
|
54
|
+
fields: parsed.error.issues.map((issue) => ({
|
|
55
|
+
field: issue.path.join("."),
|
|
56
|
+
message: issue.message,
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
const hash = requestHash("rebuild", input.baseRevisionId, parsed.data);
|
|
60
|
+
const replay = admissionReceipt(deps.db, projectId, input.idempotencyKey, hash);
|
|
61
|
+
if (replay !== undefined) {
|
|
62
|
+
if (replay.ok)
|
|
63
|
+
wake(deps, projectId, replay.value);
|
|
64
|
+
return replay;
|
|
65
|
+
}
|
|
66
|
+
const preview = previewById(deps.db, projectId, input.previewId);
|
|
67
|
+
const view = getRevisionView(deps, projectId, input.baseRevisionId);
|
|
68
|
+
if (view === undefined)
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
reason: currentRevisionId(deps.db, projectId) === undefined ? "no-project" : "conflict",
|
|
72
|
+
};
|
|
73
|
+
if (!view.current)
|
|
74
|
+
return { ok: false, reason: "conflict" };
|
|
75
|
+
if (preview === undefined || preview.baseRevisionId !== input.baseRevisionId)
|
|
76
|
+
return { ok: false, reason: "stale-preview" };
|
|
77
|
+
const confirmation = rebuildConfirmations(preview.providedReuseRequired, input.confirmedProvidedWorkKeys, preview.costs.unknown, input.acknowledgeUnknownCosts);
|
|
78
|
+
if (confirmation !== undefined)
|
|
79
|
+
return { ok: false, reason: confirmation };
|
|
80
|
+
const row = deps.db
|
|
81
|
+
.prepare("SELECT execution_json FROM rebuild_previews WHERE id=? AND project_id=?")
|
|
82
|
+
.get(preview.id, projectId);
|
|
83
|
+
if (typeof row?.execution_json !== "string")
|
|
84
|
+
return { ok: false, reason: "stale-preview" };
|
|
85
|
+
const snapshot = executionSnapshotSchema.parse(JSON.parse(row.execution_json));
|
|
86
|
+
let ready;
|
|
87
|
+
try {
|
|
88
|
+
ready = await checkReadiness(deps, snapshot, view);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
deps.log.write("warn", "project.rebuild", {
|
|
92
|
+
projectId,
|
|
93
|
+
detail: "Provider readiness could not be loaded.",
|
|
94
|
+
});
|
|
95
|
+
return { ok: false, reason: "readiness" };
|
|
96
|
+
}
|
|
97
|
+
if (ready.fields.length > 0)
|
|
98
|
+
return { ok: false, reason: "readiness", fields: ready.fields };
|
|
99
|
+
return withProjectControl(deps.db, projectId, () => {
|
|
100
|
+
const again = admissionReceipt(deps.db, projectId, input.idempotencyKey, hash);
|
|
101
|
+
if (again !== undefined) {
|
|
102
|
+
if (again.ok)
|
|
103
|
+
wake(deps, projectId, again.value);
|
|
104
|
+
return again;
|
|
105
|
+
}
|
|
106
|
+
if (currentRevisionId(deps.db, projectId) !== input.baseRevisionId)
|
|
107
|
+
return { ok: false, reason: "conflict" };
|
|
108
|
+
const current = getRevisionView(deps, projectId, input.baseRevisionId);
|
|
109
|
+
if (current === undefined)
|
|
110
|
+
return { ok: false, reason: "no-project" };
|
|
111
|
+
const catalogue = deps.catalogue.read();
|
|
112
|
+
const fresh = planPreview(deps, current, catalogue, preview.selection, preview.id);
|
|
113
|
+
if (!fresh.ok || !reviewStillCovers(preview, snapshot, fresh.value))
|
|
114
|
+
return { ok: false, reason: "stale-preview" };
|
|
115
|
+
const fields = localReadiness(deps, snapshot, current, ready.providers, catalogue);
|
|
116
|
+
if (fields.length > 0)
|
|
117
|
+
return { ok: false, reason: "readiness", fields };
|
|
118
|
+
const admitted = admitPreview(deps, {
|
|
119
|
+
preview,
|
|
120
|
+
idempotencyKey: input.idempotencyKey,
|
|
121
|
+
requestHash: hash,
|
|
122
|
+
});
|
|
123
|
+
if (admitted.ok) {
|
|
124
|
+
deps.emit(projectId, { type: "project.updated", projectId });
|
|
125
|
+
wake(deps, projectId, admitted.value);
|
|
126
|
+
}
|
|
127
|
+
return admitted;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function wake(deps, projectId, admission) {
|
|
131
|
+
const owned = deps.db
|
|
132
|
+
.prepare("SELECT 1 FROM revision_work WHERE project_id=? AND admission_id=? AND state!='done' AND dispatch_state='allowed' LIMIT 1")
|
|
133
|
+
.get(projectId, admission.admissionId) !== undefined;
|
|
134
|
+
const unfinished = owned ||
|
|
135
|
+
admission.workIds.some((id) => deps.db
|
|
136
|
+
.prepare("SELECT 1 FROM revision_work WHERE id=? AND project_id=? AND state!='done' AND dispatch_state='allowed'")
|
|
137
|
+
.get(id, projectId) !== undefined);
|
|
138
|
+
if (!unfinished && admission.replayed)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
deps.runner.tick(projectId);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
deps.log.write("warn", "project.rebuild", {
|
|
145
|
+
projectId,
|
|
146
|
+
detail: "Rebuild was admitted but the runner could not be notified.",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { currentRevisionId, revisionById } from "../revisions/repo.js";
|
|
4
|
+
import { insertWorkPiece } from "./work-records.js";
|
|
5
|
+
const reservationSchema = z.object({
|
|
6
|
+
work_key: z.string(),
|
|
7
|
+
work_id: z.string(),
|
|
8
|
+
piece_id: z.string().nullable(),
|
|
9
|
+
fingerprint: z.string(),
|
|
10
|
+
logical_key: z.string().nullable(),
|
|
11
|
+
desired_fingerprint: z.string().nullable(),
|
|
12
|
+
});
|
|
13
|
+
export function transitionRevisionWork(deps, input) {
|
|
14
|
+
const { db } = deps;
|
|
15
|
+
transact(db, () => {
|
|
16
|
+
const current = currentRevisionId(db, input.projectId);
|
|
17
|
+
if (current !== input.baseRevisionId && current !== input.revisionId)
|
|
18
|
+
throw new Error("Work transition requires the current head.");
|
|
19
|
+
const base = revisionById(db, input.projectId, input.baseRevisionId);
|
|
20
|
+
const next = revisionById(db, input.projectId, input.revisionId);
|
|
21
|
+
if (base === undefined || next === undefined)
|
|
22
|
+
throw new Error("Work transition requires owned revisions.");
|
|
23
|
+
if (JSON.stringify(Object.entries(input.fingerprints).sort()) !==
|
|
24
|
+
JSON.stringify(Object.entries(next.fingerprints).sort()))
|
|
25
|
+
throw new Error("Desired work differs from revision fingerprints.");
|
|
26
|
+
const reserved = db
|
|
27
|
+
.prepare(`SELECT r.* FROM revision_work_reservations r JOIN revision_work w ON w.id=r.work_id WHERE r.project_id=? AND r.revision_id=? AND w.state IN ('pending','running','done')`)
|
|
28
|
+
.all(input.projectId, input.baseRevisionId)
|
|
29
|
+
.map((row) => reservationSchema.parse(row));
|
|
30
|
+
const carried = new Set();
|
|
31
|
+
const baseFingerprints = input.baseFingerprints ?? base.fingerprints;
|
|
32
|
+
for (const row of reserved) {
|
|
33
|
+
const logicalKey = input.logicalKeys?.[row.work_key] ?? row.logical_key ?? row.work_key;
|
|
34
|
+
const before = baseFingerprints[logicalKey];
|
|
35
|
+
const after = input.fingerprints[logicalKey];
|
|
36
|
+
if (before !== undefined &&
|
|
37
|
+
before === after &&
|
|
38
|
+
(input.baseFingerprints !== undefined ||
|
|
39
|
+
before === (row.desired_fingerprint ?? row.fingerprint))) {
|
|
40
|
+
db.prepare(`INSERT INTO revision_work_reservations(project_id,revision_id,work_key,work_id,piece_id,fingerprint,logical_key,desired_fingerprint) VALUES (?,?,?,?,?,?,?,?)`).run(input.projectId, input.revisionId, row.work_key, row.work_id, row.piece_id, row.fingerprint, logicalKey, after);
|
|
41
|
+
carried.add(logicalKey);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const live = db.prepare("SELECT id FROM revision_work WHERE project_id=?").all(input.projectId);
|
|
45
|
+
for (const row of live) {
|
|
46
|
+
const id = z.string().parse(row.id);
|
|
47
|
+
db.prepare(`UPDATE revision_work_pieces SET dispatch_state=CASE WHEN submitted_at IS NOT NULL THEN 'draining' ELSE 'held' END WHERE work_id=? AND state!='done' AND id NOT IN (SELECT piece_id FROM revision_work_reservations WHERE revision_id=? AND piece_id IS NOT NULL)`).run(id, input.revisionId);
|
|
48
|
+
const keeps = db
|
|
49
|
+
.prepare("SELECT 1 FROM revision_work_reservations WHERE revision_id=? AND work_id=?")
|
|
50
|
+
.get(input.revisionId, id) !== undefined;
|
|
51
|
+
if (!keeps)
|
|
52
|
+
db.prepare(`UPDATE revision_work SET dispatch_state=CASE WHEN EXISTS(SELECT 1 FROM attempts WHERE work_id=?) OR EXISTS(SELECT 1 FROM revision_work_pieces WHERE work_id=? AND submitted_at IS NOT NULL) THEN 'draining' ELSE 'held' END WHERE id=? AND state!='done'`).run(id, id, id);
|
|
53
|
+
}
|
|
54
|
+
db.prepare(`DELETE FROM revision_work_reservations WHERE project_id=? AND revision_id=? AND work_key NOT IN (SELECT work_key FROM revision_work_reservations WHERE revision_id=?)`).run(input.projectId, input.baseRevisionId, input.revisionId);
|
|
55
|
+
for (const [key, fp] of Object.entries(input.fingerprints)) {
|
|
56
|
+
if (carried.has(key))
|
|
57
|
+
continue;
|
|
58
|
+
const exact = input.recipes?.find((row) => row.key === key);
|
|
59
|
+
if (exact !== undefined && exact.fingerprint !== fp)
|
|
60
|
+
throw new Error("Recipe differs from desired fingerprint.");
|
|
61
|
+
const kind = exact?.stage ?? stageForKey(key);
|
|
62
|
+
const stage = db
|
|
63
|
+
.prepare("SELECT id FROM stages WHERE project_id=? AND kind=?")
|
|
64
|
+
.get(input.projectId, kind);
|
|
65
|
+
if (stage === undefined)
|
|
66
|
+
throw new Error("Desired work has no project stage.");
|
|
67
|
+
const workId = deps.ids.next();
|
|
68
|
+
const pieceId = deps.ids.next();
|
|
69
|
+
db.prepare(`INSERT INTO revision_work(id,project_id,revision_id,stage_id,kind,fingerprint,state,dispatch_state,created_at) VALUES (?,?,?,?,?,?,'pending','held',?)`).run(workId, input.projectId, input.revisionId, z.string().parse(stage.id), kind, fp, deps.clock.now().toISOString());
|
|
70
|
+
insertWorkPiece(db, {
|
|
71
|
+
id: pieceId,
|
|
72
|
+
workId,
|
|
73
|
+
key,
|
|
74
|
+
requestFingerprint: exact?.requestFingerprint ?? fp,
|
|
75
|
+
fingerprint: fp,
|
|
76
|
+
...(exact === undefined ? {} : { logicalFingerprint: exact.logicalFingerprint }),
|
|
77
|
+
input: exact?.input ?? {
|
|
78
|
+
kind: "deferred",
|
|
79
|
+
version: 1,
|
|
80
|
+
operation: "resolve-revision-recipe",
|
|
81
|
+
template: { key, revisionId: input.revisionId, fingerprint: fp },
|
|
82
|
+
},
|
|
83
|
+
continuation: null,
|
|
84
|
+
generationToken: null,
|
|
85
|
+
state: "held",
|
|
86
|
+
dispatchState: "held",
|
|
87
|
+
submittedAt: null,
|
|
88
|
+
});
|
|
89
|
+
db.prepare(`INSERT INTO revision_work_reservations(project_id,revision_id,work_key,work_id,piece_id,fingerprint) VALUES (?,?,?,?,?,?)`).run(input.projectId, input.revisionId, key, workId, pieceId, fp);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function stageForKey(key) {
|
|
94
|
+
const prefix = key.split(":")[0];
|
|
95
|
+
if (prefix === "research")
|
|
96
|
+
return "research";
|
|
97
|
+
if (prefix === "article" || prefix === "entry")
|
|
98
|
+
return "article";
|
|
99
|
+
if (prefix === "audio")
|
|
100
|
+
return "audio";
|
|
101
|
+
if (prefix === "image")
|
|
102
|
+
return "images";
|
|
103
|
+
if (prefix === "thumbnail")
|
|
104
|
+
return "thumbnail";
|
|
105
|
+
if (prefix === "export" || prefix === "subtitles" || prefix === "video")
|
|
106
|
+
return "video";
|
|
107
|
+
throw new Error("Unknown revision work key.");
|
|
108
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { recipeInputSchema } from "./recipe-input-schema.js";
|
|
3
|
+
const pieceRow = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
work_id: z.string(),
|
|
6
|
+
work_key: z.string(),
|
|
7
|
+
request_fingerprint: z.string(),
|
|
8
|
+
fingerprint: z.string(),
|
|
9
|
+
input_json: z.string(),
|
|
10
|
+
logical_fingerprint: z.string().nullable(),
|
|
11
|
+
continuation: z.string().nullable(),
|
|
12
|
+
generation_token: z.string().nullable(),
|
|
13
|
+
state: z.enum(["pending", "running", "done", "failed", "held"]),
|
|
14
|
+
dispatch_state: z.enum(["held", "allowed", "draining"]),
|
|
15
|
+
submitted_at: z.string().nullable(),
|
|
16
|
+
});
|
|
17
|
+
export function workPieces(db, workId) {
|
|
18
|
+
return db
|
|
19
|
+
.prepare("SELECT * FROM revision_work_pieces WHERE work_id=? ORDER BY rowid")
|
|
20
|
+
.all(workId)
|
|
21
|
+
.map((value) => {
|
|
22
|
+
const row = pieceRow.parse(value);
|
|
23
|
+
return {
|
|
24
|
+
id: row.id,
|
|
25
|
+
workId: row.work_id,
|
|
26
|
+
key: row.work_key,
|
|
27
|
+
requestFingerprint: row.request_fingerprint,
|
|
28
|
+
fingerprint: row.fingerprint,
|
|
29
|
+
...(row.logical_fingerprint === null
|
|
30
|
+
? {}
|
|
31
|
+
: { logicalFingerprint: row.logical_fingerprint }),
|
|
32
|
+
input: recipeInputSchema.parse(JSON.parse(row.input_json)),
|
|
33
|
+
continuation: row.continuation,
|
|
34
|
+
generationToken: row.generation_token,
|
|
35
|
+
state: row.state,
|
|
36
|
+
dispatchState: row.dispatch_state,
|
|
37
|
+
submittedAt: row.submitted_at,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function insertWorkPiece(db, piece) {
|
|
42
|
+
const input = recipeInputSchema.parse(piece.input);
|
|
43
|
+
db.prepare(`INSERT INTO revision_work_pieces(id,work_id,work_key,request_fingerprint,fingerprint,input_json,continuation,generation_token,state,dispatch_state,submitted_at,logical_fingerprint) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`).run(piece.id, piece.workId, piece.key, piece.requestFingerprint, piece.fingerprint, JSON.stringify(input), piece.continuation, piece.generationToken, piece.state, piece.dispatchState, piece.submittedAt, piece.logicalFingerprint ?? null);
|
|
44
|
+
}
|
|
@@ -4,6 +4,7 @@ import { transact } from "../../kernel/db/tx.js";
|
|
|
4
4
|
import { allPiecesOf, deletePiece, deletePieces, setPiece, } from "../../kernel/runner/piece-repo.js";
|
|
5
5
|
import { projectById, resetStage, stagesOf } from "../admission/repo.js";
|
|
6
6
|
import { storeArticleText } from "../article/store.js";
|
|
7
|
+
import { currentRevisionId } from "../revisions/repo.js";
|
|
7
8
|
import { outputPath } from "../storage/layout.js";
|
|
8
9
|
import { pieceFile } from "../storage/reconcile.js";
|
|
9
10
|
import { deleteOutput, outputsOf } from "../storage/repo.js";
|
|
@@ -126,6 +127,8 @@ export function regenerateImage(deps, projectId, outputId) {
|
|
|
126
127
|
});
|
|
127
128
|
}
|
|
128
129
|
function load(deps, projectId) {
|
|
130
|
+
if (currentRevisionId(deps.db, projectId) !== undefined)
|
|
131
|
+
return { ok: false, reason: "not-rerunnable" };
|
|
129
132
|
const project = projectById(deps.db, projectId);
|
|
130
133
|
if (project === undefined) {
|
|
131
134
|
return { ok: false, reason: "no-project" };
|
|
@@ -200,6 +203,8 @@ function clearStage(deps, stage, outputs) {
|
|
|
200
203
|
// The caller includes these row changes with the config update's transaction and
|
|
201
204
|
// runs the returned cleanup only after commit. Finished narration is left intact.
|
|
202
205
|
export function clearUnfinishedAudio(deps, projectId) {
|
|
206
|
+
if (currentRevisionId(deps.db, projectId) !== undefined)
|
|
207
|
+
throw new Error("Versioned narration must be edited through revisions.");
|
|
203
208
|
const stage = stagesOf(deps.db, projectId).find((one) => one.kind === "audio");
|
|
204
209
|
if (stage === undefined ||
|
|
205
210
|
stage.state === "done" ||
|
|
@@ -28,7 +28,7 @@ export async function runResearch(deps, context, providers) {
|
|
|
28
28
|
}
|
|
29
29
|
const brief = { articlePrompt, values: project.config.values };
|
|
30
30
|
try {
|
|
31
|
-
await research(deps, context, providers, choice, brief);
|
|
31
|
+
return await research(deps, context, providers, choice, brief);
|
|
32
32
|
}
|
|
33
33
|
catch (error) {
|
|
34
34
|
// The adapter says the model cannot ground on the web and the wrapper has already
|
|
@@ -47,8 +47,13 @@ async function research(deps, context, providers, choice, brief) {
|
|
|
47
47
|
const add = (usage) => {
|
|
48
48
|
tokens = plusUsage(tokens, usage);
|
|
49
49
|
};
|
|
50
|
-
const
|
|
51
|
-
|
|
50
|
+
const planned = await plan(deps, context, providers, choice, brief, add);
|
|
51
|
+
if (!planned.ok)
|
|
52
|
+
return "held";
|
|
53
|
+
const researched = await researchChapters(deps, context, providers, choice, brief, planned.value, add);
|
|
54
|
+
if (!researched.ok)
|
|
55
|
+
return "held";
|
|
56
|
+
const findings = researched.value;
|
|
52
57
|
const answer = await providers.llm({
|
|
53
58
|
provider: choice.provider,
|
|
54
59
|
model: choice.model,
|
|
@@ -57,11 +62,18 @@ async function research(deps, context, providers, choice, brief) {
|
|
|
57
62
|
previewLabel: "Writing research notes",
|
|
58
63
|
check: (given) => sourcedAnswer("the synthesis", given.text),
|
|
59
64
|
});
|
|
60
|
-
|
|
65
|
+
if (!answer.ok)
|
|
66
|
+
return "held";
|
|
67
|
+
add(answer.value.usage);
|
|
61
68
|
const { projectId } = context.stage;
|
|
62
69
|
// The notes and every instruction sent are stored on the project. Each sub-agent's own output
|
|
63
70
|
// stays on its chapter row, which is where the resume reads it.
|
|
64
|
-
storeText(deps, {
|
|
71
|
+
storeText(deps, {
|
|
72
|
+
projectId,
|
|
73
|
+
stageKind: "research",
|
|
74
|
+
role: "notes",
|
|
75
|
+
text: answer.value.text.trim(),
|
|
76
|
+
});
|
|
65
77
|
storeText(deps, {
|
|
66
78
|
projectId,
|
|
67
79
|
stageKind: "research",
|
|
@@ -80,13 +92,14 @@ async function research(deps, context, providers, choice, brief) {
|
|
|
80
92
|
stage: "research",
|
|
81
93
|
detail: `${String(findings.length)} chapters researched`,
|
|
82
94
|
});
|
|
95
|
+
return "done";
|
|
83
96
|
}
|
|
84
97
|
// The chapter list, planned once and kept. A retry after a failure finds the rows the
|
|
85
98
|
// first run wrote and does not ask again.
|
|
86
99
|
async function plan(deps, context, providers, choice, brief, add) {
|
|
87
100
|
const existing = piecesOf(deps.db, context.stage.id, "chapter");
|
|
88
101
|
if (existing.length > 0) {
|
|
89
|
-
return existing;
|
|
102
|
+
return { ok: true, value: existing };
|
|
90
103
|
}
|
|
91
104
|
const answer = await providers.llm({
|
|
92
105
|
provider: choice.provider,
|
|
@@ -97,10 +110,12 @@ async function plan(deps, context, providers, choice, brief, add) {
|
|
|
97
110
|
// An empty answer, or one with no chapter in it, is a failed attempt.
|
|
98
111
|
check: (given) => chaptersFrom(given.text).length === 0 ? "the planner named no chapters" : undefined,
|
|
99
112
|
});
|
|
100
|
-
|
|
113
|
+
if (!answer.ok)
|
|
114
|
+
return answer;
|
|
115
|
+
add(answer.value.usage);
|
|
101
116
|
// No cap on the count. The list is the prompt's own section guide as often as
|
|
102
117
|
// not, and capping it would silently drop a section the article asks for.
|
|
103
|
-
const planned = chaptersFrom(answer.text).map((title, index) => ({
|
|
118
|
+
const planned = chaptersFrom(answer.value.text).map((title, index) => ({
|
|
104
119
|
id: deps.ids.next(),
|
|
105
120
|
stageId: context.stage.id,
|
|
106
121
|
kind: "chapter",
|
|
@@ -113,7 +128,7 @@ async function plan(deps, context, providers, choice, brief, add) {
|
|
|
113
128
|
insertPiece(deps.db, piece);
|
|
114
129
|
}
|
|
115
130
|
});
|
|
116
|
-
return planned;
|
|
131
|
+
return { ok: true, value: planned };
|
|
117
132
|
}
|
|
118
133
|
// One sub-agent per chapter, all in parallel. A chapter a previous run finished is not
|
|
119
134
|
// researched again, and one that fails takes down the whole stage while its siblings finish and
|
|
@@ -141,8 +156,12 @@ async function researchChapters(deps, context, providers, choice, brief, chapter
|
|
|
141
156
|
webSearch: true,
|
|
142
157
|
check: (given) => sourcedAnswer(`the researcher on "${kept.title}"`, given.text),
|
|
143
158
|
});
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
if (!answer.ok) {
|
|
160
|
+
setPiece(deps.db, piece.id, "pending", piece.payload);
|
|
161
|
+
return answer;
|
|
162
|
+
}
|
|
163
|
+
add(answer.value.usage);
|
|
164
|
+
const notes = answer.value.text.trim();
|
|
146
165
|
setPiece(deps.db, piece.id, "done", JSON.stringify({ title: kept.title, notes }));
|
|
147
166
|
done += 1;
|
|
148
167
|
report(deps, context, done, total);
|
|
@@ -158,11 +177,13 @@ async function researchChapters(deps, context, providers, choice, brief, chapter
|
|
|
158
177
|
const findings = [];
|
|
159
178
|
for (const outcome of outcomes) {
|
|
160
179
|
if (!outcome.ok) {
|
|
180
|
+
if ("reason" in outcome)
|
|
181
|
+
return outcome;
|
|
161
182
|
throw outcome.error;
|
|
162
183
|
}
|
|
163
184
|
findings.push(outcome.finding);
|
|
164
185
|
}
|
|
165
|
-
return findings;
|
|
186
|
+
return { ok: true, value: findings };
|
|
166
187
|
}
|
|
167
188
|
// K of N chapters researched, written as well as emitted, so a page opened mid-stage reads the
|
|
168
189
|
// count off the row rather than waiting for the next chapter.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { outputPath } from "../storage/layout.js";
|
|
3
|
+
import { pieceFile } from "../storage/reconcile.js";
|
|
4
|
+
import { insertAsset } from "./repo.js";
|
|
5
|
+
export function adoptAssets(deps, projectId, outputs, pieces) {
|
|
6
|
+
const assets = new Map();
|
|
7
|
+
const capture = (path, bytes) => {
|
|
8
|
+
if (assets.has(path))
|
|
9
|
+
return;
|
|
10
|
+
outputPath(deps.paths, projectId, path);
|
|
11
|
+
const asset = {
|
|
12
|
+
id: deps.ids.next(),
|
|
13
|
+
projectId,
|
|
14
|
+
path,
|
|
15
|
+
bytes,
|
|
16
|
+
createdAt: deps.clock.now().toISOString(),
|
|
17
|
+
};
|
|
18
|
+
insertAsset(deps.db, asset);
|
|
19
|
+
assets.set(path, asset);
|
|
20
|
+
};
|
|
21
|
+
for (const output of outputs)
|
|
22
|
+
capture(output.path, output.bytes);
|
|
23
|
+
for (const piece of pieces) {
|
|
24
|
+
const path = pieceFile(piece.payload);
|
|
25
|
+
if (path === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
const stats = statSync(outputPath(deps.paths, projectId, path), { throwIfNoEntry: false });
|
|
28
|
+
capture(path, stats?.isFile() === true ? stats.size : null);
|
|
29
|
+
}
|
|
30
|
+
return assets;
|
|
31
|
+
}
|