@@ -86,6 +86,29 @@ function isGreenAndFresh(
8686 return ! isStale ( row , now , maxAgeMs ) ;
8787}
8888
89+ /**
90+ * Check whether D4 (real-time chat/tools) is green for a given slug, using
91+ * worst-state-wins semantics that mirror `cell-model.ts` `resolveD4`: D4 is
92+ * green only when at least one of `chat:<slug>` / `tools:<slug>` is present
93+ * AND every present row is green-and-fresh. A present red/degraded/stale row
94+ * pulls D4 down even if its sibling is green — the old `chatGreen ||
95+ * toolsGreen` OR wrongly credited D4 when one half was failing.
96+ */
97+ function isD4Green ( live : LiveStatusMap , slug : string , now : number ) : boolean {
98+ const chatRow = live . get ( keyFor ( "chat" , slug ) ) ?? null ;
99+ const toolsRow = live . get ( keyFor ( "tools" , slug ) ) ?? null ;
100+ // Neither present → D4 has no evidence, not achieved.
101+ if ( ! chatRow && ! toolsRow ) return false ;
102+ // Every present row must be green-and-fresh (worst-state wins).
103+ for ( const present of [ chatRow , toolsRow ] ) {
104+ if ( ! present ) continue ;
105+ if ( ! isGreenAndFresh ( live , present . key , now , D4_STALE_AFTER_MS ) ) {
106+ return false ;
107+ }
108+ }
109+ return true ;
110+ }
111+
89112/**
90113 * Check whether all D5 PB rows for a given (slug, catalogFeatureId) are green
91114 * AND fresh. Returns false if the feature has no D5 mapping or any mapped row
@@ -235,20 +258,13 @@ export function deriveDepth(
235258 }
236259 achieved = 3 ;
237260
238- // D4: chat:<slug> OR tools:<slug> green (real-time window)
239- const chatGreen = isGreenAndFresh (
240- live ,
241- keyFor ( "chat" , cell . integration ) ,
242- now ,
243- D4_STALE_AFTER_MS ,
244- ) ;
245- const toolsGreen = isGreenAndFresh (
246- live ,
247- keyFor ( "tools" , cell . integration ) ,
248- now ,
249- D4_STALE_AFTER_MS ,
250- ) ;
251- if ( ! ( chatGreen || toolsGreen ) ) {
261+ // D4: chat:<slug> + tools:<slug>, worst-state wins (real-time window).
262+ // Mirrors cell-model.ts `resolveD4`: a present green chat with a present
263+ // red tools yields a NOT-green D4 (the old `chatGreen || toolsGreen` OR
264+ // credited D4 even when one half was failing). A present row that is not
265+ // green-and-fresh pulls D4 down; D4 is achieved only when at least one
266+ // chat/tools row is present and EVERY present row is green-and-fresh.
267+ if ( ! isD4Green ( live , cell . integration , now ) ) {
252268 return {
253269 achieved,
254270 maxPossible,
0 commit comments