Skip to content

feat(intelligence): OS-intelligence integrity — no-clobber merge + per-category freshness#745

Merged
remyluslosius merged 3 commits into
mainfrom
feat/os-intelligence-integrity
Jul 16, 2026
Merged

feat(intelligence): OS-intelligence integrity — no-clobber merge + per-category freshness#745
remyluslosius merged 3 commits into
mainfrom
feat/os-intelligence-integrity

Conversation

@remyluslosius

@remyluslosius remyluslosius commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

OS intelligence and discovery collect host facts over SSH but were a lossy cache: a degraded collection (SSH connects, but individual probes fail or are sudo-denied) overwrote previously-good data with empty values, and both current-state stores keep no history.

This branch delivers Phase 1 (no-clobber merge), Phase 1b-backend (per-category freshness persistence), and Phase 1b-frontend (freshness exposed on the read APIs + rendered in the host System card) of the OS-intelligence temporal / last-known-good model (design doc, engineering-local).

Phase 1 — no-clobber merge

  • Discovery (host_system_info): each probe records its fact category in SystemFacts.Observed; persist() reads the prior row in-tx and carries forward the prior value for any unobserved category. Observed values (even 0/false) write normally — a genuinely full disk (free=0), apparmor_enabled=false, "0 services" are preserved where a naive COALESCE would lose them. Spec system-host-discovery v1.5.0 (C-08, AC-24).
  • OS intelligence (host_intelligence_state): RunCycle merges prior into the snapshot for unobserved categories before both Diff and persist, so a failed probe neither emits a false change event nor blanks the stored snapshot. Spec system-os-intelligence v1.2.0 (C-03, AC-18).

Phase 1b-backend — per-category freshness

  • Migration 0052 adds category_freshness JSONB to both tables: per category {observed_at, attempt_at, status} (ok = observed this run; stale = carried-forward, last-good observed_at kept). computeFreshness (discovery) + computeSnapFreshness (collector). Specs system-host-discovery v1.6.0 (C-13, AC-25), system-os-intelligence v1.3.0 (C-09, AC-19).

Phase 1b-frontend — surfacing

  • GET /api/v1/hosts/{id}/system-info and GET /api/v1/intelligence/state/{host_id} now expose the freshness map as a shared CategoryFreshness schema (omitted for pre-0052 NULL rows). Specs api-host-system-info v1.1.0 (C-05, AC-06), api-os-intelligence v1.1.0 (C-06, AC-11).
  • The host detail System card renders a muted "Last verified " caveat beneath any value whose category is stale (Distribution / FQDN / Firewall / Disk / Memory); ok/absent freshness renders nothing. Spec frontend-host-detail-system-card v1.1.0 (C-07, AC-08).

Testing

  • gofmt · go build ./... · vet clean · frontend tsc --noEmit clean · prettier clean.
  • specter check 116/116; specter check --test 0 errors; specter coverage --strictness annotation 100% (touched specs: system-host-discovery, system-os-intelligence, api-host-system-info 6/6, api-os-intelligence 11/11, frontend-host-detail-system-card 8/8).
  • Generated server.gen.go + schema.d.ts regenerated with pinned oapi-codegen v2.7.0 (deterministic; verified re-run identical).
  • New tests (all pass against a real Postgres): TestAPI_HostSystemInfo_GET_ExposesCategoryFreshness (AC-06), TestAPI_Intelligence_State_ExposesCategoryFreshness (AC-11), TestDiscover_NoClobberOnPartialCollection (AC-24), TestDiscover_CategoryFreshness (AC-25), TestMergeUnobserved_NoClobberAndNoFalseEvents (AC-18), TestComputeSnapFreshness (AC-19); frontend host-detail-system-card 8/8 (AC-08 freshness caveat + formatTimeAgo buckets).

Not in this PR

Phase 2 (temporal fact-history substrate: append-on-change + point-in-time) and Phase 3 (drift / fleet correlation / CVE / baselines) follow, per the design doc.

A degraded collection (SSH connects but individual probes fail or are sudo-denied)
used to overwrite previously-good data with empty values, and both current-state
stores keep no history. Fix: absence-of-observation is now distinct from
observed-empty, keyed on whether the probe actually ran.

