Files
TheLastGarden/.planning/phases/01-foundations-and-doctrine/01-05-asset-provenance-PLAN.md
T
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

28 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01 05 execute 2
01-01
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
false
AEST-08
AEST-09
PIPE-03
truths artifacts key_links
`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
path provides
scripts/validate-assets.mjs 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 provides
scripts/validate-assets.test.ts 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 provides
assets/north-stars/README.md Explains the north-star reference set: what these images are, how to add new ones, the sidecar naming convention
path provides
assets/__samples__/refused/no-provenance.png Sample image with NO sidecar; validator must exclude this directory; existence proves the gate works (CONTEXT D-03)
path provides
assets/north-stars/*.png 1020 hand-curated reference images establishing the watercolor visual north-star (CONTEXT D-01)
from to via pattern
scripts/validate-assets.mjs assets/**/* node:fs/promises readdir + sibling sidecar lookup readdir.*assets|walk(ASSETS)
from to via pattern
scripts/validate-assets.mjs assets/__samples__/refused/ Hardcoded REFUSED exclusion list REFUSED|__samples__/refused
from to via pattern
scripts/validate-assets.test.ts scripts/validate-assets.mjs child_process.execFile against the script with ASSETS_DIR=<os.tmpdir()/...> + assert exit code 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 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 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 1020 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 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)
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:** <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").
Type "approved" or describe issues

<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>
- `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.

<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>
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).