Files
manage/openspec/changes/archive/2026-07-09-per-instance-hook-scoping/verify-report.md
T
Developer 29650ca512 chore(per-instance-hook-scoping): archive verified+synced change
Move to openspec/changes/archive/2026-07-09-per-instance-hook-scoping/
(R100 renames preserved). 9 artifacts. Canonical openspec/specs/
service-instance-scoping/ remains. Resolves multi-instance wrong-data bug
(hooks now scope by instance.id; instance switcher re-scopes).
Carry-overs: fetchBackupDashboard untouched (design decision 5); subquery
scoping for runs/alerts (schema asymmetry).
2026-07-10 00:17:51 +00:00

14 KiB
Raw Blame History

SDD Verify Report: Per-Instance Hook Scoping

Change: per-instance-hook-scoping Phase: verify Date: 2026-07-09 Mode: Read-only adversarial audit (no source edits, no git push) Implementing commit: 3bc7ce5 (HEAD; single slice, 12 files, +268 / 73)

Executive summary

Gate Command Result
Backend tests cd backend && PYTHONPATH=src python3 -m pytest -q PASS — 368 passed
Backend lint cd backend && PYTHONPATH=src python3 -m ruff check src tests PASS — All checks passed
Frontend build cd frontend && npm run build PASS — exit 0
Frontend lint cd frontend && npm run lint PASS — 0 errors, 1 pre-existing warning (unrelated)
Frontend tests cd frontend && npx vitest run PASS — 46 files, 165 tests passed

Implementation verdict: All 21 spec requirements (PI-101..PI-121) are satisfied against source, with one deliberate, design-documented scope refinement (the fetchBackupDashboard / get_backup_dashboard variant was intentionally excluded per design decision 5 — see PI-109/PI-110 notes). The implementation matches the 6 design decisions. Backend filter SQL is parameterized (no injection risk).

BLOCKING process finding: The implementation is committed and correct, but tasks.md checkboxes were never updated — 0/17 are checked and no apply-progress.md artifact exists. Per the SDD verify contract, unchecked implementation tasks are CRITICAL archive blockers regardless of code state. The work is functionally done; the checkbox/progress hygiene is the gap. This must be reconciled (checkbox reconciliation via apply-progress) before archive.

Spec coverage (PI-101 .. PI-121)

Req Verdict Evidence
PI-101 useAlertmanagerAlerts(serviceId) PASS useObservability.ts: queryKey: ["observability","alerts", serviceId ?? ""], queryFn: () => fetchAlertmanagerAlerts(serviceId). serviceId in key + fetch.
PI-102 useAlertmanagerStatus(serviceId) PASS queryKey: ["observability","alertmanager-status", serviceId ?? ""], arrow queryFn captures serviceId.
PI-103 usePrometheusStatus(serviceId) PASS queryKey: ["observability","prometheus-status", serviceId ?? ""], arrow queryFn.
PI-104 useBackupJobs(serviceId) PASS useBackups.ts: queryKey: ["backups","jobs", serviceId ?? ""].
PI-105 useBackupRuns(jobId,status,serviceId) PASS queryKey: ["backups","runs", jobId, status, serviceId ?? ""].
PI-106 useBackupAlerts(jobId,ack,sev,serviceId) PASS queryKey includes serviceId ?? "" as last element.
PI-107 usePrometheusTargets + useMonitoringMachines unchanged PASS Neither gains serviceId; both still global, direct queryFn refs. MetricsTab keeps usePrometheusTargets() global.
PI-108 observability fetch fns pass ?service_id PASS client.ts: fetchAlertmanagerAlerts/Status + fetchPrometheusStatus each (serviceId?)serviceId ? { service_id: serviceId } : undefined.
PI-109 backup fetch fns pass ?service_id PASS* backups.ts: fetchBackupJobs, fetchBackupRuns (3rd arg), fetchBackupAlerts (4th arg) append service_id via conditional spread. fetchBackupDashboard deliberately NOT modified (design decision 5). *Spec text PI-109 lists fetchBackupDashboard; design/tasks override this — see Findings.
PI-110 backup endpoints accept service_id PASS* routers/backups.py: get_backup_jobs, get_backup_runs, get_backup_alerts each gain service_id: str | None = None and thread it into the store. get_backup_dashboard is in routers/dashboard.py and intentionally untouched (design decision 5). *Spec PI-110 lists it; design overrides.
PI-111 SettingsStore filter by service_id PASS settings_store.py: list_backup_jobs direct WHERE service_id = ?; list_backup_runs + list_backup_alerts use job_id IN (SELECT id FROM backup_jobs WHERE service_id = ?) subquery. Truthy-check if service_id: → None AND "" skip filter (backward-compat). Parameterized — no injection.
PI-112 Alertmanager/Prometheus already accept service_id PASS (zero backend change, confirmed) monitoring.py: get_alertmanager_alerts/status + get_prometheus_status already service_id: str | None = Noneresolve_service_record(store, "<type>", service_id). No change needed.
PI-113 AlertsTab passes instance.id PASS AlertsTab.tsx: useAlertmanagerAlerts(instance.id) + useAlertmanagerStatus(instance.id). No void instance;. TODO removed; docstring updated.
PI-114 MetricsTab passes instance.id PASS MetricsTab.tsx: usePrometheusStatus(instance.id). usePrometheusTargets() stays global. TODO removed.
PI-115 JobsTab passes instance.id PASS JobsTab.tsx: useBackupJobs(instance.id), useBackupRuns(undefined, undefined, instance.id), useBackupAlerts(undefined, false, undefined, instance.id). TODO removed.
PI-116 instance switcher re-scopes PASS ServicePage.tsx: sibling switcher onClick={() => navigate(...)} → URL param change → instance useMemo recomputes → <TabComponent instance={instance} /> new prop → queryKey change → refetch. Falls out naturally.
PI-117 dashboard widgets unaffected PASS Widgets resolve via useWidgetData (separate path). Grep confirms no widget/component imports any of the 6 modified hooks. BackupDashboardWidget uses useBackupDashboard (unchanged) — its test green. Widget tests unchanged & passing.
PI-118 all new params optional PASS Every new param is optional with a default preserving today's behavior. Non-regression callers verified: backup_poller.py (list_backup_jobs(), list_backup_alerts(acknowledged=False)), domain/dashboard.py (list_backup_jobs(), list_backup_runs(job_id=...,limit=...)), build_backup_dashboard_summary(store) all call with no service_id → return all.
PI-119 backend tests + lint green PASS TestBackupServiceScoping covers jobs/runs/alerts filtered + unfiltered (incl. empty-string) + endpoint threading. 368 pytest pass; ruff clean.
PI-120 frontend typecheck/build/lint PASS npm run build exit 0; npm run lint 0 errors (1 pre-existing warning in WidgetConfigDialog.tsx, unrelated).
PI-121 hook + tab tests cover wiring PASS (1 quality note) AlertsTab.test.tsx asserts vi.mocked(useAlertmanagerAlerts).toHaveBeenCalledWith("am-1") — strong assertion. Hook test useBackups.test.ts exists covering all 6 hooks. Quality note: the hook-test "different keys" assertion (expect(a).not.toBe(b) on renderHook result refs) is weak/tautology-prone — see Findings.

