--- phase: 01 plan: 05 type: execute wave: 2 depends_on: [01-01] files_modified: - scripts/validate-assets.mjs - scripts/validate-assets.test.ts - assets/north-stars/.gitkeep - assets/north-stars/README.md - assets/__samples__/refused/no-provenance.png - assets/__samples__/refused/.gitkeep autonomous: false requirements: [AEST-08, AEST-09, PIPE-03] must_haves: truths: - "`node scripts/validate-assets.mjs` exits 0 when every asset under `/assets/` (excluding `__samples__/refused/`) carries a sibling `.provenance.json` validating against the Zod sidecar schema" - "The validator script exits non-zero with a clear error message when any asset under `/assets/` (excluding refused/) lacks a sidecar (proving the gate works — AEST-09 + PIPE-03)" - "The provenance schema enforces all 6 required fields per CLAUDE.md / AEST-08: `model_id`, `checkpoint_hash`, `prompt`, `seed`, `sampler`, `params`, plus an optional `provenance_schema_version: number` for forward-compat (per RESEARCH Open Question #2)" - "10–20 hand-curated north-star reference images are committed under `assets/north-stars/` with valid provenance sidecars (CONTEXT D-01) — OR — if no AI tool is available, a documented fallback set with provenance fields filled honestly (`model_id: 'human'`, etc., per RESEARCH Open Question #5 + Environment Availability fallback) is committed" - "A refused-sample asset under `assets/__samples__/refused/no-provenance.png` proves the gate by being explicitly excluded from validation (CONTEXT D-03)" - "A Vitest test runs the validator script against an isolated `os.tmpdir()` fixture directory containing a deliberately-missing-provenance file and asserts the script exits non-zero, with no risk of polluting the real `/assets/` tree" artifacts: - path: scripts/validate-assets.mjs provides: "Standalone Node script (~30 lines) that walks /assets/ (or whatever ASSETS_DIR points at), pairs each non-sidecar non-.gitkeep file with .provenance.json, validates against ProvenanceSchema, exits non-zero on missing/invalid" - path: scripts/validate-assets.test.ts provides: "Vitest integration test that creates an isolated per-run fixture under os.tmpdir(), runs the validator with ASSETS_DIR pointing at the tmpdir as a subprocess, asserts exit code" - path: assets/north-stars/README.md provides: "Explains the north-star reference set: what these images are, how to add new ones, the sidecar naming convention" - path: assets/__samples__/refused/no-provenance.png provides: "Sample image with NO sidecar; validator must exclude this directory; existence proves the gate works (CONTEXT D-03)" - path: "assets/north-stars/*.png" provides: "10–20 hand-curated reference images establishing the watercolor visual north-star (CONTEXT D-01)" key_links: - from: scripts/validate-assets.mjs to: "assets/**/*" via: "node:fs/promises readdir + sibling sidecar lookup" pattern: "readdir.*assets|walk\\(ASSETS\\)" - from: scripts/validate-assets.mjs to: "assets/__samples__/refused/" via: "Hardcoded REFUSED exclusion list" pattern: "REFUSED|__samples__/refused" - from: scripts/validate-assets.test.ts to: scripts/validate-assets.mjs via: "child_process.execFile against the script with ASSETS_DIR= + assert exit code" pattern: "spawn\\(.*node.*validate-assets|execFile|os\\.tmpdir" --- Build the AI asset provenance pipeline floor: a 30-line standalone Node script (`scripts/validate-assets.mjs`) that walks `/assets/` (or whatever `ASSETS_DIR` env var points at), validates each asset has a sibling `.provenance.json` file matching a Zod schema covering the 6 required fields per CLAUDE.md + AEST-08 (`model_id`, `checkpoint_hash`, `prompt`, `seed`, `sampler`, `params`) plus an optional forward-compat `provenance_schema_version` field. Excludes `assets/__samples__/refused/` so a sample sidecarless image can prove the gate exists. Commits 10–20 hand-curated north-star reference images establishing the visual style (watercolor; real-but-slightly-wrong flora) with provenance sidecars per CONTEXT D-01 — OR — if no AI tool is locally available, a documented fallback per RESEARCH Open Question #5. Ships a Vitest test that programmatically asserts the validator exits non-zero on a fixture missing provenance (PIPE-03 + AEST-09), with the negative-case fixture parked under `os.tmpdir()` so it never pollutes the real `/assets/` tree. Purpose: This is the floor of the asset pipeline that Phase 5 will scale up to production volume. CONTEXT D-01/D-02/D-03 lock the shape (sidecar + CI walker, no curator workflow, no two-stage promotion). The 10–20 reference set is the seed against which Phase 5+ asset migrations will be visually regressed. RESEARCH § Pattern 6 provides verbatim code. Output: A complete asset pipeline floor: validator script, sidecar schema, refused-sample fixture, ~10–20 north-star images with provenance sidecars, and a Vitest test enforcing the gate. This plan is `autonomous: false` because Task 2 — committing the 10–20 north-star images — requires human curation per CONTEXT D-01 + D-03 (curation gate IS the human reviewer per CONTEXT). The user must approve which images ship. If the user has no local AI image tool, the fallback (commit licensed-CC-BY photographs of real cottage gardens or hand-painted references with provenance fields filled honestly) is acceptable per RESEARCH Open Question #5 + Environment Availability — but still needs human selection. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-foundations-and-doctrine/01-CONTEXT.md @.planning/phases/01-foundations-and-doctrine/01-RESEARCH.md @.planning/phases/01-foundations-and-doctrine/01-01-SUMMARY.md @CLAUDE.md @.planning/research/PITFALLS.md Task 1: Provenance validator script + Zod sidecar schema + Vitest enforcement test scripts/validate-assets.mjs, scripts/validate-assets.test.ts, assets/__samples__/refused/no-provenance.png, assets/__samples__/refused/.gitkeep - .planning/phases/01-foundations-and-doctrine/01-RESEARCH.md § "Pattern 6: Provenance Sidecar Validator" (verbatim ~30-line script) and § "Provenance sidecar example" (the JSON shape) and § Open Question #2 (optional `provenance_schema_version` field) - .planning/phases/01-foundations-and-doctrine/01-CONTEXT.md (D-01 — full provenance metadata, 6 fields; D-02 — vendor deferred; D-03 — sidecar + CI walker, refused sample, no curator workflow) - CLAUDE.md "Code Style" — provenance metadata: `{model_id, checkpoint_hash, prompt, seed, sampler, params}` - REQUIREMENTS.md AEST-08 + AEST-09 - package.json (Plan 01 already created the `validate:assets` script that calls this file) - **validate-assets.mjs:** - Recursively walks `process.env.ASSETS_DIR ?? 'assets'` using `node:fs/promises` (Node 20+ supports `readdir({recursive: true, withFileTypes: true})`). - Skips any path under `assets/__samples__/refused/` (the gate proof — these files INTENTIONALLY have no sidecar). - Skips `.gitkeep` files. - Skips files ending in `.provenance.json` (those are sidecars, not assets). - For every other file, requires a sibling `.provenance.json` (e.g., `garden-soil-01.png` requires `garden-soil-01.png.provenance.json` per RESEARCH § Pattern 6 sidecar naming convention decision). - Reads each sidecar and validates against `ProvenanceSchema` (Zod with the 6 required fields + optional `provenance_schema_version`). - On any missing or invalid sidecar, prints a clear error and exits non-zero. - On success, prints `[provenance] all assets carry valid provenance.` and exits 0. - **validate-assets.test.ts:** - **Positive case:** Runs the validator against the real `/assets/` tree (the default — no `ASSETS_DIR` override) and asserts exit 0. This proves the north-star set + refused-sample dir together pass the gate. - **Negative case (BLOCKER 2 fix):** Generates a per-test-run unique tmpdir under `os.tmpdir()` (using `node:os` + `node:path` + `fs.mkdtemp`), drops a single PNG with no sidecar inside, runs the validator with `ASSETS_DIR=` set in the env, asserts exit code !== 0 and stderr/stdout contains the expected error message. Cleans up the tmpdir in `afterAll`. **No risk of polluting `/assets/`, no Ctrl-C cleanup hazard** — even if the test runner is killed mid-run, the OS reclaims the tmpdir on next reboot. **Step 1 — `scripts/validate-assets.mjs`** (per RESEARCH § Pattern 6 verbatim, with one improvement: the optional `provenance_schema_version` per Open Question #2): ```javascript #!/usr/bin/env node // scripts/validate-assets.mjs — Phase 1 asset provenance gate (PIPE-03, AEST-08, AEST-09) // // Walks /assets/ (or process.env.ASSETS_DIR for tests), requires every non-sidecar // non-.gitkeep file to have a sibling .provenance.json validating against // ProvenanceSchema. Excludes /assets/__samples__/refused/ (which intentionally lacks // sidecars to prove the gate). // // Per CONTEXT D-03: minimum-viable. No curator workflow, no two-stage promotion, // no pre-commit hook. Sidecar + this script + CI is the entire pipeline. // // Per CONTEXT D-01: 6 required fields per CLAUDE.md provenance metadata. // Per RESEARCH Open Question #2: optional provenance_schema_version for Phase 5 fwd-compat. import { readdir, readFile } from 'node:fs/promises'; import { join, basename } from 'node:path'; import { z } from 'zod'; const ProvenanceSchema = z.object({ model_id: z.string().min(1), checkpoint_hash: z.string().min(1), prompt: z.string().min(1), seed: z.union([z.string(), z.number()]), sampler: z.string().min(1), params: z.record(z.string(), z.unknown()), provenance_schema_version: z.number().int().positive().optional(), }); const ASSETS_DIR = process.env.ASSETS_DIR ?? 'assets'; // Refused-sample exclusion is relative to the *real* assets tree; tests pointing // ASSETS_DIR at a tmpdir won't have these paths so the exclusion is harmless. const REFUSED_PREFIXES = ['assets/__samples__/refused', 'assets/__test_fixtures__/refused']; async function* walk(dir) { let entries; try { entries = await readdir(dir, { withFileTypes: true }); } catch (e) { if (e.code === 'ENOENT') return; throw e; } for (const entry of entries) { const path = join(dir, entry.name); if (entry.isDirectory()) { yield* walk(path); } else { yield path; } } } function normalizePath(p) { return p.replaceAll('\\', '/'); } const errors = []; let assetCount = 0; for await (const path of walk(ASSETS_DIR)) { const norm = normalizePath(path); if (REFUSED_PREFIXES.some((r) => norm.startsWith(r))) continue; if (norm.endsWith('.provenance.json')) continue; if (basename(norm) === '.gitkeep') continue; if (basename(norm) === 'README.md') continue; assetCount++; const sidecar = path + '.provenance.json'; try { const raw = await readFile(sidecar, 'utf8'); const parsed = ProvenanceSchema.safeParse(JSON.parse(raw)); if (!parsed.success) { errors.push(`${path}: provenance schema validation failed — ${parsed.error.message}`); } } catch (e) { errors.push(`${path}: missing or unreadable provenance sidecar (${sidecar}): ${e.code ?? e.message}`); } } if (errors.length) { console.error('[provenance] validation failed:'); for (const err of errors) console.error(' ' + err); process.exit(1); } console.log(`[provenance] all ${assetCount} assets carry valid provenance.`); ``` Note the `ASSETS_DIR` env override — this lets the Vitest test point the script at an `os.tmpdir()` fixture directory without modifying production code, **and without leaving stray fixture files in `/assets/`** (BLOCKER 2 fix). The `REFUSED_PREFIXES` list covers paths under the real `assets/` tree; tmpdir-based test runs simply have no such paths and the exclusion is a harmless no-op. Also note: the script accepts `assets/north-stars/README.md` (skipped by `basename === 'README.md'`) — Task 2 will add this README; without the skip, the validator would demand a sidecar for the README itself. **Step 2 — Make the script executable (Unix; harmless on Windows):** ```bash chmod +x scripts/validate-assets.mjs 2>/dev/null || true ``` **Step 3 — Create the refused-sample asset.** Per CONTEXT D-03, a real image file under `assets/__samples__/refused/` proves the gate exists by being intentionally without a sidecar. Use a tiny 1x1 transparent PNG (~70 bytes); generate with Node: ```bash node -e "const fs = require('fs'); const png = Buffer.from('89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c63600100000005000146cd9c5d0000000049454e44ae426082', 'hex'); fs.writeFileSync('assets/__samples__/refused/no-provenance.png', png);" touch assets/__samples__/refused/.gitkeep ``` The `.gitkeep` ensures the directory persists if the PNG is ever removed; the PNG itself is the gate-proof artifact. **Step 4 — Run the validator manually:** `node scripts/validate-assets.mjs` should exit 0 (only the refused-sample is in `/assets/`, and it's excluded). Output: `[provenance] all 0 assets carry valid provenance.` **Step 5 — `scripts/validate-assets.test.ts`** (Vitest integration test). The negative-case fixture is created under `os.tmpdir()` per BLOCKER 2 fix — isolated from the real `/assets/` tree, no orphan-fragility risk, no Ctrl-C cleanup hazard: ```typescript import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import os from 'node:os'; const exec = promisify(execFile); const SCRIPT = 'scripts/validate-assets.mjs'; describe('PIPE-03 / AEST-09: asset provenance gate', () => { it('exits 0 against the real /assets/ tree (refused sample excluded)', async () => { const result = await exec('node', [SCRIPT]); expect(result.stdout).toMatch(/all \d+ assets carry valid provenance/); }); describe('with an isolated tmpdir fixture missing provenance', () => { let tmpDir: string; let fixtureFile: string; beforeAll(async () => { // Per-test-run unique tmpdir under os.tmpdir() — isolated from /assets/, // no risk of polluting the real tree even if the runner is killed mid-test. tmpDir = await mkdtemp(join(os.tmpdir(), 'tlg-provenance-test-')); fixtureFile = join(tmpDir, 'orphan.png'); // Tiny 1x1 PNG with no sidecar const png = Buffer.from( '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c63600100000005000146cd9c5d0000000049454e44ae426082', 'hex', ); await writeFile(fixtureFile, png); }); afterAll(async () => { await rm(tmpDir, { recursive: true, force: true }); }); it('exits non-zero with a clear error message when ASSETS_DIR points at the fixture', async () => { // Run the validator against the isolated tmpdir; the script reads ASSETS_DIR // from process.env, so the orphan.png is the only file under inspection. let exitCode = 0; let combinedOutput = ''; try { await exec('node', [SCRIPT], { env: { ...process.env, ASSETS_DIR: tmpDir } }); } catch (err: any) { exitCode = err.code ?? -1; combinedOutput = (err.stdout ?? '') + (err.stderr ?? ''); } expect(exitCode).toBe(1); expect(combinedOutput).toMatch(/validation failed/); expect(combinedOutput).toMatch(/orphan\.png/); expect(combinedOutput).toMatch(/missing.*provenance sidecar/i); }); }); }); ``` **Step 6 — Run `npm test` and confirm the validate-assets test passes.** The positive case asserts the real `/assets/` tree (refused-sample dir + Task 2's north-stars) passes the gate; the negative case runs the script against an isolated `os.tmpdir()` fixture with one orphan PNG and asserts exit 1 + the expected error message. Per RESEARCH § Pattern 6 ("Refused-sample test"), the negative-case fixture proves the gate fires; the BLOCKER 2 fix moves that fixture to `os.tmpdir()` so it cannot pollute the real `/assets/` tree. **Step 7 — Commit `feat(01-05): asset provenance validator + Zod sidecar schema + refused-sample fixture + PIPE-03 enforcement test (tmpdir-isolated)`.** node scripts/validate-assets.mjs && npx vitest run scripts/validate-assets.test.ts - `scripts/validate-assets.mjs` exists and is a runnable Node script — verify with `node --check scripts/validate-assets.mjs`. - The script defines a Zod `ProvenanceSchema` with all 6 CLAUDE.md fields plus optional `provenance_schema_version` — verify with `grep -cE "(model_id|checkpoint_hash|prompt|seed|sampler|params|provenance_schema_version)" scripts/validate-assets.mjs` returns at least 7. - The script reads from `process.env.ASSETS_DIR ?? 'assets'` (so the test can isolate via env override) — verify with `grep -q "process.env.ASSETS_DIR" scripts/validate-assets.mjs`. - The script excludes `__samples__/refused` — verify with `grep -q "__samples__/refused" scripts/validate-assets.mjs`. - The script exits non-zero on missing sidecar — verify with `grep -q "process.exit(1)" scripts/validate-assets.mjs`. - `assets/__samples__/refused/no-provenance.png` exists with no sidecar — verify with `test -f assets/__samples__/refused/no-provenance.png && ! test -f assets/__samples__/refused/no-provenance.png.provenance.json`. - **Test fixture isolation (BLOCKER 2):** `scripts/validate-assets.test.ts` uses `os.tmpdir()` for the negative-case fixture — verify with `grep -q "os.tmpdir" scripts/validate-assets.test.ts && grep -q "mkdtemp" scripts/validate-assets.test.ts`. - **Test fixture isolation (BLOCKER 2):** the negative-case test passes `ASSETS_DIR` via the `env` option of `execFile` — verify with `grep -E "ASSETS_DIR.*tmpDir|env:.*ASSETS_DIR" scripts/validate-assets.test.ts`. - **Test fixture isolation (BLOCKER 2):** no `assets/__test_fixtures__/missing` path is created during the test — verify with `! grep -q "assets/__test_fixtures__/missing" scripts/validate-assets.test.ts`. - Running the script directly exits 0: `node scripts/validate-assets.mjs` — exit code 0. - The Vitest test passes (positive + negative cases both green) — verify with `npx vitest run scripts/validate-assets.test.ts 2>&1 | grep -E "passed"` exits 0. - The Vitest test cleans up its tmpdir — verify with `grep -q "afterAll" scripts/validate-assets.test.ts && grep -q "rm.*tmpDir" scripts/validate-assets.test.ts`. Validator script (~80 lines including error handling and Windows-path normalization) at `scripts/validate-assets.mjs`; Zod sidecar schema covering all 6 fields + optional schema version; refused-sample PNG committed under `__samples__/refused/`; Vitest test that creates an **isolated `os.tmpdir()` fixture** (BLOCKER 2 fix — no real-tree pollution risk), runs the validator with `ASSETS_DIR` pointing at the tmpdir, asserts non-zero exit + clear error message, cleans up; both `node scripts/validate-assets.mjs` and `npx vitest run scripts/validate-assets.test.ts` green; commit landed. Task 2: Curate and commit 10–20 north-star reference images with provenance sidecars (CONTEXT D-01) Plan 05 Task 1 shipped: - `scripts/validate-assets.mjs` — the CI gate (exits non-zero on missing/invalid provenance) - `assets/__samples__/refused/no-provenance.png` — the proof-of-gate fixture - `scripts/validate-assets.test.ts` — Vitest test enforcing the gate (negative case isolated under `os.tmpdir()`) Task 2 ships the 10–20 hand-curated north-star reference set per CONTEXT D-01 — the visual ground truth Phase 5+ will regenerate against. Per CONTEXT D-03, the curation gate IS the human reviewer (you), not a workflow document. This task pauses for your hands-on curation. **Three valid paths** (your call): **Path A — AI-generated (recommended if you have a tool available):** 1. Use whatever AI image tool you currently have (Claude with image generation, Stable Diffusion + watercolor LoRA, Midjourney, Scenario, etc.). 2. Generate 10–20 watercolor-style images representing the visual north-star: walled cottage gardens, real-but-slightly-wrong wildflowers, golden/autumnal palette for Season 1, hand-painted feel, no fantasy elements (no D&D flora — see PROJECT.md "Out of Scope"). 3. For each generated image, write a sibling `.png.provenance.json` with all 6 required fields filled honestly (the exact `model_id` you used, the prompt verbatim, the seed if your tool surfaces one, etc.). 4. Place the pair under `assets/north-stars/.png` + `assets/north-stars/.png.provenance.json`. **Path B — Hand-painted / photograph fallback (if no AI tool is available locally):** Per RESEARCH § Open Question #5 + Environment Availability, the provenance schema accepts arbitrary `model_id` strings, so honest "human-painted" or licensed-photograph entries are valid. For each image: - `model_id`: `"human"` (or `"photograph:cc-by:"`) - `checkpoint_hash`: `"n/a"` - `prompt`: a description of what the image is - `seed`: `0` - `sampler`: `"n/a"` - `params`: `{ "notes": "Phase 1 fallback per RESEARCH Open Question #5; replaceable in Phase 5+" }` **Path C — Defer with explicit IOU:** If neither A nor B is feasible right now, commit **two** placeholder images with full honest provenance saying "placeholder" — enough to prove the schema accepts real entries — and **record the IOU in a dedicated file** at `.planning/phases/01-foundations-and-doctrine/01-05-IOU.md` (do **not** edit `.planning/STATE.md` from a phase-internal task — STATE.md is owned by the orchestrator, per WARNING 5 fix). The IOU file contains date, owner, and the deferred-work statement; the verification phase / Phase-5 entry will surface this IOU back into STATE.md if it is still open at that point. This still satisfies CONTEXT D-01's "10–20 hand-curated" loosely (with explicit IOU) and keeps the rest of Phase 1 unblocked. Whichever path you choose, also write `assets/north-stars/README.md` documenting: - What this directory is (the visual ground truth for Phase 5+ regression) - Which path was chosen (A/B/C) and why - How to add new images (sidecar naming convention, the 6 required fields) - When this set will be revisited (Phase 5 is the planned consolidation point per CONTEXT D-02) 1. Choose a path (A, B, or C) and produce the images + sidecars. 2. Place all pairs under `assets/north-stars/`. Naming: `.png` + `.png.provenance.json` (per RESEARCH Pattern 6 sidecar naming convention). 3. Write `assets/north-stars/README.md` (~10 lines, see template above). 4. Run `node scripts/validate-assets.mjs` — must exit 0 with `[provenance] all assets carry valid provenance.` where N matches the count of images you committed. 5. Run `npm test` — must remain green; the validate-assets test should now also count the new assets. 6. Run `npm run ci` — must exit 0 (lint + test + validate:assets + build). 7. Commit with message `feat(01-05): commit north-star reference images with provenance sidecars (path )`. 8. **If you chose Path C, also create `.planning/phases/01-foundations-and-doctrine/01-05-IOU.md`** with this content (do NOT edit `.planning/STATE.md` directly — that file is orchestrator-owned, per WARNING 5 fix): ```markdown # Plan 05 IOU — North-Star Reference Set Expansion **Date:** **Owner:** **Phase:** 01 (Foundations & Doctrine) **Plan:** 05 (Asset Provenance) ## Deferred Work Path C was selected for Plan 05 Task 2: the north-star reference set under `assets/north-stars/` currently contains placeholder images (target: 10–20). Expansion to the full curated set must happen before Phase 5 begins, since Phase 5+ visual regression depends on this seed. ## Trigger Phase 5 entry; or sooner if an AI image tool / hand-painted batch becomes available. ## Acceptance - 10–20 final images committed under `assets/north-stars/` with valid provenance sidecars. - This file deleted on resolution (or marked `RESOLVED` with the resolving commit hash). ``` Type `approved` when done, or describe issues encountered (e.g., "AI tool unavailable, going with Path B / Path C"). Type "approved" or describe issues | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-01-06 | Spoofing | Provenance sidecar fabrication (a contributor adds an asset with fabricated provenance) | accept (out of scope for Phase 1) | Single-developer project in Phase 1; not a real threat. RESEARCH § Security Domain explicitly defers this to Phase 8+ when external contributors enter the picture, with `human_reviewed_by` field signed by a curator. | | T-01-07 | Tampering | Path traversal via sidecar filename | accept | Sidecars are walked by the validator using `node:fs/promises readdir` and reads are confined to paths under `/assets/` (or `process.env.ASSETS_DIR`). The validator never resolves paths from sidecar contents. Not exploitable. | - `node scripts/validate-assets.mjs` exits 0 against the real `/assets/` tree (north-star set + refused-sample dir). - `npx vitest run scripts/validate-assets.test.ts` passes (the gate is structurally enforced — an asset missing provenance under an isolated `os.tmpdir()` fixture fails the script). - `assets/__samples__/refused/no-provenance.png` exists and has no sidecar (proof-of-gate artifact). - `assets/north-stars/` contains the curated reference set per Path A/B/C with valid provenance sidecars (or the explicit IOU file at `.planning/phases/01-foundations-and-doctrine/01-05-IOU.md` per Path C). - `npm run ci` exits 0 — Plan 07's CI workflow will run this. - 30-line standalone Node script validates every non-sidecar asset has a sibling `.provenance.json` per the 6-field schema. - Refused-sample fixture proves the gate by being intentionally excluded. - Vitest integration test creates an `os.tmpdir()`-isolated missing-provenance fixture, asserts non-zero exit, cleans up — no real-tree pollution risk. - 10–20 north-star reference images committed with valid sidecars (Path A / B / C per CONTEXT D-01 + RESEARCH Open Question #5). - `npm run validate:assets` (the package.json script Plan 01 created) exits 0. - Phase 5 has a working seed for visual regression and asset-pipeline scale-up. After completion, create `.planning/phases/01-foundations-and-doctrine/01-05-SUMMARY.md` documenting: - The chosen path (A / B / C) and why. - The exact count of north-star images committed (the validator output's `all assets` count). - The `model_id` values present in the provenance set (so Phase 5 sees what tools were used in Phase 1). - Confirmation that `npm run validate:assets` exits 0 and `npx vitest run scripts/validate-assets.test.ts` is green. - Note for Phase 5: the schema is `provenance_schema_version: 1` (implicit / unset); Phase 5 may bump this when vendor consolidation lands. - If Path C: explicit IOU recorded at `.planning/phases/01-foundations-and-doctrine/01-05-IOU.md` (NOT in STATE.md — STATE.md is orchestrator-owned).