From c5a60310d8536941c9eac0694207b1f23e2bb864 Mon Sep 17 00:00:00 2001 From: gus Date: Mon, 11 May 2026 21:39:39 -0300 Subject: [PATCH] visual-artifacts: contract doc audit cleanup Self-audit of reference/visual-artifact-contract.md against the shipped code. No code change; doc-only clarifications that close 6 small drift / gap items and improve 4 more. Closes: - C-1 / C-7: exit-code table now covers mktemp failures for HTML temp, manifest temp, and scratch dir (all return exit 4 as an unsafe-environment signal). - C-3: --latest documents the 30-day max-age window and how to bypass it by passing an explicit artifact path. - C-4: nano_visual_safe_screenshot_path documented as a reserved helper for a future QA-screenshot PR; explicitly NOT wired in v1. Architect's hardening backlog item #1. - C-5: interactive script body lint contract spelled out (CI greps for absence of each forbidden API plus presence of navigator.clipboard.writeText). - C-8: exit 3 row now states aggregate strict mode runs BEFORE --manifest-only early exit, matching PR 3 pass 3 lock. - C-9: stack cycle error names every unresolved node in the invalid notice. Clarifies: - C-2: custom// subdir labeled as reserved plumbing (helper exists, no v1 caller passes the custom=true flag). - C-6: manifest .phase field semantics: records the CLI kind (plan / journal / stack), not the stack name. Stack name lives in the synthetic stack: source entry. - C-10: filename stems are NOT stable across renders (PID + new timestamp every time); consumers needing a "latest" handle should re-render or read the most recent manifest by created_at. Verification: - ci/check-visual-artifact-templates.sh 38/38 - ci/e2e-visual-artifacts.sh 330/330 - visual-artifact-public-copy lint: presence + framing both green --- reference/visual-artifact-contract.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/reference/visual-artifact-contract.md b/reference/visual-artifact-contract.md index 3430df0..2a80808 100644 --- a/reference/visual-artifact-contract.md +++ b/reference/visual-artifact-contract.md @@ -32,7 +32,7 @@ bin/render-artifact.sh stack [] [--strict] [--manifest-only] | Form | Behavior | |------|----------| -| ` --latest` | Resolve source via `bin/find-artifact.sh 30 --no-session-sync`. | +| ` --latest` | Resolve source via `bin/find-artifact.sh 30 --no-session-sync`. The `30` is the max-age window in days; artifacts older than 30 days are treated as not found and the render exits 1 with "no `` artifact found in the last 30 days". Pass an explicit `` instead to render an older file. | | ` ` | Render the explicit file. Phase mismatch fails with exit 1. | | `` with no artifact argument | Equivalent to ` --latest`. | | `journal --today` | Aggregate every registered phase artifact for today's date into a sprint timeline. | @@ -53,20 +53,23 @@ bin/render-artifact.sh stack [] [--strict] [--manifest-only] |------|---------| | 0 | Render succeeded. Output path printed to stdout. | | 1 | Input error: missing artifact, invalid JSON, phase mismatch, unsupported phase, malformed stack name, `--interactive` requested outside `/plan` and `/review`. | -| 3 | Trust failure (`integrity_mismatch` always; `integrity_missing` only under `--strict`). | -| 4 | Unsafe output path: outside the visual root, symlinked subdirectory, symlinked leaf, directory at leaf, or symlinked visual root. | +| 3 | Trust failure (`integrity_mismatch` always; `integrity_missing` only under `--strict`). For `journal --strict` and `stack --strict`, the check runs BEFORE the `--manifest-only` early exit so neither path can ship a `"strict": true` manifest while an aggregated source is tampered. | +| 4 | Unsafe output path: outside the visual root, symlinked subdirectory, symlinked leaf, directory at leaf, or symlinked visual root. Also returned when `mktemp` fails to create the HTML temp, the manifest temp, or the per-render scratch directory (treated as an unsafe-environment signal, not retried). | ## Store layout ``` $NANOSTACK_STORE/visual/ / core phase HTML (plan, think, review, security, qa, ship) - custom// custom phase HTML (future, when wired) + custom// reserved for custom phase HTML (helper exists, + no caller wires it in v1) journal/ sprint journal HTML stack// custom stack DAG HTML manifests/ companion manifest JSON for every render ``` +The `custom//` path is generated by `nano_visual_output_dir true` but no v1 render-artifact.sh code path passes `true`. Treat it as plumbing reserved for a future PR that wires per-render dispatch for custom phases. + Filename format: - Phase HTML: `YYYYMMDD-HHMMSS--.html` @@ -76,6 +79,8 @@ Filename format: The PID suffix prevents collisions between same-second renders. +Filename stems are NOT stable across renders. Every render produces a fresh `YYYYMMDD-HHMMSS-` stem, so a consumer that bookmarks the URL to an HTML file loses the reference on the next render. The stable identity lives in the JSON artifact path and the SHA-256 integrity, both of which are reproduced in the manifest. Consumers that need a "latest" entry point should re-render or read the most recent manifest by `created_at`. + ## Manifest schema Every render writes a companion manifest. Schema version `1`: @@ -113,6 +118,7 @@ Validation rules: - `schema_version` equals `"1"`. - `format` equals `"html"`. - `kind` is one of `phase`, `journal`, `stack`. +- `phase` records the kind that was requested at the CLI: for phase renders this is `plan` / `think` / `review` / `security` / `qa` / `ship`; for `journal` renders it is the literal `journal`; for `stack` renders it is the literal `stack`. The stack name (e.g. `compliance-release`) is not part of `phase`; it lives in the synthetic `stack:` entry in `source_artifacts`. - `source_artifacts` length is at least 1. Phase renders have one entry; journal aggregates every registered phase; stack renders include the stack definition file (or `.nanostack/config.json` for `stack default`) as the first source, plus one entry per phase. - `output_path` is absolute and under `$NANOSTACK_STORE/visual/`. - `renderer.version` is present. @@ -142,7 +148,7 @@ Forbidden in any generated template or rendered output: - `eval(`, `new Function(`. - `
`, `document.write`, `window.open`. -The interactive inline script body is allowed to use `navigator.clipboard.writeText` only. CI enforces the forbidden list against `bin/render-artifact.sh`, `bin/lib/visual-render.sh`, and `bin/lib/html-escape.sh` via `ci/check-visual-artifact-templates.sh`. +The interactive inline script body is allowed to use `navigator.clipboard.writeText` only. CI enforces the forbidden list against `bin/render-artifact.sh`, `bin/lib/visual-render.sh`, and `bin/lib/html-escape.sh` via `ci/check-visual-artifact-templates.sh`. The lint runs grep-style absence checks for each forbidden API plus a presence check that the script body sources `navigator.clipboard.writeText` exactly. Any new clipboard / network / storage / DOM-mutation API in the interactive path requires updating both the contract and the lint. ## Escape helpers @@ -154,6 +160,10 @@ The interactive inline script body is allowed to use `navigator.clipboard.writeT Every JSON-derived string MUST pass through one of these. Fixed template HTML (page shell, table headers, button labels) may be written directly. +### Reserved helpers (defined, not wired in v1) + +`bin/lib/visual-render.sh::nano_visual_safe_screenshot_path` is an allowlist for QA screenshot rendering: it accepts absolute paths under the project or `$NANOSTACK_STORE`, rejects `..` traversal, absolute URLs (`http://`, `https://`, `data:`, `javascript:`, `file://`), and any path containing `<`, `>`, or `"`. The helper is defined so a future PR wiring `/qa` screenshot galleries can reuse it without re-deriving the allowlist. v1 does NOT render screenshots, so calling the helper from a render path is a contract change that requires updating this document and the CI lint. + For interactive mode, an additional transform in `render-artifact.sh::_js_safe_for_script` encodes every `<` in JS-embedded JSON payloads as `<` so the HTML parser cannot exit the script body via `