{"version":3,"file":"test-run-canonical.mjs","names":[],"sources":["../src/test-run-canonical.ts"],"sourcesContent":["import type {\n  TestRunEvidence,\n  TestRunRetainedEvidence,\n  TestRunSuiteExecution,\n  TestRunTargetExecution,\n} from \"./test-run-model.js\";\n\n/** Prefix of every test-run deployment admission id. */\nexport const TEST_RUN_ADMISSION_PREFIX = \"test-run:\";\n\nconst ADMISSION_FINGERPRINT = /^[a-f0-9]{64}$/;\n\nfunction retained(evidence: TestRunRetainedEvidence): TestRunRetainedEvidence {\n  return { reference: evidence.reference, fingerprint: evidence.fingerprint };\n}\n\nfunction target(execution: TestRunTargetExecution): TestRunTargetExecution {\n  return { id: execution.id, kind: execution.kind, environment: execution.environment };\n}\n\nfunction suite(execution: TestRunSuiteExecution): TestRunSuiteExecution {\n  return {\n    id: execution.id,\n    targetId: execution.targetId,\n    kind: execution.kind,\n    shardIndex: execution.shardIndex,\n    shardCount: execution.shardCount,\n    outcome: execution.outcome,\n    passed: execution.passed,\n    failed: execution.failed,\n    skipped: execution.skipped,\n    retries: execution.retries,\n    durationMs: execution.durationMs,\n    invocationFingerprint: execution.invocationFingerprint,\n    report: retained(execution.report),\n    log: retained(execution.log),\n  };\n}\n\n/**\n * Serializes a test-run evidence record canonically.\n *\n * Property order matches the record schema, targets sort by id, suites sort\n * by id then shard index, and selection identifiers sort lexicographically,\n * so equivalent records produce byte-identical JSON in every language. Call\n * validation before trusting the record; canonicalization does not make an\n * invalid record valid.\n */\nexport function canonicalTestRunJson(evidence: TestRunEvidence): string {\n  const canonical = {\n    format: evidence.format,\n    formatVersion: evidence.formatVersion ?? 1,\n    id: evidence.id,\n    application: evidence.application,\n    environment: evidence.environment,\n    contractFingerprint: evidence.contractFingerprint,\n    sourceRevision: evidence.sourceRevision,\n    release: evidence.release,\n    artifact: {\n      id: evidence.artifact.id,\n      fingerprint: evidence.artifact.fingerprint,\n      sizeBytes: evidence.artifact.sizeBytes,\n    },\n    startedAt: evidence.startedAt,\n    completedAt: evidence.completedAt,\n    validUntil: evidence.validUntil,\n    runner: {\n      identity: evidence.runner.identity,\n      authenticationEvidence: retained(evidence.runner.authenticationEvidence),\n      isolation: evidence.runner.isolation,\n      invocationFingerprint: evidence.runner.invocationFingerprint,\n      environmentEvidence: retained(evidence.runner.environmentEvidence),\n      environmentFingerprint: evidence.runner.environmentFingerprint,\n    },\n    selection: {\n      targetIds: [...evidence.selection.targetIds].sort(),\n      suiteIds: [...evidence.selection.suiteIds].sort(),\n    },\n    targets: evidence.targets\n      .map(target)\n      .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)),\n    suites: evidence.suites\n      .map(suite)\n      .sort((left, right) =>\n        left.id !== right.id ? (left.id < right.id ? -1 : 1) : left.shardIndex - right.shardIndex,\n      ),\n    verification: {\n      verifier: evidence.verification.verifier,\n      completedAt: evidence.verification.completedAt,\n      outcome: evidence.verification.outcome,\n      targetCount: evidence.verification.targetCount,\n      suiteCount: evidence.verification.suiteCount,\n      passed: evidence.verification.passed,\n      failed: evidence.verification.failed,\n      skipped: evidence.verification.skipped,\n      retries: evidence.verification.retries,\n      evidence: retained(evidence.verification.evidence),\n    },\n  };\n  return JSON.stringify(canonical);\n}\n\n/**\n * Returns the lowercase SHA-256 fingerprint of the canonical record.\n *\n * The fingerprint is the exact value admitted as `test-run:<sha256>` by\n * deployment gates. Uses the Web Crypto API available in every supported\n * runtime.\n */\nexport async function testRunFingerprint(evidence: TestRunEvidence): Promise<string> {\n  const bytes = new TextEncoder().encode(canonicalTestRunJson(evidence));\n  const digest = await globalThis.crypto.subtle.digest(\"SHA-256\", bytes);\n  let fingerprint = \"\";\n  for (const byte of new Uint8Array(digest)) {\n    fingerprint += byte.toString(16).padStart(2, \"0\");\n  }\n  return fingerprint;\n}\n\n/** Returns the `test-run:<sha256>` admission id for one record. */\nexport async function testRunAdmissionId(evidence: TestRunEvidence): Promise<string> {\n  return `${TEST_RUN_ADMISSION_PREFIX}${await testRunFingerprint(evidence)}`;\n}\n\n/**\n * Returns the fingerprint named by a `test-run:<sha256>` admission id.\n *\n * Returns `undefined` unless the prefix, length, and lowercase hexadecimal\n * grammar are all exact.\n */\nexport function parseTestRunAdmissionId(id: string): string | undefined {\n  if (!id.startsWith(TEST_RUN_ADMISSION_PREFIX)) {\n    return undefined;\n  }\n  const fingerprint = id.slice(TEST_RUN_ADMISSION_PREFIX.length);\n  return ADMISSION_FINGERPRINT.test(fingerprint) ? fingerprint : undefined;\n}\n"],"mappings":";;AAQA,MAAa,4BAA4B;AAEzC,MAAM,wBAAwB;AAE9B,SAAS,SAAS,UAA4D;CAC5E,OAAO;EAAE,WAAW,SAAS;EAAW,aAAa,SAAS;CAAY;AAC5E;AAEA,SAAS,OAAO,WAA2D;CACzE,OAAO;EAAE,IAAI,UAAU;EAAI,MAAM,UAAU;EAAM,aAAa,UAAU;CAAY;AACtF;AAEA,SAAS,MAAM,WAAyD;CACtE,OAAO;EACL,IAAI,UAAU;EACd,UAAU,UAAU;EACpB,MAAM,UAAU;EAChB,YAAY,UAAU;EACtB,YAAY,UAAU;EACtB,SAAS,UAAU;EACnB,QAAQ,UAAU;EAClB,QAAQ,UAAU;EAClB,SAAS,UAAU;EACnB,SAAS,UAAU;EACnB,YAAY,UAAU;EACtB,uBAAuB,UAAU;EACjC,QAAQ,SAAS,UAAU,MAAM;EACjC,KAAK,SAAS,UAAU,GAAG;CAC7B;AACF;;;;;;;;;;AAWA,SAAgB,qBAAqB,UAAmC;CACtE,MAAM,YAAY;EAChB,QAAQ,SAAS;EACjB,eAAe,SAAS,iBAAiB;EACzC,IAAI,SAAS;EACb,aAAa,SAAS;EACtB,aAAa,SAAS;EACtB,qBAAqB,SAAS;EAC9B,gBAAgB,SAAS;EACzB,SAAS,SAAS;EAClB,UAAU;GACR,IAAI,SAAS,SAAS;GACtB,aAAa,SAAS,SAAS;GAC/B,WAAW,SAAS,SAAS;EAC/B;EACA,WAAW,SAAS;EACpB,aAAa,SAAS;EACtB,YAAY,SAAS;EACrB,QAAQ;GACN,UAAU,SAAS,OAAO;GAC1B,wBAAwB,SAAS,SAAS,OAAO,sBAAsB;GACvE,WAAW,SAAS,OAAO;GAC3B,uBAAuB,SAAS,OAAO;GACvC,qBAAqB,SAAS,SAAS,OAAO,mBAAmB;GACjE,wBAAwB,SAAS,OAAO;EAC1C;EACA,WAAW;GACT,WAAW,CAAC,GAAG,SAAS,UAAU,SAAS,EAAE,KAAK;GAClD,UAAU,CAAC,GAAG,SAAS,UAAU,QAAQ,EAAE,KAAK;EAClD;EACA,SAAS,SAAS,QACf,IAAI,MAAM,EACV,MAAM,MAAM,UAAW,KAAK,KAAK,MAAM,KAAK,KAAK,KAAK,KAAK,MAAM,KAAK,IAAI,CAAE;EAC/E,QAAQ,SAAS,OACd,IAAI,KAAK,EACT,MAAM,MAAM,UACX,KAAK,OAAO,MAAM,KAAM,KAAK,KAAK,MAAM,KAAK,KAAK,IAAK,KAAK,aAAa,MAAM,UACjF;EACF,cAAc;GACZ,UAAU,SAAS,aAAa;GAChC,aAAa,SAAS,aAAa;GACnC,SAAS,SAAS,aAAa;GAC/B,aAAa,SAAS,aAAa;GACnC,YAAY,SAAS,aAAa;GAClC,QAAQ,SAAS,aAAa;GAC9B,QAAQ,SAAS,aAAa;GAC9B,SAAS,SAAS,aAAa;GAC/B,SAAS,SAAS,aAAa;GAC/B,UAAU,SAAS,SAAS,aAAa,QAAQ;EACnD;CACF;CACA,OAAO,KAAK,UAAU,SAAS;AACjC;;;;;;;;AASA,eAAsB,mBAAmB,UAA4C;CACnF,MAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,qBAAqB,QAAQ,CAAC;CACrE,MAAM,SAAS,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,KAAK;CACrE,IAAI,cAAc;CAClB,KAAK,MAAM,QAAQ,IAAI,WAAW,MAAM,GACtC,eAAe,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;CAElD,OAAO;AACT;;AAGA,eAAsB,mBAAmB,UAA4C;CACnF,OAAO,GAAG,4BAA4B,MAAM,mBAAmB,QAAQ;AACzE;;;;;;;AAQA,SAAgB,wBAAwB,IAAgC;CACtE,IAAI,CAAC,GAAG,WAAA,WAAoC,GAC1C;CAEF,MAAM,cAAc,GAAG,MAAM,CAAgC;CAC7D,OAAO,sBAAsB,KAAK,WAAW,IAAI,cAAc,KAAA;AACjE"}