@@ -32,6 +32,21 @@ const CLASSIFICATIONS = new Set([
3232 'project-local' ,
3333 'already-covered'
3434] )
35+ const EXISTENCE_STATUSES = new Set ( [ 'absent' , 'partial' , 'present' , 'equivalent-covered' , 'unverified' ] )
36+ const LEDGER_DISPOSITIONS = new Set ( [ 'absorb-candidate' , 'residual-candidate' , 'close-ledger' , 'defer' ] )
37+ const COVERED_EXISTENCE = new Set ( [ 'present' , 'equivalent-covered' ] )
38+ const ENFORCEMENT_LEVELS = new Set ( [ 'hard-probe' , 'structural-gate' , 'conditional-probe' , 'checklist-only' , 'none' ] )
39+ const PROBE_CLASSES = new Set ( [
40+ 'machine-sample' ,
41+ 'structural-schema' ,
42+ 'fixture-replay' ,
43+ 'extend-existing' ,
44+ 'checklist-only' ,
45+ 'probe-forbidden'
46+ ] )
47+ const PROBE_NECESSITIES = new Set ( [ 'required' , 'conditional' , 'not-required' , 'forbidden' ] )
48+ const ALWAYS_ON_IMPACTS = new Set ( [ 'none' , 'test-only' , 'conditional-path' , 'always-on-path' ] )
49+ const WEAK_ENFORCEMENT = new Set ( [ 'checklist-only' , 'none' ] )
3550
3651function stableStringify ( value ) {
3752 if ( Array . isArray ( value ) ) return `[${ value . map ( stableStringify ) . join ( ',' ) } ]`
@@ -70,6 +85,183 @@ function validateLayerChecks(layerChecks, pathPrefix) {
7085 return issues
7186}
7287
88+ function validateSourceExistence ( sourceExistence , pathPrefix , commonDecision ) {
89+ const issues = [ ]
90+ if ( commonDecision === 'absorb' ) {
91+ if ( ! sourceExistence || typeof sourceExistence !== 'object' || Array . isArray ( sourceExistence ) ) {
92+ return [ issue ( pathPrefix , 'source-existence-required' , 'commonDecision=absorb requires sourceExistence' ) ]
93+ }
94+ } else if ( ! sourceExistence ) {
95+ return issues
96+ } else if ( typeof sourceExistence !== 'object' || Array . isArray ( sourceExistence ) ) {
97+ return [ issue ( pathPrefix , 'source-existence-invalid' , 'sourceExistence must be an object' ) ]
98+ }
99+
100+ const required = [
101+ 'claimedCapability' ,
102+ 'searchAnchors' ,
103+ 'sourceRoot' ,
104+ 'existenceStatus' ,
105+ 'hitEvidence' ,
106+ 'nearNeighborCoverage' ,
107+ 'ledgerDisposition' ,
108+ 'verifiedBy'
109+ ]
110+ for ( const field of required ) {
111+ if ( sourceExistence [ field ] === undefined || sourceExistence [ field ] === null || sourceExistence [ field ] === '' ) {
112+ issues . push ( issue ( `${ pathPrefix } .${ field } ` , 'source-existence-field-missing' , `${ field } is required on sourceExistence` ) )
113+ }
114+ }
115+ if ( ! EXISTENCE_STATUSES . has ( sourceExistence . existenceStatus ) ) {
116+ issues . push ( issue ( `${ pathPrefix } .existenceStatus` , 'existence-status-invalid' , 'existenceStatus is invalid' ) )
117+ }
118+ if ( ! LEDGER_DISPOSITIONS . has ( sourceExistence . ledgerDisposition ) ) {
119+ issues . push ( issue ( `${ pathPrefix } .ledgerDisposition` , 'ledger-disposition-invalid' , 'ledgerDisposition is invalid' ) )
120+ }
121+ if ( ! Array . isArray ( sourceExistence . searchAnchors ) || sourceExistence . searchAnchors . length === 0 ) {
122+ issues . push ( issue ( `${ pathPrefix } .searchAnchors` , 'search-anchors-required' , 'searchAnchors must be non-empty' ) )
123+ }
124+ if ( ! Array . isArray ( sourceExistence . hitEvidence ) ) {
125+ issues . push ( issue ( `${ pathPrefix } .hitEvidence` , 'hit-evidence-array' , 'hitEvidence must be an array' ) )
126+ } else if (
127+ COVERED_EXISTENCE . has ( sourceExistence . existenceStatus ) ||
128+ sourceExistence . existenceStatus === 'partial'
129+ ) {
130+ if ( sourceExistence . hitEvidence . length === 0 ) {
131+ issues . push ( issue ( `${ pathPrefix } .hitEvidence` , 'hit-evidence-required' , 'present/partial/equivalent-covered requires hitEvidence' ) )
132+ }
133+ }
134+ if ( commonDecision === 'absorb' ) {
135+ if ( sourceExistence . existenceStatus === 'unverified' ) {
136+ issues . push ( issue ( `${ pathPrefix } .existenceStatus` , 'existence-unverified' , 'cannot absorb while existenceStatus=unverified' ) )
137+ }
138+ if ( COVERED_EXISTENCE . has ( sourceExistence . existenceStatus ) ) {
139+ issues . push ( issue ( `${ pathPrefix } .existenceStatus` , 'existence-already-covered' , 'present/equivalent-covered cannot use commonDecision=absorb' ) )
140+ }
141+ if ( sourceExistence . ledgerDisposition === 'close-ledger' ) {
142+ issues . push ( issue ( `${ pathPrefix } .ledgerDisposition` , 'close-ledger-not-absorb' , 'close-ledger disposition cannot absorb' ) )
143+ }
144+ }
145+ return issues
146+ }
147+
148+ /**
149+ * Classifier for SourceExistenceVerificationGate free-text / object samples.
150+ * @returns {'absorb-ok'|'residual-ok'|'close-ledger'|'ledger-status-only'|'existence-unverified'|'invalid-absorb-covered' }
151+ */
152+ function classifySourceExistenceVerificationSample ( sample ) {
153+ const text = typeof sample === 'string' ? sample : ''
154+ const obj = sample && typeof sample === 'object' && ! Array . isArray ( sample ) ? sample : null
155+ const status = obj ?. existenceStatus ||
156+ ( / e x i s t e n c e S t a t u s \s * [: = ] \s * ( [ a - z - ] + ) / i. exec ( text ) ?. [ 1 ] ) ||
157+ ( / e x i s t e n c e S t a t u s [ = : ] \s * ( [ a - z - ] + ) / i. exec ( text ) ?. [ 1 ] )
158+ const hasSearch = ( Array . isArray ( obj ?. searchAnchors ) && obj . searchAnchors . length > 0 ) ||
159+ / s e a r c h A n c h o r s | s o u r c e R o o t | s o u r c e - r o o t | 在 s o u r c e | 源 码 检 索 | r g | g r e p / i. test ( text )
160+ const ledgerOnly = / 状 态 [ : : ] \s * ( o p e n | a c t i v e | p e n d i n g ) | p e n d i n g a b s o r p t i o n | 只 根 据 台 账 | l e d g e r - s t a t u s - o n l y / i. test ( text ) && ! hasSearch
161+ if ( ledgerOnly || ( ! hasSearch && / a b s o r b | 可 吸 纳 | p u r e - o p e n / i. test ( text ) ) ) {
162+ return 'ledger-status-only'
163+ }
164+ if ( ! hasSearch || status === 'unverified' || ( ! status && / 未 验 证 | u n v e r i f i e d / i. test ( text ) ) ) {
165+ return 'existence-unverified'
166+ }
167+ if ( status === 'present' || status === 'equivalent-covered' || / a l r e a d y - c o v e r e d | a l r e a d y - f i x e d | 可 关 账 | c l o s e - l e d g e r / i. test ( text ) ) {
168+ if ( / c o m m o n D e c i s i o n \s * [: = ] \s * a b s o r b | d e c i s i o n \s * = \s * a b s o r b | 标 a b s o r b | 整 包 吸 纳 / i. test ( text ) &&
169+ ! / 不 得 | 禁 止 | r e j e c t | c l o s e - l e d g e r / i. test ( text ) ) {
170+ return 'invalid-absorb-covered'
171+ }
172+ return 'close-ledger'
173+ }
174+ if ( status === 'partial' || / r e s i d u a l - t a i l | r e s i d u a l - c a n d i d a t e | 补 洞 / i. test ( text ) ) {
175+ return 'residual-ok'
176+ }
177+ if ( status === 'absent' || / e x i s t e n c e S t a t u s \s * [: = ] \s * a b s e n t | a b s e n t \b / i. test ( text ) ) {
178+ return 'absorb-ok'
179+ }
180+ return 'existence-unverified'
181+ }
182+
183+ function validateProbeNecessity ( probeNecessity , pathPrefix , commonDecision , enforcementLevel ) {
184+ const issues = [ ]
185+ if ( commonDecision !== 'absorb' ) {
186+ return issues
187+ }
188+ if ( ! probeNecessity || typeof probeNecessity !== 'object' || Array . isArray ( probeNecessity ) ) {
189+ return [ issue ( pathPrefix , 'probe-necessity-required' , 'commonDecision=absorb requires probeNecessity' ) ]
190+ }
191+ const required = [
192+ 'probeClass' ,
193+ 'necessity' ,
194+ 'rationale' ,
195+ 'probePlan' ,
196+ 'existingProbeReuse' ,
197+ 'alwaysOnImpact' ,
198+ 'complexityDelta' ,
199+ 'falsePositiveRisk'
200+ ]
201+ for ( const field of required ) {
202+ if ( probeNecessity [ field ] === undefined || probeNecessity [ field ] === null || probeNecessity [ field ] === '' ) {
203+ issues . push ( issue ( `${ pathPrefix } .${ field } ` , 'probe-necessity-field-missing' , `${ field } is required on probeNecessity` ) )
204+ }
205+ }
206+ if ( ! PROBE_CLASSES . has ( probeNecessity . probeClass ) ) {
207+ issues . push ( issue ( `${ pathPrefix } .probeClass` , 'probe-class-invalid' , 'probeClass is invalid' ) )
208+ }
209+ if ( ! PROBE_NECESSITIES . has ( probeNecessity . necessity ) ) {
210+ issues . push ( issue ( `${ pathPrefix } .necessity` , 'probe-necessity-invalid' , 'necessity is invalid' ) )
211+ }
212+ if ( ! ALWAYS_ON_IMPACTS . has ( probeNecessity . alwaysOnImpact ) ) {
213+ issues . push ( issue ( `${ pathPrefix } .alwaysOnImpact` , 'always-on-impact-invalid' , 'alwaysOnImpact is invalid' ) )
214+ }
215+ if (
216+ ( probeNecessity . necessity === 'not-required' || probeNecessity . necessity === 'forbidden' ) &&
217+ ! probeNecessity . skipProbeReason
218+ ) {
219+ issues . push ( issue ( `${ pathPrefix } .skipProbeReason` , 'skip-probe-reason-required' , 'not-required/forbidden needs skipProbeReason' ) )
220+ }
221+ if ( probeNecessity . necessity === 'required' && / 以 后 | 后 续 | 待 定 | T O D O | 以 后 再 说 / i. test ( String ( probeNecessity . probePlan || '' ) ) ) {
222+ issues . push ( issue ( `${ pathPrefix } .probePlan` , 'probe-plan-deferred' , 'required probe cannot defer probePlan' ) )
223+ }
224+ if ( probeNecessity . probeClass === 'checklist-only' && commonDecision === 'absorb' ) {
225+ issues . push ( issue ( `${ pathPrefix } .probeClass` , 'checklist-only-not-active-absorb' , 'checklist-only cannot absorb as enforceable active' ) )
226+ }
227+ if ( probeNecessity . alwaysOnImpact === 'always-on-path' && ! / U n a f f e c t e d I n t e n t | b a s e - c h a n g i n g | 单 独 确 认 / i. test ( String ( probeNecessity . rationale || '' ) + String ( probeNecessity . falsePositiveRisk || '' ) ) ) {
228+ issues . push ( issue ( `${ pathPrefix } .alwaysOnImpact` , 'always-on-needs-base-note' , 'always-on-path requires base-impact note in rationale or falsePositiveRisk' ) )
229+ }
230+ if ( enforcementLevel && WEAK_ENFORCEMENT . has ( enforcementLevel ) ) {
231+ issues . push ( issue ( `${ pathPrefix . replace ( 'probeNecessity' , 'enforcementLevel' ) } ` , 'weak-enforcement' , 'absorb cannot use checklist-only/none enforcementLevel' ) )
232+ }
233+ if ( enforcementLevel === 'hard-probe' && ! [ 'machine-sample' , 'fixture-replay' , 'extend-existing' , 'structural-schema' ] . includes ( probeNecessity . probeClass ) ) {
234+ issues . push ( issue ( `${ pathPrefix } .probeClass` , 'hard-probe-class-mismatch' , 'hard-probe requires machine/fixture/extend/structural class' ) )
235+ }
236+ return issues
237+ }
238+
239+ /**
240+ * @returns {'enforceable'|'text-only-fake'|'checklist-fake'|'probe-deferred'|'ok-structural'|'ok-probe' }
241+ */
242+ function classifyExecutableAbsorptionSample ( sample ) {
243+ const text = typeof sample === 'string' ? sample : JSON . stringify ( sample || { } )
244+ if ( / 只 改 ( S k i l l | 正 文 | M a r k d o w n ) | t e x t - o n l y | 无 消 费 者 | 无 探 针 .* a b s o r b e d | 假 吸 纳 / i. test ( text ) && ! / 禁 止 | i n v a l i d | b l o c k e d / i. test ( text ) ) {
245+ return 'text-only-fake'
246+ }
247+ if ( / c h e c k l i s t - o n l y | 仅 清 单 | 人 工 自 觉 / i. test ( text ) && / a b s o r b | a b s o r b e d | a c t i v e / i. test ( text ) && ! / 不 得 | 禁 止 | 否 / i. test ( text ) ) {
248+ return 'checklist-fake'
249+ }
250+ if ( / n e c e s s i t y \s * [: = ] \s * r e q u i r e d / i. test ( text ) && / 以 后 再 说 | T O D O | 待 定 | 后 续 再 补 探 针 / i. test ( text ) ) {
251+ return 'probe-deferred'
252+ }
253+ if ( / e n f o r c e m e n t L e v e l \s * [: = ] \s * s t r u c t u r a l - g a t e | s t r u c t u r a l - s c h e m a / i. test ( text ) ) {
254+ return 'ok-structural'
255+ }
256+ if ( / e n f o r c e m e n t L e v e l \s * [: = ] \s * ( h a r d - p r o b e | c o n d i t i o n a l - p r o b e ) | p r o b e C l a s s \s * [: = ] \s * ( m a c h i n e - s a m p l e | e x t e n d - e x i s t i n g | f i x t u r e - r e p l a y ) / i. test ( text ) ) {
257+ return 'ok-probe'
258+ }
259+ if ( / E x e c u t a b l e A b s o r p t i o n | 不 按 流 程 | 谁 会 红 / i. test ( text ) && / h a r d - p r o b e | s t r u c t u r a l - g a t e | c o n d i t i o n a l - p r o b e / i. test ( text ) ) {
260+ return 'enforceable'
261+ }
262+ return 'text-only-fake'
263+ }
264+
73265function validateCandidate ( candidate , index ) {
74266 const pathPrefix = `candidates[${ index } ]`
75267 const issues = [ ]
@@ -87,12 +279,38 @@ function validateCandidate(candidate, index) {
87279 if ( candidate . targetLayer && ! CLASSIFICATIONS . has ( candidate . targetLayer ) ) {
88280 issues . push ( issue ( `${ pathPrefix } .targetLayer` , 'target-layer-invalid' , 'targetLayer is invalid' ) )
89281 }
282+ if ( candidate . enforcementLevel && ! ENFORCEMENT_LEVELS . has ( candidate . enforcementLevel ) ) {
283+ issues . push ( issue ( `${ pathPrefix } .enforcementLevel` , 'enforcement-level-invalid' , 'enforcementLevel is invalid' ) )
284+ }
90285 if ( ! Array . isArray ( candidate . validationRoute ) || candidate . validationRoute . length === 0 ) {
91286 issues . push ( issue ( `${ pathPrefix } .validationRoute` , 'validation-route-required' , 'validationRoute must be non-empty' ) )
92287 }
93288 issues . push ( ...validateLayerChecks ( candidate . layerChecks , `${ pathPrefix } .layerChecks` ) )
94- if ( candidate . commonDecision === 'absorb' && candidate . backlogClass !== 'pure-open' && ! candidate . skipReason ) {
95- issues . push ( issue ( `${ pathPrefix } .skipReason` , 'non-pure-open-needs-skip' , 'non pure-open candidate cannot be absorbed without skipReason' ) )
289+ issues . push ( ...validateSourceExistence ( candidate . sourceExistence , `${ pathPrefix } .sourceExistence` , candidate . commonDecision ) )
290+ issues . push ( ...validateProbeNecessity (
291+ candidate . probeNecessity ,
292+ `${ pathPrefix } .probeNecessity` ,
293+ candidate . commonDecision ,
294+ candidate . enforcementLevel
295+ ) )
296+ if ( candidate . commonDecision === 'absorb' && ! candidate . enforcementLevel ) {
297+ issues . push ( issue ( `${ pathPrefix } .enforcementLevel` , 'enforcement-level-required' , 'absorb requires enforcementLevel' ) )
298+ }
299+ if ( candidate . commonDecision === 'absorb' && WEAK_ENFORCEMENT . has ( candidate . enforcementLevel ) ) {
300+ issues . push ( issue ( `${ pathPrefix } .enforcementLevel` , 'weak-enforcement' , 'absorb cannot use checklist-only/none' ) )
301+ }
302+ if ( candidate . commonDecision === 'absorb' && candidate . backlogClass !== 'pure-open' && candidate . backlogClass !== 'residual-tail' && ! candidate . skipReason ) {
303+ issues . push ( issue ( `${ pathPrefix } .skipReason` , 'non-pure-open-needs-skip' , 'non pure-open/residual-tail candidate cannot be absorbed without skipReason' ) )
304+ }
305+ if ( candidate . commonDecision === 'absorb' && candidate . backlogClass === 'residual-tail' ) {
306+ if ( candidate . sourceExistence ?. existenceStatus !== 'partial' ) {
307+ issues . push ( issue ( `${ pathPrefix } .sourceExistence.existenceStatus` , 'residual-needs-partial' , 'residual-tail absorb requires existenceStatus=partial' ) )
308+ }
309+ }
310+ if ( candidate . commonDecision === 'absorb' && candidate . backlogClass === 'pure-open' ) {
311+ if ( candidate . sourceExistence ?. existenceStatus && candidate . sourceExistence . existenceStatus !== 'absent' ) {
312+ issues . push ( issue ( `${ pathPrefix } .sourceExistence.existenceStatus` , 'pure-open-needs-absent' , 'pure-open absorb requires existenceStatus=absent' ) )
313+ }
96314 }
97315 return issues
98316}
@@ -153,8 +371,24 @@ function buildLayeredAbsorptionDecision(candidate) {
153371 prevention : candidate . prevention || null
154372 }
155373 }
156- if ( candidate . backlogClass !== 'pure-open' ) blockers . push ( 'backlog-class-not-pure-open' )
374+ if ( candidate . backlogClass !== 'pure-open' && candidate . backlogClass !== 'residual-tail' ) {
375+ blockers . push ( 'backlog-class-not-absorbable' )
376+ }
157377 if ( ! candidate . targetOwner ) blockers . push ( 'target-owner-missing' )
378+ if ( ! candidate . sourceExistence ) blockers . push ( 'source-existence-missing' )
379+ else {
380+ if ( candidate . sourceExistence . existenceStatus === 'unverified' ) blockers . push ( 'existence-unverified' )
381+ if ( COVERED_EXISTENCE . has ( candidate . sourceExistence . existenceStatus ) ) blockers . push ( 'existence-already-covered' )
382+ if ( candidate . backlogClass === 'pure-open' && candidate . sourceExistence . existenceStatus !== 'absent' ) {
383+ blockers . push ( 'pure-open-requires-absent' )
384+ }
385+ if ( candidate . backlogClass === 'residual-tail' && candidate . sourceExistence . existenceStatus !== 'partial' ) {
386+ blockers . push ( 'residual-requires-partial' )
387+ }
388+ }
389+ if ( ! candidate . probeNecessity ) blockers . push ( 'probe-necessity-missing' )
390+ if ( ! candidate . enforcementLevel ) blockers . push ( 'enforcement-level-missing' )
391+ if ( WEAK_ENFORCEMENT . has ( candidate . enforcementLevel ) ) blockers . push ( 'weak-enforcement' )
158392 for ( const key of LAYER_KEYS ) {
159393 if ( candidate . layerChecks ?. [ key ] ?. state === 'blocked' ) blockers . push ( `layer-blocked:${ key } ` )
160394 }
@@ -204,8 +438,12 @@ function planAbsorptionCandidates(matrix) {
204438module . exports = {
205439 LAYER_KEYS ,
206440 buildLayeredAbsorptionDecision,
441+ classifyExecutableAbsorptionSample,
442+ classifySourceExistenceVerificationSample,
207443 planAbsorptionCandidates,
208444 sha256,
209445 stableStringify,
210- validateAbsorptionCandidateMatrix
446+ validateAbsorptionCandidateMatrix,
447+ validateProbeNecessity,
448+ validateSourceExistence
211449}
0 commit comments