---
phase: 51-yaml-parser-core
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - Cargo.toml
  - src/model.rs
  - fuzz/Cargo.toml
  - fuzz/fuzz_targets/fuzz_yaml_parse.rs
autonomous: true
requirements:
  - YAML-03
  - YAML-05
  - YAML-09

must_haves:
  truths:
    - "A minimal YAML string with base_table, dimensions, and metrics deserializes into a SemanticViewDefinition identical to the equivalent JSON"
    - "A full YAML string with all fields (tables, joins, facts, dimensions, metrics, metadata annotations) deserializes into a SemanticViewDefinition identical to the equivalent JSON"
    - "A YAML string exceeding 1MB is rejected before parsing begins, with an error including actual size and cap"
    - "A YAML string at exactly 1MB is accepted and parsed normally"
    - "Arbitrary YAML input never panics from_yaml (fuzz target compiles)"
  artifacts:
    - path: "Cargo.toml"
      provides: "yaml_serde dependency"
      contains: "yaml_serde"
    - path: "src/model.rs"
      provides: "from_yaml and from_yaml_with_size_cap functions"
      exports: ["from_yaml", "from_yaml_with_size_cap", "YAML_SIZE_CAP"]
    - path: "src/model.rs"
      provides: "PartialEq derive on all model structs"
      contains: "PartialEq"
    - path: "fuzz/fuzz_targets/fuzz_yaml_parse.rs"
      provides: "YAML fuzz target"
      contains: "from_yaml"
    - path: "fuzz/Cargo.toml"
      provides: "fuzz_yaml_parse binary entry"
      contains: "fuzz_yaml_parse"
  key_links:
    - from: "src/model.rs"
      to: "yaml_serde"
      via: "yaml_serde::from_str in from_yaml"
      pattern: "yaml_serde::from_str"
    - from: "src/model.rs"
      to: "src/model.rs"
      via: "from_yaml_with_size_cap calls from_yaml"
      pattern: "Self::from_yaml"
    - from: "fuzz/fuzz_targets/fuzz_yaml_parse.rs"
      to: "src/model.rs"
      via: "calls SemanticViewDefinition::from_yaml"
      pattern: "from_yaml"
---

<objective>
Add YAML deserialization to SemanticViewDefinition, proving equivalence with the existing JSON path.

Purpose: Enable YAML as a second definition format (alongside SQL DDL/JSON) for semantic views. This is the library-level foundation that Phase 52 (DDL integration) will wire into the CREATE SEMANTIC VIEW pipeline.

Output: `from_yaml` and `from_yaml_with_size_cap` methods on `SemanticViewDefinition`, PartialEq derives for equivalence assertions, comprehensive unit + proptest + fuzz coverage proving YAML/JSON produce identical structs.
</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/51-yaml-parser-core/51-RESEARCH.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
<!-- Executor should use these directly -- no codebase exploration needed. -->

From src/model.rs (line 340-425):
```rust
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct SemanticViewDefinition {
    pub base_table: String,
    #[serde(default)]
    pub tables: Vec<TableRef>,
    pub dimensions: Vec<Dimension>,
    pub metrics: Vec<Metric>,
    #[serde(default)]
    pub joins: Vec<Join>,
    #[serde(default)]
    pub facts: Vec<Fact>,
    #[serde(default)]
    pub column_type_names: Vec<String>,
    #[serde(default)]
    pub column_types_inferred: Vec<u32>,
    #[serde(default)]
    pub created_on: Option<String>,
    #[serde(default)]
    pub database_name: Option<String>,
    #[serde(default)]
    pub schema_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,
}

impl SemanticViewDefinition {
    pub fn from_json(name: &str, json: &str) -> Result<Self, String> {
        let def: Self = serde_json::from_str(json)
            .map_err(|e| format!("invalid definition for semantic view '{name}': {e}"))?;
        Ok(def)
    }
}
```

From Cargo.toml (line 34-35):
```toml
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```

Structs that need PartialEq added (currently have `Debug, Clone, Default, Serialize, Deserialize` but NOT PartialEq):
- `TableRef` (line 5)
- `Dimension` (line 32)
- `NonAdditiveDim` (line 100)
- `WindowSpec` (line 112)
- `WindowOrderBy` (line 137)
- `Metric` (line 148)
- `Fact` (line 207)
- `JoinColumn` (line 237)
- `Join` (line 290)
- `SemanticViewDefinition` (line 340)

