@@ -60,7 +60,7 @@ function validateObservedObject(value, label, identityField, errors) {
6060
6161function validateCandidateObject ( value , errors ) {
6262 if ( ! isPlainObject ( value ) ) {
63- errors . push ( 'candidate must be an object when releaseState is candidate' )
63+ errors . push ( 'candidate must be an object for a source candidate' )
6464 return
6565 }
6666 if ( ! / ^ \d + \. \d + \. \d + $ / . test ( String ( value . targetVersion || '' ) ) ) {
@@ -165,6 +165,19 @@ function compareSemver(left, right) {
165165 return 0
166166}
167167
168+ function currentTruthLifecycle ( record ) {
169+ if ( Object . prototype . hasOwnProperty . call ( record , 'sourceCandidate' ) ) {
170+ const status = record . sourceCandidate ?. status
171+ if ( ! SOURCE_CANDIDATE_STATUSES . has ( status ) ) return null
172+ return status === 'RELEASED' ? 'released' : 'candidate'
173+ }
174+ // Read-only compatibility for records predating SourceCandidateTruthV1.
175+ // Display prose never grants a state; the existing evidence is still checked.
176+ if ( Object . prototype . hasOwnProperty . call ( record , 'candidate' ) ) return 'candidate'
177+ return record . sourceVersion === record . npmLatest &&
178+ record . githubRelease ?. tag === `v${ record . sourceVersion } ` ? 'released' : null
179+ }
180+
168181function validateTruthRecord ( record ) {
169182 const errors = [ ]
170183 if ( ! isPlainObject ( record ) ) return { valid : false , errors : [ 'record must be a JSON object' ] }
@@ -182,7 +195,8 @@ function validateTruthRecord(record) {
182195 if ( typeof record . releaseState !== 'string' || ! record . releaseState . trim ( ) ) {
183196 errors . push ( 'releaseState must be a non-empty string' )
184197 }
185- if ( / ^ c a n d i d a t e \b / i. test ( String ( record . releaseState || '' ) ) ) {
198+ if ( currentTruthLifecycle ( record ) === 'candidate' ||
199+ Object . prototype . hasOwnProperty . call ( record , 'candidate' ) ) {
186200 validateCandidateObject ( record . candidate , errors )
187201 }
188202 if ( ! / ^ [ 0 - 9 a - f ] { 40 } $ / i. test ( String ( record . gitHead || '' ) ) ) {
@@ -315,8 +329,9 @@ function validateDevCodexCurrentTruth(input = {}) {
315329 const errors = [ ...parsed . errors ]
316330 const record = parsed . record
317331 if ( record ) {
318- const released = / ^ r e l e a s e d \b / i. test ( record . releaseState )
319- const candidate = / ^ c a n d i d a t e \b / i. test ( record . releaseState )
332+ const lifecycle = currentTruthLifecycle ( record )
333+ const released = lifecycle === 'released'
334+ const candidate = lifecycle === 'candidate'
320335 if ( input . packageVersion && record . sourceVersion !== input . packageVersion ) {
321336 errors . push ( `sourceVersion drift: ${ record . sourceVersion } != ${ input . packageVersion } ` )
322337 }
@@ -335,7 +350,7 @@ function validateDevCodexCurrentTruth(input = {}) {
335350 errors . push ( `sourceCandidate.candidateId drift: ${ record . sourceCandidate ?. candidateId } != ${ input . candidateId } ` )
336351 }
337352 if ( ! released && ! candidate ) {
338- errors . push ( `releaseState must describe a released source or an authorized candidate: ${ record . releaseState } ` )
353+ errors . push ( 'current lifecycle requires a valid sourceCandidate status or legacy candidate/release evidence' )
339354 } else if ( released ) {
340355 if ( input . packageVersion && record . npmLatest !== input . packageVersion ) {
341356 errors . push ( `npmLatest drift: ${ record . npmLatest } != ${ input . packageVersion } ` )
0 commit comments