---
phase: 05-hardening-and-docs
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
  - Cargo.toml
  - src/model.rs
  - fuzz/Cargo.toml
  - fuzz/fuzz_targets/fuzz_json_parse.rs
  - fuzz/fuzz_targets/fuzz_sql_expand.rs
  - fuzz/fuzz_targets/fuzz_query_names.rs
  - fuzz/corpus/fuzz_json_parse/seed_valid_minimal.json
  - fuzz/corpus/fuzz_json_parse/seed_valid_full.json
  - fuzz/corpus/fuzz_json_parse/seed_valid_joins.json
  - .github/workflows/NightlyFuzz.yml
  - .gitignore
  - Justfile
autonomous: true
requirements:
  - TEST-05

must_haves:
  truths:
    - "cargo fuzz list shows three fuzz targets (fuzz_json_parse, fuzz_sql_expand, fuzz_query_names)"
    - "cargo fuzz run fuzz_json_parse -- -max_total_time=10 completes without crashes on seed corpus"
    - "cargo fuzz run fuzz_sql_expand -- -max_total_time=10 completes without crashes"
    - "cargo fuzz run fuzz_query_names -- -max_total_time=10 completes without crashes"
    - "NightlyFuzz.yml workflow exists with scheduled cron trigger, 5min per target, crash issue creation, and corpus commit PR"
  artifacts:
    - path: "fuzz/Cargo.toml"
      provides: "Fuzz crate depending on semantic_views with arbitrary feature"
      contains: "cargo-fuzz = true"
    - path: "fuzz/fuzz_targets/fuzz_json_parse.rs"
      provides: "JSON definition parsing fuzz target"
      contains: "fuzz_target!"
    - path: "fuzz/fuzz_targets/fuzz_sql_expand.rs"
      provides: "SQL expansion fuzz target with Arbitrary-derived inputs"
      contains: "fuzz_target!"
    - path: "fuzz/fuzz_targets/fuzz_query_names.rs"
      provides: "Query-time name array fuzz target against fixed definition"
      contains: "fuzz_target!"
    - path: ".github/workflows/NightlyFuzz.yml"
      provides: "Nightly CI fuzzing with crash reporting and corpus PR"
      contains: "cargo fuzz run"
    - path: "src/model.rs"
      provides: "Arbitrary derive behind arbitrary feature flag on all model types"
      contains: "cfg_attr"
  key_links:
    - from: "fuzz/Cargo.toml"
      to: "Cargo.toml"
      via: "path dependency with arbitrary feature"
      pattern: 'features.*=.*\["arbitrary"\]'
    - from: "fuzz/fuzz_targets/fuzz_sql_expand.rs"
      to: "src/expand.rs"
      via: "calls expand() with Arbitrary-derived SemanticViewDefinition"
      pattern: "expand\\("
    - from: "fuzz/fuzz_targets/fuzz_json_parse.rs"
      to: "src/model.rs"
      via: "calls SemanticViewDefinition::from_json with arbitrary bytes"
      pattern: "from_json"
---

<objective>
Add three cargo-fuzz targets covering the FFI boundary (JSON parsing), SQL generation path (expand()), and query-time name injection (dimension/metric names against a fixed definition). Set up seed corpus, nightly CI workflow with crash reporting, and corpus auto-commit.

Purpose: Satisfy TEST-05 -- fuzz targets cover the unsafe C FFI boundary and the SQL generation path; no undefined behavior is triggered on malformed inputs.
Output: Three runnable fuzz targets, seed corpus, nightly CI workflow, Justfile recipes for fuzzing.
</objective>

<execution_context>
@/Users/paul/.claude/get-shit-done/workflows/execute-plan.md
@/Users/paul/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-hardening-and-docs/05-RESEARCH.md
@.planning/phases/05-hardening-and-docs/05-CONTEXT.md
@src/model.rs
@src/expand.rs
@Cargo.toml
@Justfile
@.gitignore
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add arbitrary feature flag and derive macros to model types, create fuzz crate with three targets and seed corpus</name>
  <files>
    Cargo.toml
    src/model.rs
    fuzz/Cargo.toml
    fuzz/fuzz_targets/fuzz_json_parse.rs
    fuzz/fuzz_targets/fuzz_sql_expand.rs
    fuzz/fuzz_targets/fuzz_query_names.rs
    fuzz/corpus/fuzz_json_parse/seed_valid_minimal.json
    fuzz/corpus/fuzz_json_parse/seed_valid_full.json
    fuzz/corpus/fuzz_json_parse/seed_valid_joins.json
    .gitignore
    Justfile
  </files>
  <action>
1. **Cargo.toml** -- Add `arbitrary` as an optional dependency and feature:
   - Add to `[features]`: `arbitrary = ["dep:arbitrary"]`
   - Add to `[dependencies]`: `arbitrary = { version = "1", optional = true, features = ["derive"] }`
   - Do NOT change existing features (default, extension) or other dependencies.

