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).
18 KiB
Archive Report — per-instance-hook-scoping
Phase: archive · Change:
per-instance-hook-scoping· Repo:/home/user/manageMode: file-backed (artifactStore: openspec). No source-code edits; only OpenSpec artifacts were touched. Not committed — the parent/orchestrator owns the archive commit. No push, nogh.
Status: ARCHIVED. All eight lifecycle phases are complete (proposal → spec → design → tasks →
apply → verify → sync → archive). Every archive precondition is verified PASS (see §2). The
canonical openspec/specs/service-instance-scoping/spec.md (a new domain created by
sdd-sync — pure ## ADDED Requirements, 21 requirements PI-101 … PI-121) remains in place as the
durable end-state spec and is not moved (archive never moves canonical specs). The change folder
was moved to openspec/changes/archive/2026-07-09-per-instance-hook-scoping/ via git mv to
preserve history.
This change is a correctness fix: it resolves the multi-instance wrong-data bug where service tabs (Alerts/Metrics/Jobs) showed data for whichever instance resolved first globally instead of the instance whose page the operator was viewing (see §7.1).
0. Archive disposition
- Disposition:
archived. The parent explicitly requested thegit mvand owns the commit, so the move is executed here and left staged for the parent's explicit-path commit. - Archive convention: OpenSpec SDD archive contract for
openspecmode — completed file-backed sync → write the in-folder archive report → move the change folder toopenspec/changes/archive/YYYY-MM-DD-{change}/. No standalone manifest/index exists underopenspec/(onlyconfig.yaml,changes/,specs/), so the folder move is the archive mechanism. Norules.archiveoverride exists inopenspec/config.yaml(verified: noarchivekey underrules). - Target archived path:
openspec/changes/archive/2026-07-09-per-instance-hook-scoping/ - Archive date:
2026-07-09(ISO). - Canonical spec left in place (not moved):
openspec/specs/service-instance-scoping/spec.md— 21 requirements (PI-101 … PI-121). Verified present and untouched after the move (sha256d5176362bc0f3c4adbbad44ee5c0b52699e7f71d69af661ea5be1e886a5a0709, unchanged pre/post move). - The other canonical domains are also left untouched:
openspec/specs/web-ui/spec.md,openspec/specs/prometheus-charting/spec.md,openspec/specs/service-storage/spec.md,openspec/specs/service-credential-testing/spec.md. - Audit-trail integrity: the change folder was moved as a whole, including the legacy flat
spec.md, the per-domain deltaspecs/service-instance-scoping/spec.md,apply-progress.md,verify-report.md, andsync-report.md, which travel with the record. Nothing was silently deleted or rewritten.
1. Native sdd-status read & discrepancy statement
The native gentle-pi.sdd-status engine supplied by the parent reports stale, non-actionable
state for this archive: state: blocked, dependencies.sync: blocked,
dependencies.archive: blocked, nextRecommended: "sdd-verify" (it is one phase behind reality).
The reported blockedReasons are carried over from an earlier point in the lifecycle and do not
reflect the current filesystem: the engine still believes domain specs are missing and the legacy
flat spec is present without a delta, when in fact the domain delta (specs/service-instance-scoping/)
and the canonical domain both exist and are synced.
Discrepancy with the parent's authoritative confirmed state — RESOLVED in favor of the parent.
The parent physically verified (and this executor re-confirmed directly against the filesystem in
§2) that all 17 tasks are ticked, apply-progress.md is present and records the work, the verify
report confirms functional completion (21/21 PASS), and the sync report confirms a SYNCED canonical
domain. Per the archive contract's non-authoritative-store carve-out guidance and the parent's
explicit instruction ("native sdd-status may report stale blockers — DISREGARD; parent verified.
PROCEED"), the stale sync: blocked / archive: blocked labels are disregarded and the archive
proceeds.
Direct filesystem re-validation (§2) is the source of truth for this report.
2. Archive preconditions (validated directly against the filesystem)
| Precondition | Evidence | Result |
|---|---|---|
| Verify report present | verify-report.md |
✓ verdict PASS (functionally) — 21/21 requirements PASS |
Verify clearly passing — no unresolved FAIL/BLOCKED/CRITICAL |
verify's single CRITICAL was a reconcilable archive-only item (17 unchecked boxes + missing apply-progress.md), now resolved; no unresolved critical verification issue remains |
✓ |
| Sync report present & successful | sync-report.md → Status: SYNCED |
✓ |
| Canonical spec exists (sync target, created) | openspec/specs/service-instance-scoping/spec.md (21 requirements, PI-101 … PI-121) |
✓ |
| Change-side domain delta exists | specs/service-instance-scoping/spec.md (## ADDED Requirements) |
✓ |
| Delta op-class = pure additive (ADDED; new domain; no MODIFIED/REMOVED) | ADDED=21, MODIFIED=0, REMOVED=0, RENAMED=0 | ✓ |
| Requirement-ID parity (flat ↔ delta ↔ canonical) | 21 == 21 == 21; identical IDs PI-101 … PI-121 | ✓ |
| proposal / design / tasks artifacts present | all populated | ✓ |
Final Task Completion Gate — zero unchecked - [ ] |
grep -cE '^\s*- \[ \]' tasks.md → 0; grep -cE '^\s*- \[x\]' → 17 |
✓ |
apply-progress.md present & records the work |
present; status "complete — all 17 tasks done, all gates green, verified"; single slice documented | ✓ |
No active same-domain (service-instance-scoping) collision |
new domain; sameDomainActiveChanges: [], collisions: [] (per sync-report §5) |
✓ |
Stale-checkbox reconciliation note. At verify time, all 17 implementation/verification checkboxes
were unchecked and apply-progress.md did not exist (verify BLOCKER-1). That condition was
reconciled before archive: apply-progress.md was authored documenting the single landed slice
(commit 3bc7ce5, +268/−73, 12 files) and the gate results, and all 17 boxes are now ticked.
apply-progress.md plus the verify report prove every previously-unchecked task complete. No
archive-time mechanical checkbox repair was needed — the gate now passes on the persisted tasks.md
as-is (0 unchecked). No partial-archive approval applies.
3. Artifacts read (archive preflight)
openspec/changes/per-instance-hook-scoping/proposal.mdopenspec/changes/per-instance-hook-scoping/spec.md(flat, authoritative planning artifact — 21 requirements PI-101 … PI-121)openspec/changes/per-instance-hook-scoping/specs/service-instance-scoping/spec.md(change-side domain delta —## ADDED Requirements)openspec/changes/per-instance-hook-scoping/design.mdopenspec/changes/per-instance-hook-scoping/tasks.mdopenspec/changes/per-instance-hook-scoping/apply-progress.mdopenspec/changes/per-instance-hook-scoping/verify-report.mdopenspec/changes/per-instance-hook-scoping/sync-report.mdopenspec/specs/service-instance-scoping/spec.md(canonical, sync target — verified present and untouched)openspec/config.yaml(rules: proposal/tasks; norules.archiveoverride)- House-style reference:
openspec/changes/archive/2026-07-09-service-credential-tester/archive-report.md
The legacy flat
spec.mdis not the only spec artifact: a per-domain delta (specs/service-instance-scoping/spec.md) and a canonical spec both exist, so the "legacy flat spec as the only artifact" archive-block condition does not apply. The flat spec travels with the archived folder as part of the audit trail.
4. Domains synced & requirement delta
| Domain | Change-side delta | Canonical | Action |
|---|---|---|---|
service-instance-scoping |
specs/service-instance-scoping/spec.md |
openspec/specs/service-instance-scoping/spec.md |
NEW domain — ADDED — 21 requirements copied into the canonical store as a brand-new spec |
- ADDED (21) — all to the new
service-instance-scopingdomain (canonical did not exist pre-change). IDs and text preserved verbatim from the verified flatspec.md. Grouped logically:- Frontend hooks (serviceId param + queryKey) — PI-101, PI-102, PI-103, PI-104, PI-105, PI-106, PI-107
- API client functions (serviceId → query param) — PI-108, PI-109
- Backend backup endpoints (service_id filter) — PI-110, PI-111, PI-112
- Tabs pass instance.id (TODO comments removed) — PI-113, PI-114, PI-115, PI-116
- Non-regression (dashboard widgets, global hooks untouched) — PI-117
- Backward compatibility — PI-118
- Test + build greenness — PI-119, PI-120, PI-121
- MODIFIED (0) — none (new domain; no pre-existing canonical requirements to replace).
- REMOVED (0) · RENAMED (0) — nothing destructive.
5. Final lifecycle status (all 8 phases done)
| Phase | Status | Evidence |
|---|---|---|
| Proposal | ✅ done | proposal.md — correctness fix; carry-over from services-as-hub-ia (2026-06-26) |
| Spec | ✅ done | flat spec.md (21) + domain delta specs/service-instance-scoping/spec.md (21 ADDED) |
| Design | ✅ done | design.md — 6 design decisions incl. decision 5 (dashboard variant excluded) |
| Tasks | ✅ done | tasks.md — 17/17 checked, zero - [ ] |
| Apply | ✅ done | 1 slice delivered (commit 3bc7ce5, +268/−73, 12 files) |
| Verify | ✅ PASS | verify-report.md — 21/21 PASS; gates green (verify's single CRITICAL was archive-only hygiene, now resolved) |
| Sync | ✅ done | sync-report.md — SYNCED; canonical service-instance-scoping domain ADDED (now 21 requirements) |
| Archive | ✅ done | this report + folder move performed |
6. Gate results (per verify-report / apply-progress)
| Gate | Command | Result |
|---|---|---|
| Backend tests | cd backend && PYTHONPATH=src python3 -m pytest -q |
PASS — 368 passed (+6 new), 2 pre-existing warnings |
| 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 (pre-existing chunk-size advisory) |
| Frontend lint | cd frontend && npm run lint |
PASS — 0 errors (1 pre-existing unrelated warning in WidgetConfigDialog.tsx) |
| Frontend tests | cd frontend && npx vitest run |
PASS — 46 files, 165 tests passed (+7 new) |
7. Carry-over follow-ups & non-blocking notes (recorded for the record)
- [USER VALUE — the correctness bug this change resolves]. When more than one Alertmanager,
Prometheus, or backups instance is configured, opening a specific instance's service page
(
/{serviceType}/{instanceId}) previously showed data for whichever instance the hook resolved as first-configured globally — not the instance being viewed. The root cause: the frontend hooks queried without aserviceIdand the tabs did not passinstance.id. This change threads aserviceIdinto the six relevant hooks (useAlertmanagerAlerts/Status,usePrometheusStatus,useBackupJobs/Runs/Alerts) — both into thequeryKey(per-instance cache) and through the fetch functions to?service_id=on the backend — and wiresinstance.idintoAlertsTab,MetricsTab, andJobsTab. Instance switching now naturally re-scopes (URL param →instancerecompute → queryKey change → refetch). This was a documented carry-over risk from theservices-as-hub-iaverify-report (2026-06-26). Resolved. - [DESIGN — intentional scope decision 5]
fetchBackupDashboard/useBackupDashboard/get_backup_dashboarddeliberately untouched. The dashboard summary variant feeds theBackupDashboardWidgetcomponent viaGET /api/dashboard/backups→build_backup_dashboard_summary(store)inrouters/dashboard.py— a dashboard widget path, not any instance-scoped tab. JobsTab callsuseBackupJobs/useBackupRuns/useBackupAlertsfor its three sub-tables, neveruseBackupDashboard. Scoping the dashboard variant would require modifyingbuild_backup_dashboard_summaryto accept aservice_idfilter and would risk the PI-117 non-regression guarantee for zero tab benefit. Design decision 5 (design §5) and tasks 1.5/1.9 explicitly exclude it;BackupDashboardWidgettest remains green. Documented inapply-progress.md. Informational. - [IMPLEMENTATION — schema asymmetry] Backup runs/alerts scoped via subquery. Only the
backup_jobstable carries theservice_idcolumn;backup_runsandbackup_alertsare attributed to a service only transitively through theirjob_idFK. Accordingly,list_backup_jobs(service_id)uses a directWHERE service_id = ?, whilelist_backup_runsandlist_backup_alertsuse a parameterized subqueryjob_id IN (SELECT id FROM backup_jobs WHERE service_id = ?). All new params are optional with a truthy-guard (if service_id:) so bothNoneand""skip the filter, preserving backward compatibility (the backup poller and the dashboard summary builder call unfiltered). The subquery is parameterized — no string interpolation, no injection risk (verify adversarial check passed). Informational. - [INFO — non-blocking, verify NB-1] Spec↔design wording mismatch on PI-109 / PI-110. The flat
spec literally lists
fetchBackupDashboard/get_backup_dashboardas in-scope for a new service param, while design decision 5 and tasks 1.5/1.9 deliberately exclude them. The implementation correctly follows the design. The delta/canonical preserved the flat spec text verbatim (PI-109 / PI-110 still mention the dashboard variants) per the sync fidelity rule (do not rewrite verified requirements during sync). If a future change wants spec and design to agree literally, it can amend in a follow-up change. No functional defect. - [INFO — non-blocking, verify NB-2] Weak hook-test assertion (PI-121).
useBackups.test.ts"different keys for different serviceIds" assertsexpect(a).not.toBe(b)on tworenderHookresult objects, which is trivially true (each render returns a new reference) and does not assert thequeryKeyactually differs. The wiring is correct in source (manually verified) and the tab test (AlertsTab.test.tsxtoHaveBeenCalledWith("am-1")) is strong, so PI-121 is satisfied. Recommend strengthening the hook test to inspect the query cache keys. Non-blocking. - [INFO] Stale generated
.pi-map.mdfiles still reference the pre-change signatures (e.g.list_backup_jobs(self)withoutservice_id,fetchBackupJobs()). Generated artifacts, not deliverable source; reconcile viaproject_map_patchwhen convenient. - [INFO] The working tree carries unrelated dirty/untracked items not owned by this archive:
an uncommitted cosmetic modification to
frontend/src/pages/ServicesPage.tsx(stray from another change) and untracked.pi-tmp/*scratch files, plus other uncommitted source edits from prior work. Per discipline these were left untouched.
8. Residual risks & destructive-merge statement
- Destructive sync / merge: none destructive. This was a brand-new canonical domain
(
service-instance-scoping) — a pure## ADDED Requirementsdelta. Zero MODIFIED and zero REMOVED requirements, so no destructive-removal guard was triggered and no destructive-sync parent approval was required. - Backend / data-contract impact: minimal and backward-compatible. All new
service_id/serviceIdparameters are optional with defaults that preserve prior behavior; unfiltered callers (backup poller, dashboard summary builder) are unaffected. The filter is parameterized (no injection). Archive touched only OpenSpec docs + the folder move. - No critical verification issues remain (CRITICAL issues are non-overridable; the single verify CRITICAL was the reconcilable checkbox/apply-progress gap, now resolved).
- No browser/visual smoke was performed (out of scope); the tabs are covered by Vitest component tests.
- Memory observation IDs: none —
artifactStore: openspec; traceability lives in the filesystem archive + canonical spec.
9. Move performed
git mv openspec/changes/per-instance-hook-scoping openspec/changes/archive/2026-07-09-per-instance-hook-scoping
- All 9 artifacts confirmed present at the archived path:
proposal.md,spec.md,specs/service-instance-scoping/spec.md(delta),design.md,tasks.md,apply-progress.md,verify-report.md,sync-report.md,archive-report.md(this file). - Canonical
openspec/specs/service-instance-scoping/spec.mdremains in place (verified untouched after the move — sha256 unchanged).openspec/specs/web-ui/spec.md,openspec/specs/prometheus-charting/spec.md,openspec/specs/service-storage/spec.md, andopenspec/specs/service-credential-testing/spec.mdalso untouched. - Renames were left staged (R100 detection preserved) for the parent's explicit-path commit.
git restore --stagedwas not run after thegit mv.
Appendix — Files written/moved by this archive (OpenSpec only; no source code)
- Written:
openspec/changes/per-instance-hook-scoping/archive-report.md(this file) — at the active path before the move; travels with the move into the archive. - Moved (via
git mv): the entireopenspec/changes/per-instance-hook-scoping/directory →openspec/changes/archive/2026-07-09-per-instance-hook-scoping/. - Left in place (durable canonical):
openspec/specs/service-instance-scoping/spec.md. - Not committed / not pushed — the parent owns the commit with explicit paths.