11const EVIDENCE_KINDS = Object . freeze ( [ 'code' , 'test' , 'runtime' , 'delivery' , 'human' ] ) ;
22const DELIVERY_POSTURES = Object . freeze ( [ 'repo_only' , 'delivery_sensitive' ] ) ;
33const CLOSURE_SURFACES = Object . freeze ( [ 'verify' , 'audit-milestone' , 'complete-milestone' ] ) ;
4+ const RELEASE_CLAIM_POSTURES = Object . freeze ( [
5+ 'repo_closeout' ,
6+ 'runtime_validated_closeout' ,
7+ 'delivery_supported_closeout' ,
8+ ] ) ;
49
510const LEGACY_EVIDENCE_ALIASES = Object . freeze ( {
611 code : 'code' ,
@@ -22,8 +27,8 @@ const EVIDENCE_MATRIX = Object.freeze({
2227 blockedSoloKinds : Object . freeze ( [ 'human' , 'delivery' ] ) ,
2328 } ) ,
2429 delivery_sensitive : Object . freeze ( {
25- requiredKinds : Object . freeze ( [ 'code' , 'runtime' ] ) ,
26- recommendedKinds : Object . freeze ( [ 'test' , 'delivery' , ' human'] ) ,
30+ requiredKinds : Object . freeze ( [ 'code' , 'runtime' , 'delivery' ] ) ,
31+ recommendedKinds : Object . freeze ( [ 'test' , 'human' ] ) ,
2732 blockedSoloKinds : Object . freeze ( [ 'code' , 'human' ] ) ,
2833 } ) ,
2934 } ) ,
@@ -53,7 +58,45 @@ const EVIDENCE_MATRIX = Object.freeze({
5358 } ) ,
5459} ) ;
5560
56- export { CLOSURE_SURFACES , DELIVERY_POSTURES , EVIDENCE_KINDS } ;
61+ const CONTRADICTION_CATEGORIES = Object . freeze ( [
62+ 'evidence' ,
63+ 'public_surface' ,
64+ 'runtime' ,
65+ 'delivery' ,
66+ 'planning_drift' ,
67+ 'generated_surface' ,
68+ ] ) ;
69+
70+ const CONTRADICTION_STATUSES = Object . freeze ( [ 'passed' , 'failed' , 'not_applicable' ] ) ;
71+
72+ const RELEASE_CLAIM_MATRIX = Object . freeze ( {
73+ repo_closeout : Object . freeze ( {
74+ deliveryPosture : 'repo_only' ,
75+ requiredClaimKinds : Object . freeze ( [ ] ) ,
76+ allowedClaim : 'Repo-local milestone or phase closeout is supported by planning and repository artifacts only.' ,
77+ invalidClaim : 'Do not imply runtime validation, delivery, publication, or public support from repo-local closeout alone.' ,
78+ } ) ,
79+ runtime_validated_closeout : Object . freeze ( {
80+ deliveryPosture : 'repo_only' ,
81+ requiredClaimKinds : Object . freeze ( [ 'runtime' ] ) ,
82+ allowedClaim : 'Runtime behavior or a runtime surface was directly executed and observed for the named runtime or surface.' ,
83+ invalidClaim : 'Do not generalize validation from one runtime or generated surface to another.' ,
84+ } ) ,
85+ delivery_supported_closeout : Object . freeze ( {
86+ deliveryPosture : 'delivery_sensitive' ,
87+ requiredClaimKinds : Object . freeze ( [ ] ) ,
88+ allowedClaim : 'Externally consumed release, support, install, or delivery claims are supported by the delivery-sensitive evidence bar.' ,
89+ invalidClaim : 'Do not imply merge, package, tag, GitHub Release, publication, generated-surface freshness, or public support without matching delivery evidence.' ,
90+ } ) ,
91+ } ) ;
92+
93+ const CONTRADICTION_BLOCKERS_BY_POSTURE = Object . freeze ( {
94+ repo_closeout : Object . freeze ( [ 'evidence' , 'public_surface' , 'planning_drift' ] ) ,
95+ runtime_validated_closeout : Object . freeze ( [ 'evidence' , 'runtime' , 'generated_surface' , 'planning_drift' ] ) ,
96+ delivery_supported_closeout : CONTRADICTION_CATEGORIES ,
97+ } ) ;
98+
99+ export { CLOSURE_SURFACES , DELIVERY_POSTURES , EVIDENCE_KINDS , RELEASE_CLAIM_POSTURES } ;
57100
58101export function normalizeEvidenceKind ( kind ) {
59102 if ( ! kind ) {
@@ -78,6 +121,11 @@ export function isClosureSurface(surface) {
78121 return CLOSURE_SURFACES . includes ( surface ) ;
79122}
80123
124+ export function normalizeReleaseClaimPosture ( posture ) {
125+ if ( ! posture ) return 'repo_closeout' ;
126+ return RELEASE_CLAIM_POSTURES . includes ( posture ) ? posture : null ;
127+ }
128+
81129export function getEvidenceContract ( surface , deliveryPosture ) {
82130 const matrix = EVIDENCE_MATRIX [ surface ] ;
83131 if ( ! matrix ) {
@@ -108,5 +156,170 @@ export function describeEvidenceSurface(surface) {
108156 surface,
109157 supportedKinds : [ ...EVIDENCE_KINDS ] ,
110158 deliveryPostures : DELIVERY_POSTURES . map ( ( deliveryPosture ) => getEvidenceContract ( surface , deliveryPosture ) ) ,
159+ releaseClaimPostures : RELEASE_CLAIM_POSTURES . map ( ( releaseClaimPosture ) => getReleaseClaimContract ( surface , releaseClaimPosture ) ) ,
160+ } ;
161+ }
162+
163+ function uniqueKinds ( kinds ) {
164+ return [ ...new Set ( kinds ) ] ;
165+ }
166+
167+ function getDowngradePosture ( observedKinds ) {
168+ if ( observedKinds . includes ( 'runtime' ) ) {
169+ return 'runtime_validated_closeout' ;
170+ }
171+ return 'repo_closeout' ;
172+ }
173+
174+ export function getReleaseClaimContract ( surface , releaseClaimPosture = 'repo_closeout' ) {
175+ const posture = normalizeReleaseClaimPosture ( releaseClaimPosture ) ;
176+ if ( ! posture ) {
177+ throw new Error ( `Unsupported release claim posture: ${ releaseClaimPosture } ` ) ;
178+ }
179+ const claim = RELEASE_CLAIM_MATRIX [ posture ] ;
180+ const evidence = getEvidenceContract ( surface , claim . deliveryPosture ) ;
181+ const requiredKinds = uniqueKinds ( [ ...evidence . requiredKinds , ...claim . requiredClaimKinds ] ) ;
182+
183+ return {
184+ surface,
185+ releaseClaimPosture : posture ,
186+ deliveryPosture : claim . deliveryPosture ,
187+ supportedKinds : [ ...EVIDENCE_KINDS ] ,
188+ requiredKinds,
189+ requiredClaimKinds : [ ...claim . requiredClaimKinds ] ,
190+ allowedClaim : claim . allowedClaim ,
191+ invalidClaim : claim . invalidClaim ,
192+ waiverRule : 'Waivers may only narrow the release claim posture or defer an unsupported claim; they never satisfy missing required evidence for the stronger claim.' ,
193+ deferralRule : 'Deferrals must name the unsupported claim, missing evidence kinds, and later workflow or milestone candidate when known.' ,
194+ contradictionCategories : [ ...CONTRADICTION_CATEGORIES ] ,
195+ } ;
196+ }
197+
198+ export function evaluateReleaseClaimPosture ( {
199+ surface,
200+ releaseClaimPosture = 'repo_closeout' ,
201+ observedKinds = [ ] ,
202+ waivedKinds = [ ] ,
203+ } = { } ) {
204+ const contract = getReleaseClaimContract ( surface , releaseClaimPosture ) ;
205+ const observed = normalizeEvidenceKinds ( observedKinds ) ;
206+ const waived = normalizeEvidenceKinds ( waivedKinds ) ;
207+ const missingKinds = contract . requiredKinds . filter ( ( kind ) => ! observed . includes ( kind ) ) ;
208+ const invalidWaivers = waived . filter ( ( kind ) => missingKinds . includes ( kind ) ) ;
209+ const hasUnsupportedStrongClaim = contract . releaseClaimPosture !== 'repo_closeout' && missingKinds . length > 0 ;
210+
211+ return {
212+ surface : contract . surface ,
213+ releaseClaimPosture : contract . releaseClaimPosture ,
214+ deliveryPosture : contract . deliveryPosture ,
215+ requiredKinds : [ ...contract . requiredKinds ] ,
216+ observedKinds : observed ,
217+ missingKinds,
218+ invalidWaivers,
219+ status : missingKinds . length === 0 && invalidWaivers . length === 0 ? 'supported' : 'unsupported' ,
220+ disposition : hasUnsupportedStrongClaim ? 'downgrade_or_defer' : missingKinds . length > 0 ? 'block_or_defer' : 'proceed' ,
221+ downgradeTo : hasUnsupportedStrongClaim ? getDowngradePosture ( observed ) : null ,
222+ deferredClaims : hasUnsupportedStrongClaim
223+ ? [ { claim : contract . releaseClaimPosture , missingKinds } ]
224+ : [ ] ,
225+ } ;
226+ }
227+
228+ export function evaluateReleaseClaimCloseoutContract ( {
229+ surface,
230+ deliveryPosture = null ,
231+ releaseClaimPosture = 'repo_closeout' ,
232+ observedKinds = [ ] ,
233+ waivedKinds = [ ] ,
234+ unsupportedClaims = [ ] ,
235+ deferrals = [ ] ,
236+ contradictionChecks = { } ,
237+ } = { } ) {
238+ const posture = evaluateReleaseClaimPosture ( {
239+ surface,
240+ releaseClaimPosture,
241+ observedKinds,
242+ waivedKinds,
243+ } ) ;
244+ const missingContradictionChecks = CONTRADICTION_CATEGORIES . filter ( ( name ) => ! ( name in contradictionChecks ) ) ;
245+ const failedContradictionChecks = Object . entries ( contradictionChecks )
246+ . filter ( ( [ , status ] ) => status === 'failed' )
247+ . map ( ( [ name ] ) => name ) ;
248+ const unknownContradictionChecks = Object . keys ( contradictionChecks )
249+ . filter ( ( name ) => ! CONTRADICTION_CATEGORIES . includes ( name ) ) ;
250+ const invalidContradictionChecks = Object . entries ( contradictionChecks )
251+ . filter ( ( [ , status ] ) => ! CONTRADICTION_STATUSES . includes ( status ) )
252+ . map ( ( [ name ] ) => name ) ;
253+ const blockingContradictionChecks = failedContradictionChecks . filter ( ( name ) =>
254+ CONTRADICTION_BLOCKERS_BY_POSTURE [ posture . releaseClaimPosture ] . includes ( name )
255+ ) ;
256+ const unresolvedUnsupportedClaims = unsupportedClaims . filter ( ( claim ) =>
257+ ! deferrals . some ( ( deferral ) => namesUnsupportedClaim ( deferral , claim ) )
258+ ) ;
259+ const blockers = [ ] ;
260+
261+ if ( deliveryPosture && deliveryPosture !== posture . deliveryPosture ) {
262+ blockers . push ( {
263+ code : 'incompatible_release_claim_posture' ,
264+ details : [ `${ deliveryPosture } cannot support ${ posture . releaseClaimPosture } ; expected ${ posture . deliveryPosture } ` ] ,
265+ } ) ;
266+ }
267+
268+ if ( posture . missingKinds . length > 0 ) {
269+ blockers . push ( { code : 'missing_required_release_evidence' , details : posture . missingKinds } ) ;
270+ }
271+ if ( posture . invalidWaivers . length > 0 ) {
272+ blockers . push ( { code : 'invalid_release_waivers' , details : posture . invalidWaivers } ) ;
273+ }
274+ if ( unresolvedUnsupportedClaims . length > 0 ) {
275+ blockers . push ( { code : 'unsupported_release_claims' , details : unresolvedUnsupportedClaims } ) ;
276+ }
277+ if ( missingContradictionChecks . length > 0 ) {
278+ blockers . push ( { code : 'missing_release_contradiction_checks' , details : missingContradictionChecks } ) ;
279+ }
280+ if ( unknownContradictionChecks . length > 0 ) {
281+ blockers . push ( { code : 'unknown_release_contradiction_checks' , details : unknownContradictionChecks } ) ;
282+ }
283+ if ( invalidContradictionChecks . length > 0 ) {
284+ blockers . push ( { code : 'invalid_release_contradiction_checks' , details : invalidContradictionChecks } ) ;
285+ }
286+ if ( blockingContradictionChecks . length > 0 ) {
287+ blockers . push ( { code : 'failed_release_contradiction_checks' , details : blockingContradictionChecks } ) ;
288+ }
289+
290+ return {
291+ ...posture ,
292+ unsupportedClaims : [ ...unsupportedClaims ] ,
293+ deferrals : [ ...deferrals ] ,
294+ failedContradictionChecks : blockingContradictionChecks ,
295+ allFailedContradictionChecks : failedContradictionChecks ,
296+ missingContradictionChecks,
297+ unknownContradictionChecks,
298+ invalidContradictionChecks,
299+ unresolvedUnsupportedClaims,
300+ blockers,
301+ status : blockers . length === 0 ? 'supported' : 'unsupported' ,
111302 } ;
112303}
304+
305+ function namesUnsupportedClaim ( deferral , claim ) {
306+ const normalizedDeferral = normalizeClaimText ( deferral ) ;
307+ const normalizedClaim = normalizeClaimText ( claim ) ;
308+ if ( ! normalizedDeferral || ! normalizedClaim ) return false ;
309+ return normalizedDeferral . includes ( normalizedClaim ) && isStructuredDeferral ( normalizedDeferral ) ;
310+ }
311+
312+ function isStructuredDeferral ( normalizedDeferral ) {
313+ const namesEvidenceKind = EVIDENCE_KINDS . some ( ( kind ) => normalizedDeferral . includes ( kind ) ) ;
314+ const namesLaterTarget = / \b ( l a t e r | n e x t | f u t u r e | w o r k f l o w | m i l e s t o n e | p h a s e | g s d d ) \b / . test ( normalizedDeferral ) ;
315+ return namesEvidenceKind && namesLaterTarget ;
316+ }
317+
318+ function normalizeClaimText ( value ) {
319+ return String ( value || '' )
320+ . trim ( )
321+ . toLowerCase ( )
322+ . replace ( / [ ^ a - z 0 - 9 ] + / g, ' ' )
323+ . replace ( / \s + / g, ' ' )
324+ . trim ( ) ;
325+ }
0 commit comments