Files
josh 39563f6934 docs(01): plan phase 1 — 7 plans across 3 waves, verified after 1 revision
Wave 1: Plan 01 (scaffold + test infra)
Wave 2: Plans 02 (eslint firewall), 03 (save layer), 04 (content pipeline),
        05 (asset provenance — autonomous:false human-curate checkpoint),
        06 (doctrine docs)
Wave 3: Plan 07 (CI workflow)

All 16 Phase-1 REQ-IDs covered. Plan-checker found 4 blockers + 6 warnings
on first pass; revision iteration 1 landed all 10 fixes; iteration 2
returned VERIFICATION PASSED. Two orchestrator judgment calls during
revision: (1) implement CORE-04 localStorage fallback in Phase 1 (the
literal requirement and ROADMAP success criterion #2 both call for it),
(2) reclassify STRY-09 as vacuously satisfied in Phase 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:09:08 -04:00

412 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 `<filename>.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)"
- "1020 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 <filename>.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: "1020 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=<os.tmpdir()/...> + assert exit code"
pattern: "spawn\\(.*node.*validate-assets|execFile|os\\.tmpdir"
---
<objective>
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 `<filename>.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 1020 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 1020 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, ~1020 north-star images with provenance sidecars, and a Vitest test enforcing the gate.
This plan is `autonomous: false` because Task 2 — committing the 1020 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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Provenance validator script + Zod sidecar schema + Vitest enforcement test</name>
<files>
scripts/validate-assets.mjs,
scripts/validate-assets.test.ts,
assets/__samples__/refused/no-provenance.png,
assets/__samples__/refused/.gitkeep
</files>
<read_first>
- .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)
</read_first>
<behavior>
- **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 `<filename>.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=<that tmpdir>` 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.
</behavior>
<action>
**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 <filename>.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)`.**
</action>
<verify>
<automated>node scripts/validate-assets.mjs &amp;&amp; npx vitest run scripts/validate-assets.test.ts</automated>
</verify>
<acceptance_criteria>
- `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`.
</acceptance_criteria>
<done>
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.
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Curate and commit 1020 north-star reference images with provenance sidecars (CONTEXT D-01)</name>
<what-built>
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 1020 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 1020 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 `<filename>.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/<descriptive-slug>.png` + `assets/north-stars/<descriptive-slug>.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:<photographer>"`)
- `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 "1020 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)
</what-built>
<how-to-verify>
1. Choose a path (A, B, or C) and produce the images + sidecars.
2. Place all pairs under `assets/north-stars/`. Naming: `<slug>.png` + `<slug>.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 <N> 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 <N> north-star reference images with provenance sidecars (path <A|B|C>)`.
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:** <YYYY-MM-DD>
**Owner:** <your name or handle>
**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 <N> placeholder images (target: 1020).
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
- 1020 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").
</how-to-verify>
<resume-signal>Type "approved" or describe issues</resume-signal>
</task>
</tasks>
<threat_model>
| 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. |
</threat_model>
<verification>
- `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.
</verification>
<success_criteria>
- 30-line standalone Node script validates every non-sidecar asset has a sibling `<filename>.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.
- 1020 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.
</success_criteria>
<output>
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 <N> 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).
</output>
</output>