{"version":3,"file":"test-run-check.mjs","names":[],"sources":["../src/test-run-check.ts"],"sourcesContent":["import type { TestRunEvidence } from \"./test-run-model.js\";\nimport type { MarquetteDiagnostic } from \"./validate.js\";\nimport {\n  admitTestRun,\n  testRunDenialCode,\n  type TestRunAdmissionDecision,\n  type TestRunCandidate,\n  type TestRunDenialCode,\n} from \"./test-run-admission.js\";\nimport { parseTestRunAdmissionId } from \"./test-run-canonical.js\";\nimport {\n  checkDigest,\n  checkIdentifier,\n  checkSourceRevision,\n  checkTimestamp,\n  error,\n  isStrictTimestamp,\n} from \"./test-run-validate-rules.js\";\n\n/**\n * Serialized `format` marker for retained tests-check records.\n *\n * Readers must reject any other value before trusting the record.\n */\nexport const TEST_RUN_CHECK_FORMAT = \"vize.test-run.check\";\n\n/**\n * Current serialized tests-check format.\n *\n * Readers must reject a higher value until they explicitly support it.\n */\nexport const TEST_RUN_CHECK_FORMAT_VERSION = 1;\n\n/**\n * Retained, release-bound `tests` check for one deployment decision.\n *\n * The record replaces every generic test-result reference — a summary blob,\n * a report path, or a green workflow label — with the exact\n * `test-run:<sha256>` admission id of an independently verified run, the six\n * candidate facts the run was admitted for, and the identity and instant of\n * the independent observer that recorded the admission. A release decision\n * retaining anything else as its tests evidence cannot pass\n * {@link verifyTestRunCheck}.\n */\nexport interface TestRunCheck {\n  /** Serialized format marker; always {@link TEST_RUN_CHECK_FORMAT}. */\n  readonly format: typeof TEST_RUN_CHECK_FORMAT;\n  /**\n   * Serialized format version.\n   *\n   * Defaults to {@link TEST_RUN_CHECK_FORMAT_VERSION}.\n   */\n  readonly formatVersion?: typeof TEST_RUN_CHECK_FORMAT_VERSION;\n  /** Exact `test-run:<sha256>` admission id of the observed run. */\n  readonly evidence: string;\n  /** Exact candidate facts the run was admitted for. */\n  readonly candidate: TestRunCandidate;\n  /**\n   * Identity of the independent observer that recorded the admission.\n   *\n   * The observer is the trusted promotion boundary, never the runner that\n   * executed the tests.\n   */\n  readonly observer: string;\n  /** Millisecond-precision UTC instant the admission was observed. */\n  readonly observedAt: string;\n}\n\n/**\n * Validates a retained tests-check record structurally.\n *\n * Diagnostics use `check.` paths and are deterministic and sorted by path,\n * code, and message. A generic evidence reference fails here with\n * `VIZE_MARQUETTE_141`: only an exact `test-run:<sha256>` admission id can\n * name retained test evidence. Structural validity never admits anything by\n * itself; {@link verifyTestRunCheck} must confirm the record against the\n * caller's candidate and the retained run. Codes, paths, messages, and\n * ordering are identical to the native implementation.\n */\nexport function validateTestRunCheck(check: TestRunCheck): MarquetteDiagnostic[] {\n  const diagnostics: MarquetteDiagnostic[] = [];\n\n  if ((check.format as string) !== TEST_RUN_CHECK_FORMAT) {\n    diagnostics.push(\n      error(\"VIZE_MARQUETTE_101\", \"check.format\", \"unsupported tests-check format marker\"),\n    );\n  }\n  if ((check.formatVersion ?? TEST_RUN_CHECK_FORMAT_VERSION) !== TEST_RUN_CHECK_FORMAT_VERSION) {\n    diagnostics.push(\n      error(\"VIZE_MARQUETTE_102\", \"check.formatVersion\", \"unsupported tests-check format version\"),\n    );\n  }\n\n  if (parseTestRunAdmissionId(check.evidence) === undefined) {\n    diagnostics.push(\n      error(\n        \"VIZE_MARQUETTE_141\",\n        \"check.evidence\",\n        \"check evidence must be test-run: followed by 64 lowercase hexadecimal characters\",\n      ),\n    );\n  }\n\n  const candidate = check.candidate;\n  checkIdentifier(candidate.application, \"check.candidate.application\", diagnostics);\n  checkIdentifier(candidate.environment, \"check.candidate.environment\", diagnostics);\n  checkDigest(candidate.contractFingerprint, \"check.candidate.contractFingerprint\", diagnostics);\n  checkSourceRevision(candidate.sourceRevision, \"check.candidate.sourceRevision\", diagnostics);\n  if (candidate.release.length === 0 || candidate.release.length > 256) {\n    diagnostics.push(\n      error(\n        \"VIZE_MARQUETTE_106\",\n        \"check.candidate.release\",\n        \"release must be between 1 and 256 characters\",\n      ),\n    );\n  }\n  checkDigest(candidate.artifactFingerprint, \"check.candidate.artifactFingerprint\", diagnostics);\n\n  checkIdentifier(check.observer, \"check.observer\", diagnostics);\n  checkTimestamp(check.observedAt, \"check.observedAt\", diagnostics);\n\n  sortDiagnostics(diagnostics);\n  return diagnostics;\n}\n\n/**\n * Verifies one retained tests check against the caller's own facts.\n *\n * The caller supplies the candidate it is deciding from its own trusted\n * facts; the retained check must validate structurally, bind that candidate\n * exactly, name an observer independent from the run's runner, and be\n * observed no earlier than the run's completed verification. The referenced\n * record is then admitted exactly like {@link admitTestRun}: canonical\n * fingerprint, candidate bindings, expiry at `now`, verification outcome,\n * and skipped-test accounting all fail closed. Diagnostics, denial codes,\n * and ordering are identical to the native implementation, as pinned by the\n * shared check-decision fixtures.\n */\nexport async function verifyTestRunCheck(\n  check: TestRunCheck,\n  candidate: TestRunCandidate,\n  evidence: TestRunEvidence,\n  now: string,\n): Promise<TestRunAdmissionDecision> {\n  const diagnostics = validateTestRunCheck(check);\n\n  const bindings = [\n    [check.candidate.application, candidate.application, \"application\", \"application\"],\n    [check.candidate.environment, candidate.environment, \"environment\", \"environment\"],\n    [\n      check.candidate.contractFingerprint,\n      candidate.contractFingerprint,\n      \"contractFingerprint\",\n      \"contract fingerprint\",\n    ],\n    [check.candidate.sourceRevision, candidate.sourceRevision, \"sourceRevision\", \"source revision\"],\n    [check.candidate.release, candidate.release, \"release\", \"release\"],\n    [\n      check.candidate.artifactFingerprint,\n      candidate.artifactFingerprint,\n      \"artifactFingerprint\",\n      \"artifact fingerprint\",\n    ],\n  ] as const;\n  for (const [recorded, expected, property, field] of bindings) {\n    if (recorded !== expected) {\n      diagnostics.push(\n        error(\n          \"VIZE_MARQUETTE_149\",\n          `check.candidate.${property}`,\n          `check does not bind the candidate ${field}`,\n        ),\n      );\n    }\n  }\n\n  if (check.observer === evidence.runner.identity) {\n    diagnostics.push(\n      error(\n        \"VIZE_MARQUETTE_151\",\n        \"check.observer\",\n        \"check observer must be independent from the run's runner\",\n      ),\n    );\n  }\n  if (isStrictTimestamp(check.observedAt) && check.observedAt < evidence.verification.completedAt) {\n    diagnostics.push(\n      error(\n        \"VIZE_MARQUETTE_150\",\n        \"check.observedAt\",\n        \"observation must not precede the completed verification\",\n      ),\n    );\n  }\n\n  diagnostics.push(...(await admitTestRun(evidence, candidate, check.evidence, now)));\n  sortDiagnostics(diagnostics);\n  const denialCodes: TestRunDenialCode[] = [...new Set(diagnostics.map(testRunDenialCode))].sort();\n  return { allowed: diagnostics.length === 0, denialCodes, diagnostics };\n}\n\nfunction sortDiagnostics(diagnostics: MarquetteDiagnostic[]): void {\n  diagnostics.sort((left, right) =>\n    left.path !== right.path\n      ? left.path < right.path\n        ? -1\n        : 1\n      : left.code !== right.code\n        ? left.code < right.code\n          ? -1\n          : 1\n        : left.message < right.message\n          ? -1\n          : left.message > right.message\n            ? 1\n            : 0,\n  );\n}\n"],"mappings":";;;;;;;;;AAwBA,MAAa,wBAAwB;;;;;;AAOrC,MAAa,gCAAgC;;;;;;;;;;;;AAgD7C,SAAgB,qBAAqB,OAA4C;CAC/E,MAAM,cAAqC,CAAC;CAE5C,IAAK,MAAM,WAAA,uBACT,YAAY,KACV,MAAM,sBAAsB,gBAAgB,uCAAuC,CACrF;CAEF,KAAK,MAAM,iBAAA,OAAA,GACT,YAAY,KACV,MAAM,sBAAsB,uBAAuB,wCAAwC,CAC7F;CAGF,IAAI,wBAAwB,MAAM,QAAQ,MAAM,KAAA,GAC9C,YAAY,KACV,MACE,sBACA,kBACA,kFACF,CACF;CAGF,MAAM,YAAY,MAAM;CACxB,gBAAgB,UAAU,aAAa,+BAA+B,WAAW;CACjF,gBAAgB,UAAU,aAAa,+BAA+B,WAAW;CACjF,YAAY,UAAU,qBAAqB,uCAAuC,WAAW;CAC7F,oBAAoB,UAAU,gBAAgB,kCAAkC,WAAW;CAC3F,IAAI,UAAU,QAAQ,WAAW,KAAK,UAAU,QAAQ,SAAS,KAC/D,YAAY,KACV,MACE,sBACA,2BACA,8CACF,CACF;CAEF,YAAY,UAAU,qBAAqB,uCAAuC,WAAW;CAE7F,gBAAgB,MAAM,UAAU,kBAAkB,WAAW;CAC7D,eAAe,MAAM,YAAY,oBAAoB,WAAW;CAEhE,gBAAgB,WAAW;CAC3B,OAAO;AACT;;;;;;;;;;;;;;AAeA,eAAsB,mBACpB,OACA,WACA,UACA,KACmC;CACnC,MAAM,cAAc,qBAAqB,KAAK;CAE9C,MAAM,WAAW;EACf;GAAC,MAAM,UAAU;GAAa,UAAU;GAAa;GAAe;EAAa;EACjF;GAAC,MAAM,UAAU;GAAa,UAAU;GAAa;GAAe;EAAa;EACjF;GACE,MAAM,UAAU;GAChB,UAAU;GACV;GACA;EACF;EACA;GAAC,MAAM,UAAU;GAAgB,UAAU;GAAgB;GAAkB;EAAiB;EAC9F;GAAC,MAAM,UAAU;GAAS,UAAU;GAAS;GAAW;EAAS;EACjE;GACE,MAAM,UAAU;GAChB,UAAU;GACV;GACA;EACF;CACF;CACA,KAAK,MAAM,CAAC,UAAU,UAAU,UAAU,UAAU,UAClD,IAAI,aAAa,UACf,YAAY,KACV,MACE,sBACA,mBAAmB,YACnB,qCAAqC,OACvC,CACF;CAIJ,IAAI,MAAM,aAAa,SAAS,OAAO,UACrC,YAAY,KACV,MACE,sBACA,kBACA,0DACF,CACF;CAEF,IAAI,kBAAkB,MAAM,UAAU,KAAK,MAAM,aAAa,SAAS,aAAa,aAClF,YAAY,KACV,MACE,sBACA,oBACA,yDACF,CACF;CAGF,YAAY,KAAK,GAAI,MAAM,aAAa,UAAU,WAAW,MAAM,UAAU,GAAG,CAAE;CAClF,gBAAgB,WAAW;CAC3B,MAAM,cAAmC,CAAC,GAAG,IAAI,IAAI,YAAY,IAAI,iBAAiB,CAAC,CAAC,EAAE,KAAK;CAC/F,OAAO;EAAE,SAAS,YAAY,WAAW;EAAG;EAAa;CAAY;AACvE;AAEA,SAAS,gBAAgB,aAA0C;CACjE,YAAY,MAAM,MAAM,UACtB,KAAK,SAAS,MAAM,OAChB,KAAK,OAAO,MAAM,OAChB,KACA,IACF,KAAK,SAAS,MAAM,OAClB,KAAK,OAAO,MAAM,OAChB,KACA,IACF,KAAK,UAAU,MAAM,UACnB,KACA,KAAK,UAAU,MAAM,UACnB,IACA,CACZ;AACF"}