/** * prompt-policy.ts — how `buildExecutionPayload` treats one packet's prompt. * * Default (`promptPolicy` omitted or `'composed'`): the runtime composes the * final prompt — it resolves `@Name` tags (substituting the descriptor AND * attaching the tagged reference), injects or strips Flow markers for the route, * appends costume and scale clauses, and appends the standing render rules. * * `'exact'`: the packet's `promptText` is submitted BYTE FOR BYTE and collects * no tag references. It is the tool's answer to working-discipline rule 5: an * `@tag` once rewrote an approved prompt and hijacked its references, and the * only guard was a human remembering to dry-run the payload. The Flow character * slot PLAN still runs for an exact packet — registered characters attach and * the unlocked-identity refusal still applies — only the text injection is * withheld, so the author writes any `@character_N` marker by hand. * * WHAT "EXACT" COVERS — THE PROMPT. The text is not rewritten by the payload * builder, by continuity feedback (`--continuity-feedback`), by the Flow * moderation-retry softener, by the content-violation retry that re-submits a * SANITISED prompt (the paid Seedance transport and Dreamina), or by Dreamina's * marker auto-prepend. In each case the render fails as written instead. Whitespace at * the very ends is trimmed, and a transport still ENCODES the text for its wire * format (Flow's `[scene_N]` framing; the free Seedance engine compiles * `@imageN` to the provider's tokens and appends a citation token for any * attached media the text does not cite, without which the provider silently * ignores that media); those are encodings, not rewrites. * * WHAT IT DOES NOT COVER — THE REFERENCE LIST. An exact packet collects no * reference from an `@tag`, which is the hijack this exists to stop. But the * registries still attach what the ROUTE needs, exactly as for any packet: a * scene's cast resolves to its Seedance Asset Library avatars (which REPLACE * the packet's raw image paths on `seedance-direct` — a raw portrait trips the * real-person filter), show-bible references, a bound voice-clone clip, and a * chain seed for a chained scene. The reference list an approver must read is * the one in the dry-run's `artifacts/run-contract.json`, which freezes the * submitted paths, the slot plan and the bytes behind them. * * The cost is the operator's and is said out loud, once per exact scene here and * on every exact packet by `prompt-lint` (`exact-prompt-policy`): nothing is * appended, so the no-speech / natural-motion rules and any costume lock must * already be in the text. */ import { VclawError } from './errors.js'; import type { FilmmakingSeedancePacket } from './filmmaking-prompts.js'; /** True for an exact packet; warns (stderr, never fatal — stdout stays clean JSON). */ export function isExactPromptPacket( packet: Pick | undefined, sceneIndex: number, warn: (line: string) => void = (line) => { console.warn(line); }, ): boolean { if (packet?.promptPolicy !== 'exact') return false; warn( `Scene ${sceneIndex} uses promptPolicy "exact": its prompt is submitted as written — @tags are not resolved, ` + 'and no costume/scale clauses or standing render rules are appended.', ); return true; } /** * Checked for EVERY packet in the artifact, not only the scenes this run * renders: a pending exact packet on scene 7 also blocks a produce of scene 0. * * A packet that is not execution-ready is simply not used: the scene falls back * to the storyboard prompt, COMPOSED. For an exact packet that is the opposite of * what its author asked for, and it happened without a word, so refuse instead. */ export function assertExactPacketIsUsable( packet: Pick, ): void { if (packet.promptPolicy !== 'exact') return; const pending = packet.references.filter((ref) => ref.status !== 'ready' || !ref.path?.trim()).map((ref) => ref.slot); const why = !packet.promptText.trim() ? 'its promptText is empty' : `reference slot(s) ${pending.join(', ') || '(none listed)'} are not ready with a path`; throw new VclawError( 'execution_blocked_by_readiness', `Scene ${packet.sceneIndex} declares promptPolicy "exact" but its packet cannot be used: ${why}. ` + 'An unusable packet falls back to the storyboard prompt, composed by the runtime — the opposite of "exact". ' + 'Make every reference ready, or remove promptPolicy from that packet.', { sceneIndex: packet.sceneIndex, pendingSlots: pending }, ); } /** True (and says so) when continuity feedback must leave this task's prompt alone. */ export function warnExactSkipsContinuity( task: { sceneIndex: number; promptPolicy?: 'exact' }, warn: (line: string) => void = (line) => { console.warn(line); }, ): boolean { if (task.promptPolicy !== 'exact') return false; warn(`[continuity] scene ${task.sceneIndex} uses promptPolicy "exact": continuity feedback is not applied to it.`); return true; }