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 `