{"version":3,"file":"mission-contract-schema.d.ts","sourceRoot":"","sources":["../../../src/core/long-horizon/mission-contract-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAoBN,KAAK,gBAAgB,EAErB,MAAM,YAAY,CAAC;AAcpB,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAmHxE;AAkqBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CAC1C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,GACtC,MAAM,EAAE,CAgBV;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAC9C,MAAM,EAAE,aAAa,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjD,CAAC,EACF,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,GACrC,MAAM,EAAE,CAkDV","sourcesContent":["/**\n * Mission Contract v1 schema validation.\n *\n * Deterministic, fail-closed validation of MissionContractV1 documents.\n * Rejects duplicates, cycles, unknown refs, missing rationale, and\n * UNKNOWN SEMANTIC FIELDS.\n *\n * Only `metadata` may contain arbitrary unknown keys.\n */\n\nimport {\n\tACCEPTANCE_CRITERION_KEYS,\n\ttype AcceptanceCriterion,\n\tCONSTRAINT_KEYS,\n\tEVIDENCE_POLICY_KEYS,\n\tEVIDENCE_POLICY_RULE_KEYS,\n\tEVIDENCE_REQUIREMENT_KEYS,\n\tFORBIDDEN_ACTION_KEYS,\n\tMAX_CONSTRAINT_ID_LENGTH,\n\tMAX_CRITERION_ID_LENGTH,\n\tMAX_FORBIDDEN_ACTION_ID_LENGTH,\n\tMAX_RATIONALE_LENGTH,\n\tMAX_REQUIREMENT_ID_LENGTH,\n\tMAX_REQUIREMENT_STATEMENT_LENGTH,\n\tMAX_WORKSTREAM_ID_LENGTH,\n\tMISSION_CONTRACT_TOP_LEVEL_KEYS,\n\ttype MissionRequirement,\n\ttype MissionWorkstream,\n\tREQUIREMENT_KEYS,\n\ttype RequirementKind,\n\ttype ValidationResult,\n\tWORKSTREAM_KEYS,\n} from \"./types.js\";\n\n// =============================================================================\n// Maximum counts\n// =============================================================================\n\nconst MAX_WORKSTREAMS = 200;\nconst MAX_REQUIREMENTS = 500;\nconst MAX_CONSTRAINTS = 200;\nconst MAX_FORBIDDEN_ACTIONS = 200;\nconst MAX_CRITERIA_PER_REQUIREMENT = 100;\nconst MAX_DEPENDENCIES_PER_REQUIREMENT = 50;\nconst MAX_EVIDENCE_RULES = 50;\n\nexport function validateMissionContract(input: unknown): ValidationResult {\n\tconst errors: string[] = [];\n\n\tif (input === null || input === undefined) {\n\t\treturn { valid: false, errors: [{ path: \"$\", message: \"Input must be a non-null object\" }] };\n\t}\n\n\tif (typeof input !== \"object\") {\n\t\treturn { valid: false, errors: [{ path: \"$\", message: \"Input must be an object\" }] };\n\t}\n\n\tconst c = input as Record<string, unknown>;\n\n\t// =========================================================================\n\t// STRICT TOP-LEVEL KEY VALIDATION — rejects unknown semantic fields\n\t// Only `metadata` may contain arbitrary unknown keys.\n\t// =========================================================================\n\tfor (const key of Object.keys(c)) {\n\t\tif (!MISSION_CONTRACT_TOP_LEVEL_KEYS.has(key)) {\n\t\t\terrors.push(`Unknown top-level field: \"${key}\"`);\n\t\t}\n\t}\n\n\t// contractVersion\n\tif (c.contractVersion !== 1) {\n\t\terrors.push(\"contractVersion must be 1\");\n\t}\n\n\t// missionId\n\tif (typeof c.missionId !== \"string\" || c.missionId.length === 0) {\n\t\terrors.push(\"missionId must be a non-empty string\");\n\t} else if (c.missionId !== c.missionId.trim()) {\n\t\terrors.push(\"missionId must not have leading/trailing whitespace\");\n\t} else if (c.missionId.length > 128) {\n\t\terrors.push(\"missionId must not exceed 128 characters\");\n\t}\n\n\t// revision\n\tif (\n\t\ttypeof c.revision !== \"number\" ||\n\t\t!Number.isFinite(c.revision) ||\n\t\tc.revision < 0 ||\n\t\t!Number.isInteger(c.revision)\n\t) {\n\t\terrors.push(\"revision must be a non-negative integer\");\n\t}\n\n\t// title\n\tif (typeof c.title !== \"string\" || c.title.length === 0) {\n\t\terrors.push(\"title must be a non-empty string\");\n\t}\n\n\t// objective\n\tif (typeof c.objective !== \"string\") {\n\t\terrors.push(\"objective must be a string\");\n\t}\n\n\t// metadata validation\n\tif (c.metadata !== undefined) {\n\t\tif (typeof c.metadata !== \"object\" || c.metadata === null || Array.isArray(c.metadata)) {\n\t\t\terrors.push(\"metadata must be an object when present\");\n\t\t}\n\t}\n\n\t// Validate sub-objects\n\tconst workstreamErrors = validateWorkstreams(c.workstreams);\n\terrors.push(...workstreamErrors);\n\n\tconst workstreamIds = collectWorkstreamIds(c.workstreams);\n\n\tconst requirementErrors = validateRequirements(c.requirements, workstreamIds);\n\terrors.push(...requirementErrors);\n\n\tconst requirementIds = collectRequirementIds(c.requirements);\n\n\t// Validate cross-references between requirements and source refs\n\terrors.push(...validateRequirementSourceRefs(c.requirements, c.metadata as Record<string, unknown> | undefined));\n\n\t// =========================================================================\n\t// GLOBAL CRITERION ID REGISTRY — acceptance-criterion IDs are globally\n\t// unique across the entire Mission Contract, not just within a single\n\t// requirement. A criterion ID must identify exactly one criterion.\n\t// =========================================================================\n\tconst { errors: globalCritErrors, globalCriterionIds } = validateGlobalCriterionIds(c.requirements);\n\terrors.push(...globalCritErrors);\n\n\t// =========================================================================\n\t// CRITERION REFERENCE VALIDATION — validate every criterion ID reference\n\t// in the contract against the global criterion registry.\n\t// =========================================================================\n\tconst refErrors = validateCriterionReferences(c, globalCriterionIds);\n\terrors.push(...refErrors);\n\n\t// Validate constraint IDs\n\tconst constraintErrors = validateConstraints(c.constraints);\n\terrors.push(...constraintErrors);\n\n\t// Validate forbidden action IDs\n\tconst forbiddenErrors = validateForbiddenActions(c.forbiddenActions);\n\terrors.push(...forbiddenErrors);\n\n\t// Validate evidence policy\n\tconst policyErrors = validateEvidencePolicy(c.evidencePolicy, requirementIds);\n\terrors.push(...policyErrors);\n\n\t// Validate requirement dependency DAG\n\tif (Array.isArray(c.requirements)) {\n\t\terrors.push(...validateRequirementDAG(c.requirements as MissionRequirement[]));\n\t}\n\n\tconst valid = errors.length === 0;\n\treturn {\n\t\tvalid,\n\t\terrors: errors.map((message) => ({ path: \"$\", message })),\n\t};\n}\n\n// =============================================================================\n// Workstreams\n// =============================================================================\n\nfunction collectWorkstreamIds(workstreams: unknown): Set<string> {\n\tconst ids = new Set<string>();\n\tif (Array.isArray(workstreams)) {\n\t\tfor (const ws of workstreams) {\n\t\t\tif (ws && typeof ws === \"object\" && typeof (ws as Record<string, unknown>).id === \"string\") {\n\t\t\t\tids.add((ws as Record<string, unknown>).id as string);\n\t\t\t}\n\t\t}\n\t}\n\treturn ids;\n}\n\nfunction collectRequirementIds(requirements: unknown): Set<string> {\n\tconst ids = new Set<string>();\n\tif (Array.isArray(requirements)) {\n\t\tfor (const r of requirements) {\n\t\t\tif (r && typeof r === \"object\" && typeof (r as Record<string, unknown>).id === \"string\") {\n\t\t\t\tids.add((r as Record<string, unknown>).id as string);\n\t\t\t}\n\t\t}\n\t}\n\treturn ids;\n}\n\nfunction validateStringField(\n\tvalue: unknown,\n\tname: string,\n\tmaxLength: number,\n\tprefix: string,\n\terrors: string[],\n\tallowEmpty: boolean = false,\n): void {\n\tif (typeof value !== \"string\") {\n\t\terrors.push(`${prefix}.${name} must be a string`);\n\t\treturn;\n\t}\n\tif (!allowEmpty && value.length === 0) {\n\t\terrors.push(`${prefix}.${name} must be non-empty`);\n\t}\n\tif (value !== value.trim()) {\n\t\terrors.push(`${prefix}.${name} must not have leading/trailing whitespace`);\n\t}\n\tif (value.length > maxLength) {\n\t\terrors.push(`${prefix}.${name} exceeds maximum length of ${maxLength}`);\n\t}\n}\n\nfunction validateIntegerField(value: unknown, name: string, min: number, prefix: string, errors: string[]): void {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || !Number.isInteger(value)) {\n\t\terrors.push(`${prefix}.${name} must be a finite integer`);\n\t\treturn;\n\t}\n\tif (value < min) {\n\t\terrors.push(`${prefix}.${name} must be >= ${min}`);\n\t}\n}\n\nfunction checkUnknownFields(\n\tobj: Record<string, unknown>,\n\tallowedKeys: ReadonlySet<string>,\n\tprefix: string,\n\terrors: string[],\n): void {\n\tfor (const key of Object.keys(obj)) {\n\t\tif (!allowedKeys.has(key)) {\n\t\t\terrors.push(`${prefix}: unknown field \"${key}\"`);\n\t\t}\n\t}\n}\n\nfunction validateWorkstreams(workstreams: unknown): string[] {\n\tconst errors: string[] = [];\n\n\tif (!Array.isArray(workstreams)) {\n\t\terrors.push(\"workstreams must be an array\");\n\t\treturn errors;\n\t}\n\n\tif (workstreams.length === 0) {\n\t\terrors.push(\"workstreams must contain at least one workstream\");\n\t\treturn errors;\n\t}\n\n\tif (workstreams.length > MAX_WORKSTREAMS) {\n\t\terrors.push(`workstreams must not exceed ${MAX_WORKSTREAMS} entries`);\n\t}\n\n\tconst ids = new Set<string>();\n\tconst workstreamsTyped: MissionWorkstream[] = [];\n\n\tfor (let i = 0; i < workstreams.length; i++) {\n\t\tconst ws = workstreams[i];\n\t\tconst prefix = `workstreams[${i}]`;\n\t\tif (!ws || typeof ws !== \"object\") {\n\t\t\terrors.push(`${prefix} must be an object`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst w = ws as Record<string, unknown>;\n\n\t\t// Strict unknown field check\n\t\tcheckUnknownFields(w, WORKSTREAM_KEYS, prefix, errors);\n\n\t\tvalidateStringField(w.id, \"id\", MAX_WORKSTREAM_ID_LENGTH, prefix, errors);\n\t\tvalidateStringField(w.title, \"title\", 256, prefix, errors);\n\t\tif (w.description !== undefined && typeof w.description !== \"string\") {\n\t\t\terrors.push(`${prefix}.description must be a string`);\n\t\t}\n\t\tif (w.parentId !== undefined) {\n\t\t\tif (typeof w.parentId !== \"string\") {\n\t\t\t\terrors.push(`${prefix}.parentId must be a string`);\n\t\t\t} else if (w.parentId !== w.parentId.trim()) {\n\t\t\t\terrors.push(`${prefix}.parentId must not have leading/trailing whitespace`);\n\t\t\t}\n\t\t}\n\t\tif (w.order !== undefined) {\n\t\t\tvalidateIntegerField(w.order, \"order\", 0, prefix, errors);\n\t\t}\n\n\t\t// Duplicate check\n\t\tif (typeof w.id === \"string\") {\n\t\t\tif (ids.has(w.id)) {\n\t\t\t\terrors.push(`Duplicate workstream id: ${w.id}`);\n\t\t\t} else {\n\t\t\t\tids.add(w.id);\n\t\t\t}\n\t\t}\n\n\t\tworkstreamsTyped.push({\n\t\t\tid: w.id as string,\n\t\t\ttitle: w.title as string,\n\t\t\tdescription: w.description as string | undefined,\n\t\t\tparentId: w.parentId as string | undefined,\n\t\t\torder: w.order as number | undefined,\n\t\t});\n\t}\n\n\t// Validate parent references (after all IDs collected)\n\tfor (let i = 0; i < workstreamsTyped.length; i++) {\n\t\tconst ws = workstreamsTyped[i];\n\t\tconst prefix = `workstreams[${i}]`;\n\t\tif (ws.parentId !== undefined) {\n\t\t\tif (!ids.has(ws.parentId)) {\n\t\t\t\terrors.push(`${prefix}.parentId references unknown workstream: ${ws.parentId}`);\n\t\t\t}\n\t\t\tif (ws.parentId === ws.id) {\n\t\t\t\terrors.push(`${prefix}.parentId cannot reference itself`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Validate workstream hierarchy is acyclic\n\terrors.push(...validateWorkstreamDAG(workstreamsTyped));\n\n\treturn errors;\n}\n\nfunction validateWorkstreamDAG(workstreams: MissionWorkstream[]): string[] {\n\tconst errors: string[] = [];\n\n\tconst adjacency = new Map<string, string[]>();\n\tconst inDegree = new Map<string, number>();\n\n\tfor (const ws of workstreams) {\n\t\tadjacency.set(ws.id, []);\n\t\tinDegree.set(ws.id, 0);\n\t}\n\n\tfor (const ws of workstreams) {\n\t\tif (ws.parentId) {\n\t\t\tconst children = adjacency.get(ws.parentId);\n\t\t\tif (children) {\n\t\t\t\tchildren.push(ws.id);\n\t\t\t\tinDegree.set(ws.id, (inDegree.get(ws.id) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Kahn's algorithm\n\tconst queue: string[] = [];\n\tfor (const [id, deg] of inDegree) {\n\t\tif (deg === 0) queue.push(id);\n\t}\n\n\tlet processed = 0;\n\twhile (queue.length > 0) {\n\t\tconst node = queue.shift()!;\n\t\tprocessed++;\n\t\tfor (const child of adjacency.get(node) ?? []) {\n\t\t\tconst deg = (inDegree.get(child) ?? 1) - 1;\n\t\t\tinDegree.set(child, deg);\n\t\t\tif (deg === 0) queue.push(child);\n\t\t}\n\t}\n\n\tif (processed < workstreams.length) {\n\t\tconst cycleNodes = Array.from(inDegree.entries())\n\t\t\t.filter(([, deg]) => deg > 0)\n\t\t\t.map(([id]) => id);\n\t\terrors.push(`Workstream hierarchy cycle detected involving: ${cycleNodes.join(\", \")}`);\n\t}\n\n\treturn errors;\n}\n\n// =============================================================================\n// Requirements\n// =============================================================================\n\nfunction validateRequirements(requirements: unknown, workstreamIds: Set<string>): string[] {\n\tconst errors: string[] = [];\n\n\tif (!Array.isArray(requirements)) {\n\t\terrors.push(\"requirements must be an array\");\n\t\treturn errors;\n\t}\n\n\tif (requirements.length === 0) {\n\t\terrors.push(\"requirements must contain at least one requirement\");\n\t\treturn errors;\n\t}\n\n\tif (requirements.length > MAX_REQUIREMENTS) {\n\t\terrors.push(`requirements must not exceed ${MAX_REQUIREMENTS} entries`);\n\t}\n\n\tconst ids = new Set<string>();\n\tconst reqs: MissionRequirement[] = [];\n\n\tfor (let i = 0; i < requirements.length; i++) {\n\t\tconst r = requirements[i];\n\t\tconst prefix = `requirements[${i}]`;\n\t\tif (!r || typeof r !== \"object\") {\n\t\t\terrors.push(`${prefix} must be an object`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst req = r as Record<string, unknown>;\n\n\t\t// Strict unknown field check\n\t\tcheckUnknownFields(req, REQUIREMENT_KEYS, prefix, errors);\n\n\t\t// id\n\t\tvalidateStringField(req.id, \"id\", MAX_REQUIREMENT_ID_LENGTH, prefix, errors);\n\t\tif (typeof req.id === \"string\") {\n\t\t\tif (ids.has(req.id)) {\n\t\t\t\terrors.push(`Duplicate requirement id: ${req.id}`);\n\t\t\t} else if (req.id.length > 0) {\n\t\t\t\tids.add(req.id);\n\t\t\t}\n\t\t}\n\n\t\t// workstreamId\n\t\tvalidateStringField(req.workstreamId, \"workstreamId\", MAX_WORKSTREAM_ID_LENGTH, prefix, errors);\n\t\tif (typeof req.workstreamId === \"string\" && req.workstreamId.length > 0 && !workstreamIds.has(req.workstreamId)) {\n\t\t\terrors.push(`${prefix}.workstreamId references unknown workstream: ${req.workstreamId}`);\n\t\t}\n\n\t\t// kind\n\t\tconst kind = req.kind;\n\t\tif (kind !== \"EXPLICIT\" && kind !== \"INFERRED\") {\n\t\t\terrors.push(`${prefix}.kind must be \"EXPLICIT\" or \"INFERRED\"`);\n\t\t}\n\n\t\t// EXPLICIT / INFERRED provenance\n\t\tif (kind === \"INFERRED\" && !req.rationale) {\n\t\t\terrors.push(`${prefix}: INFERRED requirement must include a rationale`);\n\t\t}\n\n\t\t// statement\n\t\tvalidateStringField(req.statement, \"statement\", MAX_REQUIREMENT_STATEMENT_LENGTH, prefix, errors);\n\n\t\t// rationale\n\t\tif (req.rationale !== undefined) {\n\t\t\tif (typeof req.rationale !== \"string\") {\n\t\t\t\terrors.push(`${prefix}.rationale must be a string`);\n\t\t\t} else if (req.rationale.length > MAX_RATIONALE_LENGTH) {\n\t\t\t\terrors.push(`${prefix}.rationale exceeds maximum length of ${MAX_RATIONALE_LENGTH}`);\n\t\t\t}\n\t\t}\n\n\t\t// sourceRefs\n\t\tif (!Array.isArray(req.sourceRefs)) {\n\t\t\terrors.push(`${prefix}.sourceRefs must be an array`);\n\t\t} else {\n\t\t\tfor (let j = 0; j < req.sourceRefs.length; j++) {\n\t\t\t\tif (typeof req.sourceRefs[j] !== \"string\") {\n\t\t\t\t\terrors.push(`${prefix}.sourceRefs[${j}] must be a string`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// dependencies\n\t\tif (!Array.isArray(req.dependencies)) {\n\t\t\terrors.push(`${prefix}.dependencies must be an array`);\n\t\t} else if (req.dependencies.length > MAX_DEPENDENCIES_PER_REQUIREMENT) {\n\t\t\terrors.push(`${prefix}.dependencies must not exceed ${MAX_DEPENDENCIES_PER_REQUIREMENT}`);\n\t\t} else {\n\t\t\tconst depSet = new Set<string>();\n\t\t\tfor (let j = 0; j < req.dependencies.length; j++) {\n\t\t\t\tif (typeof req.dependencies[j] !== \"string\") {\n\t\t\t\t\terrors.push(`${prefix}.dependencies[${j}] must be a string`);\n\t\t\t\t} else {\n\t\t\t\t\tif (depSet.has(req.dependencies[j])) {\n\t\t\t\t\t\terrors.push(`${prefix}.dependencies contains duplicate: ${req.dependencies[j]}`);\n\t\t\t\t\t}\n\t\t\t\t\tdepSet.add(req.dependencies[j]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// acceptanceCriteria\n\t\terrors.push(...validateAcceptanceCriteria(req.acceptanceCriteria, req.workstreamId, req.kind, prefix));\n\n\t\t// initialApplicability\n\t\tif (req.initialApplicability !== undefined) {\n\t\t\tif (req.initialApplicability !== \"APPLICABLE\" && req.initialApplicability !== \"NOT_APPLICABLE\") {\n\t\t\t\terrors.push(`${prefix}.initialApplicability must be \"APPLICABLE\" or \"NOT_APPLICABLE\"`);\n\t\t\t}\n\t\t\tif (req.initialApplicability === \"NOT_APPLICABLE\" && !req.rationale) {\n\t\t\t\terrors.push(`${prefix}: NOT_APPLICABLE initial applicability must have rationale`);\n\t\t\t}\n\t\t}\n\n\t\treqs.push({\n\t\t\tid: req.id as string,\n\t\t\tworkstreamId: req.workstreamId as string,\n\t\t\tkind: kind as RequirementKind,\n\t\t\tstatement: req.statement as string,\n\t\t\trationale: req.rationale as string | undefined,\n\t\t\tsourceRefs: (req.sourceRefs as string[]) ?? [],\n\t\t\tdependencies: (req.dependencies as string[]) ?? [],\n\t\t\tacceptanceCriteria: (req.acceptanceCriteria as AcceptanceCriterion[]) ?? [],\n\t\t\tinitialApplicability: req.initialApplicability as \"APPLICABLE\" | \"NOT_APPLICABLE\" | undefined,\n\t\t});\n\t}\n\n\t// Validate cross-requirement references after all IDs collected\n\tfor (let i = 0; i < reqs.length; i++) {\n\t\tconst req = reqs[i];\n\t\tconst prefix = `requirements[${i}]`;\n\t\tfor (const depId of req.dependencies) {\n\t\t\tif (!ids.has(depId)) {\n\t\t\t\terrors.push(`${prefix}.dependencies references unknown requirement: ${depId}`);\n\t\t\t}\n\t\t\tif (depId === req.id) {\n\t\t\t\terrors.push(`${prefix}.dependencies cannot reference itself`);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errors;\n}\n\nfunction validateRequirementSourceRefs(\n\trequirements: unknown,\n\t_metadata: Record<string, unknown> | undefined,\n): string[] {\n\tconst errors: string[] = [];\n\tif (!Array.isArray(requirements)) return errors;\n\n\t// If contract has enumerated source entries in metadata, validate refs\n\t// For now we just check sourceRefs are strings (handled above)\n\treturn errors;\n}\n\n// =============================================================================\n// Acceptance Criteria\n// =============================================================================\n\nfunction validateAcceptanceCriteria(\n\tcriteria: unknown,\n\t_workstreamId: unknown,\n\tkind: unknown,\n\tprefix: string,\n): string[] {\n\tconst errors: string[] = [];\n\n\tif (!Array.isArray(criteria)) {\n\t\terrors.push(`${prefix}.acceptanceCriteria must be an array`);\n\t\treturn errors;\n\t}\n\n\tif (criteria.length > MAX_CRITERIA_PER_REQUIREMENT) {\n\t\terrors.push(`${prefix}.acceptanceCriteria must not exceed ${MAX_CRITERIA_PER_REQUIREMENT}`);\n\t}\n\n\tconst ids = new Set<string>();\n\n\tfor (let j = 0; j < criteria.length; j++) {\n\t\tconst c = criteria[j];\n\t\tconst critPrefix = `${prefix}.acceptanceCriteria[${j}]`;\n\t\tif (!c || typeof c !== \"object\") {\n\t\t\terrors.push(`${critPrefix} must be an object`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst crit = c as Record<string, unknown>;\n\n\t\t// Strict unknown field check\n\t\tcheckUnknownFields(crit, ACCEPTANCE_CRITERION_KEYS, critPrefix, errors);\n\n\t\tvalidateStringField(crit.id, \"id\", MAX_CRITERION_ID_LENGTH, critPrefix, errors);\n\t\tif (typeof crit.id === \"string\" && crit.id.length > 0) {\n\t\t\tif (ids.has(crit.id)) {\n\t\t\t\terrors.push(`Duplicate acceptance criterion id: ${crit.id}`);\n\t\t\t} else {\n\t\t\t\tids.add(crit.id);\n\t\t\t}\n\t\t}\n\n\t\tvalidateStringField(crit.statement, \"statement\", 2048, critPrefix, errors);\n\n\t\t// requiredEvidence\n\t\tif (!Array.isArray(crit.requiredEvidence)) {\n\t\t\terrors.push(`${critPrefix}.requiredEvidence must be an array`);\n\t\t} else {\n\t\t\tif (crit.requiredEvidence.length === 0 && kind === \"EXPLICIT\") {\n\t\t\t\t// A criterion with no evidence is only valid if classified as operator judgment\n\t\t\t\t// For now we allow it but warn in documentation\n\t\t\t}\n\t\t\tfor (let k = 0; k < crit.requiredEvidence.length; k++) {\n\t\t\t\tconst evReq = crit.requiredEvidence[k];\n\t\t\t\tconst evPrefix = `${critPrefix}.requiredEvidence[${k}]`;\n\t\t\t\tif (!evReq || typeof evReq !== \"object\") {\n\t\t\t\t\terrors.push(`${evPrefix} must be an object`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst ev = evReq as Record<string, unknown>;\n\n\t\t\t\t// Strict unknown field check\n\t\t\t\tcheckUnknownFields(ev, EVIDENCE_REQUIREMENT_KEYS, evPrefix, errors);\n\n\t\t\t\tif (ev.allowedTypes !== undefined && !Array.isArray(ev.allowedTypes)) {\n\t\t\t\t\terrors.push(`${evPrefix}.allowedTypes must be an array`);\n\t\t\t\t}\n\t\t\t\tif (ev.minAuthority !== undefined && typeof ev.minAuthority !== \"string\") {\n\t\t\t\t\terrors.push(`${evPrefix}.minAuthority must be a string`);\n\t\t\t\t}\n\t\t\t\tif (ev.requiredCollectorClass !== undefined && typeof ev.requiredCollectorClass !== \"string\") {\n\t\t\t\t\terrors.push(`${evPrefix}.requiredCollectorClass must be a string`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errors;\n}\n\n// =============================================================================\n// Constraints\n// =============================================================================\n\nfunction validateConstraints(constraints: unknown): string[] {\n\tconst errors: string[] = [];\n\n\tif (!Array.isArray(constraints)) {\n\t\terrors.push(\"constraints must be an array\");\n\t\treturn errors;\n\t}\n\n\tif (constraints.length > MAX_CONSTRAINTS) {\n\t\terrors.push(`constraints must not exceed ${MAX_CONSTRAINTS} entries`);\n\t}\n\n\tconst ids = new Set<string>();\n\tconst validKinds = new Set([\"REQUIRED\", \"LIMIT\", \"ENVIRONMENT\", \"PROCESS\", \"SECURITY\", \"COMPATIBILITY\"]);\n\n\tfor (let i = 0; i < constraints.length; i++) {\n\t\tconst c = constraints[i];\n\t\tconst prefix = `constraints[${i}]`;\n\t\tif (!c || typeof c !== \"object\") {\n\t\t\terrors.push(`${prefix} must be an object`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst con = c as Record<string, unknown>;\n\n\t\t// Strict unknown field check\n\t\tcheckUnknownFields(con, CONSTRAINT_KEYS, prefix, errors);\n\n\t\tvalidateStringField(con.id, \"id\", MAX_CONSTRAINT_ID_LENGTH, prefix, errors);\n\t\tif (typeof con.id === \"string\" && con.id.length > 0) {\n\t\t\tif (ids.has(con.id)) {\n\t\t\t\terrors.push(`Duplicate constraint id: ${con.id}`);\n\t\t\t} else {\n\t\t\t\tids.add(con.id);\n\t\t\t}\n\t\t}\n\n\t\tif (typeof con.kind !== \"string\" || !validKinds.has(con.kind)) {\n\t\t\terrors.push(`${prefix}.kind must be one of: REQUIRED, LIMIT, ENVIRONMENT, PROCESS, SECURITY, COMPATIBILITY`);\n\t\t}\n\n\t\tvalidateStringField(con.statement, \"statement\", 2048, prefix, errors);\n\n\t\tif (!Array.isArray(con.sourceRefs)) {\n\t\t\terrors.push(`${prefix}.sourceRefs must be an array`);\n\t\t}\n\n\t\tif (con.severity !== \"error\" && con.severity !== \"warning\") {\n\t\t\terrors.push(`${prefix}.severity must be \"error\" or \"warning\"`);\n\t\t}\n\t}\n\n\treturn errors;\n}\n\n// =============================================================================\n// Forbidden Actions\n// =============================================================================\n\nfunction validateForbiddenActions(actions: unknown): string[] {\n\tconst errors: string[] = [];\n\n\tif (!Array.isArray(actions)) {\n\t\terrors.push(\"forbiddenActions must be an array\");\n\t\treturn errors;\n\t}\n\n\tif (actions.length > MAX_FORBIDDEN_ACTIONS) {\n\t\terrors.push(`forbiddenActions must not exceed ${MAX_FORBIDDEN_ACTIONS} entries`);\n\t}\n\n\tconst ids = new Set<string>();\n\n\tfor (let i = 0; i < actions.length; i++) {\n\t\tconst a = actions[i];\n\t\tconst prefix = `forbiddenActions[${i}]`;\n\t\tif (!a || typeof a !== \"object\") {\n\t\t\terrors.push(`${prefix} must be an object`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst fa = a as Record<string, unknown>;\n\n\t\t// Strict unknown field check\n\t\tcheckUnknownFields(fa, FORBIDDEN_ACTION_KEYS, prefix, errors);\n\n\t\tvalidateStringField(fa.id, \"id\", MAX_FORBIDDEN_ACTION_ID_LENGTH, prefix, errors);\n\t\tif (typeof fa.id === \"string\" && fa.id.length > 0) {\n\t\t\tif (ids.has(fa.id)) {\n\t\t\t\terrors.push(`Duplicate forbidden action id: ${fa.id}`);\n\t\t\t} else {\n\t\t\t\tids.add(fa.id);\n\t\t\t}\n\t\t}\n\n\t\tvalidateStringField(fa.statement, \"statement\", 2048, prefix, errors);\n\n\t\tif (!Array.isArray(fa.sourceRefs)) {\n\t\t\terrors.push(`${prefix}.sourceRefs must be an array`);\n\t\t}\n\n\t\tif (fa.severity !== \"error\" && fa.severity !== \"warning\") {\n\t\t\terrors.push(`${prefix}.severity must be \"error\" or \"warning\"`);\n\t\t}\n\t}\n\n\treturn errors;\n}\n\n// =============================================================================\n// Evidence Policy\n// =============================================================================\n\nfunction validateEvidencePolicy(policy: unknown, _requirementIds: Set<string>): string[] {\n\tconst errors: string[] = [];\n\n\tif (!policy || typeof policy !== \"object\") {\n\t\terrors.push(\"evidencePolicy must be an object\");\n\t\treturn errors;\n\t}\n\n\tconst p = policy as Record<string, unknown>;\n\n\t// Strict unknown field check\n\tcheckUnknownFields(p, EVIDENCE_POLICY_KEYS, \"evidencePolicy\", errors);\n\n\tif (!Array.isArray(p.authoritativeSources)) {\n\t\terrors.push(\"evidencePolicy.authoritativeSources must be an array\");\n\t}\n\n\tif (p.rules !== undefined) {\n\t\tif (!Array.isArray(p.rules)) {\n\t\t\terrors.push(\"evidencePolicy.rules must be an array\");\n\t\t} else {\n\t\t\tif (p.rules.length > MAX_EVIDENCE_RULES) {\n\t\t\t\terrors.push(`evidencePolicy.rules must not exceed ${MAX_EVIDENCE_RULES} entries`);\n\t\t\t}\n\t\t\tconst ruleIds = new Set<string>();\n\t\t\tfor (let i = 0; i < p.rules.length; i++) {\n\t\t\t\tconst rule = p.rules[i];\n\t\t\t\tconst rulePrefix = `evidencePolicy.rules[${i}]`;\n\t\t\t\tif (!rule || typeof rule !== \"object\") {\n\t\t\t\t\terrors.push(`${rulePrefix} must be an object`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst r = rule as Record<string, unknown>;\n\n\t\t\t\t// Strict unknown field check\n\t\t\t\tcheckUnknownFields(r, EVIDENCE_POLICY_RULE_KEYS, rulePrefix, errors);\n\n\t\t\t\tvalidateStringField(r.id, \"id\", MAX_CRITERION_ID_LENGTH, rulePrefix, errors);\n\t\t\t\tif (typeof r.id === \"string\" && r.id.length > 0) {\n\t\t\t\t\tif (ruleIds.has(r.id)) {\n\t\t\t\t\t\terrors.push(`Duplicate evidence policy rule id: ${r.id}`);\n\t\t\t\t\t} else {\n\t\t\t\t\t\truleIds.add(r.id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errors;\n}\n\n// =============================================================================\n// Global Criterion ID validation — globally unique across all requirements\n// =============================================================================\n\n/**\n * Builds the global criterion ID registry and validates uniqueness.\n * Returns both duplicate errors and the set of all valid, non-empty criterion IDs.\n */\nfunction validateGlobalCriterionIds(requirements: unknown): { errors: string[]; globalCriterionIds: Set<string> } {\n\tconst errors: string[] = [];\n\tconst globalCriterionIds = new Set<string>();\n\n\tif (!Array.isArray(requirements)) return { errors, globalCriterionIds };\n\n\tconst globalIdMap = new Map<string, { reqIdx: number; critIdx: number }>();\n\n\tfor (let i = 0; i < requirements.length; i++) {\n\t\tconst r = requirements[i];\n\t\tif (!r || typeof r !== \"object\") continue;\n\t\tconst req = r as Record<string, unknown>;\n\t\tconst criteria = req.acceptanceCriteria;\n\t\tif (!Array.isArray(criteria)) continue;\n\n\t\tfor (let j = 0; j < criteria.length; j++) {\n\t\t\tconst c = criteria[j];\n\t\t\tif (!c || typeof c !== \"object\") continue;\n\t\t\tconst crit = c as Record<string, unknown>;\n\t\t\tconst critId = crit.id;\n\n\t\t\t// Empty or whitespace-only IDs are caught by per-criterion validation above.\n\t\t\t// We only register non-empty IDs here.\n\t\t\tif (typeof critId !== \"string\" || critId.length === 0) continue;\n\n\t\t\tglobalCriterionIds.add(critId);\n\n\t\t\tconst existing = globalIdMap.get(critId);\n\t\t\tif (existing) {\n\t\t\t\terrors.push(\n\t\t\t\t\t`Duplicate acceptance criterion id: ${critId}` +\n\t\t\t\t\t\t` (requirements[${existing.reqIdx}].acceptanceCriteria[${existing.critIdx}] and requirements[${i}].acceptanceCriteria[${j}])`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tglobalIdMap.set(critId, { reqIdx: i, critIdx: j });\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { errors, globalCriterionIds };\n}\n\n// =============================================================================\n// Criterion Reference Validation — validate every criterion ID reference\n// in the contract against the global criterion registry.\n// =============================================================================\n\n/**\n * Validates that every criterion ID reference in the Mission Contract\n * resolves to exactly one acceptance criterion in the global registry.\n *\n * This pass inspects all contract fields that reference criterion IDs:\n * - evidencePolicy.authoritativeSources[*] (EvidenceAuthorityClassification[] — no criterion IDs in v1)\n * - evidencePolicy.rules[*] (EvidencePolicyRule[] — no criterion IDs in v1)\n * - Any future fields with criterion references are checked here.\n *\n * v1: No contract fields reference criterion IDs beyond the acceptance criteria\n * themselves (which are the registry). This function is wired in for future\n * compatibility and to ensure the global registry is available for external\n * validation (e.g., trusted source grant criterion IDs).\n */\nexport function validateCriterionReferences(\n\t_contract: Record<string, unknown>,\n\t_globalCriterionIds: ReadonlySet<string>,\n): string[] {\n\tconst errors: string[] = [];\n\n\t// v1: The Mission Contract has no fields that reference criterion IDs\n\t// other than acceptanceCriteria[].id (which are the registry entries).\n\t// evidencePolicy.authoritativeSources is EvidenceAuthorityClassification[]\n\t// (flat strings like \"test-result\") — no criterion IDs.\n\t// evidencePolicy.rules[*] has no criterion ID fields.\n\t//\n\t// This function is intentionally wired but currently a no-op.\n\t// If future schema versions add criterion references to the contract,\n\t// validation logic goes here.\n\tvoid _contract;\n\tvoid _globalCriterionIds;\n\n\treturn errors;\n}\n\n/**\n * Validates criterion IDs in external trusted evidence source grants\n * against the global criterion registry from the Mission Contract.\n *\n * Unlike the contract-level validateCriterionReferences (which inspects\n * contract fields), this validates TrustedEvidenceSourceGrant entries\n * used in createTrustedValidationContext.\n *\n * Returns deterministic, ordered errors with precise JSON paths.\n */\nexport function validateSourceGrantCriterionIds(\n\tgrants: ReadonlyArray<{\n\t\treadonly sourceId: string;\n\t\treadonly allowedCriterionIds?: readonly string[];\n\t}>,\n\tglobalCriterionIds: ReadonlySet<string>,\n): string[] {\n\tconst errors: string[] = [];\n\n\tfor (let i = 0; i < grants.length; i++) {\n\t\tconst grant = grants[i];\n\t\tif (!grant.allowedCriterionIds) continue;\n\n\t\tconst seen = new Set<string>();\n\n\t\tfor (let j = 0; j < grant.allowedCriterionIds.length; j++) {\n\t\t\tconst critId = grant.allowedCriterionIds[j];\n\n\t\t\t// Empty criterion IDs rejected\n\t\t\tif (critId.length === 0) {\n\t\t\t\terrors.push(`sourceGrants[${i}].allowedCriterionIds[${j}]: empty criterion ID`);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Whitespace-only criterion IDs rejected\n\t\t\tif (critId.trim().length === 0) {\n\t\t\t\terrors.push(`sourceGrants[${i}].allowedCriterionIds[${j}]: whitespace-only criterion ID`);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Leading/trailing whitespace rejected\n\t\t\tif (critId !== critId.trim()) {\n\t\t\t\terrors.push(\n\t\t\t\t\t`sourceGrants[${i}].allowedCriterionIds[${j}]: criterion ID has leading/trailing whitespace: \"${critId}\"`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Unknown criterion reference\n\t\t\tif (!globalCriterionIds.has(critId)) {\n\t\t\t\terrors.push(\n\t\t\t\t\t`sourceGrants[${i}].allowedCriterionIds[${j}]: Unknown acceptance criterion id reference: ${critId}`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Duplicate detection (preferred: reject, not silently deduplicate)\n\t\t\tif (seen.has(critId)) {\n\t\t\t\terrors.push(`sourceGrants[${i}].allowedCriterionIds[${j}]: duplicate criterion ID reference: ${critId}`);\n\t\t\t} else {\n\t\t\t\tseen.add(critId);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errors;\n}\n\n// =============================================================================\n// Requirement Dependency DAG Validation\n// =============================================================================\n\nfunction validateRequirementDAG(requirements: MissionRequirement[]): string[] {\n\tconst errors: string[] = [];\n\n\tconst ids = new Set(requirements.map((r) => r.id));\n\n\t// Build graph\n\tconst adjacency = new Map<string, string[]>();\n\tconst inDegree = new Map<string, number>();\n\n\tfor (const req of requirements) {\n\t\tadjacency.set(req.id, []);\n\t\tinDegree.set(req.id, 0);\n\t}\n\n\tfor (const req of requirements) {\n\t\tfor (const depId of req.dependencies) {\n\t\t\t// Skip unknown refs (already caught above)\n\t\t\tif (!ids.has(depId)) continue;\n\t\t\t// Edge: depId -> req.id (req depends on dep, so dep must come first)\n\t\t\tadjacency.get(depId)?.push(req.id);\n\t\t\tinDegree.set(req.id, (inDegree.get(req.id) ?? 0) + 1);\n\t\t}\n\t}\n\n\t// Kahn's\n\tconst queue: string[] = [];\n\tfor (const [id, deg] of inDegree) {\n\t\tif (deg === 0) queue.push(id);\n\t}\n\n\tlet processed = 0;\n\twhile (queue.length > 0) {\n\t\tconst node = queue.shift()!;\n\t\tprocessed++;\n\t\tfor (const neighbor of adjacency.get(node) ?? []) {\n\t\t\tconst deg = (inDegree.get(neighbor) ?? 1) - 1;\n\t\t\tinDegree.set(neighbor, deg);\n\t\t\tif (deg === 0) queue.push(neighbor);\n\t\t}\n\t}\n\n\tif (processed < requirements.length) {\n\t\tconst cycleNodes = Array.from(inDegree.entries())\n\t\t\t.filter(([, deg]) => deg > 0)\n\t\t\t.map(([id]) => id)\n\t\t\t.sort();\n\t\tif (cycleNodes.length === 2) {\n\t\t\terrors.push(`Dependency cycle detected: ${cycleNodes[0]} -> ${cycleNodes[1]} -> ${cycleNodes[0]}`);\n\t\t} else {\n\t\t\terrors.push(`Dependency cycle detected involving: ${cycleNodes.join(\", \")}`);\n\t\t}\n\t}\n\n\treturn errors;\n}\n"]}