Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions reference/visual-artifact-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ bin/render-artifact.sh stack [<name>] [--strict] [--manifest-only]

| Form | Behavior |
|------|----------|
| `<phase> --latest` | Resolve source via `bin/find-artifact.sh <phase> 30 --no-session-sync`. |
| `<phase> --latest` | Resolve source via `bin/find-artifact.sh <phase> 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 `<phase>` artifact found in the last 30 days". Pass an explicit `<artifact-path>` instead to render an older file. |
| `<phase> <artifact-path>` | Render the explicit file. Phase mismatch fails with exit 1. |
| `<phase>` with no artifact argument | Equivalent to `<phase> --latest`. |
| `journal --today` | Aggregate every registered phase artifact for today's date into a sprint timeline. |
Expand All @@ -53,20 +53,23 @@ bin/render-artifact.sh stack [<name>] [--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/
<phase>/ core phase HTML (plan, think, review, security, qa, ship)
custom/<phase>/ custom phase HTML (future, when wired)
custom/<phase>/ reserved for custom phase HTML (helper exists,
no caller wires it in v1)
journal/ sprint journal HTML
stack/<name>/ custom stack DAG HTML
manifests/ companion manifest JSON for every render
```

The `custom/<phase>/` path is generated by `nano_visual_output_dir <phase> 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-<pid>-<phase>.html`
Expand All @@ -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-<pid>` 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`:
Expand Down Expand Up @@ -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:<name>` 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.
Expand Down Expand Up @@ -142,7 +148,7 @@ Forbidden in any generated template or rendered output:
- `eval(`, `new Function(`.
- `<form>`, `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

Expand All @@ -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 `<!--<script>`, `</script>`, or `<!CDATA[` sequences. `JSON.parse` reads `<` as the literal `<`, so the clipboard payload the user pastes is unchanged.

## Trust badge wording
Expand Down Expand Up @@ -185,7 +195,7 @@ The store-local check uses `realpath` so a macOS `/tmp` → `/private/tmp` symli
- Names match `^[A-Za-z0-9_-]+$` (no whitespace, no path separators).
- Names are unique.
- Every name in any `.depends_on` exists as a declared node (no dangling references).
- The graph is acyclic (Kahn-style topological pass; rounds capped at `node_count + 1`).
- The graph is acyclic (Kahn-style topological pass; rounds capped at `node_count + 1`). When a cycle is detected, the "Stack invalid" notice names every unresolved node so the user can identify the cycle without re-running with a debugger.

A graph that fails any check produces a "Stack invalid" notice with the specific reason. The manifest still writes with a synthetic source pointing at the stack file.

Expand Down
Loading