@lingjingai/scriptctl 0.49.11 → 0.49.13
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 +8 -5
- package/changes/0.49.12.md +13 -0
- package/changes/unreleased.md +12 -3
- package/dist/cli.js +34 -1
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +1 -4
- package/dist/common.js +1 -12
- package/dist/common.js.map +1 -1
- package/dist/domain/authoring/apply.d.ts +33 -0
- package/dist/domain/authoring/apply.js +717 -0
- package/dist/domain/authoring/apply.js.map +1 -0
- package/dist/domain/authoring/export.d.ts +7 -0
- package/dist/domain/authoring/export.js +85 -0
- package/dist/domain/authoring/export.js.map +1 -0
- package/dist/domain/authoring/parser.d.ts +3 -0
- package/dist/domain/authoring/parser.js +915 -0
- package/dist/domain/authoring/parser.js.map +1 -0
- package/dist/domain/authoring/serializer.d.ts +2 -0
- package/dist/domain/authoring/serializer.js +244 -0
- package/dist/domain/authoring/serializer.js.map +1 -0
- package/dist/domain/authoring/types.d.ts +87 -0
- package/dist/domain/authoring/types.js +2 -0
- package/dist/domain/authoring/types.js.map +1 -0
- package/dist/domain/ingest/apply-plan.d.ts +1 -1
- package/dist/domain/ingest/records.d.ts +3 -2
- package/dist/domain/ingest/records.js +2 -1
- package/dist/domain/ingest/records.js.map +1 -1
- package/dist/domain/ingest/serial-context.d.ts +1 -1
- package/dist/domain/ingest/validation.js +20 -2
- package/dist/domain/ingest/validation.js.map +1 -1
- package/dist/domain/ingest/video-apply.js +7 -9
- package/dist/domain/ingest/video-apply.js.map +1 -1
- package/dist/domain/script/address.js.map +1 -1
- package/dist/domain/script/lookups.d.ts +0 -1
- package/dist/domain/script/lookups.js +0 -9
- package/dist/domain/script/lookups.js.map +1 -1
- package/dist/domain/script/parse.d.ts +3 -0
- package/dist/domain/script/parse.js +165 -0
- package/dist/domain/script/parse.js.map +1 -0
- package/dist/domain/script/patch/apply.d.ts +1 -3
- package/dist/domain/script/patch/apply.js +5 -12
- package/dist/domain/script/patch/apply.js.map +1 -1
- package/dist/domain/script/patch/inline-speaker.js +2 -2
- package/dist/domain/script/patch/inline-speaker.js.map +1 -1
- package/dist/domain/script/patch/ops-action.js +3 -3
- package/dist/domain/script/patch/ops-action.js.map +1 -1
- package/dist/domain/script/patch/ops-asset.js +4 -5
- package/dist/domain/script/patch/ops-asset.js.map +1 -1
- package/dist/domain/script/patch/ops-meta.js.map +1 -1
- package/dist/domain/script/selector.js +1 -3
- package/dist/domain/script/selector.js.map +1 -1
- package/dist/domain/script/types.d.ts +123 -0
- package/dist/domain/script/types.js +9 -0
- package/dist/domain/script/types.js.map +1 -0
- package/dist/help-text.js +105 -0
- package/dist/help-text.js.map +1 -1
- package/dist/infra/converters.d.ts +0 -1
- package/dist/infra/converters.js +0 -29
- package/dist/infra/converters.js.map +1 -1
- package/dist/infra/script-working-copy.d.ts +1 -0
- package/dist/infra/script-working-copy.js +4 -1
- package/dist/infra/script-working-copy.js.map +1 -1
- package/dist/llm/tasks/ingest/asset-curate.d.ts +1 -1
- package/dist/llm/tasks/ingest/video-serial.js +2 -2
- package/dist/llm/tasks/ingest/video-serial.js.map +1 -1
- package/dist/llm/tasks/schemas.js +2 -2
- package/dist/llm/tasks/schemas.js.map +1 -1
- package/dist/usecases/doctor.js +0 -1
- package/dist/usecases/doctor.js.map +1 -1
- package/dist/usecases/script/do.js +2 -2
- package/dist/usecases/script/do.js.map +1 -1
- package/dist/usecases/script/export-authoring.d.ts +4 -0
- package/dist/usecases/script/export-authoring.js +305 -0
- package/dist/usecases/script/export-authoring.js.map +1 -0
- package/dist/usecases/script/ids.d.ts +3 -0
- package/dist/usecases/script/ids.js +101 -0
- package/dist/usecases/script/ids.js.map +1 -0
- package/dist/usecases/script/lib.js +1 -6
- package/dist/usecases/script/lib.js.map +1 -1
- package/dist/usecases/script/write-authoring.d.ts +3 -0
- package/dist/usecases/script/write-authoring.js +186 -0
- package/dist/usecases/script/write-authoring.js.map +1 -0
- package/package.json +2 -3
- package/skills/scriptctl/SKILL.md +40 -6
- package/skills/scriptctl/references/bulk-authoring.md +165 -0
- package/dist/domain/ingest/script-v3.d.ts +0 -94
- package/dist/domain/ingest/script-v3.js +0 -7
- package/dist/domain/ingest/script-v3.js.map +0 -1
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import { CliError, EXIT_INPUT, EXIT_USAGE } from "../../common.js";
|
|
2
|
+
import { validateScriptV3 } from "../ingest/validation.js";
|
|
3
|
+
function fail(code, message, received, nextSteps, exitCode = EXIT_INPUT) {
|
|
4
|
+
throw new CliError("AUTHORING BLOCKED", message, {
|
|
5
|
+
exitCode,
|
|
6
|
+
errorCode: code,
|
|
7
|
+
received,
|
|
8
|
+
nextSteps,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function sourceLabel(source) {
|
|
12
|
+
return `line ${source.line}, column ${source.column}`;
|
|
13
|
+
}
|
|
14
|
+
function canonicalize(value) {
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value.map(canonicalize);
|
|
17
|
+
if (value !== null && typeof value === "object") {
|
|
18
|
+
const result = {};
|
|
19
|
+
for (const [key, item] of Object.entries(value).sort(([left], [right]) => left.localeCompare(right))) {
|
|
20
|
+
result[key] = canonicalize(item);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function semanticEqual(left, right) {
|
|
27
|
+
return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
|
|
28
|
+
}
|
|
29
|
+
function assetId(_kind, value) {
|
|
30
|
+
if ("actor_id" in value)
|
|
31
|
+
return value.actor_id;
|
|
32
|
+
if ("location_id" in value)
|
|
33
|
+
return value.location_id;
|
|
34
|
+
return value.prop_id;
|
|
35
|
+
}
|
|
36
|
+
function assetName(_kind, value) {
|
|
37
|
+
if ("actor_name" in value)
|
|
38
|
+
return value.actor_name;
|
|
39
|
+
if ("location_name" in value)
|
|
40
|
+
return value.location_name;
|
|
41
|
+
return value.prop_name;
|
|
42
|
+
}
|
|
43
|
+
function assetsFor(script, kind) {
|
|
44
|
+
if (kind === "actor")
|
|
45
|
+
return script.actors;
|
|
46
|
+
if (kind === "location")
|
|
47
|
+
return script.locations;
|
|
48
|
+
return script.props;
|
|
49
|
+
}
|
|
50
|
+
function pushAsset(script, asset) {
|
|
51
|
+
if ("actor_id" in asset.value)
|
|
52
|
+
script.actors.push(asset.value);
|
|
53
|
+
else if ("location_id" in asset.value)
|
|
54
|
+
script.locations.push(asset.value);
|
|
55
|
+
else
|
|
56
|
+
script.props.push(asset.value);
|
|
57
|
+
}
|
|
58
|
+
function findAsset(script, kind, id) {
|
|
59
|
+
return assetsFor(script, kind).find((asset) => assetId(kind, asset) === id);
|
|
60
|
+
}
|
|
61
|
+
function findAssetByAnyId(script, id) {
|
|
62
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
63
|
+
const value = findAsset(script, kind, id);
|
|
64
|
+
if (value !== undefined)
|
|
65
|
+
return { kind, value };
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
function prepareDeclarations(declarations) {
|
|
70
|
+
const assets = [];
|
|
71
|
+
const states = [];
|
|
72
|
+
const groupOrder = { actor: 0, location: 1, prop: 2 };
|
|
73
|
+
let lastGroup = -1;
|
|
74
|
+
let currentAsset;
|
|
75
|
+
let standaloneStarted = false;
|
|
76
|
+
const seenOwners = new Set();
|
|
77
|
+
for (const declaration of declarations) {
|
|
78
|
+
if (declaration.declarationKind !== "state") {
|
|
79
|
+
if (standaloneStarted) {
|
|
80
|
+
fail("AUTHORING_DECLARATION_ORDER", "Asset declarations must precede standalone state declarations.", [sourceLabel(declaration.source)], ["Move standalone states to the end of <create>."]);
|
|
81
|
+
}
|
|
82
|
+
const kind = declaration.declarationKind;
|
|
83
|
+
if (groupOrder[kind] < lastGroup) {
|
|
84
|
+
fail("AUTHORING_DECLARATION_ORDER", "Asset declaration groups are out of canonical order.", [sourceLabel(declaration.source), kind], ["Order asset declarations as actor, location, then prop."]);
|
|
85
|
+
}
|
|
86
|
+
lastGroup = groupOrder[kind];
|
|
87
|
+
const value = structuredClone(declaration.value);
|
|
88
|
+
value.states = [];
|
|
89
|
+
const prepared = { kind, value, source: declaration.source };
|
|
90
|
+
const ownerKey = `${kind}:${assetId(kind, value)}`;
|
|
91
|
+
if (seenOwners.has(ownerKey)) {
|
|
92
|
+
fail("AUTHORING_DECLARATION_DUPLICATE", "Asset declaration is duplicated.", [sourceLabel(declaration.source), ownerKey], ["Keep one declaration per asset."]);
|
|
93
|
+
}
|
|
94
|
+
seenOwners.add(ownerKey);
|
|
95
|
+
assets.push(prepared);
|
|
96
|
+
currentAsset = prepared;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const ownerKey = `${declaration.ownerKind}:${declaration.ownerId}`;
|
|
100
|
+
if (currentAsset !== undefined
|
|
101
|
+
&& currentAsset.kind === declaration.ownerKind
|
|
102
|
+
&& assetId(currentAsset.kind, currentAsset.value) === declaration.ownerId) {
|
|
103
|
+
currentAsset.value.states.push(structuredClone(declaration.value));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
standaloneStarted = true;
|
|
107
|
+
currentAsset = undefined;
|
|
108
|
+
states.push({
|
|
109
|
+
kind: declaration.ownerKind,
|
|
110
|
+
ownerId: declaration.ownerId,
|
|
111
|
+
value: structuredClone(declaration.value),
|
|
112
|
+
source: declaration.source,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
for (const asset of assets) {
|
|
116
|
+
const id = assetId(asset.kind, asset.value);
|
|
117
|
+
if (asset.value.states.length === 0 || asset.value.states[0].state_id !== "default") {
|
|
118
|
+
fail("AUTHORING_DEFAULT_STATE_REQUIRED", "Every declared asset must be immediately followed by its states, beginning with default.", [sourceLabel(asset.source), `${asset.kind}:${id}`], [`Add [state|${asset.kind}:${id}|default|...] directly after the asset declaration.`]);
|
|
119
|
+
}
|
|
120
|
+
if (asset.value.states.filter((state) => state.state_id === "default").length !== 1) {
|
|
121
|
+
fail("AUTHORING_DEFAULT_STATE_DUPLICATE", "Every declared asset must contain exactly one default state.", [sourceLabel(asset.source), `${asset.kind}:${id}`], ["Keep exactly one default state."]);
|
|
122
|
+
}
|
|
123
|
+
const aliases = asset.value.aliases ?? [];
|
|
124
|
+
if (new Set(aliases).size !== aliases.length || aliases.includes(assetName(asset.kind, asset.value))) {
|
|
125
|
+
fail("AUTHORING_ALIAS_INVALID", "Asset aliases must be unique and must differ from the canonical name.", [sourceLabel(asset.source), `${asset.kind}:${id}`], ["Remove duplicate aliases and the canonical name from aliases."]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { assets, states };
|
|
129
|
+
}
|
|
130
|
+
function emptyDeclarationPlan() {
|
|
131
|
+
return {
|
|
132
|
+
createdAssets: [],
|
|
133
|
+
existingAssets: [],
|
|
134
|
+
createdStates: [],
|
|
135
|
+
existingStates: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function applyDeclarations(script, declarations) {
|
|
139
|
+
const prepared = prepareDeclarations(declarations);
|
|
140
|
+
const plan = emptyDeclarationPlan();
|
|
141
|
+
for (const asset of prepared.assets) {
|
|
142
|
+
const id = assetId(asset.kind, asset.value);
|
|
143
|
+
const address = `${asset.kind}:${id}`;
|
|
144
|
+
const anyExisting = findAssetByAnyId(script, id);
|
|
145
|
+
if (anyExisting !== undefined) {
|
|
146
|
+
if (anyExisting.kind === asset.kind && semanticEqual(anyExisting.value, asset.value)) {
|
|
147
|
+
plan.existingAssets.push(address);
|
|
148
|
+
plan.existingStates.push(...asset.value.states.map((state) => `${address}/${state.state_id}`));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
fail("AUTHORING_ASSET_CONFLICT", "Create-only asset declaration conflicts with an existing asset.", [sourceLabel(asset.source), `${asset.kind}:${id}`], ["Use the existing identical definition or change it with an explicit mutation verb."]);
|
|
152
|
+
}
|
|
153
|
+
pushAsset(script, asset);
|
|
154
|
+
plan.createdAssets.push(address);
|
|
155
|
+
plan.createdStates.push(...asset.value.states.map((state) => `${address}/${state.state_id}`));
|
|
156
|
+
}
|
|
157
|
+
for (const state of prepared.states) {
|
|
158
|
+
const owner = findAsset(script, state.kind, state.ownerId);
|
|
159
|
+
if (owner === undefined) {
|
|
160
|
+
fail("AUTHORING_STATE_OWNER_NOT_FOUND", "Standalone state owner does not exist.", [sourceLabel(state.source), `${state.kind}:${state.ownerId}`], ["Declare the asset before its states or use an existing asset ID."]);
|
|
161
|
+
}
|
|
162
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
163
|
+
for (const asset of assetsFor(script, kind)) {
|
|
164
|
+
const existing = asset.states.find((candidate) => candidate.state_id === state.value.state_id);
|
|
165
|
+
if (existing === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
const sameOwner = kind === state.kind && assetId(kind, asset) === state.ownerId;
|
|
168
|
+
if (sameOwner && semanticEqual(existing, state.value))
|
|
169
|
+
continue;
|
|
170
|
+
if (state.value.state_id === "default" && !sameOwner)
|
|
171
|
+
continue;
|
|
172
|
+
fail("AUTHORING_STATE_CONFLICT", "Create-only state declaration conflicts with an existing state ID.", [sourceLabel(state.source), `${state.kind}:${state.ownerId}/${state.value.state_id}`], ["Use an unused globally unique state ID, or reuse the identical existing state."]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const stateAddress = `${state.kind}:${state.ownerId}/${state.value.state_id}`;
|
|
176
|
+
if (!owner.states.some((candidate) => candidate.state_id === state.value.state_id)) {
|
|
177
|
+
owner.states.push(state.value);
|
|
178
|
+
plan.createdStates.push(stateAddress);
|
|
179
|
+
}
|
|
180
|
+
else
|
|
181
|
+
plan.existingStates.push(stateAddress);
|
|
182
|
+
}
|
|
183
|
+
return plan;
|
|
184
|
+
}
|
|
185
|
+
function sceneCounts(scene) {
|
|
186
|
+
return { episodes: 0, scenes: scene === undefined ? 0 : 1, actions: scene?.actions.length ?? 0 };
|
|
187
|
+
}
|
|
188
|
+
function episodeCounts(episode) {
|
|
189
|
+
return {
|
|
190
|
+
episodes: episode === undefined ? 0 : 1,
|
|
191
|
+
scenes: episode?.scenes.length ?? 0,
|
|
192
|
+
actions: episode?.scenes.reduce((total, scene) => total + scene.actions.length, 0) ?? 0,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function episodesCounts(episodes) {
|
|
196
|
+
return episodes.reduce((counts, episode) => ({
|
|
197
|
+
episodes: counts.episodes + 1,
|
|
198
|
+
scenes: counts.scenes + episode.scenes.length,
|
|
199
|
+
actions: counts.actions + episode.scenes.reduce((total, scene) => total + scene.actions.length, 0),
|
|
200
|
+
}), { episodes: 0, scenes: 0, actions: 0 });
|
|
201
|
+
}
|
|
202
|
+
function scenesCounts(scenes) {
|
|
203
|
+
return {
|
|
204
|
+
episodes: 0,
|
|
205
|
+
scenes: scenes.length,
|
|
206
|
+
actions: scenes.reduce((total, scene) => total + scene.actions.length, 0),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function idDiff(before, after) {
|
|
210
|
+
const beforeSet = new Set(before);
|
|
211
|
+
const afterSet = new Set(after);
|
|
212
|
+
return {
|
|
213
|
+
retainedIds: after.filter((id) => beforeSet.has(id)),
|
|
214
|
+
createdIds: after.filter((id) => !beforeSet.has(id)),
|
|
215
|
+
removedIds: before.filter((id) => !afterSet.has(id)),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function assertSceneComplete(scene, source) {
|
|
219
|
+
if (scene.actions.length === 0) {
|
|
220
|
+
fail("AUTHORING_EMPTY_SCENE", "A production scene must contain at least one action.", [sourceLabel(source), scene.scene_id], ["Add at least one body record."]);
|
|
221
|
+
}
|
|
222
|
+
const unmaterialized = [];
|
|
223
|
+
if (scene.location !== null && !scene.location.state_id)
|
|
224
|
+
unmaterialized.push(`location:${scene.location.location_id}`);
|
|
225
|
+
for (const actor of scene.actors)
|
|
226
|
+
if (!actor.state_id)
|
|
227
|
+
unmaterialized.push(`actor:${actor.actor_id}`);
|
|
228
|
+
for (const prop of scene.props)
|
|
229
|
+
if (!prop.state_id)
|
|
230
|
+
unmaterialized.push(`prop:${prop.prop_id}`);
|
|
231
|
+
if (unmaterialized.length > 0) {
|
|
232
|
+
fail("AUTHORING_SCENE_REF_UNMATERIALIZED", "Every scene asset reference in an authored production block must bind a state.", [sourceLabel(source), ...unmaterialized], ["Add the concrete state ID to every location, cast, and prop record."]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function assertEpisodeComplete(episode, source) {
|
|
236
|
+
if (episode.scenes.length === 0) {
|
|
237
|
+
fail("AUTHORING_EMPTY_EPISODE", "A production episode must contain at least one scene.", [sourceLabel(source), episode.episode_id], ["Add at least one complete scene."]);
|
|
238
|
+
}
|
|
239
|
+
for (const scene of episode.scenes)
|
|
240
|
+
assertSceneComplete(scene, source);
|
|
241
|
+
}
|
|
242
|
+
function episodeIndex(script, episodeId) {
|
|
243
|
+
return script.episodes.findIndex((episode) => episode.episode_id === episodeId);
|
|
244
|
+
}
|
|
245
|
+
function sceneIndex(episode, sceneId) {
|
|
246
|
+
return episode.scenes.findIndex((scene) => scene.scene_id === sceneId);
|
|
247
|
+
}
|
|
248
|
+
function sceneCoordinate(script, parent, sceneId) {
|
|
249
|
+
const epIndex = episodeIndex(script, parent);
|
|
250
|
+
if (epIndex < 0)
|
|
251
|
+
fail("AUTHORING_EPISODE_NOT_FOUND", "Episode target does not exist.", [parent], ["Inspect episodes and use an existing parent."]);
|
|
252
|
+
const index = sceneIndex(script.episodes[epIndex], sceneId);
|
|
253
|
+
if (index < 0)
|
|
254
|
+
fail("AUTHORING_SCENE_NOT_FOUND", "Scene target does not exist.", [`${parent}/${sceneId}`], ["Inspect scenes and use an existing target."]);
|
|
255
|
+
return [epIndex, index];
|
|
256
|
+
}
|
|
257
|
+
function compareCoordinate(left, right) {
|
|
258
|
+
const length = Math.max(left.length, right.length);
|
|
259
|
+
for (let index = 0; index < length; index += 1) {
|
|
260
|
+
const diff = (left[index] ?? 0) - (right[index] ?? 0);
|
|
261
|
+
if (diff !== 0)
|
|
262
|
+
return diff;
|
|
263
|
+
}
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
function assertStrictOrder(coords, message) {
|
|
267
|
+
for (let index = 1; index < coords.length; index += 1) {
|
|
268
|
+
if (compareCoordinate(coords[index - 1].coord, coords[index].coord) >= 0) {
|
|
269
|
+
fail("AUTHORING_TARGET_ORDER", message, [sourceLabel(coords[index].source)], ["List replace targets in current document order without duplicates."], EXIT_USAGE);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function applyEpisodeTargets(baseline, script, targets, mode) {
|
|
274
|
+
const plans = [];
|
|
275
|
+
if (mode === "replace") {
|
|
276
|
+
const coords = targets.map((target) => {
|
|
277
|
+
const index = episodeIndex(baseline, target.value.episode_id);
|
|
278
|
+
if (index < 0)
|
|
279
|
+
fail("AUTHORING_EPISODE_NOT_FOUND", "Episode replace target does not exist.", [sourceLabel(target.source), target.value.episode_id], ["Use --create for a new episode or choose an existing ID."]);
|
|
280
|
+
return { coord: [index], source: target.source };
|
|
281
|
+
});
|
|
282
|
+
assertStrictOrder(coords, "Episode replace targets are not in document order.");
|
|
283
|
+
}
|
|
284
|
+
for (const target of targets) {
|
|
285
|
+
assertEpisodeComplete(target.value, target.source);
|
|
286
|
+
const existingIndex = episodeIndex(baseline, target.value.episode_id);
|
|
287
|
+
const existing = existingIndex < 0 ? undefined : baseline.episodes[existingIndex];
|
|
288
|
+
if (mode === "create") {
|
|
289
|
+
if (existing !== undefined) {
|
|
290
|
+
if (!semanticEqual(existing, target.value)) {
|
|
291
|
+
fail("AUTHORING_CREATE_CONFLICT", "Episode create target already exists with different content.", [sourceLabel(target.source), target.value.episode_id], ["Use --replace to replace it, or make the create block identical for a no-op retry."]);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
script.episodes.push(structuredClone(target.value));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
script.episodes[existingIndex] = structuredClone(target.value);
|
|
300
|
+
}
|
|
301
|
+
plans.push({
|
|
302
|
+
kind: "episode",
|
|
303
|
+
address: target.value.episode_id,
|
|
304
|
+
before: episodeCounts(existing),
|
|
305
|
+
after: episodeCounts(target.value),
|
|
306
|
+
...idDiff(existing?.scenes.map((scene) => scene.scene_id) ?? [], target.value.scenes.map((scene) => scene.scene_id)),
|
|
307
|
+
untouchedSiblings: Math.max(0, baseline.episodes.length - (existing === undefined ? 0 : 1)),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return plans;
|
|
311
|
+
}
|
|
312
|
+
function applySceneTargets(baseline, script, targets, mode) {
|
|
313
|
+
const plans = [];
|
|
314
|
+
if (mode === "replace") {
|
|
315
|
+
const coords = targets.map((target) => ({ coord: sceneCoordinate(baseline, target.parent, target.value.scene_id), source: target.source }));
|
|
316
|
+
assertStrictOrder(coords, "Scene replace targets are not in document order.");
|
|
317
|
+
}
|
|
318
|
+
for (const target of targets) {
|
|
319
|
+
assertSceneComplete(target.value, target.source);
|
|
320
|
+
const epIndex = episodeIndex(baseline, target.parent);
|
|
321
|
+
if (epIndex < 0)
|
|
322
|
+
fail("AUTHORING_EPISODE_NOT_FOUND", "Scene parent episode does not exist.", [sourceLabel(target.source), target.parent], ["Use an existing episode parent."]);
|
|
323
|
+
const baseEpisode = baseline.episodes[epIndex];
|
|
324
|
+
const outputEpisode = script.episodes[epIndex];
|
|
325
|
+
const existingIndex = sceneIndex(baseEpisode, target.value.scene_id);
|
|
326
|
+
const existing = existingIndex < 0 ? undefined : baseEpisode.scenes[existingIndex];
|
|
327
|
+
if (mode === "create") {
|
|
328
|
+
if (existing !== undefined) {
|
|
329
|
+
if (!semanticEqual(existing, target.value)) {
|
|
330
|
+
fail("AUTHORING_CREATE_CONFLICT", "Scene create target already exists with different content.", [sourceLabel(target.source), `${target.parent}/${target.value.scene_id}`], ["Use --replace or make the create block identical for a no-op retry."]);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
outputEpisode.scenes.push(structuredClone(target.value));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
if (existingIndex < 0)
|
|
339
|
+
fail("AUTHORING_SCENE_NOT_FOUND", "Scene replace target does not exist.", [sourceLabel(target.source), `${target.parent}/${target.value.scene_id}`], ["Use --create for a new scene or choose an existing scene."]);
|
|
340
|
+
outputEpisode.scenes[existingIndex] = structuredClone(target.value);
|
|
341
|
+
}
|
|
342
|
+
plans.push({
|
|
343
|
+
kind: "scene",
|
|
344
|
+
address: `${target.parent}/${target.value.scene_id}`,
|
|
345
|
+
before: sceneCounts(existing),
|
|
346
|
+
after: sceneCounts(target.value),
|
|
347
|
+
...idDiff(existing === undefined ? [] : [existing.scene_id], [target.value.scene_id]),
|
|
348
|
+
untouchedSiblings: Math.max(0, baseEpisode.scenes.length - (existing === undefined ? 0 : 1)),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return plans;
|
|
352
|
+
}
|
|
353
|
+
function applyEpisodeSpans(baseline, script, targets) {
|
|
354
|
+
const resolved = targets.map((target) => {
|
|
355
|
+
const from = episodeIndex(baseline, target.from);
|
|
356
|
+
const through = episodeIndex(baseline, target.through);
|
|
357
|
+
if (from < 0 || through < 0)
|
|
358
|
+
fail("AUTHORING_SPAN_ENDPOINT_NOT_FOUND", "Episode-span endpoint does not exist.", [sourceLabel(target.source), `${target.from}..${target.through}`], ["Use existing episode IDs as span endpoints."]);
|
|
359
|
+
if (from > through)
|
|
360
|
+
fail("AUTHORING_SPAN_REVERSED", "Episode-span endpoints are reversed in current document order.", [sourceLabel(target.source), `${target.from}..${target.through}`], ["Swap from and through."]);
|
|
361
|
+
if (target.episodes.length === 0)
|
|
362
|
+
fail("AUTHORING_EMPTY_SPAN", "Episode-span replacement cannot be empty.", [sourceLabel(target.source)], ["Include at least one complete episode."]);
|
|
363
|
+
for (const episode of target.episodes)
|
|
364
|
+
assertEpisodeComplete(episode, target.source);
|
|
365
|
+
return { target, from, through };
|
|
366
|
+
});
|
|
367
|
+
assertStrictOrder(resolved.map((item) => ({ coord: [item.from], source: item.target.source })), "Episode spans are not in document order.");
|
|
368
|
+
for (let index = 1; index < resolved.length; index += 1) {
|
|
369
|
+
if (resolved[index].from <= resolved[index - 1].through)
|
|
370
|
+
fail("AUTHORING_SPAN_OVERLAP", "Episode spans overlap.", [sourceLabel(resolved[index].target.source)], ["Use disjoint episode spans."]);
|
|
371
|
+
}
|
|
372
|
+
const plans = resolved.map(({ target, from, through }) => {
|
|
373
|
+
const before = baseline.episodes.slice(from, through + 1);
|
|
374
|
+
return {
|
|
375
|
+
kind: "episode-span",
|
|
376
|
+
address: `${target.from}..${target.through}`,
|
|
377
|
+
before: episodesCounts(before),
|
|
378
|
+
after: episodesCounts(target.episodes),
|
|
379
|
+
...idDiff(before.map((episode) => episode.episode_id), target.episodes.map((episode) => episode.episode_id)),
|
|
380
|
+
untouchedSiblings: baseline.episodes.length - before.length,
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
for (const { target, from, through } of [...resolved].reverse()) {
|
|
384
|
+
script.episodes.splice(from, through - from + 1, ...structuredClone(target.episodes));
|
|
385
|
+
}
|
|
386
|
+
return plans;
|
|
387
|
+
}
|
|
388
|
+
function applySceneSpans(baseline, script, targets) {
|
|
389
|
+
const resolved = targets.map((target) => {
|
|
390
|
+
const epIndex = episodeIndex(baseline, target.parent);
|
|
391
|
+
if (epIndex < 0)
|
|
392
|
+
fail("AUTHORING_EPISODE_NOT_FOUND", "Scene-span parent does not exist.", [sourceLabel(target.source), target.parent], ["Use an existing parent episode."]);
|
|
393
|
+
const episode = baseline.episodes[epIndex];
|
|
394
|
+
const from = sceneIndex(episode, target.from);
|
|
395
|
+
const through = sceneIndex(episode, target.through);
|
|
396
|
+
if (from < 0 || through < 0)
|
|
397
|
+
fail("AUTHORING_SPAN_ENDPOINT_NOT_FOUND", "Scene-span endpoint does not exist in its parent.", [sourceLabel(target.source), `${target.parent}:${target.from}..${target.through}`], ["Use scene IDs from the same existing episode."]);
|
|
398
|
+
if (from > through)
|
|
399
|
+
fail("AUTHORING_SPAN_REVERSED", "Scene-span endpoints are reversed in current document order.", [sourceLabel(target.source), `${target.from}..${target.through}`], ["Swap from and through."]);
|
|
400
|
+
if (target.scenes.length === 0)
|
|
401
|
+
fail("AUTHORING_EMPTY_SPAN", "Scene-span replacement cannot be empty.", [sourceLabel(target.source)], ["Include at least one complete scene."]);
|
|
402
|
+
for (const scene of target.scenes)
|
|
403
|
+
assertSceneComplete(scene, target.source);
|
|
404
|
+
return { target, epIndex, from, through };
|
|
405
|
+
});
|
|
406
|
+
assertStrictOrder(resolved.map((item) => ({ coord: [item.epIndex, item.from], source: item.target.source })), "Scene spans are not in document order.");
|
|
407
|
+
for (let index = 1; index < resolved.length; index += 1) {
|
|
408
|
+
const previous = resolved[index - 1];
|
|
409
|
+
const current = resolved[index];
|
|
410
|
+
if (current.epIndex === previous.epIndex && current.from <= previous.through)
|
|
411
|
+
fail("AUTHORING_SPAN_OVERLAP", "Scene spans overlap.", [sourceLabel(current.target.source)], ["Use disjoint scene spans."]);
|
|
412
|
+
}
|
|
413
|
+
const plans = resolved.map(({ target, epIndex, from, through }) => {
|
|
414
|
+
const before = baseline.episodes[epIndex].scenes.slice(from, through + 1);
|
|
415
|
+
return {
|
|
416
|
+
kind: "scene-span",
|
|
417
|
+
address: `${target.parent}/${target.from}..${target.parent}/${target.through}`,
|
|
418
|
+
before: scenesCounts(before),
|
|
419
|
+
after: scenesCounts(target.scenes),
|
|
420
|
+
...idDiff(before.map((scene) => scene.scene_id), target.scenes.map((scene) => scene.scene_id)),
|
|
421
|
+
untouchedSiblings: baseline.episodes[epIndex].scenes.length - before.length,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
const grouped = new Map();
|
|
425
|
+
for (const item of resolved)
|
|
426
|
+
grouped.set(item.epIndex, [...(grouped.get(item.epIndex) ?? []), item]);
|
|
427
|
+
for (const [epIndex, spans] of grouped) {
|
|
428
|
+
for (const { target, from, through } of [...spans].reverse()) {
|
|
429
|
+
script.episodes[epIndex].scenes.splice(from, through - from + 1, ...structuredClone(target.scenes));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return plans;
|
|
433
|
+
}
|
|
434
|
+
function applyActionSpans(baseline, script, targets) {
|
|
435
|
+
const resolved = targets.map((target) => {
|
|
436
|
+
const slash = target.parent.indexOf("/");
|
|
437
|
+
if (slash < 1 || slash === target.parent.length - 1)
|
|
438
|
+
fail("AUTHORING_SCENE_ADDRESS_INVALID", "Action-span parent is not a complete scene address.", [sourceLabel(target.source), target.parent], ["Use parent=ep_ID/scn_ID."]);
|
|
439
|
+
const parent = target.parent.slice(0, slash);
|
|
440
|
+
const sceneId = target.parent.slice(slash + 1);
|
|
441
|
+
const [epIndex, scIndex] = sceneCoordinate(baseline, parent, sceneId);
|
|
442
|
+
const scene = baseline.episodes[epIndex].scenes[scIndex];
|
|
443
|
+
if (target.from > target.through)
|
|
444
|
+
fail("AUTHORING_SPAN_REVERSED", "Action-span endpoints are reversed.", [sourceLabel(target.source), `${target.from}..${target.through}`], ["Swap from and through."]);
|
|
445
|
+
if (target.through >= scene.actions.length)
|
|
446
|
+
fail("AUTHORING_SPAN_ENDPOINT_NOT_FOUND", "Action-span endpoint is outside the current scene body.", [sourceLabel(target.source), `${target.from}..${target.through}`, `actions=${scene.actions.length}`], ["Use zero-based endpoints within the current body."]);
|
|
447
|
+
if (target.actions.length === 0)
|
|
448
|
+
fail("AUTHORING_EMPTY_SPAN", "Action-span replacement cannot be empty.", [sourceLabel(target.source)], ["Include at least one action."]);
|
|
449
|
+
return { target, epIndex, scIndex, scene, from: target.from, through: target.through };
|
|
450
|
+
});
|
|
451
|
+
assertStrictOrder(resolved.map((item) => ({ coord: [item.epIndex, item.scIndex, item.from], source: item.target.source })), "Action spans are not in document order.");
|
|
452
|
+
for (let index = 1; index < resolved.length; index += 1) {
|
|
453
|
+
const previous = resolved[index - 1];
|
|
454
|
+
const current = resolved[index];
|
|
455
|
+
if (current.epIndex === previous.epIndex && current.scIndex === previous.scIndex && current.from <= previous.through) {
|
|
456
|
+
fail("AUTHORING_SPAN_OVERLAP", "Action spans overlap.", [sourceLabel(current.target.source)], ["Use disjoint action spans."]);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const plans = resolved.map(({ target, scene, from, through }) => ({
|
|
460
|
+
kind: "action-span",
|
|
461
|
+
address: `${target.parent}#${from}..${target.parent}#${through}`,
|
|
462
|
+
before: { episodes: 0, scenes: 0, actions: through - from + 1 },
|
|
463
|
+
after: { episodes: 0, scenes: 0, actions: target.actions.length },
|
|
464
|
+
retainedIds: [],
|
|
465
|
+
createdIds: [],
|
|
466
|
+
removedIds: [],
|
|
467
|
+
untouchedSiblings: scene.actions.length - (through - from + 1),
|
|
468
|
+
}));
|
|
469
|
+
const grouped = new Map();
|
|
470
|
+
for (const item of resolved) {
|
|
471
|
+
const key = `${item.epIndex}/${item.scIndex}`;
|
|
472
|
+
grouped.set(key, [...(grouped.get(key) ?? []), item]);
|
|
473
|
+
}
|
|
474
|
+
for (const spans of grouped.values()) {
|
|
475
|
+
for (const { target, epIndex, scIndex, from, through } of [...spans].reverse()) {
|
|
476
|
+
script.episodes[epIndex].scenes[scIndex].actions.splice(from, through - from + 1, ...structuredClone(target.actions));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return plans;
|
|
480
|
+
}
|
|
481
|
+
function assertUniqueStructureIds(script) {
|
|
482
|
+
const episodeIds = new Set();
|
|
483
|
+
const sceneIds = new Set();
|
|
484
|
+
for (const episode of script.episodes) {
|
|
485
|
+
if (episodeIds.has(episode.episode_id))
|
|
486
|
+
fail("AUTHORING_EPISODE_ID_CONFLICT", "Episode ID occurs more than once.", [episode.episode_id], ["Use globally unique episode IDs."]);
|
|
487
|
+
episodeIds.add(episode.episode_id);
|
|
488
|
+
for (const scene of episode.scenes) {
|
|
489
|
+
if (sceneIds.has(scene.scene_id))
|
|
490
|
+
fail("AUTHORING_SCENE_ID_CONFLICT", "Scene ID occurs more than once.", [scene.scene_id], ["Use globally unique stable scene IDs."]);
|
|
491
|
+
sceneIds.add(scene.scene_id);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function stableValidationPath(path, script) {
|
|
496
|
+
const episodeMatch = /^\$\.episodes\[(\d+)]/.exec(path);
|
|
497
|
+
if (episodeMatch === null)
|
|
498
|
+
return path;
|
|
499
|
+
const episode = script.episodes[Number(episodeMatch[1])];
|
|
500
|
+
if (episode === undefined)
|
|
501
|
+
return path;
|
|
502
|
+
let stable = path.replace(episodeMatch[0], `$.episodes[${episode.episode_id}]`);
|
|
503
|
+
const sceneMatch = /^(\$\.episodes\[\d+]\.scenes)\[(\d+)]/.exec(path);
|
|
504
|
+
if (sceneMatch !== null) {
|
|
505
|
+
const scene = episode.scenes[Number(sceneMatch[2])];
|
|
506
|
+
if (scene !== undefined) {
|
|
507
|
+
const oldPrefix = `${episodeMatch[0]}.scenes[${sceneMatch[2]}]`;
|
|
508
|
+
stable = path.replace(oldPrefix, `$.episodes[${episode.episode_id}].scenes[${scene.scene_id}]`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return stable;
|
|
512
|
+
}
|
|
513
|
+
function validationKey(issue, script) {
|
|
514
|
+
return `${issue.code}\u0000${stableValidationPath(issue.path, script)}\u0000${issue.message}`;
|
|
515
|
+
}
|
|
516
|
+
function assertValidationAllowed(before, after, authored) {
|
|
517
|
+
const result = validateScriptV3(after);
|
|
518
|
+
if (result.ok)
|
|
519
|
+
return result;
|
|
520
|
+
const beforeKeys = new Set(before === null
|
|
521
|
+
? []
|
|
522
|
+
: validateScriptV3(before).errors.map((issue) => validationKey(issue, before)));
|
|
523
|
+
const authoredEpisodeIds = new Set();
|
|
524
|
+
const authoredSceneIds = new Set();
|
|
525
|
+
for (const target of authored) {
|
|
526
|
+
if (target.kind === "script") {
|
|
527
|
+
for (const episode of target.episodes) {
|
|
528
|
+
authoredEpisodeIds.add(episode.episode_id);
|
|
529
|
+
for (const scene of episode.scenes)
|
|
530
|
+
authoredSceneIds.add(scene.scene_id);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else if (target.kind === "episode") {
|
|
534
|
+
authoredEpisodeIds.add(target.value.episode_id);
|
|
535
|
+
for (const scene of target.value.scenes)
|
|
536
|
+
authoredSceneIds.add(scene.scene_id);
|
|
537
|
+
}
|
|
538
|
+
else if (target.kind === "episode-span") {
|
|
539
|
+
for (const episode of target.episodes) {
|
|
540
|
+
authoredEpisodeIds.add(episode.episode_id);
|
|
541
|
+
for (const scene of episode.scenes)
|
|
542
|
+
authoredSceneIds.add(scene.scene_id);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else if (target.kind === "scene")
|
|
546
|
+
authoredSceneIds.add(target.value.scene_id);
|
|
547
|
+
else if (target.kind === "scene-span")
|
|
548
|
+
for (const scene of target.scenes)
|
|
549
|
+
authoredSceneIds.add(scene.scene_id);
|
|
550
|
+
else if (target.kind === "action-span")
|
|
551
|
+
authoredSceneIds.add(target.parent.split("/")[1] ?? "");
|
|
552
|
+
}
|
|
553
|
+
const blocking = result.errors.filter((issue) => {
|
|
554
|
+
if (!beforeKeys.has(validationKey(issue, after)))
|
|
555
|
+
return true;
|
|
556
|
+
const episodeMatch = /^\$\.episodes\[(\d+)]/.exec(issue.path);
|
|
557
|
+
if (!episodeMatch)
|
|
558
|
+
return false;
|
|
559
|
+
const episode = after.episodes[Number(episodeMatch[1])];
|
|
560
|
+
if (episode === undefined)
|
|
561
|
+
return true;
|
|
562
|
+
if (authoredEpisodeIds.has(episode.episode_id))
|
|
563
|
+
return true;
|
|
564
|
+
const sceneMatch = /^\$\.episodes\[\d+]\.scenes\[(\d+)]/.exec(issue.path);
|
|
565
|
+
const scene = sceneMatch === null ? undefined : episode.scenes[Number(sceneMatch[1])];
|
|
566
|
+
return scene !== undefined && authoredSceneIds.has(scene.scene_id);
|
|
567
|
+
});
|
|
568
|
+
if (blocking.length > 0) {
|
|
569
|
+
fail("AUTHORING_VALIDATION_FAILED", "The proposed script has blocking validation errors.", blocking.map((issue) => `${issue.path}: ${issue.code} ${issue.message}`), ["Repair the authoring source and rerun the plan."]);
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
function orphanAssets(script) {
|
|
574
|
+
const referenced = new Set();
|
|
575
|
+
for (const episode of script.episodes) {
|
|
576
|
+
for (const scene of episode.scenes) {
|
|
577
|
+
if (scene.location !== null)
|
|
578
|
+
referenced.add(scene.location.location_id);
|
|
579
|
+
for (const actor of scene.actors)
|
|
580
|
+
referenced.add(actor.actor_id);
|
|
581
|
+
for (const prop of scene.props)
|
|
582
|
+
referenced.add(prop.prop_id);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return [
|
|
586
|
+
...script.actors.map((asset) => asset.actor_id),
|
|
587
|
+
...script.locations.map((asset) => asset.location_id),
|
|
588
|
+
...script.props.map((asset) => asset.prop_id),
|
|
589
|
+
].filter((id) => !referenced.has(id));
|
|
590
|
+
}
|
|
591
|
+
function buildScriptTarget(document) {
|
|
592
|
+
const target = document.targets[0];
|
|
593
|
+
if (target?.kind !== "script")
|
|
594
|
+
fail("AUTHORING_TARGET_MODE", "Script create requires exactly one script target.", [], ["Use a single <script> block."], EXIT_USAGE);
|
|
595
|
+
for (const episode of target.episodes)
|
|
596
|
+
assertEpisodeComplete(episode, target.source);
|
|
597
|
+
const script = {
|
|
598
|
+
version: 3,
|
|
599
|
+
title: target.title,
|
|
600
|
+
worldview: target.worldview,
|
|
601
|
+
style: target.style,
|
|
602
|
+
actors: [],
|
|
603
|
+
locations: [],
|
|
604
|
+
props: [],
|
|
605
|
+
episodes: structuredClone(target.episodes),
|
|
606
|
+
};
|
|
607
|
+
if (target.worldviewRaw !== undefined)
|
|
608
|
+
script.worldview_raw = target.worldviewRaw;
|
|
609
|
+
if (target.synopsis !== undefined)
|
|
610
|
+
script.synopsis = target.synopsis;
|
|
611
|
+
if (target.logline !== undefined)
|
|
612
|
+
script.logline = target.logline;
|
|
613
|
+
if (target.theme !== undefined)
|
|
614
|
+
script.theme = target.theme;
|
|
615
|
+
if (target.mainCharacters !== undefined)
|
|
616
|
+
script.main_characters = structuredClone(target.mainCharacters);
|
|
617
|
+
if (target.extend !== undefined)
|
|
618
|
+
script.extend = structuredClone(target.extend);
|
|
619
|
+
const declarations = applyDeclarations(script, document.declarations);
|
|
620
|
+
assertUniqueStructureIds(script);
|
|
621
|
+
return { script, declarations };
|
|
622
|
+
}
|
|
623
|
+
export function applyAuthoring(current, document, mode) {
|
|
624
|
+
if (document.targetKind === "script") {
|
|
625
|
+
if (mode !== "create")
|
|
626
|
+
fail("AUTHORING_TARGET_MODE", "Script targets support create only.", [mode], ["Use --create."], EXIT_USAGE);
|
|
627
|
+
const built = buildScriptTarget(document);
|
|
628
|
+
const script = built.script;
|
|
629
|
+
if (current !== null && !semanticEqual(current, script)) {
|
|
630
|
+
fail("AUTHORING_CREATE_CONFLICT", "The script target already exists with different content.", ["script"], ["Use episode replacement for an existing script, or write an identical script for a no-op retry."]);
|
|
631
|
+
}
|
|
632
|
+
const declarations = current === null
|
|
633
|
+
? built.declarations
|
|
634
|
+
: {
|
|
635
|
+
createdAssets: [],
|
|
636
|
+
existingAssets: [...built.declarations.existingAssets, ...built.declarations.createdAssets],
|
|
637
|
+
createdStates: [],
|
|
638
|
+
existingStates: [...built.declarations.existingStates, ...built.declarations.createdStates],
|
|
639
|
+
};
|
|
640
|
+
const validation = assertValidationAllowed(current, script, document.targets);
|
|
641
|
+
return {
|
|
642
|
+
script,
|
|
643
|
+
changed: current === null || !semanticEqual(current, script),
|
|
644
|
+
declarations,
|
|
645
|
+
targets: [{
|
|
646
|
+
kind: "script",
|
|
647
|
+
address: "global",
|
|
648
|
+
before: current === null ? { episodes: 0, scenes: 0, actions: 0 } : episodesCounts(current.episodes),
|
|
649
|
+
after: episodesCounts(script.episodes),
|
|
650
|
+
...idDiff(current?.episodes.map((episode) => episode.episode_id) ?? [], script.episodes.map((episode) => episode.episode_id)),
|
|
651
|
+
untouchedSiblings: 0,
|
|
652
|
+
}],
|
|
653
|
+
retainedOrphanAssets: orphanAssets(script),
|
|
654
|
+
validation,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (current === null) {
|
|
658
|
+
fail("AUTHORING_SCRIPT_NOT_FOUND", "A local Script v3 file is required for this target kind.", ["<missing>"], ["Create a script first or select an existing --script-path."]);
|
|
659
|
+
}
|
|
660
|
+
if ((document.targetKind === "episode-span" || document.targetKind === "scene-span" || document.targetKind === "action-span") && mode !== "replace") {
|
|
661
|
+
fail("AUTHORING_TARGET_MODE", `${document.targetKind} supports replace only.`, [mode], ["Use --replace."], EXIT_USAGE);
|
|
662
|
+
}
|
|
663
|
+
const baseline = structuredClone(current);
|
|
664
|
+
const script = structuredClone(current);
|
|
665
|
+
const declarations = applyDeclarations(script, document.declarations);
|
|
666
|
+
let targets;
|
|
667
|
+
switch (document.targetKind) {
|
|
668
|
+
case "episode": {
|
|
669
|
+
const selected = document.targets.flatMap((target) => target.kind === "episode" ? [target] : []);
|
|
670
|
+
if (selected.length !== document.targets.length)
|
|
671
|
+
fail("AUTHORING_TARGET_MODE", "Authoring targets are not homogeneous.", [], ["Keep one target kind per file."], EXIT_USAGE);
|
|
672
|
+
targets = applyEpisodeTargets(baseline, script, selected, mode);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case "scene": {
|
|
676
|
+
const selected = document.targets.flatMap((target) => target.kind === "scene" ? [target] : []);
|
|
677
|
+
if (selected.length !== document.targets.length)
|
|
678
|
+
fail("AUTHORING_TARGET_MODE", "Authoring targets are not homogeneous.", [], ["Keep one target kind per file."], EXIT_USAGE);
|
|
679
|
+
targets = applySceneTargets(baseline, script, selected, mode);
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
case "episode-span": {
|
|
683
|
+
const selected = document.targets.flatMap((target) => target.kind === "episode-span" ? [target] : []);
|
|
684
|
+
if (selected.length !== document.targets.length)
|
|
685
|
+
fail("AUTHORING_TARGET_MODE", "Authoring targets are not homogeneous.", [], ["Keep one target kind per file."], EXIT_USAGE);
|
|
686
|
+
targets = applyEpisodeSpans(baseline, script, selected);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case "scene-span": {
|
|
690
|
+
const selected = document.targets.flatMap((target) => target.kind === "scene-span" ? [target] : []);
|
|
691
|
+
if (selected.length !== document.targets.length)
|
|
692
|
+
fail("AUTHORING_TARGET_MODE", "Authoring targets are not homogeneous.", [], ["Keep one target kind per file."], EXIT_USAGE);
|
|
693
|
+
targets = applySceneSpans(baseline, script, selected);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
case "action-span": {
|
|
697
|
+
const selected = document.targets.flatMap((target) => target.kind === "action-span" ? [target] : []);
|
|
698
|
+
if (selected.length !== document.targets.length)
|
|
699
|
+
fail("AUTHORING_TARGET_MODE", "Authoring targets are not homogeneous.", [], ["Keep one target kind per file."], EXIT_USAGE);
|
|
700
|
+
targets = applyActionSpans(baseline, script, selected);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
default:
|
|
704
|
+
fail("AUTHORING_TARGET_MODE", "Unsupported authoring target.", [document.targetKind], ["Use a supported homogeneous target kind."]);
|
|
705
|
+
}
|
|
706
|
+
assertUniqueStructureIds(script);
|
|
707
|
+
const validation = assertValidationAllowed(current, script, document.targets);
|
|
708
|
+
return {
|
|
709
|
+
script,
|
|
710
|
+
changed: !semanticEqual(current, script),
|
|
711
|
+
declarations,
|
|
712
|
+
targets,
|
|
713
|
+
retainedOrphanAssets: orphanAssets(script),
|
|
714
|
+
validation,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
//# sourceMappingURL=apply.js.map
|