@lingjingai/scriptctl 0.32.0 → 0.33.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/changes/0.33.0.md +17 -0
- package/dist/domain/direct/runner.js +1 -1
- package/dist/domain/direct/runner.js.map +1 -1
- package/dist/domain/direct/stage.d.ts +1 -1
- package/dist/domain/direct/stage.js +1 -0
- package/dist/domain/direct/stage.js.map +1 -1
- package/dist/domain/direct/stages/index.d.ts +2 -1
- package/dist/domain/direct/stages/index.js +3 -1
- package/dist/domain/direct/stages/index.js.map +1 -1
- package/dist/domain/direct/stages/state-curation.d.ts +2 -0
- package/dist/domain/direct/stages/state-curation.js +10 -0
- package/dist/domain/direct/stages/state-curation.js.map +1 -0
- package/dist/domain/direct-core.d.ts +21 -0
- package/dist/domain/direct-core.js +662 -6
- package/dist/domain/direct-core.js.map +1 -1
- package/dist/help-text.js +6 -1
- package/dist/help-text.js.map +1 -1
- package/dist/infra/providers.d.ts +3 -0
- package/dist/infra/providers.js +151 -3
- package/dist/infra/providers.js.map +1 -1
- package/dist/usecases/direct.d.ts +1 -0
- package/dist/usecases/direct.js +256 -9
- package/dist/usecases/direct.js.map +1 -1
- package/package.json +1 -1
|
@@ -3927,7 +3927,7 @@ export function validateAssetGroupingPlan(script, kind, rawPlan) {
|
|
|
3927
3927
|
retainedByName.set(key, list);
|
|
3928
3928
|
}
|
|
3929
3929
|
const duplicateMerges = [];
|
|
3930
|
-
const
|
|
3930
|
+
const omittedSingletons = [];
|
|
3931
3931
|
for (const id of inputIds) {
|
|
3932
3932
|
if (mentioned.has(id))
|
|
3933
3933
|
continue;
|
|
@@ -3937,7 +3937,13 @@ export function validateAssetGroupingPlan(script, kind, rawPlan) {
|
|
|
3937
3937
|
const key = assetGroupingNameKey(kind, asset[assetNameKey(kind)]);
|
|
3938
3938
|
const targets = key ? retainedByName.get(key) ?? [] : [];
|
|
3939
3939
|
if (targets.length === 0) {
|
|
3940
|
-
|
|
3940
|
+
groups.push({
|
|
3941
|
+
kind,
|
|
3942
|
+
ids: [id],
|
|
3943
|
+
reason: "omitted by grouping; review as standalone asset",
|
|
3944
|
+
});
|
|
3945
|
+
mentioned.add(id);
|
|
3946
|
+
omittedSingletons.push(id);
|
|
3941
3947
|
continue;
|
|
3942
3948
|
}
|
|
3943
3949
|
duplicateMerges.push({
|
|
@@ -3949,9 +3955,6 @@ export function validateAssetGroupingPlan(script, kind, rawPlan) {
|
|
|
3949
3955
|
reason: "name-level duplicate omitted by asset grouping",
|
|
3950
3956
|
});
|
|
3951
3957
|
}
|
|
3952
|
-
if (omittedInvalid.length > 0) {
|
|
3953
|
-
curationBlocked("Asset grouping omitted non-duplicate asset ids.", omittedInvalid.slice(0, 40));
|
|
3954
|
-
}
|
|
3955
3958
|
return {
|
|
3956
3959
|
version: 1,
|
|
3957
3960
|
format: "asset-grouping-v1",
|
|
@@ -3966,6 +3969,7 @@ export function validateAssetGroupingPlan(script, kind, rawPlan) {
|
|
|
3966
3969
|
group_count: groups.length,
|
|
3967
3970
|
mentioned_count: mentioned.size,
|
|
3968
3971
|
omitted_duplicate_count: duplicateMerges.length,
|
|
3972
|
+
omitted_singleton_count: omittedSingletons.length,
|
|
3969
3973
|
normalization_count: normalizations.length,
|
|
3970
3974
|
},
|
|
3971
3975
|
};
|
|
@@ -5049,6 +5053,14 @@ function parseCurationExpression(expression) {
|
|
|
5049
5053
|
match = /^PROP\s+(actor|location|prop)\s+(\S+)\s+->\s+(\S+)\s+"((?:\\.|[^"\\])*)"$/i.exec(command);
|
|
5050
5054
|
if (match) {
|
|
5051
5055
|
const target = match[3];
|
|
5056
|
+
const normalizedTarget = target.toLowerCase();
|
|
5057
|
+
const targetPrefix = /^([A-Za-z]+):(.+)$/.exec(target);
|
|
5058
|
+
if ((targetPrefix && targetPrefix[1].toLowerCase() !== "prop") ||
|
|
5059
|
+
normalizedTarget === "actor" ||
|
|
5060
|
+
normalizedTarget === "location" ||
|
|
5061
|
+
normalizedTarget === "prop") {
|
|
5062
|
+
curationBlocked("PROP target must be NEW or an existing prop id.", [expression]);
|
|
5063
|
+
}
|
|
5052
5064
|
const decision = {
|
|
5053
5065
|
kind: match[1].toLowerCase(),
|
|
5054
5066
|
source_id: match[2],
|
|
@@ -5058,7 +5070,7 @@ function parseCurationExpression(expression) {
|
|
|
5058
5070
|
reason,
|
|
5059
5071
|
};
|
|
5060
5072
|
if (target.toUpperCase() !== "NEW")
|
|
5061
|
-
decision["target_id"] = target;
|
|
5073
|
+
decision["target_id"] = targetPrefix ? targetPrefix[2] : target;
|
|
5062
5074
|
return decision;
|
|
5063
5075
|
}
|
|
5064
5076
|
curationBlocked("Unknown or malformed curation expression.", [expression]);
|
|
@@ -6171,6 +6183,650 @@ export function curateScriptAssets(script, rawCuration = null) {
|
|
|
6171
6183
|
};
|
|
6172
6184
|
}
|
|
6173
6185
|
// ---------------------------------------------------------------------------
|
|
6186
|
+
// State curation
|
|
6187
|
+
// ---------------------------------------------------------------------------
|
|
6188
|
+
export const STATE_CURATION_MAX_ASSETS = 4;
|
|
6189
|
+
export const STATE_CURATION_MAX_CONTEXT_CHARS = 28000;
|
|
6190
|
+
function stateCurationBlocked(message, received) {
|
|
6191
|
+
throw new CliError("DIRECT STATE CURATION BLOCKED: invalid provider plan", message, {
|
|
6192
|
+
exitCode: EXIT_NEEDS_AGENT,
|
|
6193
|
+
required: ["valid state curation expressions referencing existing asset/state ids"],
|
|
6194
|
+
received,
|
|
6195
|
+
nextSteps: ["Inspect state_curation.json, adjust the draft manually, or rerun direct init after provider plan fixes."],
|
|
6196
|
+
});
|
|
6197
|
+
}
|
|
6198
|
+
function stateCurationSourceKey(kind, assetId, stateId) {
|
|
6199
|
+
return `${kind}:${assetId}/${stateId}`;
|
|
6200
|
+
}
|
|
6201
|
+
function stateCurationDecisionSourceKey(decision) {
|
|
6202
|
+
return stateCurationSourceKey(strOf(decision["kind"]), strOf(decision["asset_id"]), strOf(decision["state_id"]));
|
|
6203
|
+
}
|
|
6204
|
+
function stateCurationUsageFor(script) {
|
|
6205
|
+
const usage = new Map();
|
|
6206
|
+
const ensure = (kind, assetId, stateId) => {
|
|
6207
|
+
const key = stateCurationSourceKey(kind, assetId, stateId);
|
|
6208
|
+
const current = usage.get(key);
|
|
6209
|
+
if (current)
|
|
6210
|
+
return current;
|
|
6211
|
+
const next = { sceneRefs: new Set(), stateChanges: new Set() };
|
|
6212
|
+
usage.set(key, next);
|
|
6213
|
+
return next;
|
|
6214
|
+
};
|
|
6215
|
+
for (const ep of asList(script["episodes"])) {
|
|
6216
|
+
const episodeId = strOf(ep["episode_id"]);
|
|
6217
|
+
for (const scene of asList(ep["scenes"])) {
|
|
6218
|
+
const sceneId = strOf(scene["scene_id"]);
|
|
6219
|
+
const sceneRef = [episodeId, sceneId].filter((item) => item).join("/");
|
|
6220
|
+
const ctx = sceneContext(scene);
|
|
6221
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
6222
|
+
const idKey = assetIdKey(kind);
|
|
6223
|
+
const refKey = assetListKey(kind);
|
|
6224
|
+
for (const ref of asList(ctx[refKey])) {
|
|
6225
|
+
const assetId = strOf(ref[idKey]);
|
|
6226
|
+
const stateId = strOf(ref["state_id"]);
|
|
6227
|
+
if (assetId && stateId && sceneRef)
|
|
6228
|
+
ensure(kind, assetId, stateId).sceneRefs.add(sceneRef);
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
for (const [actionIndex, action] of asList(scene["actions"]).entries()) {
|
|
6232
|
+
if (!isDict(action))
|
|
6233
|
+
continue;
|
|
6234
|
+
for (const change of asList(action["state_changes"])) {
|
|
6235
|
+
if (!isDict(change))
|
|
6236
|
+
continue;
|
|
6237
|
+
const kind = strOf(change["target_kind"]);
|
|
6238
|
+
if (!isAssetKindValue(kind))
|
|
6239
|
+
continue;
|
|
6240
|
+
const assetId = strOf(change["target_id"]);
|
|
6241
|
+
if (!assetId)
|
|
6242
|
+
continue;
|
|
6243
|
+
const actionRef = `${sceneRef}#${actionIndex}`;
|
|
6244
|
+
const fromStateId = strOf(change["from_state_id"]);
|
|
6245
|
+
const toStateId = strOf(change["to_state_id"]);
|
|
6246
|
+
if (fromStateId)
|
|
6247
|
+
ensure(kind, assetId, fromStateId).stateChanges.add(actionRef);
|
|
6248
|
+
if (toStateId)
|
|
6249
|
+
ensure(kind, assetId, toStateId).stateChanges.add(actionRef);
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
return usage;
|
|
6255
|
+
}
|
|
6256
|
+
function statefulAssetsForCuration(script) {
|
|
6257
|
+
const refs = [];
|
|
6258
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
6259
|
+
for (const asset of asList(script[assetListKey(kind)])) {
|
|
6260
|
+
if (!isDict(asset))
|
|
6261
|
+
continue;
|
|
6262
|
+
const assetId = strOf(asset[assetIdKey(kind)]).trim();
|
|
6263
|
+
if (!assetId || asList(asset["states"]).filter(isDict).length === 0)
|
|
6264
|
+
continue;
|
|
6265
|
+
refs.push({ kind, asset_id: assetId });
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
return refs;
|
|
6269
|
+
}
|
|
6270
|
+
function stateCurationAssetByRef(script, ref) {
|
|
6271
|
+
return assetMapById(script, ref.kind).get(ref.asset_id) ?? null;
|
|
6272
|
+
}
|
|
6273
|
+
function compactStateCurationText(value, maxChars = 120) {
|
|
6274
|
+
const text = strOf(value).replace(/\s+/g, " ").replace(/\|/g, "/").trim();
|
|
6275
|
+
return text.length > maxChars ? `${text.slice(0, maxChars - 1)}…` : text;
|
|
6276
|
+
}
|
|
6277
|
+
function formatStateCurationUsage(usage) {
|
|
6278
|
+
if (!usage)
|
|
6279
|
+
return "refs=0 changes=0";
|
|
6280
|
+
return `refs=${usage.sceneRefs.size} changes=${usage.stateChanges.size}`;
|
|
6281
|
+
}
|
|
6282
|
+
function stateCurationPrefix(kind) {
|
|
6283
|
+
if (kind === "actor")
|
|
6284
|
+
return "A";
|
|
6285
|
+
if (kind === "location")
|
|
6286
|
+
return "L";
|
|
6287
|
+
return "P";
|
|
6288
|
+
}
|
|
6289
|
+
function formatStateCurationAssetBlock(script, ref, usageByKey) {
|
|
6290
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6291
|
+
if (!asset)
|
|
6292
|
+
return [];
|
|
6293
|
+
const name = compactStateCurationText(asset[assetNameKey(ref.kind)], 80);
|
|
6294
|
+
const description = compactStateCurationText(asset["description"], 160);
|
|
6295
|
+
const states = asList(asset["states"]).filter(isDict);
|
|
6296
|
+
const lines = [
|
|
6297
|
+
`${stateCurationPrefix(ref.kind)} | ${ref.kind} | ${ref.asset_id} | ${name || "-"} | states=${states.length} | desc=${description || "-"}`,
|
|
6298
|
+
];
|
|
6299
|
+
for (const state of states) {
|
|
6300
|
+
const stateId = compactStateCurationText(state["state_id"], 40);
|
|
6301
|
+
const stateName = compactStateCurationText(state["state_name"], 80);
|
|
6302
|
+
if (!stateId || !stateName)
|
|
6303
|
+
continue;
|
|
6304
|
+
const usage = usageByKey.get(stateCurationSourceKey(ref.kind, ref.asset_id, stateId));
|
|
6305
|
+
lines.push(` S | ${stateId} | ${stateName} | ${formatStateCurationUsage(usage)} | desc=${compactStateCurationText(state["description"], 180) || "-"}`);
|
|
6306
|
+
}
|
|
6307
|
+
return lines;
|
|
6308
|
+
}
|
|
6309
|
+
export function buildStateCurationContextText(script, refs) {
|
|
6310
|
+
const selectedRefs = refs && refs.length > 0 ? refs : statefulAssetsForCuration(script);
|
|
6311
|
+
const usage = stateCurationUsageFor(script);
|
|
6312
|
+
const lines = [];
|
|
6313
|
+
lines.push("# State Curation Context");
|
|
6314
|
+
lines.push(`title: ${compactStateCurationText(script["title"], 120) || "-"}`);
|
|
6315
|
+
lines.push("");
|
|
6316
|
+
lines.push("Goal: compress each asset's states to reusable production visual states.");
|
|
6317
|
+
lines.push("");
|
|
6318
|
+
lines.push("Policy:");
|
|
6319
|
+
lines.push("- A state is a large reusable visual asset form, not a plot/action/emotion/pose label.");
|
|
6320
|
+
lines.push("- KEEP only states that require a visibly different reusable asset: major form transformation, Q version, non-human/beast/god/giant form, durable outfit/appearance redesign, major damage/repair condition, or a prop/location physical condition that must recur.");
|
|
6321
|
+
lines.push("- MERGE states that are only degree/progress variants of the same major appearance. Rename one representative to the broad canonical state, then merge the variants into it.");
|
|
6322
|
+
lines.push("- DROP or ACTION_STATE transient states: emotions, poses, unconscious/asleep/lying/sitting, holding/carrying an item, temporary glow/VFX intensity, minor mark brightness/thickness/spread, one-off ordinary clothing, camera effects, or scene-only actions.");
|
|
6323
|
+
lines.push("- For actor states, held objects are not actor states. If the state is just holding/using an item, use ACTION_STATE unless the item must remain a separate prop.");
|
|
6324
|
+
lines.push("- Every S line below must receive exactly one expression.");
|
|
6325
|
+
lines.push("- Use only state_id values listed under the same asset block. Do not invent ids or cross-asset merge targets.");
|
|
6326
|
+
lines.push("- If a broad canonical state name is needed, RENAME_STATE one existing representative state and MERGE_STATE the variants into it.");
|
|
6327
|
+
lines.push("");
|
|
6328
|
+
lines.push("Expression grammar:");
|
|
6329
|
+
lines.push("- <exp>KEEP_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6330
|
+
lines.push("- <exp>MERGE_STATE <kind> <asset_id> <state_id> -> <target_state_id> # reason</exp>");
|
|
6331
|
+
lines.push("- <exp>DROP_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6332
|
+
lines.push("- <exp>ACTION_STATE <kind> <asset_id> <state_id> # reason</exp>");
|
|
6333
|
+
lines.push("- <exp>RENAME_STATE <kind> <asset_id> <state_id> \"<new_name>\" # reason</exp>");
|
|
6334
|
+
lines.push("- <kind> is one of actor, location, prop. Use double quotes for names; escape only \\\" and \\\\.");
|
|
6335
|
+
lines.push("");
|
|
6336
|
+
lines.push("Assets:");
|
|
6337
|
+
for (const ref of selectedRefs) {
|
|
6338
|
+
const block = formatStateCurationAssetBlock(script, ref, usage);
|
|
6339
|
+
if (block.length > 0)
|
|
6340
|
+
lines.push(...block);
|
|
6341
|
+
}
|
|
6342
|
+
lines.push("");
|
|
6343
|
+
lines.push("required_state_ids:");
|
|
6344
|
+
for (const ref of selectedRefs) {
|
|
6345
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6346
|
+
if (!asset)
|
|
6347
|
+
continue;
|
|
6348
|
+
for (const state of asList(asset["states"])) {
|
|
6349
|
+
const stateId = strOf(state["state_id"]);
|
|
6350
|
+
if (stateId)
|
|
6351
|
+
lines.push(`- ${stateCurationSourceKey(ref.kind, ref.asset_id, stateId)}`);
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
return `${lines.join("\n")}\n`;
|
|
6355
|
+
}
|
|
6356
|
+
export function buildStateCurationChunks(script, options = {}) {
|
|
6357
|
+
const maxAssets = Math.max(1, Math.floor(options.maxAssets ?? STATE_CURATION_MAX_ASSETS));
|
|
6358
|
+
const maxContextChars = Math.max(2000, Math.floor(options.maxContextChars ?? STATE_CURATION_MAX_CONTEXT_CHARS));
|
|
6359
|
+
const refs = statefulAssetsForCuration(script);
|
|
6360
|
+
const chunks = [];
|
|
6361
|
+
let current = [];
|
|
6362
|
+
const flush = () => {
|
|
6363
|
+
if (current.length === 0)
|
|
6364
|
+
return;
|
|
6365
|
+
chunks.push({ part_id: `part.${String(chunks.length + 1).padStart(3, "0")}`, refs: current });
|
|
6366
|
+
current = [];
|
|
6367
|
+
};
|
|
6368
|
+
for (const ref of refs) {
|
|
6369
|
+
const singletonLength = buildStateCurationContextText(script, [ref]).length;
|
|
6370
|
+
if (current.length === 0) {
|
|
6371
|
+
current.push(ref);
|
|
6372
|
+
if (singletonLength > maxContextChars)
|
|
6373
|
+
flush();
|
|
6374
|
+
continue;
|
|
6375
|
+
}
|
|
6376
|
+
const candidate = [...current, ref];
|
|
6377
|
+
if (candidate.length > maxAssets || buildStateCurationContextText(script, candidate).length > maxContextChars) {
|
|
6378
|
+
flush();
|
|
6379
|
+
current.push(ref);
|
|
6380
|
+
if (singletonLength > maxContextChars)
|
|
6381
|
+
flush();
|
|
6382
|
+
}
|
|
6383
|
+
else {
|
|
6384
|
+
current = candidate;
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
flush();
|
|
6388
|
+
return chunks;
|
|
6389
|
+
}
|
|
6390
|
+
function splitStateCurationExpressionReason(expression) {
|
|
6391
|
+
let inQuote = false;
|
|
6392
|
+
let escaped = false;
|
|
6393
|
+
for (let index = 0; index < expression.length; index += 1) {
|
|
6394
|
+
const ch = expression[index];
|
|
6395
|
+
if (escaped) {
|
|
6396
|
+
escaped = false;
|
|
6397
|
+
continue;
|
|
6398
|
+
}
|
|
6399
|
+
if (ch === "\\") {
|
|
6400
|
+
escaped = true;
|
|
6401
|
+
continue;
|
|
6402
|
+
}
|
|
6403
|
+
if (ch === "\"") {
|
|
6404
|
+
inQuote = !inQuote;
|
|
6405
|
+
continue;
|
|
6406
|
+
}
|
|
6407
|
+
if (ch === "#" && !inQuote) {
|
|
6408
|
+
const command = expression.slice(0, index).trim();
|
|
6409
|
+
const reason = expression.slice(index + 1).trim();
|
|
6410
|
+
if (!command)
|
|
6411
|
+
stateCurationBlocked("State curation expression is missing an operation before # reason.", [expression]);
|
|
6412
|
+
if (!reason)
|
|
6413
|
+
stateCurationBlocked("State curation expression is missing reason text after #.", [expression]);
|
|
6414
|
+
return { command, reason };
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
stateCurationBlocked("State curation expression must include `# reason`.", [expression]);
|
|
6418
|
+
}
|
|
6419
|
+
function unquoteStateCurationExpressionString(value) {
|
|
6420
|
+
let out = "";
|
|
6421
|
+
let escaped = false;
|
|
6422
|
+
for (const ch of value) {
|
|
6423
|
+
if (escaped) {
|
|
6424
|
+
if (ch !== "\\" && ch !== "\"")
|
|
6425
|
+
stateCurationBlocked("Unsupported escape sequence in state curation expression string.", [`\\${ch}`]);
|
|
6426
|
+
out += ch;
|
|
6427
|
+
escaped = false;
|
|
6428
|
+
}
|
|
6429
|
+
else if (ch === "\\") {
|
|
6430
|
+
escaped = true;
|
|
6431
|
+
}
|
|
6432
|
+
else {
|
|
6433
|
+
out += ch;
|
|
6434
|
+
}
|
|
6435
|
+
}
|
|
6436
|
+
if (escaped)
|
|
6437
|
+
stateCurationBlocked("Unterminated escape sequence in state curation expression string.", [value]);
|
|
6438
|
+
return out;
|
|
6439
|
+
}
|
|
6440
|
+
function parseStateCurationExpression(expression) {
|
|
6441
|
+
if (expression.includes("\n") || expression.includes("\r")) {
|
|
6442
|
+
stateCurationBlocked("Each state curation expression must be a single line.", [expression]);
|
|
6443
|
+
}
|
|
6444
|
+
const { command, reason } = splitStateCurationExpressionReason(expression.trim());
|
|
6445
|
+
if (/^NOOP$/i.test(command))
|
|
6446
|
+
return null;
|
|
6447
|
+
let match = /^KEEP_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)$/i.exec(command);
|
|
6448
|
+
if (match) {
|
|
6449
|
+
return { kind: match[1].toLowerCase(), asset_id: match[2], state_id: match[3], decision: "keep", reason };
|
|
6450
|
+
}
|
|
6451
|
+
match = /^MERGE_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)\s+->\s+(\S+)$/i.exec(command);
|
|
6452
|
+
if (match) {
|
|
6453
|
+
return {
|
|
6454
|
+
kind: match[1].toLowerCase(),
|
|
6455
|
+
asset_id: match[2],
|
|
6456
|
+
state_id: match[3],
|
|
6457
|
+
decision: "merge",
|
|
6458
|
+
target_state_id: match[4],
|
|
6459
|
+
reason,
|
|
6460
|
+
};
|
|
6461
|
+
}
|
|
6462
|
+
match = /^DROP_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)$/i.exec(command);
|
|
6463
|
+
if (match) {
|
|
6464
|
+
return { kind: match[1].toLowerCase(), asset_id: match[2], state_id: match[3], decision: "drop", reason };
|
|
6465
|
+
}
|
|
6466
|
+
match = /^ACTION_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)$/i.exec(command);
|
|
6467
|
+
if (match) {
|
|
6468
|
+
return { kind: match[1].toLowerCase(), asset_id: match[2], state_id: match[3], decision: "action_desc", reason };
|
|
6469
|
+
}
|
|
6470
|
+
match = /^RENAME_STATE\s+(actor|location|prop)\s+(\S+)\s+(\S+)\s+"((?:\\.|[^"\\])*)"$/i.exec(command);
|
|
6471
|
+
if (match) {
|
|
6472
|
+
return {
|
|
6473
|
+
kind: match[1].toLowerCase(),
|
|
6474
|
+
asset_id: match[2],
|
|
6475
|
+
state_id: match[3],
|
|
6476
|
+
decision: "rename",
|
|
6477
|
+
new_name: unquoteStateCurationExpressionString(match[4]),
|
|
6478
|
+
reason,
|
|
6479
|
+
};
|
|
6480
|
+
}
|
|
6481
|
+
stateCurationBlocked("Unknown or malformed state curation expression.", [expression]);
|
|
6482
|
+
}
|
|
6483
|
+
function extractStateCurationExpressions(rawText) {
|
|
6484
|
+
const expressions = [];
|
|
6485
|
+
const re = /<exp>([\s\S]*?)<\/exp>/g;
|
|
6486
|
+
let outside = "";
|
|
6487
|
+
let lastIndex = 0;
|
|
6488
|
+
let match;
|
|
6489
|
+
while ((match = re.exec(rawText)) !== null) {
|
|
6490
|
+
outside += rawText.slice(lastIndex, match.index);
|
|
6491
|
+
expressions.push(strOf(match[1]).trim());
|
|
6492
|
+
lastIndex = match.index + match[0].length;
|
|
6493
|
+
}
|
|
6494
|
+
outside += rawText.slice(lastIndex);
|
|
6495
|
+
if (outside.trim()) {
|
|
6496
|
+
stateCurationBlocked("State curation response must contain only <exp>...</exp> lines.", [outside.trim().slice(0, 200)]);
|
|
6497
|
+
}
|
|
6498
|
+
if (expressions.length === 0) {
|
|
6499
|
+
stateCurationBlocked("State curation response did not contain any <exp> expressions.", [rawText.slice(0, 200)]);
|
|
6500
|
+
}
|
|
6501
|
+
return expressions;
|
|
6502
|
+
}
|
|
6503
|
+
export function parseStateCurationExpressions(rawText) {
|
|
6504
|
+
const decisions = [];
|
|
6505
|
+
const expressions = extractStateCurationExpressions(rawText);
|
|
6506
|
+
let noop = false;
|
|
6507
|
+
for (const expression of expressions) {
|
|
6508
|
+
const decision = parseStateCurationExpression(expression);
|
|
6509
|
+
if (decision === null)
|
|
6510
|
+
noop = true;
|
|
6511
|
+
else
|
|
6512
|
+
decisions.push(decision);
|
|
6513
|
+
}
|
|
6514
|
+
if (noop && decisions.length > 0) {
|
|
6515
|
+
stateCurationBlocked("NOOP cannot be combined with actionable state curation expressions.", [rawText.slice(0, 200)]);
|
|
6516
|
+
}
|
|
6517
|
+
return {
|
|
6518
|
+
version: 1,
|
|
6519
|
+
format: "state-curation-exp-v1",
|
|
6520
|
+
expression_text: rawText,
|
|
6521
|
+
expressions,
|
|
6522
|
+
decisions,
|
|
6523
|
+
};
|
|
6524
|
+
}
|
|
6525
|
+
function renderStateCurationDecisionExpression(decision) {
|
|
6526
|
+
const kind = strOf(decision["kind"]);
|
|
6527
|
+
const assetId = strOf(decision["asset_id"]);
|
|
6528
|
+
const stateId = strOf(decision["state_id"]);
|
|
6529
|
+
const reason = strOf(decision["reason"]).trim() || "state curation decision";
|
|
6530
|
+
const action = strOf(decision["decision"]);
|
|
6531
|
+
if (action === "keep")
|
|
6532
|
+
return `KEEP_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6533
|
+
if (action === "merge")
|
|
6534
|
+
return `MERGE_STATE ${kind} ${assetId} ${stateId} -> ${strOf(decision["target_state_id"])} # ${reason}`;
|
|
6535
|
+
if (action === "drop")
|
|
6536
|
+
return `DROP_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6537
|
+
if (action === "action_desc")
|
|
6538
|
+
return `ACTION_STATE ${kind} ${assetId} ${stateId} # ${reason}`;
|
|
6539
|
+
if (action === "rename")
|
|
6540
|
+
return `RENAME_STATE ${kind} ${assetId} ${stateId} ${quoteCurationExpressionString(decision["new_name"])} # ${reason}`;
|
|
6541
|
+
stateCurationBlocked("Cannot render unknown state curation decision.", [`decision: ${action || "<empty>"}`]);
|
|
6542
|
+
}
|
|
6543
|
+
export function formatStateCurationDecisionExpressions(plan) {
|
|
6544
|
+
const decisions = asList(plan["decisions"]).filter(isDict);
|
|
6545
|
+
if (decisions.length === 0)
|
|
6546
|
+
return "<exp>NOOP # no state curation changes</exp>";
|
|
6547
|
+
return `${decisions.map((decision) => `<exp>${renderStateCurationDecisionExpression(decision)}</exp>`).join("\n")}\n`;
|
|
6548
|
+
}
|
|
6549
|
+
function stateCurationAllSourceKeys(script) {
|
|
6550
|
+
const keys = [];
|
|
6551
|
+
for (const ref of statefulAssetsForCuration(script)) {
|
|
6552
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6553
|
+
if (!asset)
|
|
6554
|
+
continue;
|
|
6555
|
+
for (const state of asList(asset["states"])) {
|
|
6556
|
+
const stateId = strOf(state["state_id"]);
|
|
6557
|
+
if (stateId)
|
|
6558
|
+
keys.push(stateCurationSourceKey(ref.kind, ref.asset_id, stateId));
|
|
6559
|
+
}
|
|
6560
|
+
}
|
|
6561
|
+
return keys;
|
|
6562
|
+
}
|
|
6563
|
+
export function stateCurationSourceKeysForRefs(script, refs) {
|
|
6564
|
+
const keys = [];
|
|
6565
|
+
for (const ref of refs) {
|
|
6566
|
+
const asset = stateCurationAssetByRef(script, ref);
|
|
6567
|
+
if (!asset)
|
|
6568
|
+
continue;
|
|
6569
|
+
for (const state of asList(asset["states"])) {
|
|
6570
|
+
const stateId = strOf(state["state_id"]);
|
|
6571
|
+
if (stateId)
|
|
6572
|
+
keys.push(stateCurationSourceKey(ref.kind, ref.asset_id, stateId));
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
return keys;
|
|
6576
|
+
}
|
|
6577
|
+
function stateCurationDecisionMap(decisions) {
|
|
6578
|
+
const map = new Map();
|
|
6579
|
+
for (const decision of decisions) {
|
|
6580
|
+
map.set(stateCurationDecisionSourceKey(decision), decision);
|
|
6581
|
+
}
|
|
6582
|
+
return map;
|
|
6583
|
+
}
|
|
6584
|
+
export function missingStateCurationRequiredSourceKeys(script, plan, requiredKeys) {
|
|
6585
|
+
const keys = requiredKeys && requiredKeys.length > 0 ? [...requiredKeys] : stateCurationAllSourceKeys(script);
|
|
6586
|
+
const decisions = stateCurationDecisionMap(asList(plan["decisions"]).filter(isDict));
|
|
6587
|
+
return keys.filter((key) => !decisions.has(key));
|
|
6588
|
+
}
|
|
6589
|
+
function assertStateExistsForCuration(script, kind, assetId, stateId) {
|
|
6590
|
+
const asset = assertAssetExists(script, kind, assetId);
|
|
6591
|
+
for (const state of asList(asset["states"])) {
|
|
6592
|
+
if (strOf(state["state_id"]) === stateId)
|
|
6593
|
+
return state;
|
|
6594
|
+
}
|
|
6595
|
+
stateCurationBlocked("State curation decision references an unknown state.", [stateCurationSourceKey(kind, assetId, stateId)]);
|
|
6596
|
+
}
|
|
6597
|
+
function validateStateCurationDecision(script, decision) {
|
|
6598
|
+
const kind = strOf(decision["kind"]);
|
|
6599
|
+
if (!isAssetKindValue(kind))
|
|
6600
|
+
stateCurationBlocked("Unknown state curation kind.", [`kind: ${kind || "<empty>"}`]);
|
|
6601
|
+
const assetId = strOf(decision["asset_id"]);
|
|
6602
|
+
const stateId = strOf(decision["state_id"]);
|
|
6603
|
+
const action = strOf(decision["decision"]);
|
|
6604
|
+
const reason = strOf(decision["reason"]);
|
|
6605
|
+
if (!assetId || !stateId)
|
|
6606
|
+
stateCurationBlocked("State curation decision is missing asset_id/state_id.", [`${kind}:${assetId}/${stateId}`]);
|
|
6607
|
+
if (!reason)
|
|
6608
|
+
stateCurationBlocked("State curation decision is missing reason.", [`${kind}:${assetId}/${stateId}`]);
|
|
6609
|
+
assertStateExistsForCuration(script, kind, assetId, stateId);
|
|
6610
|
+
if (action === "merge") {
|
|
6611
|
+
const targetStateId = strOf(decision["target_state_id"]);
|
|
6612
|
+
if (!targetStateId || targetStateId === stateId) {
|
|
6613
|
+
stateCurationBlocked("MERGE_STATE requires a different target_state_id.", [`${kind}:${assetId}/${stateId}`]);
|
|
6614
|
+
}
|
|
6615
|
+
assertStateExistsForCuration(script, kind, assetId, targetStateId);
|
|
6616
|
+
}
|
|
6617
|
+
else if (action === "rename") {
|
|
6618
|
+
const newName = strOf(decision["new_name"]).trim();
|
|
6619
|
+
if (!newName)
|
|
6620
|
+
stateCurationBlocked("RENAME_STATE requires a non-empty new_name.", [`${kind}:${assetId}/${stateId}`]);
|
|
6621
|
+
}
|
|
6622
|
+
else if (action !== "keep" && action !== "drop" && action !== "action_desc") {
|
|
6623
|
+
stateCurationBlocked("Unknown state curation decision.", [`decision: ${action || "<empty>"}`]);
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
function normalizeStateCurationMergeTargets(decisions) {
|
|
6627
|
+
const bySource = stateCurationDecisionMap(decisions);
|
|
6628
|
+
for (const decision of decisions) {
|
|
6629
|
+
if (strOf(decision["decision"]) !== "merge")
|
|
6630
|
+
continue;
|
|
6631
|
+
const kind = strOf(decision["kind"]);
|
|
6632
|
+
const assetId = strOf(decision["asset_id"]);
|
|
6633
|
+
let targetStateId = strOf(decision["target_state_id"]);
|
|
6634
|
+
const seen = new Set([stateCurationDecisionSourceKey(decision)]);
|
|
6635
|
+
while (targetStateId) {
|
|
6636
|
+
const targetKey = stateCurationSourceKey(kind, assetId, targetStateId);
|
|
6637
|
+
if (seen.has(targetKey))
|
|
6638
|
+
stateCurationBlocked("State curation merge plan contains a cycle.", [`cycle at ${targetKey}`]);
|
|
6639
|
+
seen.add(targetKey);
|
|
6640
|
+
const targetDecision = bySource.get(targetKey);
|
|
6641
|
+
if (!targetDecision)
|
|
6642
|
+
break;
|
|
6643
|
+
const targetAction = strOf(targetDecision["decision"]);
|
|
6644
|
+
if (targetAction === "merge") {
|
|
6645
|
+
targetStateId = strOf(targetDecision["target_state_id"]);
|
|
6646
|
+
decision["target_state_id"] = targetStateId;
|
|
6647
|
+
continue;
|
|
6648
|
+
}
|
|
6649
|
+
if (targetAction === "drop" || targetAction === "action_desc") {
|
|
6650
|
+
decision["decision"] = targetAction;
|
|
6651
|
+
delete decision["target_state_id"];
|
|
6652
|
+
}
|
|
6653
|
+
break;
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
export function assertStateCurationPlan(script, plan, requiredKeys) {
|
|
6658
|
+
const rawDecisions = asList(plan["decisions"]).filter(isDict);
|
|
6659
|
+
const decisions = [...stateCurationDecisionMap(rawDecisions).values()];
|
|
6660
|
+
for (const decision of decisions)
|
|
6661
|
+
validateStateCurationDecision(script, decision);
|
|
6662
|
+
normalizeStateCurationMergeTargets(decisions);
|
|
6663
|
+
const missing = missingStateCurationRequiredSourceKeys(script, { decisions }, requiredKeys);
|
|
6664
|
+
if (missing.length > 0) {
|
|
6665
|
+
stateCurationBlocked("State curation plan is missing required state decisions.", missing.slice(0, 80));
|
|
6666
|
+
}
|
|
6667
|
+
return {
|
|
6668
|
+
...plan,
|
|
6669
|
+
decisions,
|
|
6670
|
+
};
|
|
6671
|
+
}
|
|
6672
|
+
function mapStateForCuration(kind, assetId, stateId, replacements, removed) {
|
|
6673
|
+
const id = strOf(stateId);
|
|
6674
|
+
if (!id)
|
|
6675
|
+
return null;
|
|
6676
|
+
const key = stateCurationSourceKey(kind, assetId, id);
|
|
6677
|
+
const replacement = replacements.get(key);
|
|
6678
|
+
if (replacement)
|
|
6679
|
+
return replacement;
|
|
6680
|
+
if (removed.has(key))
|
|
6681
|
+
return null;
|
|
6682
|
+
return id;
|
|
6683
|
+
}
|
|
6684
|
+
function rewriteRefsAfterStateCuration(script, replacements, removed) {
|
|
6685
|
+
let sceneRefsCleared = 0;
|
|
6686
|
+
let sceneRefsMerged = 0;
|
|
6687
|
+
let stateChangesRemoved = 0;
|
|
6688
|
+
let stateChangesRewritten = 0;
|
|
6689
|
+
for (const ep of asList(script["episodes"])) {
|
|
6690
|
+
for (const scene of asList(ep["scenes"])) {
|
|
6691
|
+
const ctx = sceneContext(scene);
|
|
6692
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
6693
|
+
const idKey = assetIdKey(kind);
|
|
6694
|
+
const refKey = assetListKey(kind);
|
|
6695
|
+
for (const ref of asList(ctx[refKey])) {
|
|
6696
|
+
if (!isDict(ref))
|
|
6697
|
+
continue;
|
|
6698
|
+
const assetId = strOf(ref[idKey]);
|
|
6699
|
+
const prior = strOf(ref["state_id"]);
|
|
6700
|
+
const next = mapStateForCuration(kind, assetId, prior, replacements, removed);
|
|
6701
|
+
if (!next && prior) {
|
|
6702
|
+
ref["state_id"] = null;
|
|
6703
|
+
sceneRefsCleared += 1;
|
|
6704
|
+
}
|
|
6705
|
+
else if (next && next !== prior) {
|
|
6706
|
+
ref["state_id"] = next;
|
|
6707
|
+
sceneRefsMerged += 1;
|
|
6708
|
+
}
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
setSceneContext(scene, ctx);
|
|
6712
|
+
for (const action of asList(scene["actions"])) {
|
|
6713
|
+
if (!isDict(action) || action["state_changes"] === undefined || action["state_changes"] === null)
|
|
6714
|
+
continue;
|
|
6715
|
+
const nextChanges = [];
|
|
6716
|
+
for (const change of asList(action["state_changes"])) {
|
|
6717
|
+
if (!isDict(change))
|
|
6718
|
+
continue;
|
|
6719
|
+
const kind = strOf(change["target_kind"]);
|
|
6720
|
+
if (!isAssetKindValue(kind))
|
|
6721
|
+
continue;
|
|
6722
|
+
const assetId = strOf(change["target_id"]);
|
|
6723
|
+
const toState = mapStateForCuration(kind, assetId, change["to_state_id"], replacements, removed);
|
|
6724
|
+
if (!toState) {
|
|
6725
|
+
stateChangesRemoved += 1;
|
|
6726
|
+
continue;
|
|
6727
|
+
}
|
|
6728
|
+
const fromState = mapStateForCuration(kind, assetId, change["from_state_id"], replacements, removed);
|
|
6729
|
+
if (fromState && fromState === toState) {
|
|
6730
|
+
stateChangesRemoved += 1;
|
|
6731
|
+
continue;
|
|
6732
|
+
}
|
|
6733
|
+
const next = { ...change, to_state_id: toState };
|
|
6734
|
+
if (fromState)
|
|
6735
|
+
next["from_state_id"] = fromState;
|
|
6736
|
+
else
|
|
6737
|
+
delete next["from_state_id"];
|
|
6738
|
+
if (toState !== strOf(change["to_state_id"]) || fromState !== strOf(change["from_state_id"]))
|
|
6739
|
+
stateChangesRewritten += 1;
|
|
6740
|
+
nextChanges.push(next);
|
|
6741
|
+
}
|
|
6742
|
+
if (nextChanges.length > 0)
|
|
6743
|
+
action["state_changes"] = nextChanges;
|
|
6744
|
+
else
|
|
6745
|
+
delete action["state_changes"];
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
return {
|
|
6750
|
+
scene_refs_cleared: sceneRefsCleared,
|
|
6751
|
+
scene_refs_merged: sceneRefsMerged,
|
|
6752
|
+
state_changes_removed: stateChangesRemoved,
|
|
6753
|
+
state_changes_rewritten: stateChangesRewritten,
|
|
6754
|
+
};
|
|
6755
|
+
}
|
|
6756
|
+
export function applyStateCurationPlan(script, rawPlan) {
|
|
6757
|
+
const plan = assertStateCurationPlan(script, rawPlan);
|
|
6758
|
+
const decisions = asList(plan["decisions"]).filter(isDict);
|
|
6759
|
+
const beforeByKind = { actor: 0, location: 0, prop: 0 };
|
|
6760
|
+
const afterByKind = { actor: 0, location: 0, prop: 0 };
|
|
6761
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
6762
|
+
for (const asset of asList(script[assetListKey(kind)]))
|
|
6763
|
+
beforeByKind[kind] += asList(asset["states"]).filter(isDict).length;
|
|
6764
|
+
}
|
|
6765
|
+
const replacements = new Map();
|
|
6766
|
+
const removed = new Set();
|
|
6767
|
+
const decisionSummary = {
|
|
6768
|
+
keep: 0,
|
|
6769
|
+
merge: 0,
|
|
6770
|
+
drop: 0,
|
|
6771
|
+
action_desc: 0,
|
|
6772
|
+
rename: 0,
|
|
6773
|
+
};
|
|
6774
|
+
for (const decision of decisions) {
|
|
6775
|
+
const kind = strOf(decision["kind"]);
|
|
6776
|
+
const assetId = strOf(decision["asset_id"]);
|
|
6777
|
+
const stateId = strOf(decision["state_id"]);
|
|
6778
|
+
const key = stateCurationSourceKey(kind, assetId, stateId);
|
|
6779
|
+
const action = strOf(decision["decision"]);
|
|
6780
|
+
decisionSummary[action] = Number(decisionSummary[action] ?? 0) + 1;
|
|
6781
|
+
if (action === "rename") {
|
|
6782
|
+
const state = assertStateExistsForCuration(script, kind, assetId, stateId);
|
|
6783
|
+
state["state_name"] = strOf(decision["new_name"]).trim();
|
|
6784
|
+
}
|
|
6785
|
+
else if (action === "merge") {
|
|
6786
|
+
replacements.set(key, strOf(decision["target_state_id"]));
|
|
6787
|
+
removed.add(key);
|
|
6788
|
+
}
|
|
6789
|
+
else if (action === "drop" || action === "action_desc") {
|
|
6790
|
+
removed.add(key);
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
const rewriteSummary = rewriteRefsAfterStateCuration(script, replacements, removed);
|
|
6794
|
+
for (const kind of ["actor", "location", "prop"]) {
|
|
6795
|
+
for (const asset of asList(script[assetListKey(kind)])) {
|
|
6796
|
+
if (!isDict(asset))
|
|
6797
|
+
continue;
|
|
6798
|
+
const assetId = strOf(asset[assetIdKey(kind)]);
|
|
6799
|
+
asset["states"] = asList(asset["states"]).filter((state) => {
|
|
6800
|
+
if (!isDict(state))
|
|
6801
|
+
return false;
|
|
6802
|
+
const key = stateCurationSourceKey(kind, assetId, strOf(state["state_id"]));
|
|
6803
|
+
return !removed.has(key);
|
|
6804
|
+
});
|
|
6805
|
+
afterByKind[kind] += asList(asset["states"]).filter(isDict).length;
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
const statesBefore = beforeByKind.actor + beforeByKind.location + beforeByKind.prop;
|
|
6809
|
+
const statesAfter = afterByKind.actor + afterByKind.location + afterByKind.prop;
|
|
6810
|
+
return {
|
|
6811
|
+
version: 1,
|
|
6812
|
+
format: "state-curation-exp-v1",
|
|
6813
|
+
summary: {
|
|
6814
|
+
states_before: statesBefore,
|
|
6815
|
+
states_after: statesAfter,
|
|
6816
|
+
states_removed: statesBefore - statesAfter,
|
|
6817
|
+
actor_states_before: beforeByKind.actor,
|
|
6818
|
+
actor_states_after: afterByKind.actor,
|
|
6819
|
+
location_states_before: beforeByKind.location,
|
|
6820
|
+
location_states_after: afterByKind.location,
|
|
6821
|
+
prop_states_before: beforeByKind.prop,
|
|
6822
|
+
prop_states_after: afterByKind.prop,
|
|
6823
|
+
decisions: decisionSummary,
|
|
6824
|
+
...rewriteSummary,
|
|
6825
|
+
},
|
|
6826
|
+
decisions,
|
|
6827
|
+
};
|
|
6828
|
+
}
|
|
6829
|
+
// ---------------------------------------------------------------------------
|
|
6174
6830
|
// State binding
|
|
6175
6831
|
// ---------------------------------------------------------------------------
|
|
6176
6832
|
function stateBindingBlocked(message, received) {
|