Enums that ALREADY have PartialEq:
- `SortOrder` (line 59)
- `NullsOrder` (line 79)
- `Cardinality` (line 250)
- `AccessModifier` (line 271)
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add yaml_serde dependency, PartialEq derives, from_yaml functions, and fuzz target</name>
  <files>Cargo.toml, src/model.rs, fuzz/Cargo.toml, fuzz/fuzz_targets/fuzz_yaml_parse.rs</files>
  <read_first>
    - Cargo.toml (current dependencies, line 31-37)
    - src/model.rs (full file -- need derive lines for all 10 structs plus from_json pattern at lines 414-425)
    - fuzz/Cargo.toml (existing fuzz entries)
    - fuzz/fuzz_targets/fuzz_json_parse.rs (pattern to mirror)
  </read_first>
  <behavior>
    - Test 1: `SemanticViewDefinition::from_yaml("test", "base_table: orders\ndimensions:\n  - name: region\n    expr: region\nmetrics:\n  - name: revenue\n    expr: SUM(amount)")` returns Ok with base_table == "orders", 1 dimension, 1 metric
    - Test 2: `SemanticViewDefinition::from_yaml("test", "not: valid: yaml: {{")` returns Err containing "invalid YAML definition for semantic view 'test'"
    - Test 3: `SemanticViewDefinition::from_yaml_with_size_cap("test", &"x".repeat(1_048_577))` returns Err containing "exceeds size limit" and "1048577 bytes" and "1048576 byte cap"
    - Test 4: `SemanticViewDefinition::from_yaml_with_size_cap("test", &valid_yaml_at_exactly_1mb)` returns Ok (boundary accepted)
  </behavior>
  <action>
1. **Cargo.toml** -- Add `yaml_serde = "0.10"` as an unconditional dependency in `[dependencies]` (line 36, after serde_json). This is the user-selected library (yaml/yaml-serde on GitHub, MIT OR Apache-2.0, no RUSTSEC advisories). Do NOT feature-gate it -- it must be available under both default (bundled) and extension features, same as serde_json.

2. **src/model.rs** -- Add `PartialEq` to the derive macro on these 10 structs (all field types are PartialEq-safe -- String, Vec, Option, u32, bool, enums that already derive PartialEq):
   - `TableRef` (line 5): change `#[derive(Debug, Clone, Default, Serialize, Deserialize)]` to `#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]`
   - `Dimension` (line 32): same pattern
   - `NonAdditiveDim` (line 100): same pattern
   - `WindowSpec` (line 112): same pattern
   - `WindowOrderBy` (line 137): same pattern
   - `Metric` (line 148): same pattern
   - `Fact` (line 207): same pattern
   - `JoinColumn` (line 237): same pattern
   - `Join` (line 290): same pattern
   - `SemanticViewDefinition` (line 340): same pattern

3. **src/model.rs** -- Add a new `impl SemanticViewDefinition` block (after the existing `from_json` impl block ending at line 425) with:
   ```rust
   impl SemanticViewDefinition {
       /// Maximum YAML input size (1 MiB). Sanity guard against oversized input.
       /// This is NOT a security boundary -- creating semantic views is a
       /// privileged operation guarded by warehouse auth. See trust assumption docs.
       pub const YAML_SIZE_CAP: usize = 1_048_576;

       /// Parse a YAML string into a typed semantic view definition.
       ///
       /// Returns an error if the YAML is syntactically invalid or missing
       /// required fields. The `name` parameter appears in the error message.
       pub fn from_yaml(name: &str, yaml: &str) -> Result<Self, String> {
           let def: Self = yaml_serde::from_str(yaml)
               .map_err(|e| format!("invalid YAML definition for semantic view '{name}': {e}"))?;
           Ok(def)
       }

       /// Parse YAML with a size cap check.
       ///
       /// Rejects input exceeding [`YAML_SIZE_CAP`] (1 MiB) before parsing.
       /// Returns an error including the actual size and the cap.
       pub fn from_yaml_with_size_cap(name: &str, yaml: &str) -> Result<Self, String> {
           if yaml.len() > Self::YAML_SIZE_CAP {
               return Err(format!(
                   "YAML definition for semantic view '{name}' exceeds size limit \
                    ({} bytes > {} byte cap)",
                   yaml.len(),
                   Self::YAML_SIZE_CAP,
               ));
           }
           Self::from_yaml(name, yaml)
       }
   }
   ```