2. **src/model.rs** -- Add conditional Arbitrary derive to all model types:
   - Add `#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]` to: `Dimension`, `Metric`, `Join`, `SemanticViewDefinition`
   - Place this attribute AFTER existing derives (`Debug, Clone, Serialize, Deserialize`).
   - NOTE: `SemanticViewDefinition` has `#[serde(deny_unknown_fields)]` -- the `Arbitrary` derive is unaffected by this serde attribute, so no changes needed.

3. **fuzz/Cargo.toml** -- Create the fuzz crate (NOT part of the parent workspace):
   ```toml
   [package]
   name = "semantic-views-fuzz"
   version = "0.0.0"
   publish = false
   edition = "2021"

   [package.metadata]
   cargo-fuzz = true

   [dependencies]
   libfuzzer-sys = "0.4"
   arbitrary = { version = "1", features = ["derive"] }
   semantic_views = { path = "..", features = ["arbitrary"] }

   # Prevent this from being absorbed into the parent workspace
   [workspace]

   [[bin]]
   name = "fuzz_json_parse"
   path = "fuzz_targets/fuzz_json_parse.rs"
   doc = false

   [[bin]]
   name = "fuzz_sql_expand"
   path = "fuzz_targets/fuzz_sql_expand.rs"
   doc = false

   [[bin]]
   name = "fuzz_query_names"
   path = "fuzz_targets/fuzz_query_names.rs"
   doc = false
   ```

4. **fuzz/fuzz_targets/fuzz_json_parse.rs** -- Target 1: JSON definition parsing.
   Feed arbitrary byte strings to `SemanticViewDefinition::from_json()`. Only requires the input be valid UTF-8 (skip non-UTF8 gracefully). Must not panic on any input; errors from `from_json` are acceptable.

5. **fuzz/fuzz_targets/fuzz_sql_expand.rs** -- Target 2: SQL expansion.
   Use `#[derive(Arbitrary)]` on a `FuzzInput` struct containing a `SemanticViewDefinition` (via the arbitrary feature), plus `Vec<String>` for dim_names and metric_names. Call `expand("fuzz_view", &input.def, &req)`. If `Ok(sql)` is returned, assert: (a) sql is not empty, (b) sql starts with "WITH". Errors are expected and acceptable.

6. **fuzz/fuzz_targets/fuzz_query_names.rs** -- Target 3: Query-time name arrays.
   Define a `fixed_definition()` function returning a known-good `SemanticViewDefinition` (reuse the orders fixture: base_table "orders", dimensions region/month, metrics revenue/count, filter "status = 'active'", no joins). Fuzz only the dimension and metric name strings via `#[derive(Arbitrary)]` on `NameFuzzInput { dim_names: Vec<String>, metric_names: Vec<String> }`. Call `expand()` with the fixed definition and fuzzed names. Same assertions as Target 2 on success.

7. **Seed corpus** -- Create three seed files in `fuzz/corpus/fuzz_json_parse/`:
   - `seed_valid_minimal.json`: `{"base_table":"t","dimensions":[],"metrics":[]}`
   - `seed_valid_full.json`: full definition with dimensions, metrics, filters (reuse from model.rs tests)
   - `seed_valid_joins.json`: definition with joins and source_table fields

8. **.gitignore** -- Add `fuzz/artifacts/` to gitignore (crash artifacts are debug data, not to be committed). Do NOT gitignore `fuzz/corpus/` -- corpus IS committed.

9. **Justfile** -- Add three new recipes:
   - `fuzz target="fuzz_json_parse"`: runs `cargo fuzz run {{target}} -- -max_total_time=300` (5 min default)
   - `fuzz-all`: runs all three targets sequentially with 5 min each
   - `fuzz-cmin target="fuzz_json_parse"`: runs `cargo fuzz cmin {{target}}` for corpus minimization

IMPORTANT: The fuzz crate must depend on the default feature of semantic_views (which includes duckdb/bundled), NOT the extension feature. Fuzz targets exercise pure Rust logic (model parsing, expand()), not the DuckDB loadable-extension stubs. See 05-RESEARCH.md Pitfall 2.

Verify `cargo test` still passes after model.rs changes (the `#[cfg_attr]` attribute is conditional and should not affect non-arbitrary builds).
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test --lib -- model::tests 2>&amp;1 | tail -5</automated>
    <manual>Verify fuzz/Cargo.toml has [workspace] key to prevent parent workspace absorption. Verify .gitignore has fuzz/artifacts/ entry.</manual>
    <sampling_rate>run after this task commits, before next task begins</sampling_rate>
  </verify>
  <done>
    - Cargo.toml has `arbitrary` optional dependency and feature
    - All four model types (Dimension, Metric, Join, SemanticViewDefinition) have conditional Arbitrary derive
    - fuzz/Cargo.toml exists as independent workspace with three [[bin]] targets
    - Three fuzz target source files exist with correct harness logic
    - Three seed corpus files exist in fuzz/corpus/fuzz_json_parse/
    - .gitignore includes fuzz/artifacts/
    - Justfile has fuzz, fuzz-all, and fuzz-cmin recipes
    - cargo test still passes (arbitrary feature not activated by default)
  </done>
</task>

<task type="auto">
  <name>Task 2: Create nightly fuzz CI workflow with crash reporting and corpus auto-commit PR</name>
  <files>
    .github/workflows/NightlyFuzz.yml
  </files>
  <action>
