@vizejs/marquette 0.299.1 → 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.
- package/LICENSE +21 -0
- package/README.md +24 -3
- package/dist/application-contract.schema.json +167 -0
- package/dist/test-run-admission.d.mts +58 -2
- package/dist/test-run-admission.d.mts.map +1 -1
- package/dist/test-run-admission.mjs +100 -2
- package/dist/test-run-admission.mjs.map +1 -1
- package/dist/test-run-admission.schema.json +84 -0
- package/dist/test-run-canonical-CB-KzZxk.d.mts +35 -0
- package/dist/test-run-canonical-CB-KzZxk.d.mts.map +1 -0
- package/dist/test-run-canonical.d.mts +2 -35
- package/dist/test-run-check.d.mts +80 -0
- package/dist/test-run-check.d.mts.map +1 -0
- package/dist/test-run-check.mjs +116 -0
- package/dist/test-run-check.mjs.map +1 -0
- package/dist/test-run-check.schema.json +82 -0
- package/dist/test-run-evidence.schema.json +266 -0
- package/dist/test-run-transition.d.mts +146 -0
- package/dist/test-run-transition.d.mts.map +1 -0
- package/dist/test-run-transition.mjs +188 -0
- package/dist/test-run-transition.mjs.map +1 -0
- package/dist/test-run-transition.schema.json +148 -0
- package/dist/{test-run-validate-XF0vyoWI.mjs → test-run-validate-C_KR031E.mjs} +5 -5
- package/dist/test-run-validate-C_KR031E.mjs.map +1 -0
- package/dist/test-run-validate.mjs +1 -1
- package/package.json +27 -14
- package/dist/test-run-canonical.d.mts.map +0 -1
- package/dist/test-run-validate-XF0vyoWI.mjs.map +0 -1
|
@@ -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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"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://vizejs.dev/schemas/marquette/test-run-transition.schema.json",
|
|
4
|
+
"title": "Vize Release Transition",
|
|
5
|
+
"description": "One durable atomic release transition: a single canonical record binding one release decision to the complete accepted anti-replay state, chained to its predecessor by canonical SHA-256 fingerprint. Host durability contract: a conforming host must write the complete canonical bytes to a temporary location, flush them to durable storage, then atomically rename or commit so exactly one complete chain tip exists at every instant; on recovery it must verify the tip against its retained predecessor before deciding anything new, and discard - never repair - a torn or partial record. Verification is fail-closed and identical across JavaScript, Rust, Go, and JVM hosts; the shared tests/fixtures/test-run-evidence transition-decision fixtures are the conformance source of truth for new host implementations.",
|
|
6
|
+
"$ref": "#/$defs/transition",
|
|
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; the only admissible form of accepted 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 decision was made for. Application and environment must stay constant along one chain.",
|
|
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
|
+
"diagnostic": {
|
|
57
|
+
"description": "One retained diagnostic, shaped and serialized exactly like a live diagnostic.",
|
|
58
|
+
"type": "object",
|
|
59
|
+
"additionalProperties": false,
|
|
60
|
+
"required": ["code", "severity", "path", "message"],
|
|
61
|
+
"properties": {
|
|
62
|
+
"code": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"pattern": "^VIZE_MARQUETTE_[0-9]{3}$"
|
|
65
|
+
},
|
|
66
|
+
"severity": { "enum": ["error", "warning"] },
|
|
67
|
+
"path": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"minLength": 1
|
|
70
|
+
},
|
|
71
|
+
"message": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"minLength": 1
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"decision": {
|
|
78
|
+
"description": "Retained allow-or-deny decision exactly as it was produced. allowed must agree with the diagnostics, and denialCodes must equal the published mapping of the diagnostics; verification rejects a record claiming an outcome its own diagnostics contradict.",
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": false,
|
|
81
|
+
"required": ["allowed", "denialCodes", "diagnostics"],
|
|
82
|
+
"properties": {
|
|
83
|
+
"allowed": { "type": "boolean" },
|
|
84
|
+
"denialCodes": {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"uniqueItems": true,
|
|
87
|
+
"items": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"diagnostics": {
|
|
93
|
+
"type": "array",
|
|
94
|
+
"items": { "$ref": "#/$defs/diagnostic" }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"transition": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"additionalProperties": false,
|
|
101
|
+
"required": [
|
|
102
|
+
"format",
|
|
103
|
+
"formatVersion",
|
|
104
|
+
"sequence",
|
|
105
|
+
"previous",
|
|
106
|
+
"decidedAt",
|
|
107
|
+
"candidate",
|
|
108
|
+
"evidence",
|
|
109
|
+
"decision",
|
|
110
|
+
"accepted"
|
|
111
|
+
],
|
|
112
|
+
"properties": {
|
|
113
|
+
"format": {
|
|
114
|
+
"description": "Serialized format marker; readers must reject any other value.",
|
|
115
|
+
"const": "vize.test-run.transition"
|
|
116
|
+
},
|
|
117
|
+
"formatVersion": {
|
|
118
|
+
"description": "Serialized format version; readers must reject a higher value until they explicitly support it.",
|
|
119
|
+
"const": 1
|
|
120
|
+
},
|
|
121
|
+
"sequence": {
|
|
122
|
+
"description": "One-based chain position; each transition extends its predecessor by exactly one.",
|
|
123
|
+
"type": "integer",
|
|
124
|
+
"minimum": 1,
|
|
125
|
+
"maximum": 9007199254740991
|
|
126
|
+
},
|
|
127
|
+
"previous": {
|
|
128
|
+
"description": "Canonical SHA-256 fingerprint of the predecessor transition; null only at genesis (sequence 1).",
|
|
129
|
+
"anyOf": [{ "$ref": "#/$defs/digest" }, { "type": "null" }]
|
|
130
|
+
},
|
|
131
|
+
"decidedAt": {
|
|
132
|
+
"description": "Millisecond-precision UTC instant of the decision; never earlier than the predecessor's.",
|
|
133
|
+
"$ref": "#/$defs/timestamp"
|
|
134
|
+
},
|
|
135
|
+
"candidate": { "$ref": "#/$defs/candidate" },
|
|
136
|
+
"evidence": { "$ref": "#/$defs/admissionId" },
|
|
137
|
+
"decision": { "$ref": "#/$defs/decision" },
|
|
138
|
+
"accepted": {
|
|
139
|
+
"description": "Complete anti-replay state after this transition: every admission id ever accepted in this chain, sorted and unique. An allowed transition adds exactly its own evidence to its predecessor's state; a denied transition leaves the state unchanged; an already accepted id can never be accepted again.",
|
|
140
|
+
"type": "array",
|
|
141
|
+
"maxItems": 4096,
|
|
142
|
+
"uniqueItems": true,
|
|
143
|
+
"items": { "$ref": "#/$defs/admissionId" }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -26,8 +26,8 @@ function checkDigest(value, path, diagnostics) {
|
|
|
26
26
|
if (!DIGEST.test(value)) diagnostics.push(error("VIZE_MARQUETTE_104", path, "fingerprint must be 64 lowercase hexadecimal characters"));
|
|
27
27
|
}
|
|
28
28
|
/** Validates an exact source revision digest. */
|
|
29
|
-
function checkSourceRevision(value, diagnostics) {
|
|
30
|
-
if (!SOURCE_REVISION.test(value)) diagnostics.push(error("VIZE_MARQUETTE_105",
|
|
29
|
+
function checkSourceRevision(value, path, diagnostics) {
|
|
30
|
+
if (!SOURCE_REVISION.test(value)) diagnostics.push(error("VIZE_MARQUETTE_105", path, "source revision must be 40 to 128 lowercase hexadecimal characters"));
|
|
31
31
|
}
|
|
32
32
|
/** Returns whether `value` is a millisecond-precision UTC calendar instant. */
|
|
33
33
|
function isStrictTimestamp(value) {
|
|
@@ -195,7 +195,7 @@ function validateTestRunEvidence(evidence) {
|
|
|
195
195
|
checkIdentifier(evidence.application, "application", diagnostics);
|
|
196
196
|
checkIdentifier(evidence.environment, "environment", diagnostics);
|
|
197
197
|
checkDigest(evidence.contractFingerprint, "contractFingerprint", diagnostics);
|
|
198
|
-
checkSourceRevision(evidence.sourceRevision, diagnostics);
|
|
198
|
+
checkSourceRevision(evidence.sourceRevision, "sourceRevision", diagnostics);
|
|
199
199
|
if (evidence.release.length === 0 || evidence.release.length > 256) diagnostics.push(error("VIZE_MARQUETTE_106", "release", "release must be between 1 and 256 characters"));
|
|
200
200
|
checkIdentifier(evidence.artifact.id, "artifact.id", diagnostics);
|
|
201
201
|
checkDigest(evidence.artifact.fingerprint, "artifact.fingerprint", diagnostics);
|
|
@@ -224,6 +224,6 @@ function validateTestRunEvidence(evidence) {
|
|
|
224
224
|
return diagnostics;
|
|
225
225
|
}
|
|
226
226
|
//#endregion
|
|
227
|
-
export {
|
|
227
|
+
export { checkDigest as a, checkSourceRevision as c, isStrictTimestamp as d, TEST_RUN_MAX_TARGETS as i, checkTimestamp as l, TEST_RUN_MAX_SHARDS as n, checkIdentifier as o, TEST_RUN_MAX_SUITES as r, checkSafeInteger as s, validateTestRunEvidence as t, error as u };
|
|
228
228
|
|
|
229
|
-
//# sourceMappingURL=test-run-validate-
|
|
229
|
+
//# sourceMappingURL=test-run-validate-C_KR031E.mjs.map
|