Coverage: 21/21 requirements satisfied against source.

Adversarial checks

  • Any tab still calling a scoped hook without instance.id? No. All three tabs pass instance.id; no remaining void instance; anywhere in service-tabs/.
  • Instance switcher actually re-scopes? Yes — navigate(...) re-derives instance from URL → queryKey changes → refetch (PI-116).
  • Dashboard widget regression? None. fetchBackupDashboard / useBackupDashboard / get_backup_dashboard / build_backup_dashboard_summary all untouched; BackupDashboardWidget test green.
  • Subquery SQL injection? Safe — both subqueries use ? placeholders with params.append(service_id). No string interpolation of user input.
  • Cache-key collision? All 6 modified hooks include serviceId ?? "" as last queryKey element (verified per hook). Single stable key for undefined; distinct key per instance.

Structured status & actionContext findings

  • Native status engine reports state: blocked, taskProgress 0/17, applyProgress: missing, nextRecommended: "domain specs are missing or partial", with blockedReasons citing missing domain specs + legacy flat spec present without domain specs.
  • These blockers are artifact-hygiene, not implementation gaps. The code is committed and all gates are green. The substantive blockers to archive are:
    1. tasks.md checkboxes: 0/17 checked (all still - [ ]). Implementation is done; checkboxes were not reconciled.
    2. No apply-progress.md artifact exists.
    3. No domain specs/ deltas — a flat spec.md is present without domain spec packages (same pattern the status engine flags). This is a sync-stage concern (sync reconciles flat→domain), not a verify blocker; verify is satisfied.
  • actionContext.mode: repo-local, allowedEditRoots: ["/home/user/manage"] — implementation files are all within allowed roots. Ownership proven.

Strict TDD

Strict TDD is not active in openspec/config.yaml for this workflow (no strict-TDD directive present; this verify run was not gated on a TDD cycle-evidence table). N/A.

Review workload / PR boundary

  • tasks.md forecast: single PR, ~257 lines, low budget risk, no chained PRs.
  • Actual: single commit 3bc7ce5, +268/73, 12 files — matches the single-slice forecast. No scope creep. No chained-PR boundary violation.

Findings