Create `.github/workflows/NightlyFuzz.yml` following the pattern from 05-RESEARCH.md Pattern 6, with these specifics:

1. **Trigger**: `schedule` cron `0 3 * * *` (daily 03:00 UTC) + `workflow_dispatch` for manual testing.

2. **Permissions**: `contents: write`, `issues: write`, `pull-requests: write`.

3. **Environment variable**: `FUZZ_TIME: 300` (5 minutes per target).

4. **Job: fuzz** -- Matrix strategy with `fail-fast: false` across three targets: `[fuzz_json_parse, fuzz_sql_expand, fuzz_query_names]`.
   Steps:
   - `actions/checkout@v4`
   - `dtolnay/rust-toolchain@nightly`
   - `cargo install cargo-fuzz`
   - Run fuzzer: `cargo fuzz run ${{ matrix.target }} -- -max_total_time=${{ env.FUZZ_TIME }}`
     - Use `id: fuzz` and `continue-on-error: true`
   - Upload crash artifacts on failure: `actions/upload-artifact@v4` with path `fuzz/artifacts/${{ matrix.target }}/`
   - Open GitHub issue on failure: `actions/github-script@v7` creating issue with title `Fuzz crash: {target}`, body with run link and reproduction steps, labels `['bug', 'fuzzing']`

5. **Job: commit-corpus** -- Runs AFTER all fuzz jobs complete (`needs: [fuzz]`). This avoids the race condition where parallel matrix jobs try to push simultaneously (see 05-RESEARCH.md Pitfall 6).
   Steps:
   - `actions/checkout@v4`
   - `dtolnay/rust-toolchain@nightly`
   - `cargo install cargo-fuzz`
   - Re-run each target briefly (10 seconds) to ensure corpus directory is populated with the latest format: `for target in fuzz_json_parse fuzz_sql_expand fuzz_query_names; do cargo fuzz run $target -- -max_total_time=10 || true; done`
   - Check for corpus changes: `git add fuzz/corpus/ && git diff --cached --quiet && echo "No corpus changes" && exit 0`
   - If changes exist, create a PR using `peter-evans/create-pull-request@v7`:
     - branch: `chore/fuzz-corpus-update`
     - title: `chore(fuzz): update corpus`
     - body: "Nightly fuzzing discovered new coverage-increasing inputs."
     - commit-message: `chore(fuzz): update corpus from nightly run`
     - delete-branch: true

Note: Use `peter-evans/create-pull-request@v7` for corpus PR creation -- this is the same action already used by the DuckDB version monitor workflow (see `.github/workflows/DuckDBVersionMonitor.yml`), maintaining consistency.
  </action>
  <verify>
    <automated>test -f /Users/paul/Documents/Dev/Personal/duckdb-semantic-views/.github/workflows/NightlyFuzz.yml && grep -q "cargo fuzz run" /Users/paul/Documents/Dev/Personal/duckdb-semantic-views/.github/workflows/NightlyFuzz.yml && grep -q "commit-corpus" /Users/paul/Documents/Dev/Personal/duckdb-semantic-views/.github/workflows/NightlyFuzz.yml && echo "PASS" || echo "FAIL"</automated>
    <manual>Review workflow has: cron trigger, 3-target matrix, crash artifact upload, issue creation on crash, separate commit-corpus job with PR creation</manual>
    <sampling_rate>run after this task commits, before next task begins</sampling_rate>
  </verify>
  <done>
    - NightlyFuzz.yml exists with daily cron trigger and workflow_dispatch
    - Fuzz job runs three targets in parallel with 5min timeout each
    - Crash artifacts uploaded on failure
    - GitHub issue opened on crash with target name, run link, and reproduction command
    - Separate commit-corpus job avoids race condition
    - Corpus updates submitted as PR (not direct push) using peter-evans/create-pull-request
  </done>
</task>

</tasks>

<verification>
1. `cargo test` passes (model.rs changes are backward-compatible)
2. `cargo fuzz list` shows three targets: fuzz_json_parse, fuzz_sql_expand, fuzz_query_names
3. `cargo fuzz run fuzz_json_parse -- -max_total_time=10` completes without UB (some crashes from panics may be found -- these should be fixed)
4. `cargo fuzz run fuzz_sql_expand -- -max_total_time=10` completes without UB
5. `cargo fuzz run fuzz_query_names -- -max_total_time=10` completes without UB
6. NightlyFuzz.yml passes `actionlint` or manual inspection
7. Justfile `fuzz`, `fuzz-all`, `fuzz-cmin` recipes are present
</verification>

<success_criteria>
- Three fuzz targets are runnable via `cargo fuzz run <target>`
- Seed corpus provides effective starting points for the JSON parsing target
- Nightly CI workflow automates fuzzing with crash reporting and corpus maintenance
- No panics discovered in initial fuzz runs (or any found are fixed before completing)
- TEST-05 requirement is satisfied
</success_criteria>

<output>
After completion, create `.planning/phases/05-hardening-and-docs/05-01-SUMMARY.md`
</output>