4. **fuzz/fuzz_targets/fuzz_yaml_parse.rs** -- Create new file mirroring fuzz_json_parse.rs:
   ```rust
   #![no_main]
   use libfuzzer_sys::fuzz_target;

   fuzz_target!(|data: &[u8]| {
       if let Ok(s) = std::str::from_utf8(data) {
           // Must not panic regardless of input.
           // Errors are fine -- panics/UB are not.
           let _ = semantic_views::model::SemanticViewDefinition::from_yaml("fuzz_test", s);
       }
   });
   ```

5. **fuzz/Cargo.toml** -- Add a new `[[bin]]` entry at the end:
   ```toml
   [[bin]]
   name = "fuzz_yaml_parse"
   path = "fuzz_targets/fuzz_yaml_parse.rs"
   doc = false
   ```
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test model::tests::valid_definition_roundtrips -- --nocapture 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - Cargo.toml contains `yaml_serde = "0.10"` in [dependencies] section
    - src/model.rs contains `pub const YAML_SIZE_CAP: usize = 1_048_576`
    - src/model.rs contains `pub fn from_yaml(name: &str, yaml: &str) -> Result<Self, String>`
    - src/model.rs contains `pub fn from_yaml_with_size_cap(name: &str, yaml: &str) -> Result<Self, String>`
    - src/model.rs contains `yaml_serde::from_str(yaml)` inside from_yaml
    - src/model.rs contains `Self::from_yaml(name, yaml)` inside from_yaml_with_size_cap
    - src/model.rs from_yaml error format contains `invalid YAML definition for semantic view`
    - src/model.rs from_yaml_with_size_cap error format contains `exceeds size limit`
    - All 10 struct derives include PartialEq: TableRef, Dimension, NonAdditiveDim, WindowSpec, WindowOrderBy, Metric, Fact, JoinColumn, Join, SemanticViewDefinition
    - fuzz/fuzz_targets/fuzz_yaml_parse.rs exists and contains `from_yaml("fuzz_test", s)`
    - fuzz/Cargo.toml contains `name = "fuzz_yaml_parse"`
    - `cargo test` exits 0 (existing tests still pass with PartialEq added)
  </acceptance_criteria>
  <done>yaml_serde dependency added, PartialEq derives on all 10 model structs, from_yaml and from_yaml_with_size_cap implemented on SemanticViewDefinition, fuzz target created, all existing tests pass</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Comprehensive YAML test suite (unit tests + proptest)</name>
  <files>src/model.rs, tests/yaml_proptest.rs</files>
  <read_first>
    - src/model.rs (full file -- need the from_yaml functions from Task 1, existing test module starting at line 427, and all struct definitions for constructing test fixtures)
    - tests/expand_proptest.rs (proptest pattern to follow -- imports, proptest! macro usage, arbitrary feature)
    - .planning/phases/51-yaml-parser-core/51-RESEARCH.md (full YAML examples in Code Examples section, field naming notes, pitfall documentation)
  </read_first>
  <behavior>
    - Test: Minimal YAML (base_table + 1 dim + 1 metric) deserializes correctly
    - Test: Full YAML with all fields (tables with pk_columns/unique_constraints/comment/synonyms, joins with fk_columns/ref_columns/name/cardinality, facts with comment/synonyms/access, dimensions with source_table/output_type/comment/synonyms, metrics with using_relationships/comment/synonyms/access/non_additive_by/window_spec, view-level comment) deserializes correctly
    - Test: YAML with omitted optional fields defaults correctly (empty vecs, None options)
    - Test: Enum variants roundtrip correctly in YAML (AccessModifier::Private, SortOrder::Desc, NullsOrder::First, Cardinality::OneToOne)
    - Test: YAML and JSON produce identical SemanticViewDefinition for same logical input
    - Test: Invalid YAML syntax returns error with view name in message
    - Test: Missing required field (base_table) returns error
    - Test: Size cap boundary: 1MB exactly accepted, 1MB+1 rejected
    - Test: Size cap error message contains actual byte count and cap value
    - Test: Unknown fields in YAML are accepted (no deny_unknown_fields)
    - Proptest: For any arbitrary SemanticViewDefinition, serialize to JSON and YAML, deserialize both, assert structural equality
  </behavior>
  <action>
