@@ -74,14 +74,15 @@ export async function getOsspreyMetrics(qx: QueryExecutor): Promise<OsspreyMetri
7474 ] = await Promise . all ( [
7575 qx . selectOne ( `
7676 SELECT
77- COUNT(*)::text AS "totalPackages",
78- COUNT(*) FILTER (WHERE p.is_critical = true)::text AS "criticalPackages",
79- COUNT(*) FILTER (WHERE p.is_critical = true AND s.status IN ('assessing','active','needs_attention'))::text AS covered,
80- COUNT(*) FILTER (WHERE p.is_critical = true AND s.status = 'needs_attention')::text AS "needsAttention",
81- COUNT(*) FILTER (WHERE p.is_critical = true AND s.status = 'escalated')::text AS escalated,
82- COUNT(*) FILTER (WHERE p.is_critical = true AND ( s.status = 'unassigned' OR s.id IS NULL)) ::text AS "unassignedCritical"
77+ COUNT(*)::text AS "totalPackages",
78+ COUNT(*) FILTER (WHERE p.has_critical_vulnerability = true)::text AS "criticalPackages",
79+ COUNT(*) FILTER (WHERE s.status IN ('assessing','active','needs_attention'))::text AS covered,
80+ COUNT(*) FILTER (WHERE s.status = 'needs_attention')::text AS "needsAttention",
81+ COUNT(*) FILTER (WHERE s.status = 'escalated')::text AS escalated,
82+ COUNT(*) FILTER (WHERE s.status = 'unassigned' OR s.id IS NULL)::text AS "unassignedCritical"
8383 FROM packages p
8484 LEFT JOIN stewardships s ON s.package_id = p.id
85+ WHERE p.is_critical = true
8586 ` ) ,
8687 qx . selectOne ( `
8788 SELECT COUNT(DISTINCT ss.user_id)::text AS count
@@ -92,13 +93,13 @@ export async function getOsspreyMetrics(qx: QueryExecutor): Promise<OsspreyMetri
9293 ` ) ,
9394 ] )
9495
95- const critical = parseInt ( counts . criticalPackages , 10 )
96+ const total = parseInt ( counts . totalPackages , 10 )
9697 const covered = parseInt ( counts . covered , 10 )
9798
9899 return {
99- totalPackages : parseInt ( counts . totalPackages , 10 ) ,
100- criticalPackages : critical ,
101- coveragePercent : critical > 0 ? Math . round ( ( covered / critical ) * 1000 ) / 10 : 0 ,
100+ totalPackages : total ,
101+ criticalPackages : parseInt ( counts . criticalPackages , 10 ) ,
102+ coveragePercent : total > 0 ? Math . round ( ( covered / total ) * 1000 ) / 10 : 0 ,
102103 coverageTrend : null , // TODO: requires snapshot mechanism or stewardship_activity timestamp analysis
103104 activeStewards : parseInt ( stewardRow . count , 10 ) ,
104105 unassignedCritical : parseInt ( counts . unassignedCritical , 10 ) ,
@@ -425,26 +426,8 @@ export async function listPackagesForApi(
425426 // Separate paginated params from filter-only params used by the fallback COUNT query
426427 const queryParams = { ...params , limit : opts . pageSize , offset : ( opts . page - 1 ) * opts . pageSize }
427428
428- // Shared LATERAL clauses — included in both the main query and the count fallback
429- // so that WHERE conditions referencing them work in both paths.
430- const stewardsLateral =
431- opts . includeStewards === true
432- ? `
433- LEFT JOIN LATERAL (
434- SELECT COALESCE(
435- json_agg(
436- json_build_object('userId', ss.user_id, 'role', ss.role, 'assignedAt', ss.assigned_at)
437- ORDER BY ss.assigned_at ASC
438- ) FILTER (WHERE ss.id IS NOT NULL),
439- '[]'::json
440- ) AS stewards
441- FROM stewardship_stewards ss
442- WHERE ss.stewardship_id = s.id
443- AND ss.deleted_at IS NULL
444- ) ss_agg ON true`
445- : ''
446-
447- const laterals = `
429+ // Laterals needed for WHERE filter conditions — included in both the main query and the COUNT fallback.
430+ const filterLaterals = `
448431 LEFT JOIN stewardships s ON s.package_id = p.id
449432 LEFT JOIN LATERAL (
450433 SELECT COUNT(*)::int AS cnt FROM advisory_packages WHERE package_id = p.id
@@ -465,20 +448,41 @@ export async function listPackagesForApi(
465448 WHERE pr.package_id = p.id
466449 ORDER BY pr.confidence DESC
467450 LIMIT 1
468- ) r_sc ON true
469- ${ stewardsLateral }
470- ${
471- opts . includeLastActivity === true
472- ? `
451+ ) r_sc ON true`
452+
453+ // Additional laterals for SELECT output only — not needed in the COUNT fallback.
454+ const stewardsLateral =
455+ opts . includeStewards === true
456+ ? `
457+ LEFT JOIN LATERAL (
458+ SELECT COALESCE(
459+ json_agg(
460+ json_build_object('userId', ss.user_id, 'role', ss.role, 'assignedAt', ss.assigned_at)
461+ ORDER BY ss.assigned_at ASC
462+ ) FILTER (WHERE ss.id IS NOT NULL),
463+ '[]'::json
464+ ) AS stewards
465+ FROM stewardship_stewards ss
466+ WHERE ss.stewardship_id = s.id
467+ AND ss.deleted_at IS NULL
468+ ) ss_agg ON true`
469+ : ''
470+
471+ const lastActLateral =
472+ opts . includeLastActivity === true
473+ ? `
473474 LEFT JOIN LATERAL (
474475 SELECT sa.activity_type, sa.content, sa.created_at
475476 FROM stewardship_activity sa
476477 WHERE sa.stewardship_id = s.id
477478 ORDER BY sa.created_at DESC
478479 LIMIT 1
479480 ) last_act ON true`
480- : ''
481- } `
481+ : ''
482+
483+ const laterals = `${ filterLaterals }
484+ ${ stewardsLateral }
485+ ${ lastActLateral } `
482486
483487 const rows : PackageListRow [ ] = await qx . select (
484488 `
@@ -518,10 +522,11 @@ export async function listPackagesForApi(
518522 } else {
519523 // Window function returns no rows when the page is beyond the result set.
520524 // Fall back to a separate COUNT so the caller always gets the real total.
525+ // Use filterLaterals (not laterals) — stewards/last_act laterals are SELECT-only and not needed here.
521526 const countRow : { count : string } = await qx . selectOne (
522527 `SELECT COUNT(*)::text AS count
523528 FROM packages p
524- ${ laterals }
529+ ${ filterLaterals }
525530 ${ where } ` ,
526531 params ,
527532 )
0 commit comments