- Discovery: each probe records its fact CATEGORY in SystemFacts.Observed;
  persist() reads the prior host_system_info row in-tx and carries forward the
  prior value for any unobserved category (observed values, even 0/false, write
  normally). Correctly handles cases a naive COALESCE can't (a genuinely full
  disk, apparmor=false). Spec system-host-discovery v1.5.0 (C-08, AC-24).
- OS intelligence: RunCycle merges prior into the snapshot for unobserved
  categories BEFORE both Diff and persist, so a failed probe neither emits a
  false change event nor blanks the stored snapshot. Observed set is transient
  (json:"-"). Spec system-os-intelligence v1.2.0 (C-03, AC-18).
- Verified the intelligence scheduler's recordSuccess only touches
  next_intelligence_at and never clobbers the snapshot.
Phase 1b of the last-known-good model: the no-clobber merge keeps a failed probe
from blanking good data; this records WHEN each carried-forward value was last
actually seen, so a consumer can tell fresh data from stale.

- Migration 0052 adds category_freshness JSONB to host_system_info and
  host_intelligence_state: one key per fact category ->
  {observed_at, attempt_at, status}. status ok = observed this run; stale = not
  observed but has a prior observation (prior observed_at kept, attempt advances).
- Discovery persist and the collector state write compute and store it from the
  Observed set + the prior freshness (computeFreshness / computeSnapFreshness).
- Specs: system-host-discovery v1.6.0 (C-13, AC-25), system-os-intelligence
  v1.3.0 (C-09, AC-19). DB + unit tests.

The API surfacing + freshness-aware UI (render a stale value as 'unknown, last
good X ago') is the remaining Phase 1b sub-step.
@remyluslosius

Copy link
Copy Markdown
Contributor Author

Added Phase 1b (backend): per-category collection freshness.

  • Migration 0052 adds category_freshness JSONB to host_system_info + host_intelligence_state (per category: observed_at/attempt_at/status, where ok=observed this run, stale=carried-forward with the last-good observed_at kept).
  • Discovery persist + collector state write compute and store it from the Observed set + prior freshness.
  • Specs: system-host-discovery v1.6.0 (C-13, AC-25), system-os-intelligence v1.3.0 (C-09, AC-19). DB + unit tests, all green; specter 116/116; both specs at full coverage.

Remaining Phase 1b sub-step (not in this PR yet): API exposure + freshness-aware UI (render a stale value as "unknown, last good X ago").

@github-actions github-actions Bot added size/XL and removed size/L labels Jul 15, 2026
@remyluslosius remyluslosius changed the title fix(intelligence): no-clobber merge for discovery + OS intelligence feat(intelligence): OS-intelligence integrity — no-clobber merge + per-category freshness Jul 15, 2026
…+ System card

Expose the category_freshness map (migration 0052) on the two host read
endpoints and render staleness in the host detail System card so an
operator can tell a freshly-observed value from a carried-forward one.

Backend:
- GET /api/v1/hosts/{id}/system-info now selects category_freshness and
  exposes it as CategoryFreshness (omitted when NULL, pre-0052 rows).
- GET /api/v1/intelligence/state/{host_id} does the same for the
  intelligence snapshot's per-category freshness.
- Shared FreshnessEntry {status, observed_at, attempt_at} + CategoryFreshness
  map added to api/openapi.yaml; server.gen.go + schema.d.ts regenerated.

Frontend:
- CardSystem renders a muted "Last verified <relative>" caveat beneath a
  value whose category is stale (Distribution/FQDN/Firewall/Disk/Memory);
  ok/absent freshness renders nothing.

Specs: api-host-system-info v1.1.0 (C-05/AC-06), api-os-intelligence v1.1.0
(C-06/AC-11), frontend-host-detail-system-card v1.1.0 (C-07/AC-08). All three
at 100% annotation coverage; specter check + check --test clean.
@remyluslosius
remyluslosius merged commit 67ecf11 into main Jul 16, 2026
19 checks passed
@remyluslosius
remyluslosius deleted the feat/os-intelligence-integrity branch July 16, 2026 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant