@vizejs/marquette 0.302.0 → 0.303.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"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,EAAE;CAE7C,IAAK,MAAM,WAAA,uBACT,YAAY,KACV,MAAM,sBAAsB,gBAAgB,wCAAwC,CACrF;CAEH,KAAK,MAAM,iBAAA,OAAA,GACT,YAAY,KACV,MAAM,sBAAsB,uBAAuB,yCAAyC,CAC7F;CAGH,IAAI,wBAAwB,MAAM,SAAS,KAAK,KAAA,GAC9C,YAAY,KACV,MACE,sBACA,kBACA,mFACD,CACF;CAGH,MAAM,YAAY,MAAM;CACxB,gBAAgB,UAAU,aAAa,+BAA+B,YAAY;CAClF,gBAAgB,UAAU,aAAa,+BAA+B,YAAY;CAClF,YAAY,UAAU,qBAAqB,uCAAuC,YAAY;CAC9F,oBAAoB,UAAU,gBAAgB,kCAAkC,YAAY;CAC5F,IAAI,UAAU,QAAQ,WAAW,KAAK,UAAU,QAAQ,SAAS,KAC/D,YAAY,KACV,MACE,sBACA,2BACA,+CACD,CACF;CAEH,YAAY,UAAU,qBAAqB,uCAAuC,YAAY;CAE9F,gBAAgB,MAAM,UAAU,kBAAkB,YAAY;CAC9D,eAAe,MAAM,YAAY,oBAAoB,YAAY;CAEjE,gBAAgB,YAAY;CAC5B,OAAO;;;;;;;;;;;;;;;AAgBT,eAAsB,mBACpB,OACA,WACA,UACA,KACmC;CACnC,MAAM,cAAc,qBAAqB,MAAM;CAE/C,MAAM,WAAW;EACf;GAAC,MAAM,UAAU;GAAa,UAAU;GAAa;GAAe;GAAc;EAClF;GAAC,MAAM,UAAU;GAAa,UAAU;GAAa;GAAe;GAAc;EAClF;GACE,MAAM,UAAU;GAChB,UAAU;GACV;GACA;GACD;EACD;GAAC,MAAM,UAAU;GAAgB,UAAU;GAAgB;GAAkB;GAAkB;EAC/F;GAAC,MAAM,UAAU;GAAS,UAAU;GAAS;GAAW;GAAU;EAClE;GACE,MAAM,UAAU;GAChB,UAAU;GACV;GACA;GACD;EACF;CACD,KAAK,MAAM,CAAC,UAAU,UAAU,UAAU,UAAU,UAClD,IAAI,aAAa,UACf,YAAY,KACV,MACE,sBACA,mBAAmB,YACnB,qCAAqC,QACtC,CACF;CAIL,IAAI,MAAM,aAAa,SAAS,OAAO,UACrC,YAAY,KACV,MACE,sBACA,kBACA,2DACD,CACF;CAEH,IAAI,kBAAkB,MAAM,WAAW,IAAI,MAAM,aAAa,SAAS,aAAa,aAClF,YAAY,KACV,MACE,sBACA,oBACA,0DACD,CACF;CAGH,YAAY,KAAK,GAAI,MAAM,aAAa,UAAU,WAAW,MAAM,UAAU,IAAI,CAAE;CACnF,gBAAgB,YAAY;CAC5B,MAAM,cAAmC,CAAC,GAAG,IAAI,IAAI,YAAY,IAAI,kBAAkB,CAAC,CAAC,CAAC,MAAM;CAChG,OAAO;EAAE,SAAS,YAAY,WAAW;EAAG;EAAa;EAAa;;AAGxE,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,EACX"}
@@ -0,0 +1,82 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://vizejs.dev/schemas/marquette/test-run-check.schema.json",
4
+ "title": "Vize Test Run Check",
5
+ "description": "Retained, release-bound tests check for one deployment decision. This record replaces every generic test-result reference: a release decision may only satisfy its tests evidence with the exact test-run:<sha256> admission id of an independently verified run, bound to the six exact candidate facts and to the independent observer that recorded the admission. Verification is fail-closed and identical across JavaScript, Rust, Go, and JVM hosts; the shared tests/fixtures/test-run-evidence check-decision fixtures are the conformance source of truth for new host implementations.",
6
+ "$ref": "#/$defs/check",
7
+ "$defs": {
8
+ "identifier": {
9
+ "type": "string",
10
+ "minLength": 1,
11
+ "maxLength": 128,
12
+ "pattern": "^[a-z0-9][a-z0-9._-]*$"
13
+ },
14
+ "digest": {
15
+ "type": "string",
16
+ "pattern": "^[a-f0-9]{64}$"
17
+ },
18
+ "admissionId": {
19
+ "description": "Exact test-run:<sha256> admission id. Summary blobs, report paths, run URLs, and green workflow labels are not admissible test evidence.",
20
+ "type": "string",
21
+ "pattern": "^test-run:[a-f0-9]{64}$"
22
+ },
23
+ "timestamp": {
24
+ "type": "string",
25
+ "format": "date-time",
26
+ "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\\.[0-9]{3}Z$"
27
+ },
28
+ "candidate": {
29
+ "description": "Exact release candidate the run was admitted for. Every field must equal the deciding gate's own facts; verification rejects any difference.",
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": [
33
+ "application",
34
+ "environment",
35
+ "contractFingerprint",
36
+ "sourceRevision",
37
+ "release",
38
+ "artifactFingerprint"
39
+ ],
40
+ "properties": {
41
+ "application": { "$ref": "#/$defs/identifier" },
42
+ "environment": { "$ref": "#/$defs/identifier" },
43
+ "contractFingerprint": { "$ref": "#/$defs/digest" },
44
+ "sourceRevision": {
45
+ "type": "string",
46
+ "pattern": "^[a-f0-9]{40,128}$"
47
+ },
48
+ "release": {
49
+ "type": "string",
50
+ "minLength": 1,
51
+ "maxLength": 256
52
+ },
53
+ "artifactFingerprint": { "$ref": "#/$defs/digest" }
54
+ }
55
+ },
56
+ "check": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": ["format", "formatVersion", "evidence", "candidate", "observer", "observedAt"],
60
+ "properties": {
61
+ "format": {
62
+ "description": "Serialized format marker; readers must reject any other value.",
63
+ "const": "vize.test-run.check"
64
+ },
65
+ "formatVersion": {
66
+ "description": "Serialized format version; readers must reject a higher value until they explicitly support it.",
67
+ "const": 1
68
+ },
69
+ "evidence": { "$ref": "#/$defs/admissionId" },
70
+ "candidate": { "$ref": "#/$defs/candidate" },
71
+ "observer": {
72
+ "description": "Identity of the independent observer that recorded the admission — the trusted promotion boundary, never the runner that executed the tests. Verification rejects a check whose observer equals the run's runner identity.",
73
+ "$ref": "#/$defs/identifier"
74
+ },
75
+ "observedAt": {
76
+ "description": "Millisecond-precision UTC instant the admission was observed. Verification rejects an observation earlier than the run's completed verification.",
77
+ "$ref": "#/$defs/timestamp"
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,146 @@
1
+ import { MarquetteDiagnostic, MarquetteDiagnosticSeverity } from "./validate.mjs";
2
+ import { TestRunAdmissionDecision, TestRunCandidate, TestRunDenialCode } from "./test-run-admission.mjs";
3
+
4
+ //#region src/test-run-transition-model.d.ts
5
+ /**
6
+ * Serialized `format` marker for release-transition records.
7
+ *
8
+ * Readers must reject any other value before trusting the record.
9
+ */
10
+ declare const TEST_RUN_TRANSITION_FORMAT = "vize.test-run.transition";
11
+ /**
12
+ * Current serialized release-transition format.
13
+ *
14
+ * Readers must reject a higher value until they explicitly support it.
15
+ */
16
+ declare const TEST_RUN_TRANSITION_FORMAT_VERSION = 1;
17
+ /** Maximum admission ids one transition may carry as accepted state. */
18
+ declare const TEST_RUN_TRANSITION_MAX_ACCEPTED = 4096;
19
+ /**
20
+ * One retained diagnostic inside a durable transition record.
21
+ *
22
+ * The shape and serialization match live diagnostics exactly; the retained
23
+ * form is plain data so persisted records can be read back by any host.
24
+ */
25
+ interface TestRunRetainedDiagnostic {
26
+ /** Stable machine-readable diagnostic code. */
27
+ readonly code: string;
28
+ /** Severity recorded for the diagnostic; any diagnostic denies. */
29
+ readonly severity: MarquetteDiagnosticSeverity;
30
+ /** JSON-style path into the decided input. */
31
+ readonly path: string;
32
+ /** Human-readable explanation recorded with the decision. */
33
+ readonly message: string;
34
+ }
35
+ /**
36
+ * One retained allow-or-deny decision inside a durable transition record.
37
+ *
38
+ * The shape and serialization match live decisions exactly. Validation
39
+ * rejects a retained decision whose `allowed` flag, denial codes, or
40
+ * diagnostic ordering disagree with the published mapping, so a record
41
+ * cannot claim an outcome its own diagnostics contradict.
42
+ */
43
+ interface TestRunRetainedDecision {
44
+ /** Whether the release decision admitted the candidate. */
45
+ readonly allowed: boolean;
46
+ /** Deduplicated denial causes sorted lexicographically; empty if allowed. */
47
+ readonly denialCodes: readonly TestRunDenialCode[];
48
+ /** Complete diagnostics in the stable path, code, message order. */
49
+ readonly diagnostics: readonly TestRunRetainedDiagnostic[];
50
+ }
51
+ /**
52
+ * One durable atomic release transition.
53
+ *
54
+ * The record binds the decision, the exact candidate and evidence it
55
+ * decided, and the complete accepted anti-replay state after the decision
56
+ * into one canonical document. `sequence` grows by exactly one per
57
+ * transition and `previous` names the predecessor's canonical SHA-256
58
+ * fingerprint, so a chain tip proves the entire decision history and the
59
+ * accepted set can never drift from the decision that produced it.
60
+ *
61
+ * Host durability contract: write the complete canonical bytes to a
62
+ * temporary location, flush them to durable storage, then atomically rename
63
+ * or commit so exactly one complete chain tip exists at every instant; on
64
+ * recovery, verify the tip against its retained predecessor with
65
+ * {@link verifyTestRunTransition} before deciding anything new, and discard
66
+ * — never repair — a torn or partial record.
67
+ */
68
+ interface TestRunTransition {
69
+ /** Serialized format marker; always {@link TEST_RUN_TRANSITION_FORMAT}. */
70
+ readonly format: typeof TEST_RUN_TRANSITION_FORMAT;
71
+ /**
72
+ * Serialized format version.
73
+ *
74
+ * Defaults to {@link TEST_RUN_TRANSITION_FORMAT_VERSION}.
75
+ */
76
+ readonly formatVersion?: typeof TEST_RUN_TRANSITION_FORMAT_VERSION;
77
+ /** One-based position of this transition in its chain. */
78
+ readonly sequence: number;
79
+ /** Canonical fingerprint of the predecessor; `null` only at genesis. */
80
+ readonly previous: string | null;
81
+ /** Millisecond-precision UTC instant the decision was made. */
82
+ readonly decidedAt: string;
83
+ /** Exact candidate the decision was made for. */
84
+ readonly candidate: TestRunCandidate;
85
+ /** Exact `test-run:<sha256>` admission id the decision evaluated. */
86
+ readonly evidence: string;
87
+ /** Retained decision exactly as it was produced. */
88
+ readonly decision: TestRunRetainedDecision;
89
+ /**
90
+ * Complete anti-replay state after this transition: every admission id
91
+ * ever accepted in this chain, sorted and unique.
92
+ */
93
+ readonly accepted: readonly string[];
94
+ }
95
+ /**
96
+ * Serializes a release transition canonically.
97
+ *
98
+ * Property order matches the record schema and the accepted state sorts
99
+ * lexicographically after deduplication, so equivalent transitions produce
100
+ * byte-identical JSON in every language. These are the exact bytes a host
101
+ * must write atomically and the exact bytes the chain fingerprint covers.
102
+ * Call validation before trusting the record; canonicalization does not
103
+ * make an invalid record valid.
104
+ */
105
+ declare function canonicalTestRunTransitionJson(transition: TestRunTransition): string;
106
+ /**
107
+ * Returns the lowercase SHA-256 fingerprint of the canonical transition.
108
+ *
109
+ * The fingerprint is the exact value the successor transition must name as
110
+ * `previous`, forming the durable chain. Uses the Web Crypto API available
111
+ * in every supported runtime.
112
+ */
113
+ declare function testRunTransitionFingerprint(transition: TestRunTransition): Promise<string>;
114
+ //#endregion
115
+ //#region src/test-run-transition.d.ts
116
+ /**
117
+ * Validates one release transition structurally.
118
+ *
119
+ * Diagnostics use `transition.` paths and are deterministic and sorted by
120
+ * path, code, and message. Validation confirms the record alone is
121
+ * internally coherent — grammar, decision consistency against the published
122
+ * diagnostic mapping, and an allowed decision accepting its own evidence —
123
+ * but only {@link verifyTestRunTransition} can confirm the record extends
124
+ * the durable chain. Codes, paths, messages, and ordering are identical to
125
+ * the native implementation.
126
+ */
127
+ declare function validateTestRunTransition(transition: TestRunTransition): MarquetteDiagnostic[];
128
+ /**
129
+ * Verifies one release transition against the durable chain tip.
130
+ *
131
+ * `previous` is the retained, already-verified predecessor — `null` only
132
+ * when deciding the very first transition of a chain. The transition must
133
+ * validate structurally, extend the predecessor's sequence, fingerprint,
134
+ * scope, and decision time exactly, never re-accept evidence the
135
+ * predecessor already accepted, and carry an accepted state equal to the
136
+ * predecessor's state plus exactly the newly accepted evidence (unchanged
137
+ * for a denial). Any diagnostic rejects the transition: a conforming host
138
+ * must not persist it, and on recovery must discard a tip this function
139
+ * rejects. Diagnostics, denial codes, and ordering are identical to the
140
+ * native implementation, as pinned by the shared transition-decision
141
+ * fixtures.
142
+ */
143
+ declare function verifyTestRunTransition(transition: TestRunTransition, previous: TestRunTransition | null): Promise<TestRunAdmissionDecision>;
144
+ //#endregion
145
+ export { TEST_RUN_TRANSITION_FORMAT, TEST_RUN_TRANSITION_FORMAT_VERSION, TEST_RUN_TRANSITION_MAX_ACCEPTED, type TestRunRetainedDecision, type TestRunRetainedDiagnostic, type TestRunTransition, canonicalTestRunTransitionJson, testRunTransitionFingerprint, validateTestRunTransition, verifyTestRunTransition };
146
+ //# sourceMappingURL=test-run-transition.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-run-transition.d.mts","names":[],"sources":["../src/test-run-transition-model.ts","../src/test-run-transition.ts"],"mappings":";;;;;;AAQA;;;cAAa,0BAAA;;AAOb;;;;cAAa,kCAAA;AAGb;AAAA,cAAa,gCAAA;;;;AAQb;;;UAAiB,yBAAA;EAEN;EAAA,SAAA,IAAA;EAEU;EAAA,SAAV,QAAA,EAAU,2BAAA;EAIV;EAAA,SAFA,IAAA;EAEO;EAAA,SAAP,OAAA;AAAA;;;;;;;;;UAWM,uBAAA;EA0BA;EAAA,SAxBN,OAAA;;WAEA,WAAA,WAAsB,iBAAA;EA8BC;EAAA,SA5BvB,WAAA,WAAsB,yBAAA;AAAA;;;;;;;;;;;;;;;;;;UAoBhB,iBAAA;EAsC6B;EAAA,SApCnC,MAAA,SAAe,0BAAA;EAoCiC;;AAsC3D;;;EAtC2D,SA9BhD,aAAA,UAAuB,kCAAA;EAoE6B;EAAA,SAlEpD,QAAA;EAkEwE;EAAA,SAhExE,QAAA;EAgE+E;EAAA,SA9D/E,SAAA;;WAEA,SAAA,EAAW,gBAAA;ECzCN;EAAA,SD2CL,QAAA;;WAEA,QAAA,EAAU,uBAAA;EC7CiC;;;;EAAA,SDkD3C,QAAA;AAAA;;;;;;;;;;;iBAaK,8BAAA,CAA+B,UAAA,EAAY,iBAAA;;;;;;;;iBAsCrC,4BAAA,CAA6B,UAAA,EAAY,iBAAA,GAAoB,OAAA;;;AA3InF;;;;;AAOA;;;;;AAGA;AAVA,iBCsCgB,yBAAA,CAA0B,UAAA,EAAY,iBAAA,GAAoB,mBAAA;;;;ADpB1E;;;;;;;;;;;AAmBA;iBC0GsB,uBAAA,CACpB,UAAA,EAAY,iBAAA,EACZ,QAAA,EAAU,iBAAA,UACT,OAAA,CAAQ,wBAAA"}
@@ -0,0 +1,188 @@
1
+ import { a as checkDigest, c as checkSourceRevision, d as isStrictTimestamp, l as checkTimestamp, o as checkIdentifier, s as checkSafeInteger, u as error } from "./test-run-validate-C_KR031E.mjs";
2
+ import { parseTestRunAdmissionId } from "./test-run-canonical.mjs";
3
+ import { testRunDenialCode } from "./test-run-admission.mjs";
4
+ //#region src/test-run-transition-model.ts
5
+ /**
6
+ * Serialized `format` marker for release-transition records.
7
+ *
8
+ * Readers must reject any other value before trusting the record.
9
+ */
10
+ const TEST_RUN_TRANSITION_FORMAT = "vize.test-run.transition";
11
+ /**
12
+ * Current serialized release-transition format.
13
+ *
14
+ * Readers must reject a higher value until they explicitly support it.
15
+ */
16
+ const TEST_RUN_TRANSITION_FORMAT_VERSION = 1;
17
+ /** Maximum admission ids one transition may carry as accepted state. */
18
+ const TEST_RUN_TRANSITION_MAX_ACCEPTED = 4096;
19
+ /**
20
+ * Serializes a release transition canonically.
21
+ *
22
+ * Property order matches the record schema and the accepted state sorts
23
+ * lexicographically after deduplication, so equivalent transitions produce
24
+ * byte-identical JSON in every language. These are the exact bytes a host
25
+ * must write atomically and the exact bytes the chain fingerprint covers.
26
+ * Call validation before trusting the record; canonicalization does not
27
+ * make an invalid record valid.
28
+ */
29
+ function canonicalTestRunTransitionJson(transition) {
30
+ const canonical = {
31
+ format: transition.format,
32
+ formatVersion: transition.formatVersion ?? 1,
33
+ sequence: transition.sequence,
34
+ previous: transition.previous,
35
+ decidedAt: transition.decidedAt,
36
+ candidate: {
37
+ application: transition.candidate.application,
38
+ environment: transition.candidate.environment,
39
+ contractFingerprint: transition.candidate.contractFingerprint,
40
+ sourceRevision: transition.candidate.sourceRevision,
41
+ release: transition.candidate.release,
42
+ artifactFingerprint: transition.candidate.artifactFingerprint
43
+ },
44
+ evidence: transition.evidence,
45
+ decision: {
46
+ allowed: transition.decision.allowed,
47
+ denialCodes: [...new Set(transition.decision.denialCodes)].sort(),
48
+ diagnostics: transition.decision.diagnostics.map((diagnostic) => ({
49
+ code: diagnostic.code,
50
+ severity: diagnostic.severity,
51
+ path: diagnostic.path,
52
+ message: diagnostic.message
53
+ }))
54
+ },
55
+ accepted: [...new Set(transition.accepted)].sort()
56
+ };
57
+ return JSON.stringify(canonical);
58
+ }
59
+ /**
60
+ * Returns the lowercase SHA-256 fingerprint of the canonical transition.
61
+ *
62
+ * The fingerprint is the exact value the successor transition must name as
63
+ * `previous`, forming the durable chain. Uses the Web Crypto API available
64
+ * in every supported runtime.
65
+ */
66
+ async function testRunTransitionFingerprint(transition) {
67
+ const bytes = new TextEncoder().encode(canonicalTestRunTransitionJson(transition));
68
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
69
+ let fingerprint = "";
70
+ for (const byte of new Uint8Array(digest)) fingerprint += byte.toString(16).padStart(2, "0");
71
+ return fingerprint;
72
+ }
73
+ //#endregion
74
+ //#region src/test-run-transition-rules.ts
75
+ /** Validates the accepted anti-replay state carried by one transition. */
76
+ function validateAccepted(transition, diagnostics) {
77
+ const accepted = transition.accepted;
78
+ if (accepted.length > 4096) diagnostics.push(error("VIZE_MARQUETTE_131", "transition.accepted", "transition must accept at most 4096 admission ids"));
79
+ if (accepted.some((id) => parseTestRunAdmissionId(id) === void 0)) diagnostics.push(error("VIZE_MARQUETTE_141", "transition.accepted", "accepted admission ids must be test-run: followed by 64 lowercase hexadecimal characters"));
80
+ if (accepted.some((id, index) => index > 0 && accepted[index - 1] >= id)) diagnostics.push(error("VIZE_MARQUETTE_154", "transition.accepted", "accepted admission ids must be sorted and unique"));
81
+ if (transition.decision.allowed && !accepted.includes(transition.evidence)) diagnostics.push(error("VIZE_MARQUETTE_156", "transition.accepted", "an allowed transition must accept its own evidence"));
82
+ }
83
+ const DIAGNOSTIC_CODE = /^VIZE_MARQUETTE_[0-9]{3}$/;
84
+ /** Validates the retained decision carried by one transition. */
85
+ function validateDecision(transition, diagnostics) {
86
+ const decision = transition.decision;
87
+ if (decision.allowed && (decision.denialCodes.length > 0 || decision.diagnostics.length > 0)) diagnostics.push(error("VIZE_MARQUETTE_155", "transition.decision", "an allowed decision must carry no diagnostics"));
88
+ if (!decision.allowed && decision.diagnostics.length === 0) diagnostics.push(error("VIZE_MARQUETTE_155", "transition.decision", "a denied decision must carry its diagnostics"));
89
+ if (decision.diagnostics.some((diagnostic) => !DIAGNOSTIC_CODE.test(diagnostic.code))) diagnostics.push(error("VIZE_MARQUETTE_155", "transition.decision", "diagnostic codes must be stable VIZE_MARQUETTE codes"));
90
+ if (!decision.diagnostics.every((diagnostic, index) => {
91
+ if (index === 0) return true;
92
+ const left = decision.diagnostics[index - 1];
93
+ return left.path < diagnostic.path || left.path === diagnostic.path && (left.code < diagnostic.code || left.code === diagnostic.code && left.message <= diagnostic.message);
94
+ })) diagnostics.push(error("VIZE_MARQUETTE_155", "transition.decision", "decision diagnostics must be sorted by path, code, and message"));
95
+ const recomputed = [...new Set(decision.diagnostics.map((diagnostic) => testRunDenialCode(diagnostic)))].sort();
96
+ const retained = [...decision.denialCodes];
97
+ if (recomputed.length !== retained.length || recomputed.some((code, index) => code !== retained[index])) diagnostics.push(error("VIZE_MARQUETTE_155", "transition.decision", "denial codes must match the published diagnostic mapping"));
98
+ }
99
+ //#endregion
100
+ //#region src/test-run-transition.ts
101
+ /**
102
+ * Validates one release transition structurally.
103
+ *
104
+ * Diagnostics use `transition.` paths and are deterministic and sorted by
105
+ * path, code, and message. Validation confirms the record alone is
106
+ * internally coherent — grammar, decision consistency against the published
107
+ * diagnostic mapping, and an allowed decision accepting its own evidence —
108
+ * but only {@link verifyTestRunTransition} can confirm the record extends
109
+ * the durable chain. Codes, paths, messages, and ordering are identical to
110
+ * the native implementation.
111
+ */
112
+ function validateTestRunTransition(transition) {
113
+ const diagnostics = [];
114
+ if (transition.format !== "vize.test-run.transition") diagnostics.push(error("VIZE_MARQUETTE_101", "transition.format", "unsupported release-transition format marker"));
115
+ if ((transition.formatVersion ?? 1) !== 1) diagnostics.push(error("VIZE_MARQUETTE_102", "transition.formatVersion", "unsupported release-transition format version"));
116
+ if (!Number.isInteger(transition.sequence) || transition.sequence < 1) diagnostics.push(error("VIZE_MARQUETTE_152", "transition.sequence", "transition sequence must be at least one"));
117
+ checkSafeInteger(transition.sequence, "transition.sequence", diagnostics);
118
+ if (transition.previous !== null && transition.sequence === 1) diagnostics.push(error("VIZE_MARQUETTE_153", "transition.previous", "genesis transition must not name a predecessor"));
119
+ else if (transition.previous !== null) checkDigest(transition.previous, "transition.previous", diagnostics);
120
+ else if (transition.sequence > 1) diagnostics.push(error("VIZE_MARQUETTE_153", "transition.previous", "transition must name its predecessor"));
121
+ checkTimestamp(transition.decidedAt, "transition.decidedAt", diagnostics);
122
+ const candidate = transition.candidate;
123
+ checkIdentifier(candidate.application, "transition.candidate.application", diagnostics);
124
+ checkIdentifier(candidate.environment, "transition.candidate.environment", diagnostics);
125
+ checkDigest(candidate.contractFingerprint, "transition.candidate.contractFingerprint", diagnostics);
126
+ checkSourceRevision(candidate.sourceRevision, "transition.candidate.sourceRevision", diagnostics);
127
+ if (candidate.release.length === 0 || candidate.release.length > 256) diagnostics.push(error("VIZE_MARQUETTE_106", "transition.candidate.release", "release must be between 1 and 256 characters"));
128
+ checkDigest(candidate.artifactFingerprint, "transition.candidate.artifactFingerprint", diagnostics);
129
+ if (parseTestRunAdmissionId(transition.evidence) === void 0) diagnostics.push(error("VIZE_MARQUETTE_141", "transition.evidence", "transition evidence must be test-run: followed by 64 lowercase hexadecimal characters"));
130
+ validateAccepted(transition, diagnostics);
131
+ validateDecision(transition, diagnostics);
132
+ sortDiagnostics(diagnostics);
133
+ return diagnostics;
134
+ }
135
+ /**
136
+ * Verifies one release transition against the durable chain tip.
137
+ *
138
+ * `previous` is the retained, already-verified predecessor — `null` only
139
+ * when deciding the very first transition of a chain. The transition must
140
+ * validate structurally, extend the predecessor's sequence, fingerprint,
141
+ * scope, and decision time exactly, never re-accept evidence the
142
+ * predecessor already accepted, and carry an accepted state equal to the
143
+ * predecessor's state plus exactly the newly accepted evidence (unchanged
144
+ * for a denial). Any diagnostic rejects the transition: a conforming host
145
+ * must not persist it, and on recovery must discard a tip this function
146
+ * rejects. Diagnostics, denial codes, and ordering are identical to the
147
+ * native implementation, as pinned by the shared transition-decision
148
+ * fixtures.
149
+ */
150
+ async function verifyTestRunTransition(transition, previous) {
151
+ const diagnostics = validateTestRunTransition(transition);
152
+ let priorAccepted = [];
153
+ if (previous === null) {
154
+ if (transition.sequence !== 1) diagnostics.push(error("VIZE_MARQUETTE_157", "transition.sequence", "transition requires its predecessor to verify"));
155
+ } else {
156
+ priorAccepted = previous.accepted;
157
+ if (transition.sequence !== previous.sequence + 1) diagnostics.push(error("VIZE_MARQUETTE_157", "transition.sequence", "transition must extend its predecessor's sequence"));
158
+ if (transition.previous !== await testRunTransitionFingerprint(previous)) diagnostics.push(error("VIZE_MARQUETTE_157", "transition.previous", "transition must name its predecessor's canonical fingerprint"));
159
+ for (const [recorded, expected, path] of [[
160
+ transition.candidate.application,
161
+ previous.candidate.application,
162
+ "transition.candidate.application"
163
+ ], [
164
+ transition.candidate.environment,
165
+ previous.candidate.environment,
166
+ "transition.candidate.environment"
167
+ ]]) if (recorded !== expected) diagnostics.push(error("VIZE_MARQUETTE_157", path, "transition must stay within its predecessor's scope"));
168
+ if (isStrictTimestamp(transition.decidedAt) && isStrictTimestamp(previous.decidedAt) && transition.decidedAt < previous.decidedAt) diagnostics.push(error("VIZE_MARQUETTE_157", "transition.decidedAt", "transition must not predate its predecessor"));
169
+ if (transition.decision.allowed && previous.accepted.includes(transition.evidence)) diagnostics.push(error("VIZE_MARQUETTE_158", "transition.evidence", "accepted evidence must not be accepted again"));
170
+ }
171
+ const expected = [...new Set(transition.decision.allowed ? [...priorAccepted, transition.evidence] : priorAccepted)].sort();
172
+ const accepted = transition.accepted;
173
+ if (accepted.length !== expected.length || accepted.some((id, index) => id !== expected[index])) diagnostics.push(error("VIZE_MARQUETTE_159", "transition.accepted", "accepted state must equal its predecessor's state plus exactly the newly accepted evidence"));
174
+ sortDiagnostics(diagnostics);
175
+ const denialCodes = [...new Set(diagnostics.map(testRunDenialCode))].sort();
176
+ return {
177
+ allowed: diagnostics.length === 0,
178
+ denialCodes,
179
+ diagnostics
180
+ };
181
+ }
182
+ function sortDiagnostics(diagnostics) {
183
+ diagnostics.sort((left, right) => left.path !== right.path ? left.path < right.path ? -1 : 1 : left.code !== right.code ? left.code < right.code ? -1 : 1 : left.message < right.message ? -1 : left.message > right.message ? 1 : 0);
184
+ }
185
+ //#endregion
186
+ export { TEST_RUN_TRANSITION_FORMAT, TEST_RUN_TRANSITION_FORMAT_VERSION, TEST_RUN_TRANSITION_MAX_ACCEPTED, canonicalTestRunTransitionJson, testRunTransitionFingerprint, validateTestRunTransition, verifyTestRunTransition };
187
+
188
+ //# sourceMappingURL=test-run-transition.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-run-transition.mjs","names":[],"sources":["../src/test-run-transition-model.ts","../src/test-run-transition-rules.ts","../src/test-run-transition.ts"],"sourcesContent":["import type { MarquetteDiagnosticSeverity } from \"./validate.js\";\nimport type { TestRunCandidate, TestRunDenialCode } from \"./test-run-admission.js\";\n\n/**\n * Serialized `format` marker for release-transition records.\n *\n * Readers must reject any other value before trusting the record.\n */\nexport const TEST_RUN_TRANSITION_FORMAT = \"vize.test-run.transition\";\n\n/**\n * Current serialized release-transition format.\n *\n * Readers must reject a higher value until they explicitly support it.\n */\nexport const TEST_RUN_TRANSITION_FORMAT_VERSION = 1;\n\n/** Maximum admission ids one transition may carry as accepted state. */\nexport const TEST_RUN_TRANSITION_MAX_ACCEPTED = 4096;\n\n/**\n * One retained diagnostic inside a durable transition record.\n *\n * The shape and serialization match live diagnostics exactly; the retained\n * form is plain data so persisted records can be read back by any host.\n */\nexport interface TestRunRetainedDiagnostic {\n /** Stable machine-readable diagnostic code. */\n readonly code: string;\n /** Severity recorded for the diagnostic; any diagnostic denies. */\n readonly severity: MarquetteDiagnosticSeverity;\n /** JSON-style path into the decided input. */\n readonly path: string;\n /** Human-readable explanation recorded with the decision. */\n readonly message: string;\n}\n\n/**\n * One retained allow-or-deny decision inside a durable transition record.\n *\n * The shape and serialization match live decisions exactly. Validation\n * rejects a retained decision whose `allowed` flag, denial codes, or\n * diagnostic ordering disagree with the published mapping, so a record\n * cannot claim an outcome its own diagnostics contradict.\n */\nexport interface TestRunRetainedDecision {\n /** Whether the release decision admitted the candidate. */\n readonly allowed: boolean;\n /** Deduplicated denial causes sorted lexicographically; empty if allowed. */\n readonly denialCodes: readonly TestRunDenialCode[];\n /** Complete diagnostics in the stable path, code, message order. */\n readonly diagnostics: readonly TestRunRetainedDiagnostic[];\n}\n\n/**\n * One durable atomic release transition.\n *\n * The record binds the decision, the exact candidate and evidence it\n * decided, and the complete accepted anti-replay state after the decision\n * into one canonical document. `sequence` grows by exactly one per\n * transition and `previous` names the predecessor's canonical SHA-256\n * fingerprint, so a chain tip proves the entire decision history and the\n * accepted set can never drift from the decision that produced it.\n *\n * Host durability contract: write the complete canonical bytes to a\n * temporary location, flush them to durable storage, then atomically rename\n * or commit so exactly one complete chain tip exists at every instant; on\n * recovery, verify the tip against its retained predecessor with\n * {@link verifyTestRunTransition} before deciding anything new, and discard\n * — never repair — a torn or partial record.\n */\nexport interface TestRunTransition {\n /** Serialized format marker; always {@link TEST_RUN_TRANSITION_FORMAT}. */\n readonly format: typeof TEST_RUN_TRANSITION_FORMAT;\n /**\n * Serialized format version.\n *\n * Defaults to {@link TEST_RUN_TRANSITION_FORMAT_VERSION}.\n */\n readonly formatVersion?: typeof TEST_RUN_TRANSITION_FORMAT_VERSION;\n /** One-based position of this transition in its chain. */\n readonly sequence: number;\n /** Canonical fingerprint of the predecessor; `null` only at genesis. */\n readonly previous: string | null;\n /** Millisecond-precision UTC instant the decision was made. */\n readonly decidedAt: string;\n /** Exact candidate the decision was made for. */\n readonly candidate: TestRunCandidate;\n /** Exact `test-run:<sha256>` admission id the decision evaluated. */\n readonly evidence: string;\n /** Retained decision exactly as it was produced. */\n readonly decision: TestRunRetainedDecision;\n /**\n * Complete anti-replay state after this transition: every admission id\n * ever accepted in this chain, sorted and unique.\n */\n readonly accepted: readonly string[];\n}\n\n/**\n * Serializes a release transition canonically.\n *\n * Property order matches the record schema and the accepted state sorts\n * lexicographically after deduplication, so equivalent transitions produce\n * byte-identical JSON in every language. These are the exact bytes a host\n * must write atomically and the exact bytes the chain fingerprint covers.\n * Call validation before trusting the record; canonicalization does not\n * make an invalid record valid.\n */\nexport function canonicalTestRunTransitionJson(transition: TestRunTransition): string {\n const canonical = {\n format: transition.format,\n formatVersion: transition.formatVersion ?? TEST_RUN_TRANSITION_FORMAT_VERSION,\n sequence: transition.sequence,\n previous: transition.previous,\n decidedAt: transition.decidedAt,\n candidate: {\n application: transition.candidate.application,\n environment: transition.candidate.environment,\n contractFingerprint: transition.candidate.contractFingerprint,\n sourceRevision: transition.candidate.sourceRevision,\n release: transition.candidate.release,\n artifactFingerprint: transition.candidate.artifactFingerprint,\n },\n evidence: transition.evidence,\n decision: {\n allowed: transition.decision.allowed,\n denialCodes: [...new Set(transition.decision.denialCodes)].sort(),\n diagnostics: transition.decision.diagnostics.map((diagnostic) => ({\n code: diagnostic.code,\n severity: diagnostic.severity,\n path: diagnostic.path,\n message: diagnostic.message,\n })),\n },\n accepted: [...new Set(transition.accepted)].sort(),\n };\n return JSON.stringify(canonical);\n}\n\n/**\n * Returns the lowercase SHA-256 fingerprint of the canonical transition.\n *\n * The fingerprint is the exact value the successor transition must name as\n * `previous`, forming the durable chain. Uses the Web Crypto API available\n * in every supported runtime.\n */\nexport async function testRunTransitionFingerprint(transition: TestRunTransition): Promise<string> {\n const bytes = new TextEncoder().encode(canonicalTestRunTransitionJson(transition));\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","import type { MarquetteDiagnostic } from \"./validate.js\";\nimport { testRunDenialCode } from \"./test-run-admission.js\";\nimport { parseTestRunAdmissionId } from \"./test-run-canonical.js\";\nimport {\n TEST_RUN_TRANSITION_MAX_ACCEPTED,\n type TestRunRetainedDiagnostic,\n type TestRunTransition,\n} from \"./test-run-transition-model.js\";\nimport { error } from \"./test-run-validate-rules.js\";\n\n/** Validates the accepted anti-replay state carried by one transition. */\nexport function validateAccepted(\n transition: TestRunTransition,\n diagnostics: MarquetteDiagnostic[],\n): void {\n const accepted = transition.accepted;\n if (accepted.length > TEST_RUN_TRANSITION_MAX_ACCEPTED) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_131\",\n \"transition.accepted\",\n \"transition must accept at most 4096 admission ids\",\n ),\n );\n }\n if (accepted.some((id) => parseTestRunAdmissionId(id) === undefined)) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_141\",\n \"transition.accepted\",\n \"accepted admission ids must be test-run: followed by 64 lowercase hexadecimal characters\",\n ),\n );\n }\n if (accepted.some((id, index) => index > 0 && (accepted[index - 1] as string) >= id)) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_154\",\n \"transition.accepted\",\n \"accepted admission ids must be sorted and unique\",\n ),\n );\n }\n if (transition.decision.allowed && !accepted.includes(transition.evidence)) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_156\",\n \"transition.accepted\",\n \"an allowed transition must accept its own evidence\",\n ),\n );\n }\n}\n\nconst DIAGNOSTIC_CODE = /^VIZE_MARQUETTE_[0-9]{3}$/;\n\n/** Validates the retained decision carried by one transition. */\nexport function validateDecision(\n transition: TestRunTransition,\n diagnostics: MarquetteDiagnostic[],\n): void {\n const decision = transition.decision;\n if (decision.allowed && (decision.denialCodes.length > 0 || decision.diagnostics.length > 0)) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_155\",\n \"transition.decision\",\n \"an allowed decision must carry no diagnostics\",\n ),\n );\n }\n if (!decision.allowed && decision.diagnostics.length === 0) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_155\",\n \"transition.decision\",\n \"a denied decision must carry its diagnostics\",\n ),\n );\n }\n if (decision.diagnostics.some((diagnostic) => !DIAGNOSTIC_CODE.test(diagnostic.code))) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_155\",\n \"transition.decision\",\n \"diagnostic codes must be stable VIZE_MARQUETTE codes\",\n ),\n );\n }\n const sorted = decision.diagnostics.every((diagnostic, index) => {\n if (index === 0) {\n return true;\n }\n const left = decision.diagnostics[index - 1] as TestRunRetainedDiagnostic;\n return (\n left.path < diagnostic.path ||\n (left.path === diagnostic.path &&\n (left.code < diagnostic.code ||\n (left.code === diagnostic.code && left.message <= diagnostic.message)))\n );\n });\n if (!sorted) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_155\",\n \"transition.decision\",\n \"decision diagnostics must be sorted by path, code, and message\",\n ),\n );\n }\n const recomputed = [\n ...new Set(\n decision.diagnostics.map((diagnostic) =>\n testRunDenialCode(diagnostic as MarquetteDiagnostic),\n ),\n ),\n ].sort();\n const retained = [...decision.denialCodes];\n if (\n recomputed.length !== retained.length ||\n recomputed.some((code, index) => code !== retained[index])\n ) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_155\",\n \"transition.decision\",\n \"denial codes must match the published diagnostic mapping\",\n ),\n );\n }\n}\n","import type { MarquetteDiagnostic } from \"./validate.js\";\nimport {\n testRunDenialCode,\n type TestRunAdmissionDecision,\n type TestRunDenialCode,\n} from \"./test-run-admission.js\";\nimport { parseTestRunAdmissionId } from \"./test-run-canonical.js\";\nimport {\n TEST_RUN_TRANSITION_FORMAT,\n TEST_RUN_TRANSITION_FORMAT_VERSION,\n testRunTransitionFingerprint,\n type TestRunTransition,\n} from \"./test-run-transition-model.js\";\nimport { validateAccepted, validateDecision } from \"./test-run-transition-rules.js\";\nimport {\n checkDigest,\n checkIdentifier,\n checkSafeInteger,\n checkSourceRevision,\n checkTimestamp,\n error,\n isStrictTimestamp,\n} from \"./test-run-validate-rules.js\";\n\nexport {\n TEST_RUN_TRANSITION_FORMAT,\n TEST_RUN_TRANSITION_FORMAT_VERSION,\n TEST_RUN_TRANSITION_MAX_ACCEPTED,\n canonicalTestRunTransitionJson,\n testRunTransitionFingerprint,\n type TestRunRetainedDecision,\n type TestRunRetainedDiagnostic,\n type TestRunTransition,\n} from \"./test-run-transition-model.js\";\n\n/**\n * Validates one release transition structurally.\n *\n * Diagnostics use `transition.` paths and are deterministic and sorted by\n * path, code, and message. Validation confirms the record alone is\n * internally coherent — grammar, decision consistency against the published\n * diagnostic mapping, and an allowed decision accepting its own evidence —\n * but only {@link verifyTestRunTransition} can confirm the record extends\n * the durable chain. Codes, paths, messages, and ordering are identical to\n * the native implementation.\n */\nexport function validateTestRunTransition(transition: TestRunTransition): MarquetteDiagnostic[] {\n const diagnostics: MarquetteDiagnostic[] = [];\n\n if ((transition.format as string) !== TEST_RUN_TRANSITION_FORMAT) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_101\",\n \"transition.format\",\n \"unsupported release-transition format marker\",\n ),\n );\n }\n const version = transition.formatVersion ?? TEST_RUN_TRANSITION_FORMAT_VERSION;\n if (version !== TEST_RUN_TRANSITION_FORMAT_VERSION) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_102\",\n \"transition.formatVersion\",\n \"unsupported release-transition format version\",\n ),\n );\n }\n\n if (!Number.isInteger(transition.sequence) || transition.sequence < 1) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_152\",\n \"transition.sequence\",\n \"transition sequence must be at least one\",\n ),\n );\n }\n checkSafeInteger(transition.sequence, \"transition.sequence\", diagnostics);\n if (transition.previous !== null && transition.sequence === 1) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_153\",\n \"transition.previous\",\n \"genesis transition must not name a predecessor\",\n ),\n );\n } else if (transition.previous !== null) {\n checkDigest(transition.previous, \"transition.previous\", diagnostics);\n } else if (transition.sequence > 1) {\n diagnostics.push(\n error(\"VIZE_MARQUETTE_153\", \"transition.previous\", \"transition must name its predecessor\"),\n );\n }\n checkTimestamp(transition.decidedAt, \"transition.decidedAt\", diagnostics);\n\n const candidate = transition.candidate;\n checkIdentifier(candidate.application, \"transition.candidate.application\", diagnostics);\n checkIdentifier(candidate.environment, \"transition.candidate.environment\", diagnostics);\n checkDigest(\n candidate.contractFingerprint,\n \"transition.candidate.contractFingerprint\",\n diagnostics,\n );\n checkSourceRevision(candidate.sourceRevision, \"transition.candidate.sourceRevision\", diagnostics);\n if (candidate.release.length === 0 || candidate.release.length > 256) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_106\",\n \"transition.candidate.release\",\n \"release must be between 1 and 256 characters\",\n ),\n );\n }\n checkDigest(\n candidate.artifactFingerprint,\n \"transition.candidate.artifactFingerprint\",\n diagnostics,\n );\n\n if (parseTestRunAdmissionId(transition.evidence) === undefined) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_141\",\n \"transition.evidence\",\n \"transition evidence must be test-run: followed by 64 lowercase hexadecimal characters\",\n ),\n );\n }\n validateAccepted(transition, diagnostics);\n validateDecision(transition, diagnostics);\n\n sortDiagnostics(diagnostics);\n return diagnostics;\n}\n\n/**\n * Verifies one release transition against the durable chain tip.\n *\n * `previous` is the retained, already-verified predecessor — `null` only\n * when deciding the very first transition of a chain. The transition must\n * validate structurally, extend the predecessor's sequence, fingerprint,\n * scope, and decision time exactly, never re-accept evidence the\n * predecessor already accepted, and carry an accepted state equal to the\n * predecessor's state plus exactly the newly accepted evidence (unchanged\n * for a denial). Any diagnostic rejects the transition: a conforming host\n * must not persist it, and on recovery must discard a tip this function\n * rejects. Diagnostics, denial codes, and ordering are identical to the\n * native implementation, as pinned by the shared transition-decision\n * fixtures.\n */\nexport async function verifyTestRunTransition(\n transition: TestRunTransition,\n previous: TestRunTransition | null,\n): Promise<TestRunAdmissionDecision> {\n const diagnostics = validateTestRunTransition(transition);\n\n let priorAccepted: readonly string[] = [];\n if (previous === null) {\n if (transition.sequence !== 1) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_157\",\n \"transition.sequence\",\n \"transition requires its predecessor to verify\",\n ),\n );\n }\n } else {\n priorAccepted = previous.accepted;\n if (transition.sequence !== previous.sequence + 1) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_157\",\n \"transition.sequence\",\n \"transition must extend its predecessor's sequence\",\n ),\n );\n }\n if (transition.previous !== (await testRunTransitionFingerprint(previous))) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_157\",\n \"transition.previous\",\n \"transition must name its predecessor's canonical fingerprint\",\n ),\n );\n }\n for (const [recorded, expected, path] of [\n [\n transition.candidate.application,\n previous.candidate.application,\n \"transition.candidate.application\",\n ],\n [\n transition.candidate.environment,\n previous.candidate.environment,\n \"transition.candidate.environment\",\n ],\n ] as const) {\n if (recorded !== expected) {\n diagnostics.push(\n error(\"VIZE_MARQUETTE_157\", path, \"transition must stay within its predecessor's scope\"),\n );\n }\n }\n if (\n isStrictTimestamp(transition.decidedAt) &&\n isStrictTimestamp(previous.decidedAt) &&\n transition.decidedAt < previous.decidedAt\n ) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_157\",\n \"transition.decidedAt\",\n \"transition must not predate its predecessor\",\n ),\n );\n }\n if (transition.decision.allowed && previous.accepted.includes(transition.evidence)) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_158\",\n \"transition.evidence\",\n \"accepted evidence must not be accepted again\",\n ),\n );\n }\n }\n\n const expected = [\n ...new Set(\n transition.decision.allowed ? [...priorAccepted, transition.evidence] : priorAccepted,\n ),\n ].sort();\n const accepted = transition.accepted;\n if (accepted.length !== expected.length || accepted.some((id, index) => id !== expected[index])) {\n diagnostics.push(\n error(\n \"VIZE_MARQUETTE_159\",\n \"transition.accepted\",\n \"accepted state must equal its predecessor's state plus exactly the newly accepted evidence\",\n ),\n );\n }\n\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":"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"}