1. **src/model.rs** -- Add YAML-specific tests inside the existing `#[cfg(test)] mod tests` block. Add a new submodule `mod yaml_tests` inside `mod tests`. The tests go AFTER the existing test functions (the existing tests end around line 1301). Add these test functions:

```rust
mod yaml_tests {
    use super::*;

    #[test]
    fn minimal_yaml_deserializes() {
        let yaml = "base_table: orders\ndimensions:\n  - name: region\n    expr: region\nmetrics:\n  - name: revenue\n    expr: SUM(amount)\n";
        let def = SemanticViewDefinition::from_yaml("orders", yaml).unwrap();
        assert_eq!(def.base_table, "orders");
        assert_eq!(def.dimensions.len(), 1);
        assert_eq!(def.dimensions[0].name, "region");
        assert_eq!(def.dimensions[0].expr, "region");
        assert_eq!(def.metrics.len(), 1);
        assert_eq!(def.metrics[0].name, "revenue");
        assert_eq!(def.metrics[0].expr, "SUM(amount)");
    }

    #[test]
    fn full_yaml_all_fields() {
        let yaml = r#"
base_table: orders
tables:
  - alias: o
    table: orders
    pk_columns:
      - id
    unique_constraints:
      - - email
      - - first_name
        - last_name
    comment: Main orders table
    synonyms:
      - order_facts
  - alias: c
    table: customers
    pk_columns:
      - id
joins:
  - table: c
    from_alias: o
    fk_columns:
      - customer_id
    ref_columns:
      - id
    name: order_to_customer
    cardinality: ManyToOne
facts:
  - name: unit_price
    expr: "o.price / o.qty"
    source_table: o
    output_type: DOUBLE
    comment: Price per unit
    synonyms:
      - price_per_item
    access: Private
dimensions:
  - name: region
    expr: o.region
    source_table: o
    output_type: VARCHAR
    comment: Geographic region
    synonyms:
      - area
      - territory
metrics:
  - name: revenue
    expr: SUM(o.amount)
    source_table: o
    output_type: "DECIMAL(18,2)"
    comment: Total revenue
    synonyms:
      - total_revenue
    access: Public
    using_relationships:
      - order_to_customer
  - name: balance
    expr: SUM(o.amount)
    source_table: o
    non_additive_by:
      - dimension: date_dim
        order: Desc
        nulls: First
  - name: avg_qty_7d
    expr: AVG(total_qty)
    window_spec:
      window_function: AVG
      inner_metric: total_qty
      excluding_dims:
        - date_dim
      order_by:
        - expr: date_dim
          order: Asc
          nulls: Last
      frame_clause: "RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW"
comment: Revenue analytics view
"#;
        let def = SemanticViewDefinition::from_yaml("orders", yaml).unwrap();
        // Tables
        assert_eq!(def.tables.len(), 2);
        assert_eq!(def.tables[0].alias, "o");
        assert_eq!(def.tables[0].table, "orders");
        assert_eq!(def.tables[0].pk_columns, vec!["id"]);
        assert_eq!(def.tables[0].unique_constraints.len(), 2);
        assert_eq!(def.tables[0].comment.as_deref(), Some("Main orders table"));
        assert_eq!(def.tables[0].synonyms, vec!["order_facts"]);
        // Joins
        assert_eq!(def.joins.len(), 1);
        assert_eq!(def.joins[0].table, "c");
        assert_eq!(def.joins[0].from_alias, "o");
        assert_eq!(def.joins[0].fk_columns, vec!["customer_id"]);
        assert_eq!(def.joins[0].ref_columns, vec!["id"]);
        assert_eq!(def.joins[0].name.as_deref(), Some("order_to_customer"));
        assert_eq!(def.joins[0].cardinality, Cardinality::ManyToOne);
        // Facts
        assert_eq!(def.facts.len(), 1);
        assert_eq!(def.facts[0].name, "unit_price");
        assert_eq!(def.facts[0].access, AccessModifier::Private);
        assert_eq!(def.facts[0].comment.as_deref(), Some("Price per unit"));
        assert_eq!(def.facts[0].synonyms, vec!["price_per_item"]);
        // Dimensions
        assert_eq!(def.dimensions.len(), 1);
        assert_eq!(def.dimensions[0].source_table.as_deref(), Some("o"));
        assert_eq!(def.dimensions[0].output_type.as_deref(), Some("VARCHAR"));
        assert_eq!(def.dimensions[0].comment.as_deref(), Some("Geographic region"));
        assert_eq!(def.dimensions[0].synonyms, vec!["area", "territory"]);
        // Metrics
        assert_eq!(def.metrics.len(), 3);
        assert_eq!(def.metrics[0].access, AccessModifier::Public);
        assert_eq!(def.metrics[0].using_relationships, vec!["order_to_customer"]);
        assert_eq!(def.metrics[0].comment.as_deref(), Some("Total revenue"));
        assert_eq!(def.metrics[0].synonyms, vec!["total_revenue"]);
        // Semi-additive metric
        assert_eq!(def.metrics[1].non_additive_by.len(), 1);
        assert_eq!(def.metrics[1].non_additive_by[0].dimension, "date_dim");
        assert_eq!(def.metrics[1].non_additive_by[0].order, SortOrder::Desc);
        assert_eq!(def.metrics[1].non_additive_by[0].nulls, NullsOrder::First);
        // Window metric
        let ws = def.metrics[2].window_spec.as_ref().unwrap();
        assert_eq!(ws.window_function, "AVG");
        assert_eq!(ws.inner_metric, "total_qty");
        assert_eq!(ws.excluding_dims, vec!["date_dim"]);
        assert_eq!(ws.order_by.len(), 1);
        assert_eq!(ws.order_by[0].expr, "date_dim");
        assert_eq!(ws.order_by[0].order, SortOrder::Asc);
        assert_eq!(ws.order_by[0].nulls, NullsOrder::Last);
        assert!(ws.frame_clause.as_deref().unwrap().contains("RANGE BETWEEN"));
        // View-level comment
        assert_eq!(def.comment.as_deref(), Some("Revenue analytics view"));
    }

    #[test]
    fn optional_fields_default_when_omitted() {
        let yaml = "base_table: t\ndimensions: []\nmetrics: []\n";
        let def = SemanticViewDefinition::from_yaml("test", yaml).unwrap();
        assert!(def.tables.is_empty());
        assert!(def.joins.is_empty());
        assert!(def.facts.is_empty());
        assert!(def.column_type_names.is_empty());
        assert!(def.column_types_inferred.is_empty());
        assert!(def.created_on.is_none());
        assert!(def.database_name.is_none());
        assert!(def.schema_name.is_none());
        assert!(def.comment.is_none());
    }

    #[test]
    fn enum_variants_roundtrip_yaml() {
        // AccessModifier::Private
        let yaml = "base_table: t\ndimensions: []\nmetrics:\n  - name: m\n    expr: SUM(x)\n    access: Private\n";
        let def = SemanticViewDefinition::from_yaml("test", yaml).unwrap();
        assert_eq!(def.metrics[0].access, AccessModifier::Private);

        // Cardinality::OneToOne
        let yaml2 = "base_table: t\ndimensions: []\nmetrics: []\njoins:\n  - table: c\n    cardinality: OneToOne\n";
        let def2 = SemanticViewDefinition::from_yaml("test", yaml2).unwrap();
        assert_eq!(def2.joins[0].cardinality, Cardinality::OneToOne);
    }

    #[test]
    fn yaml_json_produce_identical_structs() {
        let yaml = "base_table: orders\ndimensions:\n  - name: region\n    expr: region\nmetrics:\n  - name: revenue\n    expr: SUM(amount)\n";
        let json = r#"{"base_table":"orders","dimensions":[{"name":"region","expr":"region"}],"metrics":[{"name":"revenue","expr":"SUM(amount)"}]}"#;
        let from_yaml = SemanticViewDefinition::from_yaml("test", yaml).unwrap();
        let from_json = SemanticViewDefinition::from_json("test", json).unwrap();
        assert_eq!(from_yaml, from_json);
    }

    #[test]
    fn invalid_yaml_syntax_returns_error_with_name() {
        let result = SemanticViewDefinition::from_yaml("my_view", "{{invalid yaml");
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.contains("my_view"), "error should contain view name: {err}");
        assert!(err.contains("invalid YAML definition"), "error should contain prefix: {err}");
    }

    #[test]
    fn missing_required_field_is_error() {
        let yaml = "dimensions: []\nmetrics: []\n";
        // base_table is a required String field -- empty string is default for String,
        // so omitting it in YAML still deserializes (String defaults to "").
        // This matches JSON behavior: {"dimensions":[],"metrics":[]} also succeeds
        // with base_table = "".
        let result = SemanticViewDefinition::from_yaml("test", yaml);
        // Should succeed (same as JSON path -- base_table defaults to empty string)
        assert!(result.is_ok());
        assert_eq!(result.unwrap().base_table, "");
    }

    #[test]
    fn size_cap_rejects_oversized_input() {
        let oversized = "a".repeat(1_048_577); // 1 byte over cap
        let result = SemanticViewDefinition::from_yaml_with_size_cap("big", &oversized);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.contains("exceeds size limit"), "error: {err}");
        assert!(err.contains("1048577 bytes"), "should contain actual size: {err}");
        assert!(err.contains("1048576 byte cap"), "should contain cap: {err}");
    }

    #[test]
    fn size_cap_accepts_exactly_1mb() {
        // Build a valid YAML string padded to exactly 1MB
        let prefix = "base_table: t\ndimensions: []\nmetrics: []\n# ";
        let pad_len = SemanticViewDefinition::YAML_SIZE_CAP - prefix.len();
        let padded = format!("{prefix}{}", "x".repeat(pad_len));
        assert_eq!(padded.len(), SemanticViewDefinition::YAML_SIZE_CAP);
        let result = SemanticViewDefinition::from_yaml_with_size_cap("test", &padded);
        assert!(result.is_ok(), "exactly 1MB should be accepted: {}", result.unwrap_err());
    }

    #[test]
    fn unknown_fields_accepted_in_yaml() {
        let yaml = "base_table: t\ndimensions: []\nmetrics: []\nextra_field: surprise\n";
        assert!(
            SemanticViewDefinition::from_yaml("test", yaml).is_ok(),
            "unknown fields must not cause rejection (no deny_unknown_fields)"
        );
    }

    #[test]
    fn yaml_json_roundtrip_via_serialize() {
        // Build a struct, serialize to both formats, deserialize both, assert equal
        let def = SemanticViewDefinition {
            base_table: "orders".to_string(),
            dimensions: vec![Dimension {
                name: "region".to_string(),
                expr: "region".to_string(),
                ..Default::default()
            }],
            metrics: vec![Metric {
                name: "revenue".to_string(),
                expr: "SUM(amount)".to_string(),
                access: AccessModifier::Private,
                ..Default::default()
            }],
            facts: vec![Fact {
                name: "unit_price".to_string(),
                expr: "price / qty".to_string(),
                access: AccessModifier::Private,
                ..Default::default()
            }],
            comment: Some("test view".to_string()),
            ..Default::default()
        };
        let json_str = serde_json::to_string(&def).unwrap();
        let yaml_str = yaml_serde::to_string(&def).unwrap();

        let from_json = SemanticViewDefinition::from_json("test", &json_str).unwrap();
        let from_yaml = SemanticViewDefinition::from_yaml("test", &yaml_str).unwrap();
        assert_eq!(from_json, from_yaml);
    }
}
```

