Files
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

247 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Archive Report — `per-instance-hook-scoping`
> Phase: **archive** · Change: `per-instance-hook-scoping` · Repo: `/home/user/manage`
> Mode: file-backed (`artifactStore: openspec`). No source-code edits; only OpenSpec artifacts
> were touched. **Not committed** — the parent/orchestrator owns the archive commit. No push, no `gh`.
**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 the `git mv` and 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 `openspec` mode — completed file-backed
sync → write the in-folder archive report → move the change folder to
`openspec/changes/archive/YYYY-MM-DD-{change}/`. No standalone manifest/index exists under
`openspec/` (only `config.yaml`, `changes/`, `specs/`), so the folder move **is** the archive
mechanism. No `rules.archive` override exists in `openspec/config.yaml` (verified: no `archive`
key under `rules`).
- **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 (sha256
`d5176362bc0f3c4adbbad44ee5c0b52699e7f71d69af661ea5be1e886a5a0709`, 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 delta `specs/service-instance-scoping/spec.md`, `apply-progress.md`,
`verify-report.md`, and `sync-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.md`
- `openspec/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.md`
- `openspec/changes/per-instance-hook-scoping/tasks.md`
- `openspec/changes/per-instance-hook-scoping/apply-progress.md`
- `openspec/changes/per-instance-hook-scoping/verify-report.md`
- `openspec/changes/per-instance-hook-scoping/sync-report.md`
- `openspec/specs/service-instance-scoping/spec.md` (canonical, sync target — verified present and untouched)
- `openspec/config.yaml` (rules: proposal/tasks; no `rules.archive` override)
- House-style reference: `openspec/changes/archive/2026-07-09-service-credential-tester/archive-report.md`
> The legacy flat `spec.md` is **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-scoping` domain (canonical did not exist
pre-change). IDs and text preserved verbatim from the verified flat `spec.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)
1. **[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 a `serviceId` and the tabs did not pass `instance.id`. This change threads a
`serviceId` into the six relevant hooks (`useAlertmanagerAlerts/Status`, `usePrometheusStatus`,
`useBackupJobs/Runs/Alerts`) — both into the `queryKey` (per-instance cache) and through the fetch
functions to `?service_id=` on the backend — and wires `instance.id` into `AlertsTab`,
`MetricsTab`, and `JobsTab`. Instance switching now naturally re-scopes (URL param → `instance`
recompute → queryKey change → refetch). This was a documented carry-over risk from the
`services-as-hub-ia` verify-report (2026-06-26). Resolved.
2. **[DESIGN — intentional scope decision 5] `fetchBackupDashboard` / `useBackupDashboard` /
`get_backup_dashboard` deliberately untouched.** The dashboard summary variant feeds the
`BackupDashboardWidget` component via `GET /api/dashboard/backups`
`build_backup_dashboard_summary(store)` in `routers/dashboard.py` — a **dashboard widget path**,
not any instance-scoped tab. JobsTab calls `useBackupJobs`/`useBackupRuns`/`useBackupAlerts` for
its three sub-tables, never `useBackupDashboard`. Scoping the dashboard variant would require
modifying `build_backup_dashboard_summary` to accept a `service_id` filter 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; `BackupDashboardWidget` test remains green. Documented in
`apply-progress.md`. Informational.
3. **[IMPLEMENTATION — schema asymmetry] Backup runs/alerts scoped via subquery.** Only the
`backup_jobs` table carries the `service_id` column; `backup_runs` and `backup_alerts` are
attributed to a service only transitively through their `job_id` FK. Accordingly,
`list_backup_jobs(service_id)` uses a direct `WHERE service_id = ?`, while `list_backup_runs` and
`list_backup_alerts` use a parameterized subquery
`job_id IN (SELECT id FROM backup_jobs WHERE service_id = ?)`. All new params are optional with a
truthy-guard (`if service_id:`) so both `None` and `""` 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.
4. **[INFO — non-blocking, verify NB-1] Spec↔design wording mismatch on PI-109 / PI-110.** The flat
spec literally lists `fetchBackupDashboard` / `get_backup_dashboard` as 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.
5. **[INFO — non-blocking, verify NB-2] Weak hook-test assertion (PI-121).** `useBackups.test.ts`
"different keys for different serviceIds" asserts `expect(a).not.toBe(b)` on two `renderHook`
result objects, which is trivially true (each render returns a new reference) and does not assert
the `queryKey` actually differs. The 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 the query cache keys. Non-blocking.
6. **[INFO]** Stale generated `.pi-map.md` files still reference the pre-change signatures (e.g.
`list_backup_jobs(self)` without `service_id`, `fetchBackupJobs()`). Generated artifacts, not
deliverable source; reconcile via `project_map_patch` when convenient.
7. **[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 Requirements` delta. 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` /
`serviceId` parameters 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.md` remains 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`, and
`openspec/specs/service-credential-testing/spec.md` also untouched.
- Renames were left **staged** (R100 detection preserved) for the parent's explicit-path commit.
`git restore --staged` was **not** run after the `git 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 entire
`openspec/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.