Skip to content

hooksHealth() returns a flattened object that does not match its declared { status, stats } type #1765

Description

@xmasyx

Summary

hooksHealth() in LIFEOS/PULSE/modules/hooks.ts declares a return type of { status: string; stats: HookStats } but returns { status: "ok", ...stats }. The spread flattens HookStats into the top level, so the returned object has no stats key at all. The declared type is a lie, and any consumer reading .stats gets undefined.

Verified in v7.28.3 at LifeOS/install/LIFEOS/PULSE/modules/hooks.ts:141-143.

interface HookStats {
  requests: number
  skillGuard: { total: number; blocked: number; passed: number }
  agentGuard: { total: number; warned: number; passed: number }
}

export function hooksHealth(): { status: string; stats: HookStats } {
  return { status: "ok", ...stats }   // <- flattened, no `stats` key
}

Impact

pulse.ts:493 assigns the result straight into the health payload:

subsystems.hooks = hooksHealth()

So the health endpoint serves { status, requests, skillGuard, agentGuard } instead of the documented { status, stats }. Anything reading subsystems.hooks.stats.requests silently reads undefined — no throw, no log, just a metric that is always empty.

Reproduction

bunx tsc --noEmit --strict   # over the hooks module

Minimal repro:

interface HookStats { requests: number }
const stats: HookStats = { requests: 0 }
export function hooksHealth(): { status: string; stats: HookStats } {
  return { status: "ok", ...stats }
}
error TS2741: Property 'stats' is missing in type
'{ requests: number; status: string; }' but required in type
'{ status: string; stats: HookStats; }'.

Proposed fix

Nest the stats instead of spreading them, which matches the declared type and the shape the health consumers expect:

 export function hooksHealth(): { status: string; stats: HookStats } {
-  return { status: "ok", ...stats }
+  return { status: "ok", stats: { ...stats } }
 }

(The inner spread keeps the returned snapshot decoupled from the live mutable stats object, so a later stats.requests++ cannot retroactively change a payload already handed out.)

If the flattened shape is the intended wire format instead, the fix is the mirror image — change the signature to HookStats & { status: string } — but then pulse.ts should be checked for readers of .stats.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions