import { sha256Text, stableCinemaJson, } from './cinema-shot-compiler.js'; import type { CinemaCharacterSheetArtifact, CinemaContractIssue, CinemaContractValidation, CinemaGenerationLedgerArtifact, CinemaReferencePackArtifact, CinemaShotManifestArtifact, CinemaWalkingSkeletonBundle, } from './cinema-types.js'; const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/; function result(issues: CinemaContractIssue[]): CinemaContractValidation { return { ok: issues.length === 0, issues }; } function add(issues: CinemaContractIssue[], code: string, path: string, message: string): void { issues.push({ code, path, message }); } function requireText( issues: CinemaContractIssue[], value: string, path: string, code = 'required-text', ): void { if (value.trim().length === 0) add(issues, code, path, 'must be a non-empty string'); } function requireHash(issues: CinemaContractIssue[], value: string, path: string): void { if (!SHA256_PATTERN.test(value)) add(issues, 'invalid-content-hash', path, 'must be a lowercase sha256 content hash'); } function duplicates(values: string[]): string[] { const seen = new Set(); return [...new Set(values.filter((value) => seen.has(value) || !seen.add(value)))]; } function requireUnique(issues: CinemaContractIssue[], values: string[], path: string): void { const repeated = duplicates(values); if (repeated.length > 0) add(issues, 'duplicate-id', path, `contains duplicate values: ${repeated.join(', ')}`); } export function validateCinemaCharacterSheet( sheet: CinemaCharacterSheetArtifact, ): CinemaContractValidation { const issues: CinemaContractIssue[] = []; requireText(issues, sheet.projectSlug, 'projectSlug'); requireText(issues, sheet.characterVersionId, 'characterVersionId'); requireText(issues, sheet.characterName, 'characterName'); requireText(issues, sheet.identityAnchor.assetId, 'identityAnchor.assetId'); requireText(issues, sheet.identityAnchor.path, 'identityAnchor.path'); requireHash(issues, sheet.identityAnchor.contentHash, 'identityAnchor.contentHash'); if (sheet.identityAnchor.identityBearingFaceCount !== 1 || sheet.identityAnchor.locked !== true) { add(issues, 'identity-anchor-unlocked', 'identityAnchor', 'must be a locked single-face identity anchor'); } if (sheet.bodyPlates.length !== 2) { add(issues, 'body-plate-count', 'bodyPlates', 'must contain exactly front and back headless body plates'); } const views = sheet.bodyPlates.map((plate) => plate.view); if (!(views.includes('front') && views.includes('back')) || new Set(views).size !== 2) { add(issues, 'body-plate-views', 'bodyPlates', 'must contain exactly one front and one back view'); } sheet.bodyPlates.forEach((plate, index) => { const path = `bodyPlates[${index}]`; requireText(issues, plate.assetId, `${path}.assetId`); requireText(issues, plate.path, `${path}.path`); requireHash(issues, plate.contentHash, `${path}.contentHash`); requireText(issues, plate.maskArtifactId, `${path}.maskArtifactId`); requireHash(issues, plate.garmentBoundaryEvidence.maskContentHash, `${path}.garmentBoundaryEvidence.maskContentHash`); requireUnique(issues, plate.garmentBoundaryEvidence.requiredFeatures, `${path}.garmentBoundaryEvidence.requiredFeatures`); requireUnique(issues, plate.garmentBoundaryEvidence.preservedFeatures, `${path}.garmentBoundaryEvidence.preservedFeatures`); requireUnique(issues, plate.garmentBoundaryEvidence.evidenceIds, `${path}.garmentBoundaryEvidence.evidenceIds`); if (plate.garmentBoundaryEvidence.requiredFeatures.length === 0 || plate.garmentBoundaryEvidence.evidenceIds.length === 0) { add(issues, 'garment-boundary-evidence', `${path}.garmentBoundaryEvidence`, 'requires protected features and evidence'); } const missingFeatures = plate.garmentBoundaryEvidence.requiredFeatures.filter((feature) => !plate.garmentBoundaryEvidence.preservedFeatures.includes(feature)); if (missingFeatures.length > 0) add(issues, 'garment-feature-damage', `${path}.garmentBoundaryEvidence.preservedFeatures`, `missing protected features: ${missingFeatures.join(', ')}`); if (plate.headTreatment !== 'removed' || plate.identityBearingFaceCount !== 0 || plate.faceFragmentCount !== 0) { add(issues, 'body-plate-face-leak', path, 'must be fully headless with zero face fragments'); } if (plate.garmentBoundaryStatus !== 'pass') { add(issues, 'garment-boundary-failed', `${path}.garmentBoundaryStatus`, 'must pass garment-boundary QC'); } }); const sourceIds = [sheet.identityAnchor.assetId, ...sheet.bodyPlates.map((plate) => plate.assetId)]; requireUnique(issues, sourceIds, 'characterSheet.sourceAssetIds'); requireUnique( issues, [sheet.identityAnchor.contentHash, ...sheet.bodyPlates.map((plate) => plate.contentHash)], 'characterSheet.sourceContentHashes', ); requireText(issues, sheet.compositeSheet.assetId, 'compositeSheet.assetId'); requireText(issues, sheet.compositeSheet.path, 'compositeSheet.path'); requireHash(issues, sheet.compositeSheet.contentHash, 'compositeSheet.contentHash'); if (sheet.compositeSheet.template !== 'single-face-front-back-v1') { add(issues, 'invalid-sheet-template', 'compositeSheet.template', 'must use the single-face front/back template'); } if (sheet.compositeSheet.identityBearingFaceCount !== 1) { add(issues, 'composite-face-count', 'compositeSheet.identityBearingFaceCount', 'must contain exactly one identity-bearing face'); } requireUnique(issues, sheet.compositeSheet.componentAssetIds, 'compositeSheet.componentAssetIds'); const expectedComponents = [...sourceIds].sort(); const actualComponents = [...sheet.compositeSheet.componentAssetIds].sort(); if (expectedComponents.length !== actualComponents.length || expectedComponents.some((id, index) => id !== actualComponents[index])) { add(issues, 'composite-components', 'compositeSheet.componentAssetIds', 'must reference only the locked face and both headless body plates'); } if (sheet.qc.status !== 'pass') add(issues, 'qc-not-passed', 'qc.status', 'automated QC must pass'); if (sheet.qc.issues.length > 0) add(issues, 'qc-open-issues', 'qc.issues', 'must be empty before use'); if (sheet.qc.humanReview.status !== 'approved') { add(issues, 'human-review-not-approved', 'qc.humanReview.status', 'human review must approve the character version'); } requireText(issues, sheet.qc.humanReview.reviewerId, 'qc.humanReview.reviewerId'); if (sheet.qc.humanReview.evidenceIds.length === 0) { add(issues, 'missing-review-evidence', 'qc.humanReview.evidenceIds', 'must contain review evidence'); } return result(issues); } export function validateCinemaReferencePack( pack: CinemaReferencePackArtifact, ): CinemaContractValidation { const issues: CinemaContractIssue[] = []; requireText(issues, pack.projectSlug, 'projectSlug'); requireText(issues, pack.packId, 'packId'); requireText(issues, pack.shotId, 'shotId'); if (pack.references.length === 0) add(issues, 'empty-reference-pack', 'references', 'must contain at least one reference'); if (pack.referenceBudget.actual !== pack.references.length) { add(issues, 'reference-budget-count', 'referenceBudget.actual', 'must equal the number of references'); } if (!pack.referenceBudget.withinLimit || pack.referenceBudget.actual > pack.referenceBudget.maximum) { add(issues, 'reference-budget-exceeded', 'referenceBudget', 'reference count must remain within its declared maximum'); } requireUnique(issues, pack.references.map((reference) => reference.assetId), 'references.assetId'); const faceCounts = new Map(); const states = new Map>(); pack.references.forEach((reference, index) => { const path = `references[${index}]`; requireText(issues, reference.assetId, `${path}.assetId`); requireText(issues, reference.path, `${path}.path`); requireText(issues, reference.subjectId, `${path}.subjectId`); requireHash(issues, reference.contentHash, `${path}.contentHash`); const characterRole = ['identity_face', 'body_front_headless', 'body_back_headless', 'wardrobe', 'silhouette'].includes(reference.role); if (characterRole && !reference.characterVersionId) { add(issues, 'missing-character-version', `${path}.characterVersionId`, `${reference.role} must bind an immutable character version`); } if (reference.role === 'identity_face' && reference.identityBearingFaceCount !== 1) { add(issues, 'identity-face-count', `${path}.identityBearingFaceCount`, 'identity_face must be the single identity-bearing face source'); } if (reference.identityVariant && reference.role !== 'identity_face') { add(issues, 'identity-variant-role', `${path}.identityVariant`, 'identity variants are valid only on identity_face references'); } if ((reference.role === 'body_front_headless' || reference.role === 'body_back_headless') && reference.identityBearingFaceCount !== 0) { add(issues, 'headless-body-face-leak', `${path}.identityBearingFaceCount`, 'headless body references cannot carry a face'); } if (reference.characterVersionId) { faceCounts.set( reference.characterVersionId, (faceCounts.get(reference.characterVersionId) ?? 0) + reference.identityBearingFaceCount, ); } if (reference.state) { requireText(issues, reference.state.group, `${path}.state.group`); requireText(issues, reference.state.value, `${path}.state.value`); const key = `${reference.subjectId}\u0000${reference.state.group}`; const values = states.get(key) ?? new Set(); values.add(reference.state.value); states.set(key, values); } }); for (const [characterVersionId, count] of faceCounts) { if (count !== 1) { add(issues, 'character-face-source-count', `references.characterVersionId=${characterVersionId}`, `must resolve to exactly one identity-bearing face source; found ${count}`); } } for (const [key, valuesSet] of states) { const [subjectId, stateGroup] = key.split('\u0000'); const values = [...valuesSet]; if (values.length <= 1) continue; const matching = pack.transitions.filter((transition) => transition.subjectId === subjectId && transition.stateGroup === stateGroup && values.includes(transition.startValue) && values.includes(transition.endValue) && transition.startValue !== transition.endValue, ); if (values.length !== 2 || matching.length !== 1) { add(issues, 'ambiguous-reference-state', `references.state.${subjectId}.${stateGroup}`, 'conflicting states require exactly one explicit two-state mechanical transition'); } } pack.transitions.forEach((transition, index) => { const path = `transitions[${index}]`; [transition.subjectId, transition.stateGroup, transition.startValue, transition.endValue, transition.trigger, transition.direction, transition.interaction].forEach((value, fieldIndex) => { const fields = ['subjectId', 'stateGroup', 'startValue', 'endValue', 'trigger', 'direction', 'interaction']; requireText(issues, value, `${path}.${fields[fieldIndex]}`); }); if (transition.startValue === transition.endValue) add(issues, 'transition-no-change', path, 'must change state'); if (transition.orderedSteps.length === 0 || transition.orderedSteps.some((step) => step.trim().length === 0)) { add(issues, 'transition-steps', `${path}.orderedSteps`, 'must contain explicit ordered steps'); } if (transition.startSeconds < 0 || transition.endSeconds <= transition.startSeconds) { add(issues, 'transition-timing', path, 'must have an increasing non-negative time range'); } }); return result(issues); } export function validateCinemaShotManifest( manifest: CinemaShotManifestArtifact, ): CinemaContractValidation { const issues: CinemaContractIssue[] = []; [manifest.projectSlug, manifest.shotId, manifest.referencePackId].forEach((value, index) => { requireText(issues, value, ['projectSlug', 'shotId', 'referencePackId'][index]); }); if (manifest.sceneIndex < 0 || !Number.isInteger(manifest.sceneIndex)) add(issues, 'scene-index', 'sceneIndex', 'must be a non-negative integer'); if (manifest.ordinal < 1 || !Number.isInteger(manifest.ordinal)) add(issues, 'shot-ordinal', 'ordinal', 'must be a positive integer'); if (!(manifest.durationSeconds > 0)) add(issues, 'shot-duration', 'durationSeconds', 'must be greater than zero'); [manifest.story.beatId, manifest.story.scenePurpose, manifest.story.characterObjective, manifest.story.expectedChange] .forEach((value, index) => requireText(issues, value, `story.${['beatId', 'scenePurpose', 'characterObjective', 'expectedChange'][index]}`)); requireText(issues, manifest.visualLaw.prefix, 'visualLaw.prefix'); requireHash(issues, manifest.visualLaw.prefixHash, 'visualLaw.prefixHash'); if (manifest.visualLaw.prefixHash !== sha256Text(manifest.visualLaw.prefix)) { add(issues, 'visual-law-hash-mismatch', 'visualLaw.prefixHash', 'must match the exact visual-law prefix'); } manifest.visualLaw.overrides.forEach((override, index) => { [override.field, override.from, override.to, override.reason].forEach((value, fieldIndex) => requireText(issues, value, `visualLaw.overrides[${index}].${['field', 'from', 'to', 'reason'][fieldIndex]}`)); }); [manifest.camera.position, manifest.camera.movement].forEach((value, index) => requireText(issues, value, `camera.${['position', 'movement'][index]}`)); if (manifest.actionBeats.length === 0) add(issues, 'empty-action-timeline', 'actionBeats', 'must contain timed action beats'); const ordered = [...manifest.actionBeats].sort((left, right) => left.order - right.order); ordered.forEach((beat, index) => { const path = `actionBeats[order=${beat.order}]`; if (beat.order !== index + 1) add(issues, 'action-order-gap', `${path}.order`, 'orders must be contiguous from 1'); requireText(issues, beat.description, `${path}.description`); if (beat.startSeconds < 0 || beat.endSeconds <= beat.startSeconds || beat.endSeconds > manifest.durationSeconds) { add(issues, 'action-timing', path, 'must be increasing, non-negative, and within shot duration'); } if (index > 0 && beat.startSeconds < ordered[index - 1].endSeconds) { add(issues, 'action-overlap', path, 'must not overlap the previous action beat'); } }); requireUnique(issues, manifest.occupancy.characterIds, 'occupancy.characterIds'); requireUnique(issues, manifest.occupancy.vehicleIds, 'occupancy.vehicleIds'); requireUnique(issues, manifest.occupancy.propIds, 'occupancy.propIds'); if (manifest.audio.intentionalSilence) { const modes = [manifest.audio.music, manifest.audio.dialogue, manifest.audio.sfx, manifest.audio.ambience]; if (modes.some((mode) => mode !== 'forbidden')) { add(issues, 'silence-audio-conflict', 'audio', 'intentional silence requires every audio mode to be forbidden'); } } requireText(issues, manifest.compiledPrompt.text, 'compiledPrompt.text'); requireHash(issues, manifest.compiledPrompt.contentHash, 'compiledPrompt.contentHash'); if (manifest.compiledPrompt.contentHash !== sha256Text(manifest.compiledPrompt.text)) { add(issues, 'prompt-hash-mismatch', 'compiledPrompt.contentHash', 'must match the compiled prompt text'); } requireHash(issues, manifest.providerParameters.contentHash, 'providerParameters.contentHash'); if (manifest.providerParameters.contentHash !== sha256Text(stableCinemaJson(manifest.providerParameters.values))) { add(issues, 'provider-parameters-hash-mismatch', 'providerParameters.contentHash', 'must match canonical provider parameters'); } return result(issues); } export function validateCinemaGenerationLedger( ledger: CinemaGenerationLedgerArtifact, ): CinemaContractValidation { const issues: CinemaContractIssue[] = []; requireText(issues, ledger.projectSlug, 'projectSlug'); requireUnique(issues, ledger.attempts.map((attempt) => attempt.attemptId), 'attempts.attemptId'); const outcomes = ledger.outcomes ?? []; requireUnique(issues, outcomes.map((outcome) => outcome.outcomeId), 'outcomes.outcomeId'); requireUnique(issues, outcomes.map((outcome) => outcome.attemptId), 'outcomes.attemptId'); requireUnique(issues, ledger.promotions.map((promotion) => promotion.promotionId), 'promotions.promotionId'); const attempts = new Map(ledger.attempts.map((attempt) => [attempt.attemptId, attempt])); const promotions = new Map(ledger.promotions.map((promotion) => [promotion.promotionId, promotion])); const shotPayloads = new Set(); ledger.attempts.forEach((attempt, index) => { const path = `attempts[${index}]`; [attempt.attemptId, attempt.shotId, attempt.routeId].forEach((value, fieldIndex) => requireText(issues, value, `${path}.${['attemptId', 'shotId', 'routeId'][fieldIndex]}`)); requireHash(issues, attempt.payloadHash, `${path}.payloadHash`); const payloadKey = `${attempt.shotId}\u0000${attempt.payloadHash}`; if (shotPayloads.has(payloadKey)) add(issues, 'duplicate-attempt-payload', `${path}.payloadHash`, 'same shot cannot record the same immutable payload twice'); shotPayloads.add(payloadKey); if (attempt.estimatedCost.amount < 0 || attempt.actualCost.amount < 0) add(issues, 'negative-cost', path, 'costs cannot be negative'); if (attempt.estimatedCost.currency !== attempt.actualCost.currency) add(issues, 'cost-currency-mismatch', path, 'estimated and actual cost currencies must match'); if (attempt.parentAttemptId) { const parent = attempts.get(attempt.parentAttemptId); if (!parent) add(issues, 'missing-parent-attempt', `${path}.parentAttemptId`, 'must reference an earlier immutable attempt'); else { if (parent.shotId !== attempt.shotId) add(issues, 'parent-shot-mismatch', `${path}.parentAttemptId`, 'parent and child must belong to the same shot'); if (parent.payloadHash === attempt.payloadHash) add(issues, 'retry-payload-unchanged', `${path}.payloadHash`, 'a retry must compile to a changed payload'); } if (attempt.changedVariables.length === 0) add(issues, 'retry-change-unrecorded', `${path}.changedVariables`, 'a child attempt must declare changed variables'); } else if (attempt.changedVariables.length > 0) { add(issues, 'root-attempt-changes', `${path}.changedVariables`, 'a root attempt cannot claim retry changes'); } }); outcomes.forEach((outcome, index) => { const path = `outcomes[${index}]`; const attempt = attempts.get(outcome.attemptId); [outcome.outcomeId, outcome.attemptId, outcome.shotId, outcome.queueTaskId, outcome.providerJobId, outcome.media.path] .forEach((value, fieldIndex) => requireText(issues, value, `${path}.${['outcomeId', 'attemptId', 'shotId', 'queueTaskId', 'providerJobId', 'media.path'][fieldIndex]}`)); [outcome.executionHash, outcome.payloadHash, outcome.media.contentHash, outcome.contentHash] .forEach((value, fieldIndex) => requireHash(issues, value, `${path}.${['executionHash', 'payloadHash', 'media.contentHash', 'contentHash'][fieldIndex]}`)); if (outcome.status !== 'completed') add(issues, 'outcome-status', `${path}.status`, 'must equal completed'); if (Number.isNaN(Date.parse(outcome.observedAt))) add(issues, 'outcome-time', `${path}.observedAt`, 'must be an ISO timestamp'); if (!Number.isInteger(outcome.media.byteLength) || outcome.media.byteLength <= 0) add(issues, 'outcome-bytes', `${path}.media.byteLength`, 'must be a positive integer'); if (outcome.actualCost.amount < 0) add(issues, 'negative-cost', `${path}.actualCost`, 'cost cannot be negative'); if (outcome.evidenceIds.length === 0) add(issues, 'outcome-evidence', `${path}.evidenceIds`, 'completion requires evidence'); if (new Set(outcome.evidenceIds).size !== outcome.evidenceIds.length) add(issues, 'duplicate-evidence', `${path}.evidenceIds`, 'must be unique'); if (!attempt) add(issues, 'missing-outcome-attempt', `${path}.attemptId`, 'must reference an immutable attempt'); else { if (attempt.shotId !== outcome.shotId) add(issues, 'outcome-shot-mismatch', `${path}.shotId`, 'must match the attempt shot'); if (attempt.payloadHash !== outcome.payloadHash) add(issues, 'outcome-payload-mismatch', `${path}.payloadHash`, 'must match the attempt payload'); if (attempt.providerJobId && attempt.providerJobId !== outcome.providerJobId) add(issues, 'outcome-provider-mismatch', `${path}.providerJobId`, 'must match the submitted provider job'); if (attempt.estimatedCost.currency !== outcome.actualCost.currency) add(issues, 'outcome-currency-mismatch', `${path}.actualCost.currency`, 'must match the attempt currency'); } const { contentHash: _contentHash, ...hashInput } = outcome; if (outcome.contentHash !== sha256Text(stableCinemaJson(hashInput))) add(issues, 'outcome-content-hash', `${path}.contentHash`, 'must match canonical outcome content'); }); ledger.promotions.forEach((promotion, index) => { const path = `promotions[${index}]`; const attempt = attempts.get(promotion.attemptId); if (!attempt) add(issues, 'missing-promoted-attempt', `${path}.attemptId`, 'must reference an existing attempt'); else { if (attempt.shotId !== promotion.shotId) add(issues, 'promotion-shot-mismatch', `${path}.attemptId`, 'promotion and attempt must belong to the same shot'); const completedByOutcome = outcomes.some((outcome) => outcome.attemptId === attempt.attemptId && outcome.status === 'completed'); if (!['completed', 'dry-run-complete'].includes(attempt.status) && !completedByOutcome) add(issues, 'attempt-not-promotable', `${path}.attemptId`, 'only completed attempts can be promoted'); } if (promotion.evidenceIds.length === 0) add(issues, 'promotion-evidence-missing', `${path}.evidenceIds`, 'promotion requires review evidence'); if (promotion.storyPurposeVerified !== true) add(issues, 'story-purpose-unverified', `${path}.storyPurposeVerified`, 'promotion requires story-purpose verification'); if (promotion.supersedesPromotionId) { const prior = promotions.get(promotion.supersedesPromotionId); if (!prior) add(issues, 'missing-superseded-promotion', `${path}.supersedesPromotionId`, 'must reference an existing promotion'); else if (prior.shotId !== promotion.shotId) add(issues, 'superseded-shot-mismatch', `${path}.supersedesPromotionId`, 'can supersede only a promotion for the same shot'); } }); const superseded = new Set(ledger.promotions.map((promotion) => promotion.supersedesPromotionId).filter((id): id is string => id !== null)); const activeCounts = new Map(); ledger.promotions.filter((promotion) => !superseded.has(promotion.promotionId)).forEach((promotion) => { activeCounts.set(promotion.shotId, (activeCounts.get(promotion.shotId) ?? 0) + 1); }); for (const [shotId, count] of activeCounts) { if (count !== 1) add(issues, 'multiple-active-promotions', `promotions.shotId=${shotId}`, `must have one active promotion; found ${count}`); } return result(issues); } function mergeIssues( destination: CinemaContractIssue[], prefix: string, validation: CinemaContractValidation, ): void { destination.push(...validation.issues.map((issue) => ({ ...issue, path: `${prefix}.${issue.path}` }))); } export function validateCinemaWalkingSkeletonBundle( bundle: CinemaWalkingSkeletonBundle, ): CinemaContractValidation { const issues: CinemaContractIssue[] = []; bundle.characterSheets.forEach((sheet, index) => mergeIssues(issues, `characterSheets[${index}]`, validateCinemaCharacterSheet(sheet))); bundle.referencePacks.forEach((pack, index) => mergeIssues(issues, `referencePacks[${index}]`, validateCinemaReferencePack(pack))); bundle.shotManifests.forEach((manifest, index) => mergeIssues(issues, `shotManifests[${index}]`, validateCinemaShotManifest(manifest))); mergeIssues(issues, 'ledger', validateCinemaGenerationLedger(bundle.ledger)); if (bundle.characterSheets.length !== 1) add(issues, 'fixture-character-count', 'characterSheets', 'walking skeleton must use exactly one synthetic character'); if (bundle.referencePacks.length !== 3) add(issues, 'fixture-pack-count', 'referencePacks', 'walking skeleton must contain exactly three reference packs'); if (bundle.shotManifests.length !== 3) add(issues, 'fixture-shot-count', 'shotManifests', 'walking skeleton must contain exactly three shot manifests'); const projects = new Set([ ...bundle.characterSheets.map((sheet) => sheet.projectSlug), ...bundle.referencePacks.map((pack) => pack.projectSlug), ...bundle.shotManifests.map((manifest) => manifest.projectSlug), bundle.ledger.projectSlug, ]); if (projects.size !== 1) add(issues, 'cross-project-bundle', 'projectSlug', 'all artifacts must belong to one project'); requireUnique(issues, bundle.shotManifests.map((manifest) => manifest.shotId), 'shotManifests.shotId'); const sheets = new Set(bundle.characterSheets.map((sheet) => sheet.characterVersionId)); const packs = new Map(bundle.referencePacks.map((pack) => [pack.packId, pack])); const shots = new Set(bundle.shotManifests.map((manifest) => manifest.shotId)); bundle.referencePacks.forEach((pack, packIndex) => pack.references.forEach((reference, referenceIndex) => { if (reference.characterVersionId && !sheets.has(reference.characterVersionId)) { add(issues, 'unresolved-character-version', `referencePacks[${packIndex}].references[${referenceIndex}].characterVersionId`, 'must resolve to an approved character sheet'); } })); bundle.shotManifests.forEach((manifest, index) => { const pack = packs.get(manifest.referencePackId); if (!pack || pack.shotId !== manifest.shotId) add(issues, 'shot-pack-binding', `shotManifests[${index}].referencePackId`, 'must resolve to the reference pack for the same shot'); }); bundle.ledger.attempts.forEach((attempt, index) => { if (!shots.has(attempt.shotId)) add(issues, 'attempt-shot-unresolved', `ledger.attempts[${index}].shotId`, 'must resolve to a manifest'); if (attempt.routeId !== 'fixture-dry-run' || attempt.status !== 'dry-run-complete') { add(issues, 'fixture-provider-execution', `ledger.attempts[${index}]`, 'walking skeleton attempts must use the non-network dry-run route'); } if (attempt.estimatedCost.amount !== 0 || attempt.actualCost.amount !== 0) { add(issues, 'fixture-cost', `ledger.attempts[${index}]`, 'walking skeleton must record zero estimated and actual cost'); } }); if (!bundle.ledger.attempts.some((attempt) => attempt.parentAttemptId !== null && attempt.changedVariables.length > 0)) { add(issues, 'fixture-retry-missing', 'ledger.attempts', 'walking skeleton must prove a controlled changed-variable retry'); } const superseded = new Set(bundle.ledger.promotions.map((promotion) => promotion.supersedesPromotionId).filter((id): id is string => id !== null)); const active = bundle.ledger.promotions.filter((promotion) => !superseded.has(promotion.promotionId)); if (active.length !== 3 || new Set(active.map((promotion) => promotion.shotId)).size !== 3) { add(issues, 'fixture-promotions', 'ledger.promotions', 'walking skeleton must have one active evidence-bearing promotion per shot'); } return result(issues); }