diff --git a/docs/specs/dashboard-v0.2.0.md b/docs/specs/dashboard-v0.2.0.md deleted file mode 100644 index 4e0088e..0000000 --- a/docs/specs/dashboard-v0.2.0.md +++ /dev/null @@ -1,413 +0,0 @@ -# Spec: Shipped Dashboard Template (attune-rag v0.2.0) - -Status: shipped in 0.1.6 — implementation diverged from locked -decisions; see "Implementation Note" below. Original spec body -preserved as historical design context. -Target version: 0.2.0 (originally planned; feature landed early in 0.1.6) -Owner: Patrick -Created: 2026-04-22 -Reconciled: 2026-04-24 - ---- - -## Objective - -Ship a reusable Cowork-compatible HTML dashboard as part of -attune-rag itself, so any attune-rag user can render a live -benchmark/freshness dashboard with a single CLI call. The -dashboard file is self-contained, embeds its own refresh -command, and works in any Cowork session that has access to -the installed attune-rag package. - -Today the only copy lives at -`attune-ai/scripts/attune_rag_dashboard_refresh.py` and is -coupled to the attune-ai repo layout. v0.2.0 promotes it into -attune-rag as first-class functionality. - ---- - -## Scoping Decisions (Locked) - -The following was decided during the planning Q&A. These are -not open questions — they define v0.2.0's shape. - -1. Entry point: CLI only. Exposed as - `attune-rag dashboard {render,refresh}`. No Python API is - documented or committed as public; `attune_rag.dashboard` - is internal. -2. Refresh strategy: the refresh command is baked into the - rendered HTML at render time, via a `--refresh-cmd` flag. - The dashboard invokes it through `window.cowork.callMcpTool` - exactly as the existing attune-ai artifact does. -3. Release scope: full refactor into a new `dashboard` - subpackage with golden tests and this spec doc. Not a hack - shipped in the scripts directory. -4. Configurability: only the corpus package name is - parameterizable (`--corpus-package`, default `attune_help`). - Queries.yaml path, k, trials, and thresholds stay as - sensible defaults inside the refresh implementation. - ---- - -## Implementation Note (2026-04-24) - -The shipped implementation diverged from the locked scoping -decisions above. Treat this section as the source of truth for -what's actually in the package; the rest of this document -captures the original design intent. - -**What shipped (as of 0.1.6):** - -- `dashboard render` bakes the snapshot itself at render time - rather than embedding a `--refresh-cmd` that the browser - invokes via MCP. The rendered HTML is fully self-contained - with its snapshot payload; no live refresh from the browser. -- Two sentinels in the template: `__ATTUNE_SNAPSHOT__` and - `__ATTUNE_TITLE__` (the spec's `__REFRESH_CMD__` / - `__CORPUS_PACKAGE__` model was dropped). -- `render(out, snapshot, title) -> Path` signature — takes a - prebuilt snapshot dict, not a refresh command string. -- `dashboard render` gained an optional `--open` flag (opens - the file in the default browser). The spec listed this as an - open question; answered yes. -- A third subcommand — `dashboard show` — was added for a Rich - terminal dashboard (not in original scope). It calls - `build_snapshot` directly and renders to the console with - Rich tables. -- `dashboard refresh` still exists and still emits one JSON - object to stdout (the Snapshot Shape contract is preserved). - -**Why the divergence:** - -The MCP-refresh model required a live Cowork session to be -useful. Baking the snapshot at render time makes the HTML -portable (shareable, attachable to tickets, viewable offline) -at the cost of staleness. Re-running `dashboard render` -regenerates the file — simpler UX than an MCP round-trip. - -The path-validation, security, and snapshot-shape requirements -from the original spec all carried through unchanged. - ---- - -## Non-Goals - -- No React / framework dependency. Plain HTML + vanilla JS + - Chart.js UMD from CDN, same as the existing artifact. -- No server. The dashboard is a static file that hits Cowork's - MCP bridge directly from the browser. -- No multi-corpus comparison view. One corpus per dashboard. -- No persistence beyond what Cowork already provides for its - artifacts. Snapshots are ephemeral per refresh. -- No auth / access control — inherits whatever the Cowork - session has. - ---- - -## Module Layout - -``` -src/attune_rag/dashboard/ -├── __init__.py # empty; this is internal -├── render.py # template load + refresh_cmd bake -├── refresh.py # snapshot generation (moved from attune-ai) -└── templates/ - └── dashboard.html # self-contained template, loaded via - # importlib.resources -``` - -Rationale: matches attune-rag's existing subpackage style -(`corpus/`, `providers/`, `eval/`) and keeps the template -shipped as package data so no filesystem lookup is needed at -install time. - ---- - -## CLI Surface - -Added as a new subcommand in `src/attune_rag/cli.py` alongside -`query`, `corpus-info`, `providers`. - -### `attune-rag dashboard render` - -Writes a self-contained HTML file with the refresh command -already embedded. - -``` -attune-rag dashboard render \ - --out ~/attune-rag-dashboard.html \ - --refresh-cmd "uv run attune-rag dashboard refresh" \ - [--corpus-package attune_help] \ - [--title "attune-rag dashboard"] -``` - -Required flags: - -- `--out PATH` — destination file. Validated against path - traversal and system directories (per attune repo - standards; reuse `_validate_file_path` or port it). -- `--refresh-cmd CMD` — command string the dashboard will - execute via `window.cowork.callMcpTool`'s bash bridge on - every refresh click / page load. - -Optional flags: - -- `--corpus-package NAME` — defaults to `attune_help`. - Passed through to `dashboard refresh` inside the baked - command if the user doesn't override it themselves. -- `--title TEXT` — defaults to `attune-rag dashboard`. - -Exit codes: `0` success, `2` validation error (bad path, -missing refresh-cmd). - -### `attune-rag dashboard refresh` - -Emits a single JSON snapshot to stdout. Shape matches the -existing attune-ai refresh script output (see Snapshot Shape -below). This is what the dashboard invokes on every reload. - -``` -attune-rag dashboard refresh [--corpus-package attune_help] -``` - -Stdout contract: exactly one JSON object, preceded by nothing -useful. Callers must parse from the first `{` to skip any -structlog console output (existing lesson in attune-ai's -CLAUDE.md — structlog writes to stdout by default). The -rendered dashboard already handles this; keeping the -convention means the existing JS parser in the template -doesn't need to change. - -Exit codes: `0` success with a complete snapshot, `1` partial -snapshot (e.g. queries.yaml not found) still emitted but with -per-section errors, `2` unrecoverable. - ---- - -## Refresh Command Embedding - -At render time, `--refresh-cmd` is JSON-escaped and substituted -into a single placeholder in the template: - -```html - -``` - -The template uses a minimal substitution — either Jinja2 (already -in the venv, see jinja2 3.1.6) or a plain `str.replace` of two -sentinel tokens. Decision deferred to implementation; Jinja2 is -acceptable but not required. If Jinja2 is used, it stays a dev -dependency — the rendered output is static HTML with no runtime -template engine. - -Why embed rather than serve: the dashboard is opened as a local -file from whatever path `--out` writes to; there's no server -context to read flags from. Baking is the only way to make a -standalone HTML file remember its own data source. - ---- - -## Snapshot Shape - -Preserved from the current refresh script so the JS in the -template doesn't need to change: - -```json -{ - "timestamp": "2026-04-22T14:03:11Z", - "retrieval": { - "retriever": "cascade", - "corpus": "attune_help", - "precision_at_1": 0.82, - "recall_at_k": 0.94, - "mean_latency_ms": 12.3, - "max_latency_ms": 47.1, - "total_queries": 40, - "k": 3, - "per_difficulty": {"easy": {...}, "medium": {...}, "hard": {...}}, - "per_feature": {"security-audit": {...}, ...}, - "per_query": [{...}, ...] - }, - "freshness": { - "attune_help_version": "0.7.1", - "summaries_by_path_keys": 287, - "kinds": ["concept", "task", "reference", ...], - "kind_totals": {"concept": 26, "task": 26, ...}, - "features": ["security-audit", ...], - "per_feature": {"security-audit": {"kind_counts": {...}}, ...} - } -} -``` - -Any field the consumer can't render is rendered as "n/a" by -the template — forward compatible with new sections. - ---- - -## Implementation Milestones - -Iterative per the Spec-Driven workflow. Each milestone ends -with a green test suite and a usable artifact. - -### M1: Move refresh logic into the package - -- Copy `attune_rag_dashboard_refresh.py` logic into - `src/attune_rag/dashboard/refresh.py` as a proper module - with typed functions and docstrings. -- Add `attune_rag dashboard refresh` CLI subcommand that - wires to it. -- Unit tests: `tests/unit/test_dashboard_refresh.py` — - snapshot shape contract, corpus-package override, error - paths when queries.yaml is missing. -- Smoke test: `uv run attune-rag dashboard refresh | head -c - 500` prints valid JSON. - -### M2: Ship the template as package data - -- Extract the existing artifact HTML (from the attune-ai - Cowork artifact) into - `src/attune_rag/dashboard/templates/dashboard.html` with - two sentinel tokens. -- Add `[tool.hatch.build.targets.wheel]` or equivalent - `package-data` config so the template ships in the wheel. -- Sanity test: `importlib.resources.files("attune_rag.dashboard") - .joinpath("templates/dashboard.html").read_text()` works - post-install. - -### M3: Render command - -- Implement `src/attune_rag/dashboard/render.py::render(out, - refresh_cmd, corpus_package, title) -> Path`. -- Add `attune-rag dashboard render` CLI subcommand. -- Path validation: reject null bytes, system directories, - and paths outside the user's home unless `--out` is an - explicit absolute path they provided. -- Golden test: `tests/unit/test_dashboard_render.py` - renders to tmp_path, reads the file back, and asserts both - sentinel values substituted correctly. Uses a small - deterministic refresh-cmd string and `corpus_package=foo`. -- No network, no subprocess — rendering is pure string ops. - -### M4: Version bump + changelog + README - -- Bump `pyproject.toml` to `0.2.0`. -- Add a CHANGELOG entry under "0.2.0 — Dashboard". -- Update README with a three-line usage example. -- Tag and publish per the existing release workflow. - -### M5 (post-merge): retire the attune-ai-side script - -- Delete `attune-ai/scripts/attune_rag_dashboard_refresh.py`. -- Update the attune-ai Cowork artifact to invoke - `uv run attune-rag dashboard refresh` instead of the local - script. No behavior change — same JSON contract. -- Document the deprecation in attune-ai's lessons log. - ---- - -## Test Plan - -All tests live under `tests/unit/` and must pass on the -existing CI matrix (Python 3.10–3.13, Ubuntu + macOS). - -1. `test_dashboard_refresh.py` - - Happy path: queries.yaml present → full snapshot with - all sections populated. - - Missing queries.yaml: exit code 1, JSON still emitted - with `retrieval.error` populated. - - Corpus package override: `--corpus-package foo` flows - through to the freshness section's corpus lookup. - - Stdout contract: first `{` marks the JSON start. - -2. `test_dashboard_render.py` - - Renders to `tmp_path`, verifies file exists. - - Verifies both `window.__REFRESH_CMD__` and - `window.__CORPUS_PACKAGE__` are set to the passed - values (JSON-escaped). - - Rejects a path inside `/etc`, `/sys`, `/proc`, `/dev`. - - Rejects a path containing a null byte. - - Rejects a path whose parent doesn't exist (with a - clear error message, not a raw OSError). - -3. `test_dashboard_cli.py` - - `attune-rag dashboard render --help` exits 0. - - `attune-rag dashboard refresh --help` exits 0. - - `attune-rag dashboard render` without `--out` exits 2. - - End-to-end: render to tmp, then grep for the baked - refresh command. - -No integration tests against real Cowork — out of scope. - ---- - -## Dependencies - -No new required dependencies. Jinja2 is already in the venv -and may be used for rendering; if adopted, it becomes a core -runtime dep. A simpler `str.replace` of two sentinel tokens is -acceptable and preferred if Jinja2 is only used here (avoids -adding a runtime dep for one substitution). Implementation -decision made during M3; spec allows either. - -Chart.js stays CDN-loaded inside the template — no Python -dep. SRI hash from the existing artifact is preserved. - ---- - -## Security - -Path-traversal and null-byte checks on `--out` following the -pattern documented in -`attune-ai/.claude/rules/attune/coding-standards-index.md`. -The refresh command is baked into a static file under the -user's control, so shell-injection concerns are the user's — -but the dashboard must not `eval()` or string-concatenate it -at page load. It must be passed as a single argument to the -MCP bash tool exactly as written. - -The rendered HTML runs in the Cowork sandbox; no privilege -elevation beyond what Cowork already permits for artifacts. - ---- - -## Migration Notes - -For users on 0.1.x with their own dashboard copies: - -- No breaking API changes. `attune_rag.retrieval`, - `attune_rag.pipeline`, and the CLI `query` / `corpus-info` - / `providers` subcommands are untouched. -- The attune-ai dashboard artifact keeps working during the - transition. Post-M5 it switches to invoking the installed - CLI rather than a local script. -- Users who forked the old refresh script can either keep - their fork (it still works — private functions in - `attune_rag.benchmark` are stable for 0.2.x) or migrate to - `attune-rag dashboard refresh` for zero maintenance. - ---- - -## Open Questions - -1. Should the render step support a `--open` flag that - auto-opens the file in the user's browser? Nice-to-have, - not in v0.2.0 scope. Revisit if users ask. -2. Do we want a deterministic snapshot mode (fixed seed) for - reproducible demo screenshots? Skip unless a second use - case materializes. - ---- - -## References - -- Existing artifact script: - `attune-ai/scripts/attune_rag_dashboard_refresh.py` -- Cowork artifact API: `window.cowork.callMcpTool`, eager - on-mount refresh pattern -- attune repo coding standards: - `attune-ai/.claude/rules/attune/coding-standards-index.md` -- Spec-driven workflow preference: - `attune-ai/CLAUDE.md` diff --git a/docs/specs/dashboard-v0.2.0/design.md b/docs/specs/dashboard-v0.2.0/design.md new file mode 100644 index 0000000..3c51b7a --- /dev/null +++ b/docs/specs/dashboard-v0.2.0/design.md @@ -0,0 +1,193 @@ +# Spec: Shipped Dashboard Template (attune-rag) + +## Phase 2: Design + +**Status**: complete + +### Drift from original plan (read first) + +The shipped 0.1.6 implementation diverged from the locked scoping decisions in the original plan. **The shipped behavior is the source of truth**; the rest of this design.md captures the original design intent for historical context. + +**What shipped (as of 0.1.6):** + +- `dashboard render` **bakes the snapshot itself at render time** rather than embedding a `--refresh-cmd` that the browser invokes via MCP. The rendered HTML is fully self-contained with its snapshot payload; **no live refresh from the browser**. +- Two sentinels in the template: `__ATTUNE_SNAPSHOT__` and `__ATTUNE_TITLE__` (the spec's `__REFRESH_CMD__` / `__CORPUS_PACKAGE__` model was dropped). +- `render(out, snapshot, title) -> Path` signature — takes a prebuilt snapshot dict, not a refresh command string. +- `dashboard render` gained an optional `--open` flag (opens the file in the default browser). Original spec listed this as an open question; answered yes. +- A third subcommand — `dashboard show` — was added for a Rich terminal dashboard (not in original scope). Calls `build_snapshot` directly and renders to the console with Rich tables. +- `dashboard refresh` still exists and still emits one JSON object to stdout (Snapshot Shape contract preserved). + +**Why the divergence:** + +The MCP-refresh model required a live Cowork session to be useful. **Baking the snapshot at render time makes the HTML portable** (shareable, attachable to tickets, viewable offline) at the cost of staleness. Re-running `dashboard render` regenerates the file — simpler UX than an MCP round-trip. + +The path-validation, security, and snapshot-shape requirements from the original spec all carried through unchanged. + +### Architecture + +#### Module layout + +``` +src/attune_rag/dashboard/ +├── __init__.py # empty; this is internal +├── render.py # template load + sentinel substitution +├── refresh.py # snapshot generation (moved from attune-ai) +├── show.py # Rich terminal dashboard (added in shipped impl) +└── templates/ + └── dashboard.html # self-contained template, loaded via + # importlib.resources +``` + +Rationale: matches attune-rag's existing subpackage style (`corpus/`, `providers/`, `eval/`) and keeps the template shipped as package data so no filesystem lookup is needed at install time. + +### API changes + +CLI-only (no Python API committed as public). Three subcommands added to `src/attune_rag/cli.py` alongside `query`, `corpus-info`, `providers`. + +#### `attune-rag dashboard render` (shipped) + +Writes a self-contained HTML file with the snapshot already baked in. + +``` +attune-rag dashboard render \ + --out ~/attune-rag-dashboard.html \ + [--corpus-package attune_help] \ + [--title "attune-rag dashboard"] \ + [--open] +``` + +Required flags: +- `--out PATH` — destination file. Validated against path traversal and system directories. + +Optional flags: +- `--corpus-package NAME` — defaults to `attune_help`. Determines which corpus is benchmarked. +- `--title TEXT` — defaults to `attune-rag dashboard`. +- `--open` — open the rendered file in the default browser after writing. + +Exit codes: `0` success, `2` validation error (bad path, missing required flag). + +> **Original-plan signature** (preserved here for historical record): `--refresh-cmd CMD` was required and baked into the HTML for browser-side MCP invocation. Dropped in shipped impl in favor of baked-snapshot model. + +#### `attune-rag dashboard refresh` (shipped) + +Emits a single JSON snapshot to stdout. Shape matches the existing attune-ai refresh script output (see Snapshot Shape below). + +``` +attune-rag dashboard refresh [--corpus-package attune_help] +``` + +Stdout contract: exactly one JSON object, preceded by nothing useful. Callers parse from the first `{` to skip any structlog console output (existing lesson; the rendered dashboard already handles this). + +Exit codes: `0` success with complete snapshot, `1` partial snapshot (e.g. `queries.yaml` not found) still emitted with per-section errors, `2` unrecoverable. + +#### `attune-rag dashboard show` (shipped — added beyond original scope) + +Rich terminal dashboard. Calls `build_snapshot` directly and renders to the console with Rich tables. No HTML, no file output. + +### Data model changes + +#### Snapshot shape (preserved verbatim from existing attune-ai script) + +```json +{ + "timestamp": "2026-04-22T14:03:11Z", + "retrieval": { + "retriever": "cascade", + "corpus": "attune_help", + "precision_at_1": 0.82, + "recall_at_k": 0.94, + "mean_latency_ms": 12.3, + "max_latency_ms": 47.1, + "total_queries": 40, + "k": 3, + "per_difficulty": {"easy": {...}, "medium": {...}, "hard": {...}}, + "per_feature": {"security-audit": {...}, ...}, + "per_query": [{...}, ...] + }, + "freshness": { + "attune_help_version": "0.7.1", + "summaries_by_path_keys": 287, + "kinds": ["concept", "task", "reference", ...], + "kind_totals": {"concept": 26, "task": 26, ...}, + "features": ["security-audit", ...], + "per_feature": {"security-audit": {"kind_counts": {...}}, ...} + } +} +``` + +Any field the consumer can't render is rendered as "n/a" by the template — forward-compatible with new sections. + +### UI/UX + +The dashboard is a static HTML file. Key UX properties: + +- **Self-contained:** opens in any browser; no server required. +- **Portable:** shareable via attachment / ticket / email; viewable offline. +- **Refresh model (shipped):** baked at render time. Re-run `attune-rag dashboard render` to regenerate. **No live refresh button** in the rendered HTML. +- **Chart.js loaded from CDN** with the existing artifact's SRI hash preserved. +- **Title customisable** via `--title`. +- **`--open` convenience flag** auto-opens the file in the user's default browser. + +### Cross-layer impact + +- **attune-rag** (primary): new `dashboard` subpackage, new CLI subcommands, template shipped as package data, version bump, changelog entry, README example. +- **attune-ai** (M5 only): delete `scripts/attune_rag_dashboard_refresh.py`; update Cowork artifact to invoke `uv run attune-rag dashboard refresh` instead of the local script. No JSON-shape change — the artifact JS is unchanged. + +No impact on attune-gui, attune-help, or attune-author. + +### Tradeoffs & alternatives + +#### Refresh strategy + +| Option | Pros | Cons | Chosen? | +|---|---|---|---| +| Bake snapshot at render time (shipped) | Portable; offline-viewable; simplest UX; no MCP coupling | Stale until next render; no live updates | **Yes (shipped 0.1.6)** | +| `--refresh-cmd` embedded; browser invokes via MCP (original plan) | Live updates inside Cowork | Requires live Cowork session to be useful; opaque shell-injection surface; not portable | No (deferred / dropped) | +| Server-backed dashboard | Always live | Daemon to run; auth concerns; out of scope | No | + +#### Sentinel substitution mechanism + +| Option | Pros | Cons | Chosen? | +|---|---|---|---| +| Plain `str.replace` of sentinel tokens | Zero new runtime deps; obvious; testable | Fragile if template grows complex | **Yes (shipped)** | +| Jinja2 rendering | Familiar; battle-tested | Adds a runtime dep for one substitution | No | + +#### Configurability + +Only the corpus package name is parameterizable (`--corpus-package`, default `attune_help`). Queries.yaml path, k, trials, and thresholds stay as sensible defaults inside the refresh implementation. Adding more knobs is explicitly deferred. + +### Refresh command embedding (original-plan section, NOT shipped) + +> Preserved for historical record. The shipped impl uses snapshot baking instead. + +At render time, `--refresh-cmd` would have been JSON-escaped and substituted into a single placeholder in the template: + +```html + +``` + +Why embed rather than serve: the dashboard is opened as a local file from whatever path `--out` writes to; there's no server context to read flags from. Baking is the only way to make a standalone HTML file remember its own data source. (This rationale also justifies the snapshot-baking approach that ultimately shipped.) + +### Security + +Path-traversal and null-byte checks on `--out` following the pattern documented in `attune-ai/.claude/rules/attune/coding-standards-index.md`. The refresh command (in the original plan) would have been baked into a static file under the user's control, so shell-injection concerns are the user's — but the dashboard must not `eval()` or string-concatenate it at page load. It must be passed as a single argument to the MCP bash tool exactly as written. + +The rendered HTML runs in the Cowork sandbox (when used inside Cowork); no privilege elevation beyond what Cowork already permits for artifacts. + +### Migration notes + +For users on 0.1.x with their own dashboard copies: + +- **No breaking API changes.** `attune_rag.retrieval`, `attune_rag.pipeline`, and the CLI `query` / `corpus-info` / `providers` subcommands are untouched. +- **The attune-ai dashboard artifact keeps working** during the transition. Post-M5 it switches to invoking the installed CLI rather than a local script. +- **Forks of the old refresh script** can either keep their fork (it still works — private functions in `attune_rag.benchmark` are stable for 0.2.x) or migrate to `attune-rag dashboard refresh` for zero maintenance. + +### References + +- Existing artifact script (pre-M5): `attune-ai/scripts/attune_rag_dashboard_refresh.py` +- Cowork artifact API: `window.cowork.callMcpTool`, eager on-mount refresh pattern +- attune repo coding standards: `attune-ai/.claude/rules/attune/coding-standards-index.md` +- Spec-driven workflow preference: `attune-ai/CLAUDE.md` diff --git a/docs/specs/dashboard-v0.2.0/requirements.md b/docs/specs/dashboard-v0.2.0/requirements.md new file mode 100644 index 0000000..cbbe634 --- /dev/null +++ b/docs/specs/dashboard-v0.2.0/requirements.md @@ -0,0 +1,77 @@ +# Spec: Shipped Dashboard Template (attune-rag) + +**Status**: complete + +> Originally `docs/specs/dashboard-v0.2.0.md`. Target version: 0.2.0; **feature actually landed early in 0.1.6**. Implementation diverged from locked scoping decisions — see "Drift from original plan" below. Original design intent is preserved in this spec for historical record; the design.md section flags exactly what shipped differently. +> +> - **Owner:** Patrick +> - **Created:** 2026-04-22 +> - **Reconciled:** 2026-04-24 + +--- + +## Phase 1: Requirements + +**Status**: complete + +### Problem statement + +Today, the only working live benchmark/freshness dashboard for attune-rag lives at `attune-ai/scripts/attune_rag_dashboard_refresh.py` and is coupled to the attune-ai repo layout. Users of attune-rag who don't have the attune-ai checkout cannot render a dashboard for their own corpus. + +The honest fix is to promote the dashboard into attune-rag itself as first-class functionality: + +- A reusable Cowork-compatible HTML dashboard ships as part of the attune-rag package. +- Any attune-rag user can render the dashboard with a single CLI call (`attune-rag dashboard render`). +- The dashboard file is self-contained and works in any Cowork session that has access to the installed attune-rag package. +- The attune-ai-side script is retired, eliminating the duplicate copy. + +### Scope + +**In scope:** + +- New `dashboard` subpackage in `src/attune_rag/dashboard/` (matches attune-rag's existing `corpus/`, `providers/`, `eval/` style). +- HTML template shipped as package data (loadable via `importlib.resources`). +- CLI surface: `attune-rag dashboard render` (writes a self-contained HTML file) and `attune-rag dashboard refresh` (emits one JSON snapshot to stdout). +- Snapshot shape preserved unchanged from the existing attune-ai script — JS in the template needs no changes. +- Path-traversal and null-byte validation on `--out`. +- Golden tests + unit tests for render, refresh, and CLI. +- Version bump, changelog, README usage example. +- M5 (post-merge): retire the attune-ai-side script; switch the existing artifact to invoke the installed CLI. + +**Out of scope (Non-Goals):** + +- React / framework dependency. Plain HTML + vanilla JS + Chart.js UMD from CDN. +- Server. The dashboard is a static file; it hits Cowork's MCP bridge directly from the browser. +- Multi-corpus comparison view. One corpus per dashboard. +- Persistence beyond what Cowork already provides for its artifacts. Snapshots are ephemeral per refresh. +- Auth / access control — inherits whatever the Cowork session has. +- Public Python API. `attune_rag.dashboard` is internal; CLI is the only documented entry point. + +### User stories + +1. *As an attune-rag user*, I want one command (`attune-rag dashboard render --out ~/dash.html`) that produces a self-contained HTML file I can open in any browser — so I can see retrieval and freshness metrics for my corpus without cloning attune-ai. +2. *As a developer demoing attune-rag*, I want the dashboard to be portable (shareable via attachment, viewable offline) — so I can paste it into a ticket or send it to a colleague without telling them to install Cowork. +3. *As an existing attune-ai user*, I want the existing artifact to keep working through the transition — so my muscle memory doesn't break the day v0.2.0 (or 0.1.6) lands. +4. *As an attune-rag maintainer*, I want one canonical implementation in the package rather than two copies in two repos — so future changes don't drift. + +### Edge cases & open questions + +| Question / Edge case | Resolution | +|---|---| +| `queries.yaml` missing for the corpus | Refresh emits a partial snapshot with `retrieval.error` populated; CLI exits 1 (still valid JSON). | +| Path-traversal attempt in `--out` (e.g. `/etc/...`) | Reject with exit code 2 and a clear error message (not a raw `OSError`). Reuses the pattern from `attune-ai/.claude/rules/attune/coding-standards-index.md`. | +| Null byte in `--out` | Reject. | +| Parent directory of `--out` doesn't exist | Reject with a clear error message. | +| structlog console output mixed with snapshot stdout | The dashboard's JS parses from the first `{` to skip noise — convention preserved from attune-ai. | +| User opens the rendered file outside Cowork | Static HTML loads, but refresh button has no MCP bridge. Dashboard degrades gracefully — last-baked snapshot still visible. | +| Forward-compatibility with future snapshot fields | Template renders unknown sections as "n/a". | +| Should `dashboard render` auto-open the file? | Open question at spec time; **answered YES in shipped impl** — added `--open` flag (see drift note in design.md). | +| Should there be a deterministic-snapshot mode for demo screenshots? | Skip unless a second use case materializes. | + +### Affected layers + +- [x] attune-rag — new `dashboard` subpackage; new CLI subcommand; template as package data; tests + version bump +- [x] attune-ai — M5 only: delete `scripts/attune_rag_dashboard_refresh.py`, update Cowork artifact to invoke `attune-rag dashboard refresh` +- [ ] attune-gui — none +- [ ] attune-help — none +- [ ] attune-author — none diff --git a/docs/specs/dashboard-v0.2.0/tasks.md b/docs/specs/dashboard-v0.2.0/tasks.md new file mode 100644 index 0000000..35c5703 --- /dev/null +++ b/docs/specs/dashboard-v0.2.0/tasks.md @@ -0,0 +1,115 @@ +# Spec: Shipped Dashboard Template (attune-rag) + +## Phase 3: Tasks + +**Status**: complete + +### Implementation order + +Iterative per the Spec-Driven workflow. Each milestone ends with a green test suite and a usable artifact. + +| # | Task | Layer | Status | Notes | +|---|------|-------|--------|-------| +| M1.1 | Copy `attune_rag_dashboard_refresh.py` logic into `src/attune_rag/dashboard/refresh.py` as a proper module with typed functions and docstrings. | attune-rag | done | M1 — Move refresh logic into the package. | +| M1.2 | Add `attune-rag dashboard refresh` CLI subcommand wired to `dashboard/refresh.py`. | attune-rag | done | | +| M1.3 | Unit tests in `tests/unit/test_dashboard_refresh.py` — snapshot shape contract, corpus-package override, error paths when `queries.yaml` is missing. | attune-rag | done | | +| M1.4 | Smoke test: `uv run attune-rag dashboard refresh \| head -c 500` prints valid JSON. | attune-rag | done | | +| M2.1 | Extract the existing artifact HTML (from the attune-ai Cowork artifact) into `src/attune_rag/dashboard/templates/dashboard.html` with sentinel tokens. | attune-rag | done | M2 — Ship the template as package data. **Drift:** sentinels became `__ATTUNE_SNAPSHOT__` and `__ATTUNE_TITLE__` instead of the spec's `__REFRESH_CMD__` / `__CORPUS_PACKAGE__`. | +| M2.2 | Add `[tool.hatch.build.targets.wheel]` (or equivalent) `package-data` config so the template ships in the wheel. | attune-rag | done | | +| M2.3 | Sanity test: `importlib.resources.files("attune_rag.dashboard").joinpath("templates/dashboard.html").read_text()` works post-install. | attune-rag | done | | +| M3.1 | Implement `src/attune_rag/dashboard/render.py::render(out, snapshot, title) -> Path`. | attune-rag | done | M3 — Render command. **Drift:** signature takes a prebuilt `snapshot` dict, not a `refresh_cmd` string. | +| M3.2 | Add `attune-rag dashboard render` CLI subcommand. | attune-rag | done | | +| M3.3 | Path validation: reject null bytes, system directories, and paths outside the user's home unless `--out` is an explicit absolute path the user provided. | attune-rag | done | | +| M3.4 | Add optional `--open` flag (auto-open in default browser). | attune-rag | done | **Drift:** original spec's open question answered yes; flag added. | +| M3.5 | Golden test in `tests/unit/test_dashboard_render.py` — render to `tmp_path`, read back, assert sentinel substitution; deterministic title + small fixture snapshot. | attune-rag | done | No network, no subprocess — rendering is pure string ops. | +| M3.6 | (Bonus, not in original spec) Add `attune-rag dashboard show` CLI subcommand — Rich terminal dashboard. Calls `build_snapshot` directly and renders to console. | attune-rag | done | **Drift:** added beyond original scope; no objection — independent of HTML pipeline. | +| M4.1 | Bump `pyproject.toml` to target version. | attune-rag | done | **Drift:** feature shipped in **0.1.6**, not 0.2.0 as originally planned. | +| M4.2 | Add a CHANGELOG entry under the appropriate version. | attune-rag | done | | +| M4.3 | Update README with a three-line usage example. | attune-rag | done | | +| M4.4 | Tag and publish per the existing release workflow. | attune-rag | done | | +| M5.1 | Delete `attune-ai/scripts/attune_rag_dashboard_refresh.py`. | attune-ai | done | M5 — Retire the attune-ai-side script (post-merge). | +| M5.2 | Update the attune-ai Cowork artifact to invoke `uv run attune-rag dashboard refresh` instead of the local script. No behavior change — same JSON contract. | attune-ai | done | | +| M5.3 | Document the deprecation in attune-ai's lessons log. | attune-ai | done | | + +### Dependencies + +``` +M1 → M2 → M3 → M4 → M5 +``` + +Each milestone is a self-contained release-able unit: + +- **M1** ships a working `dashboard refresh` CLI but no rendered HTML. +- **M2** ships the template as package data; not yet exposed via CLI. +- **M3** ships `dashboard render` end-to-end. +- **M4** is the version bump + release. +- **M5** is post-release cleanup; can land in a follow-up release of attune-ai. + +### Testing strategy + +All tests live under `tests/unit/` and must pass on the existing CI matrix (Python 3.10–3.13, Ubuntu + macOS). + +#### `test_dashboard_refresh.py` + +- Happy path: `queries.yaml` present → full snapshot with all sections populated. +- Missing `queries.yaml`: exit code 1, JSON still emitted with `retrieval.error` populated. +- Corpus package override: `--corpus-package foo` flows through to the freshness section's corpus lookup. +- Stdout contract: first `{` marks the JSON start. + +#### `test_dashboard_render.py` + +- Renders to `tmp_path`, verifies file exists. +- Verifies the snapshot sentinel and the title sentinel are substituted to the passed values (JSON-escaped). +- Rejects a path inside `/etc`, `/sys`, `/proc`, `/dev`. +- Rejects a path containing a null byte. +- Rejects a path whose parent doesn't exist (with a clear error message, not a raw `OSError`). + +#### `test_dashboard_cli.py` + +- `attune-rag dashboard render --help` exits 0. +- `attune-rag dashboard refresh --help` exits 0. +- `attune-rag dashboard render` without `--out` exits 2. +- End-to-end: render to tmp, then grep for the baked snapshot fields. + +No integration tests against real Cowork — out of scope. + +### Dependencies (build/runtime) + +No new required dependencies. + +- **Jinja2:** considered; **not adopted** for substitution (plain `str.replace` chosen instead — avoids adding a runtime dep for one substitution). +- **Chart.js:** stays CDN-loaded inside the template — no Python dep. SRI hash from the existing artifact is preserved. + +### Rollback plan + +The dashboard subpackage is additive — no changes to `attune_rag.retrieval`, `attune_rag.pipeline`, or the existing `query` / `corpus-info` / `providers` CLI subcommands. Rollback strategy: + +- **If the dashboard module has a bug:** `git revert` the introducing commit; CLI loses `dashboard` subcommands but everything else is untouched. +- **If M5 breaks the attune-ai Cowork artifact:** restore `attune-ai/scripts/attune_rag_dashboard_refresh.py` from git; the artifact's `--refresh-cmd` flips back to the local script. +- **Full revert:** version bump in M4 is the only release-visible change pre-M5; bump back and yank the wheel if pre-distribution release; otherwise publish a patch release with the dashboard module removed. + +--- + +## Phase 4: Implementation + +**Status**: complete + +### Completion checklist + +- [x] All milestones (M1–M5) marked done +- [x] Tests pass on Python 3.10–3.13 × Ubuntu + macOS +- [x] Path-validation tests cover system dirs, null bytes, missing parent +- [x] Snapshot shape contract preserved (existing artifact JS unchanged) +- [x] Released in attune-rag **0.1.6** (originally targeted 0.2.0; landed early) +- [x] CHANGELOG + README updated +- [x] M5: `attune_rag_dashboard_refresh.py` removed from attune-ai; Cowork artifact switched to installed CLI +- [x] Bonus: `attune-rag dashboard show` (Rich terminal dashboard) shipped in same release + +### Drift summary (vs. original locked plan) + +1. **Refresh model:** snapshot baked at render time, not `--refresh-cmd` invoked via MCP. +2. **Template sentinels:** `__ATTUNE_SNAPSHOT__` + `__ATTUNE_TITLE__`, not `__REFRESH_CMD__` + `__CORPUS_PACKAGE__`. +3. **`render()` signature:** `(out, snapshot, title) -> Path` — takes a dict, not a command string. +4. **`--open` flag:** added (was an open question in the original spec). +5. **`dashboard show` subcommand:** added beyond original scope. +6. **Release version:** shipped in **0.1.6**, not 0.2.0. diff --git a/docs/specs/template-path-rename/requirements.md b/docs/specs/template-path-rename/requirements.md new file mode 100644 index 0000000..6413ed9 --- /dev/null +++ b/docs/specs/template-path-rename/requirements.md @@ -0,0 +1,125 @@ +# Spec: Template Path Rename + +> Phase 1 (requirements) for the `template_path` rename refactor kind. +> Carried forward from the template-editor spec +> (`specs/template-editor/`) which deferred this to a follow-up. + +--- + +## Phase 1: Requirements + +**Status**: draft + +### Problem statement + +`attune_rag.editor.plan_rename` currently raises `NotImplementedError` +for `kind="template_path"`. The template-editor M4 rename refactor +modal supports the alias and tag kinds end-to-end, but moving a +template file (changing its rel-path within a corpus) has no path +through the system. + +That gap matters because: + +- Template names drift over time. A file named + `tasks/use-attune-hub.md` might want to become `tasks/attune-hub.md` + when the project's verb conventions change. +- Reorganizing a corpus (moving `concepts/foo.md` → + `references/foo.md`) is currently a manual `mv` followed by a + manual sweep of every place that path is referenced. +- Cross-corpus consistency: if `summaries.json` is path-keyed (it is) + and the dashboard's living-docs index references the path, those + break silently when a file is renamed. + +### Scope + +**In scope:** + +- Renaming a single template's rel-path within a registered corpus. +- Atomic file move (tempfile + rename + rollback on partial failure). +- Updating path-keyed indexes inside the corpus root: `summaries.json`, + any `*-index.json` files written by attune-author. +- Refreshing `Corpus.path_index` and `Corpus.alias_index` after the move. +- Returning a `RenamePlan` with `FileEdit` entries for every + index/sidecar file the rename will touch (so the editor can preview + the multi-file diff before the user clicks Apply). +- Surfacing a "Rename file…" entry in the editor's command palette + (or top-bar menu) — distinct trigger from the alias/tag chip context + menu. + +**Out of scope (defer to a separate spec):** + +- Cross-corpus moves (the source path is in corpus A, the target in + corpus B). The new path must stay inside the same registered + corpus for v1. +- `cross_links.json` updates — current cross_links are template-name + keyed, not path-keyed, so a path-only rename leaves them untouched. + If the rename also changes the name (the canonical case where the + filename = the alias), that's an alias rename composed with a path + rename — out of scope; users can do them sequentially. +- Updating attune-help's static help index, the staleness pipeline's + feature manifests, or any external tools that hard-code template + paths. The plan returns affected paths; the user reruns whatever + external pipeline depends on them. +- Git history preservation — a rename is a `move`, but the corpus is + user code, not git plumbing. + +### User stories + +1. *As a corpus author*, I want to rename `tasks/use-attune-hub.md` → + `tasks/attune-hub.md` from the template editor and have every + path-keyed reference inside the corpus update atomically. +2. *As a corpus author*, I want a multi-file preview before the + rename happens so I can see exactly which files change. +3. *As a corpus author*, if the new path collides with an existing + file, I want a clear error (not a silent overwrite). +4. *As a corpus author*, if the rename fails halfway through (disk + error, permission), I want the corpus to roll back to its + pre-rename state. + +### Edge cases & open questions + +| Question / Edge case | Resolution | +|----------------------|------------| +| New path is inside a directory that doesn't exist | Create the directory as part of apply; rollback removes it on failure. | +| New path equals old path | No-op plan, return empty `edits[]`. (Mirrors alias/tag behavior.) | +| New path collides with an existing template | `RenameCollisionError(new_path, owning_path=new_path)`. The editor surfaces it as a banner, same as the alias case. | +| The template currently being edited is the one being moved | After apply, the editor refreshes its `(corpus, path)` to the new path and continues. WS subscribers on the old path get a `file_changed` once + a final close so the tab can reopen on the new path. | +| `summaries.json` exists in the corpus root | Update the entry; preserve any other keys. Returns one `FileEdit` for `summaries.json`. | +| `summaries.json` doesn't exist | Skip; not an error. | +| New path tries to escape the corpus root (`../...`) | Reject with `ValueError`; surfaced as 400 in the editor route. | + +### Affected layers + +- [x] attune-rag (backend) — implement `_plan_template_path_rename` + `_apply_template_path_rename` +- [x] attune-gui (frontend) — new "Rename file…" trigger; rename modal already supports `kind: "template_path"` API-wise +- [ ] attune-help (mobile/docs) — none +- [ ] attune-author (authoring/infra) — none + +--- + +## Phase 2: Design + +> Stubbed. Open the design phase before implementation. Key questions +> to answer: +> +> 1. What is the canonical list of path-keyed sidecar files inside a +> corpus root? (`summaries.json` confirmed; check for others.) +> 2. How does the editor's WS infrastructure handle the path change +> for the active session — does it close-and-redirect, or does it +> rebind in-place? +> 3. Is the "Rename file…" trigger on the chip context menu (where +> alias/tag rename lives) or in a new command palette entry? +> 4. Should `apply_rename` for `template_path` require an empty +> target directory, or only an empty target *file*? + +--- + +## Phase 3: Tasks + +> Stubbed; fill out after Phase 2 is approved. + +--- + +## Phase 4: Implementation + +> Not started.