Blocking

  • BLOCKER-1 (CRITICAL — archive blocker; process hygiene, NOT code): tasks.md checkboxes unreconciled — 0/17 checked, no apply-progress.md. The implementation for all 17 sub-tasks is present and verified, but tasks.md still shows every task as - [ ] (e.g. - [ ] **1.1 Add service_id filter to list_backup_jobs**- [ ] **1.17 Verify Slice 1 …**). Per the SDD verify contract, unchecked implementation tasks are CRITICAL archive blockers. The native status engine correctly reflects this (0/17). Exact unchecked lines (all 17):
    • - [ ] **1.1 Add \service_id` filter to `list_backup_jobs`**`
    • - [ ] **1.2 Add \service_id` filter to `list_backup_runs` (subquery)**`
    • - [ ] **1.3 Add \service_id` filter to `list_backup_alerts` (subquery)**`
    • - [ ] **1.4 Add backend tests for backup service scoping**
    • - [ ] **1.5 Add \service_id` query param to backup endpoints**`
    • - [ ] **1.6 Add backend test for endpoint threading**
    • - [ ] **1.7 Confirm Alertmanager/Prometheus endpoints need no change**
    • - [ ] **1.8 Add \serviceId` param to observability fetch functions**`
    • - [ ] **1.9 Add \serviceId` param to backup fetch functions (NOT Dashboard)**`
    • - [ ] **1.10 Add \serviceId` to `useObservability` hooks**`
    • - [ ] **1.11 Add \serviceId` to `useBackups` hooks**`
    • - [ ] **1.12 Wire \instance.id` into AlertsTab**`
    • - [ ] **1.13 Wire \instance.id` into MetricsTab**`
    • - [ ] **1.14 Wire \instance.id` into JobsTab**`
    • - [ ] **1.15 Add hook test for per-instance queryKey**
    • - [ ] **1.16 Add tab test for \instance.id` wiring**`
    • - [ ] **1.17 Verify Slice 1 (build + lint + test green)**
    • Resolution (apply/sync phase, not verify): create apply-progress.md recording the work done in 3bc7ce5, then flip the 17 checkboxes to [x]. This is a stale-checkbox reconciliation explicitly permitted by the archive-exception clause once apply-progress proves the work.

Non-blocking

  • NB-1 (spec↔design wording mismatch — amend spec): Spec PI-109 and PI-110 literally list fetchBackupDashboard / get_backup_dashboard as in-scope for a new service param. Design decision 5 and tasks 1.5/1.9 deliberately exclude them (dashboard widget path, PI-117 risk). The implementation correctly follows the design. Recommend amending spec.md PI-109/PI-110 to drop the dashboard variant so spec and design agree. No functional defect.
  • NB-2 (weak assertion quality, PI-121 hook test): useBackups.test.ts "different keys for different serviceIds" asserts expect(a).not.toBe(b) on two renderHook result objects. This is trivially true (each renderHook returns a new reference) and does not actually assert the queryKey differs — the test would pass even if queryKeys collided. The hook wiring is correct in source (manually verified), and the tab test (AlertsTab.test.tsx toHaveBeenCalledWith("am-1")) is strong, so PI-121 is satisfied. Recommend strengthening the hook test to inspect queryClient.getQueryCache().getAll() keys or assert queryFn (mocked fetchBackupJobs) is invoked with the distinct serviceId.
  • NB-3 (project-map staleness): Several .pi-map.md artifacts still describe the pre-change signatures (e.g. list_backup_jobs(self) without service_id, fetchBackupJobs(), router docs without service_id). These are generated map docs, not source. Cosmetic; reconcile via project_map_patch when convenient. Not part of this change's source scope.

Commands run (exact)

Command Result
cd backend && PYTHONPATH=src python3 -m pytest -q 368 passed, 2 warnings
cd backend && PYTHONPATH=src python3 -m pytest tests/test_backups.py -q -k "ServiceScoping or endpoint_threads or filtered or unfiltered" 6 passed, 2 deselected
cd backend && PYTHONPATH=src python3 -m ruff check src tests All checks passed!
cd frontend && npm run build exit 0 (tsc -b + vite build)
cd frontend && npm run lint 0 errors, 1 warning (pre-existing, WidgetConfigDialog.tsx)
cd frontend && npx vitest run 46 files, 165 tests passed

Changed files (commit 3bc7ce5)

  • backend/src/media_library_viewer_api/routers/backups.py
  • backend/src/media_library_viewer_api/services/settings_store.py
  • backend/tests/test_backups.py
  • frontend/src/api/backups.ts
  • frontend/src/api/client.ts
  • frontend/src/hooks/useBackups.ts
  • frontend/src/hooks/useObservability.ts
  • frontend/src/hooks/__tests__/useBackups.test.ts (new)
  • frontend/src/pages/service-tabs/AlertsTab.tsx
  • frontend/src/pages/service-tabs/MetricsTab.tsx
  • frontend/src/pages/service-tabs/JobsTab.tsx
  • frontend/src/pages/service-tabs/__tests__/AlertsTab.test.tsx (updated)

Conclusion

The per-instance-hook-scoping change is functionally complete and correct: all 21 requirements pass against source, all 5 gates are green, non-regression is confirmed, and the subquery SQL is injection-safe. The only blocker is process hygienetasks.md checkboxes are stale (0/17) and apply-progress.md is missing. Reconcile those (write apply-progress, flip checkboxes) to clear the archive gate. No code changes are required.