2. **tests/yaml_proptest.rs** -- Create a new integration-level proptest file:
```rust
use proptest::prelude::*;
use semantic_views::model::SemanticViewDefinition;

proptest! {
    #![proptest_config(ProptestConfig::with_cases(256))]

    #[test]
    fn yaml_json_roundtrip_equivalence(def in any::<SemanticViewDefinition>()) {
        // Serialize the arbitrary struct to both JSON and YAML
        let json_str = serde_json::to_string(&def).expect("JSON serialize");
        let yaml_str = yaml_serde::to_string(&def).expect("YAML serialize");

        // Deserialize both
        let from_json = SemanticViewDefinition::from_json("proptest", &json_str)
            .expect("JSON deserialize");
        let from_yaml = SemanticViewDefinition::from_yaml("proptest", &yaml_str)
            .expect("YAML deserialize");

        // Assert structural equality
        prop_assert_eq!(from_json, from_yaml);
    }
}
```

Note: The proptest file requires `yaml_serde` in dev-dependencies for the test binary. Add `yaml_serde = "0.10"` to `[dev-dependencies]` in Cargo.toml if `cargo test` for the integration test complains. However, since yaml_serde is already in `[dependencies]`, it should be available to test code without a separate dev-dependency entry.
  </action>
  <verify>
    <automated>cd /Users/paul/Documents/Dev/Personal/duckdb-semantic-views && cargo test yaml -- --nocapture 2>&1 | tail -20 && cargo test yaml_proptest -- --nocapture 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - src/model.rs contains `mod yaml_tests` inside `mod tests`
    - src/model.rs yaml_tests contains test function `minimal_yaml_deserializes`
    - src/model.rs yaml_tests contains test function `full_yaml_all_fields`
    - src/model.rs yaml_tests contains test function `optional_fields_default_when_omitted`
    - src/model.rs yaml_tests contains test function `enum_variants_roundtrip_yaml`
    - src/model.rs yaml_tests contains test function `yaml_json_produce_identical_structs`
    - src/model.rs yaml_tests contains test function `invalid_yaml_syntax_returns_error_with_name`
    - src/model.rs yaml_tests contains test function `size_cap_rejects_oversized_input`
    - src/model.rs yaml_tests contains test function `size_cap_accepts_exactly_1mb`
    - src/model.rs yaml_tests contains test function `unknown_fields_accepted_in_yaml`
    - src/model.rs yaml_tests contains test function `yaml_json_roundtrip_via_serialize`
    - tests/yaml_proptest.rs exists and contains `fn yaml_json_roundtrip_equivalence`
    - tests/yaml_proptest.rs contains `prop_assert_eq!(from_json, from_yaml)`
    - `cargo test yaml` exits 0 with all yaml_tests passing
    - `cargo test yaml_proptest` exits 0
    - `just test-all` exits 0 (full quality gate)
  </acceptance_criteria>
  <done>All 11 unit tests pass (minimal, full, defaults, enums, equivalence, error cases, size cap boundary, unknown fields, serialize roundtrip), proptest passes 256 cases proving YAML/JSON structural equivalence for arbitrary SemanticViewDefinition inputs, `just test-all` green</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| YAML input -> from_yaml | YAML strings are provided by privileged warehouse administrators. This is NOT an untrusted input boundary. Creating a semantic view is a DDL operation that requires database write access. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-51-01 | Denial of Service | from_yaml (anchor/alias bomb) | accept | Per user decision #2: anchor/alias bomb protection is NOT a priority. YAML input comes from privileged warehouse administrators only. The 1MB size cap (`YAML_SIZE_CAP`) is a sanity guard, not a security boundary. Trust assumption documented in code comment on `YAML_SIZE_CAP`. |
| T-51-02 | Tampering | from_yaml (type confusion via YAML tags) | mitigate | serde typed deserialization rejects unexpected types at compile time. YAML tags cannot inject arbitrary types -- serde only deserializes into the declared struct fields with their declared Rust types. No custom deserializers used. |
| T-51-03 | Elevation of Privilege | from_yaml (code execution via YAML tags) | accept | Not applicable -- serde does not execute arbitrary constructors. yaml_serde/libyaml-rs is a pure data parser. No code execution path exists in the deserialization pipeline. |
</threat_model>

<verification>
1. `cargo test yaml` -- all YAML unit tests pass
2. `cargo test yaml_proptest` -- proptest equivalence passes 256 cases
3. `just test-all` -- full quality gate (unit + proptest + sqllogictest + DuckLake CI)
4. `just check-fuzz` -- fuzz target compiles (requires nightly; non-blocking if nightly unavailable)
</verification>

<success_criteria>
- yaml_serde 0.10 added as unconditional dependency in Cargo.toml
- `from_yaml` and `from_yaml_with_size_cap` exist on SemanticViewDefinition
- PartialEq derived on all 10 model structs
- 11 unit tests cover: minimal/full YAML, defaults, enum roundtrip, YAML-JSON equivalence, error cases, size cap boundary, unknown fields, serialize roundtrip
- Proptest proves YAML-JSON equivalence for 256 arbitrary SemanticViewDefinition instances
- Fuzz target fuzz_yaml_parse compiles
- `just test-all` passes
</success_criteria>

<output>
After completion, create `.planning/phases/51-yaml-parser-core/51-01-SUMMARY.md`
</output>
