---
phase: 65.1-phase-65-code-review-remediation
plan: 12
type: execute
wave: 5
depends_on:
  - 65.1-02b
  - 65.1-10
  - 65.1-01
files_modified:
  - cpp/src/shim.cpp
  - test/integration/test_load_extension_twice_idempotent.py
  - tests/parser_hook_idempotent.rs
autonomous: true
requirements:
  - REM-WR-09

must_haves:
  truths:
    - "`sv_register_parser_hooks` iterates `DBConfig::GetCallbackManager().ParserExtensions()` BEFORE calling `ParserExtension::Register(...)` and compares each existing entry's `parser_override` function pointer against the file-static `sv_parser_override` symbol"
    - "If a match is found, the entire `ParserExtension` build + `Register` block is SKIPPED (so no fresh `SemanticViewsParserInfo` is created — avoids leak)"
    - "If no match, the existing registration body runs unchanged"
    - "A code comment block above the dedup check explains the iterator + function-pointer equality reasoning AND cites Pitfall 3 (theoretical concurrent-LOAD race documented as out of scope)"
    - "Integration test `test_load_extension_twice_idempotent.py` (Plan 01 stub) is populated: opens a DuckDB connection, `LOAD semantic_views` twice; asserts both calls succeed AND a sentinel DDL operation after the second LOAD still works"
    - "B-07 fix per plan-checker: a Rust integration test `tests/parser_hook_idempotent.rs` provides STRUCTURAL verification — invokes `sv_register_parser_hooks` twice and asserts the parser-extension list has exactly one matching entry (via the explicitly-chosen instrumentation FFI per Task 3)"
  artifacts:
    - path: cpp/src/shim.cpp
      provides: "Idempotent parser-hook registration in sv_register_parser_hooks"
      contains: "ParserExtensions()"
    - path: test/integration/test_load_extension_twice_idempotent.py
      provides: "Populated regression test exercising LOAD-twice (behavioural)"
      contains: "LOAD semantic_views"
    - path: tests/parser_hook_idempotent.rs
      provides: "Structural verification test: invokes sv_register_parser_hooks twice + asserts the parser-extension list count is 1"
      contains: "sv_count_parser_extensions"
  key_links:
    - from: cpp/src/shim.cpp::sv_register_parser_hooks
      to: DBConfig::GetCallbackManager().ParserExtensions()
      via: "iterator read-side check; function-pointer comparison against sv_parser_override"
      pattern: "ParserExtensions\\(\\)"
---

<objective>
Make `sv_register_parser_hooks` idempotent across same-process repeat-LOAD of the `semantic_views` extension per CONTEXT D-21. Today, every LOAD appends a new `ParserExtension` to `DBConfig::parser_extensions` because `ParserExtension::Register` has no dedup API. Over many partial-failure retries this accumulates an unbounded list of hooks pointing at the same function — a documented "soft leak" called out by WR-09.

The fix uses the existing public iterator `DBConfig::GetCallbackManager().ParserExtensions()` (`cpp/include/duckdb.cpp:281157-281161` — confirmed by researcher) to check if `sv_parser_override` is already registered. If so, skip the entire registration block (including the `SemanticViewsParserInfo` allocation — important to avoid the parser_info shared_ptr leak per RESEARCH lines 706-710).

Helper-TF (catalog function) registrations downstream use `OnCreateConflict::ALTER_ON_CONFLICT` and are naturally idempotent — no change there.

Per CONTEXT D-22: researcher confirmed the API DOES expose a clean iterator path, so the README-fallback alternative is NOT needed. Drop any text suggesting D-22.

Plus: populate the Plan 01 stub `test/integration/test_load_extension_twice_idempotent.py` to exercise the LOAD-twice scenario as a regression guard.

Wave 3 because:
- Depends on Plan 02 (sv_register_parser_hooks now carries `error_buf, error_buf_len` from WR-02 work)
- Depends on Plan 10 (shim.cpp serialization — WR-06 runtime probe also lives in sv_register_parser_hooks; this plan stacks on top)

Implements D-21 (WR-09).
</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/STATE.md
@.planning/phases/65.1-phase-65-code-review-remediation/65.1-CONTEXT.md
@.planning/phases/65.1-phase-65-code-review-remediation/65.1-RESEARCH.md
@.planning/phases/65.1-phase-65-code-review-remediation/65.1-PATTERNS.md
@cpp/src/shim.cpp
@cpp/include/duckdb.cpp
@test/integration/test_load_extension_twice_idempotent.py
@test/integration/test_concurrent_reads_per_call_conn.py
</context>

<interfaces>
ParserExtension class (`cpp/include/duckdb.cpp:23915-23932`):
```
class ParserExtension {
public:
    parse_function_t parse_function = nullptr;
    plan_function_t plan_function = nullptr;
    parser_override_function_t parser_override = nullptr;
    shared_ptr<ParserExtensionInfo> parser_info;
    static void Register(DBConfig &config, ParserExtension extension);
};
```

`DBConfig::GetCallbackManager()` returns a mutable `ExtensionCallbackManager &`.
`ExtensionCallbackManager::ParserExtensions()` returns an iterator helper (range-for compatible).
`ExtensionCallbackManager::HasParserExtensions()` is also available.

Existing registration block in `sv_register_parser_hooks` (PATTERNS.md lines 285-297):
```
ParserExtension ext;
ext.parser_override = sv_parser_override;
ext.parse_function  = sv_parse_stub;
ext.plan_function   = sv_plan_unreachable;
std::shared_ptr<ParserExtensionInfo> info_std(
    new SemanticViewsParserInfo(rust_state));
ext.parser_info = duckdb::shared_ptr<ParserExtensionInfo>(info_std);
auto &config = DBConfig::GetConfig(db);
ParserExtension::Register(config, ext);
```

Target shape (PATTERNS.md lines 299-333):
```
auto &config = DBConfig::GetConfig(db);
auto &cbmgr = config.GetCallbackManager();

// Idempotence check: ParserExtension::Register always appends — without this
// dedup, repeat LOAD semantic_views accumulates hooks in DBConfig::parser_extensions.
// Function-pointer equality is portable ISO C++ because sv_parser_override is a
// file-static C++ function (same TU on both sides of ==). The same-thread repeat-LOAD
// pattern is the target; theoretical concurrent-LOAD race is out of scope per
// Phase 65.1 WR-09 Pitfall 3 (see 65.1-RESEARCH.md).
bool already_registered = false;
for (auto &existing : cbmgr.ParserExtensions()) {
    if (existing.parser_override == sv_parser_override) {
        already_registered = true;
        break;
    }
}

if (!already_registered) {
    ParserExtension ext;
    ext.parser_override = sv_parser_override;
    ext.parse_function  = sv_parse_stub;
    ext.plan_function   = sv_plan_unreachable;
    std::shared_ptr<ParserExtensionInfo> info_std(
        new SemanticViewsParserInfo(rust_state));
    ext.parser_info = duckdb::shared_ptr<ParserExtensionInfo>(info_std);
    ParserExtension::Register(config, ext);

    config.SetOption("allow_parser_override_extension", Value("FALLBACK"));
}
// Helper-TF registrations below use ALTER_ON_CONFLICT — naturally idempotent.
```
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Wrap parser-extension Register in idempotence check</name>
  <read_first>
    - cpp/src/shim.cpp lines 2460-2545 (sv_register_parser_hooks — find the exact location of the `ParserExtension::Register` call AND the `config.SetOption("allow_parser_override_extension", ...)` call that follows)
    - cpp/include/duckdb.cpp lines 281063-281098, 281157-281195 (Register implementation + ParserExtensions iterator + HasParserExtensions — verify the iterator surface is what RESEARCH describes)
    - .planning/phases/65.1-phase-65-code-review-remediation/65.1-PATTERNS.md lines 282-333 (target shape)
    - .planning/phases/65.1-phase-65-code-review-remediation/65.1-RESEARCH.md lines 668-714 (full WR-09 detail including the Pitfall 3 race note)
    - Plan 10 changes — runtime probe should already be at the TOP of this function; this task adds the idempotence check AFTER the probe but BEFORE the registration body
  </read_first>
  <files>cpp/src/shim.cpp</files>
  <action>
1. Locate the parser-extension build + Register block in `sv_register_parser_hooks`. After Plan 10, the structure should be: function entry → wrapper unwrap → WR-06 runtime probe → existing parser-extension build + Register → helper-TF registrations.

2. Verify `ParserExtensions()` iterator and `ExtensionCallbackManager::GetCallbackManager` are accessible from where you are. If a `using duckdb::DBConfig;` or similar is needed, add it consistently with surrounding code style.

3. Replace the existing parser-extension build + Register block with the target shape from `<interfaces>` above. Critical points:
   - The `auto &config = DBConfig::GetConfig(db);` line moves UP (before the iterator check)
   - The entire `ParserExtension ext; ... ext.parser_info = ...; Register(config, ext);` block goes INSIDE `if (!already_registered) { ... }`
   - The `config.SetOption("allow_parser_override_extension", Value("FALLBACK"))` call ALSO goes inside the guard — repeat invocations would have no functional effect (the option's value doesn't change), but bundling them is cleaner.
   - The `SemanticViewsParserInfo` allocation MUST be inside the guard so we don't leak a fresh info object on the skip path.

4. Add the comment block from `<interfaces>` above the `for (auto &existing : ...)` loop, explaining:
   - Why `ParserExtension::Register` always appends (no dedup API)
   - Why function-pointer comparison is portable here (file-static `sv_parser_override`)
   - That the target is same-process repeat-LOAD; concurrent-thread LOAD race is out of scope (Pitfall 3)
   - Cite Phase 65.1 D-21

5. DO NOT modify the helper-TF registration block (the `sv_register_table_function` / `sv_register_scalar_function` calls below the parser-extension block). Those use `OnCreateConflict::ALTER_ON_CONFLICT` and are naturally idempotent.

6. Build + test: extension must continue to load cleanly on first LOAD; the new path is exercised only by Task 2's integration test on second LOAD.
  </action>
  <verify>
    <automated>just build &gt; /tmp/claude/p12_t1_build.log 2>&amp;1; RC=$?; tail -50 /tmp/claude/p12_t1_build.log; test $RC -eq 0 &amp;&amp; grep -nE "ParserExtensions\\(\\)" cpp/src/shim.cpp &amp;&amp; grep -nE "already_registered" cpp/src/shim.cpp &amp;&amp; just test-sql &gt; /tmp/claude/p12_t1_sql.log 2>&amp;1; RC2=$?; tail -40 /tmp/claude/p12_t1_sql.log; test $RC2 -eq 0</automated>
  </verify>
  <acceptance_criteria>
    - `grep -n "ParserExtensions()" cpp/src/shim.cpp` returns at least one match inside sv_register_parser_hooks
    - `grep -n "already_registered" cpp/src/shim.cpp` returns at least one match
    - The `SemanticViewsParserInfo` allocation is INSIDE the `if (!already_registered)` block
    - Function-pointer comparison `existing.parser_override == sv_parser_override` present
    - Comment block above the dedup check cites Phase 65.1 D-21 + WR-09
    - `just build`, `just test-sql` exit 0 — first-load behaviour unchanged
  </acceptance_criteria>
  <done>WR-09 closed structurally: parser-hook registration is now idempotent across same-process repeat-LOAD.</done>
</task>

<task type="auto">
  <name>Task 2: Populate test_load_extension_twice_idempotent.py with regression assertion</name>
  <read_first>
    - test/integration/test_load_extension_twice_idempotent.py (Plan 01 stub)
    - test/integration/test_concurrent_reads_per_call_conn.py (PEP-723 + open_master fixture)
    - test/integration/test_ducklake_helpers.py (EXT_DIR / EXT_PATH / get_extension_path helpers)
    - .planning/phases/65.1-phase-65-code-review-remediation/65.1-PATTERNS.md lines 711-724 (test design notes)
    - .planning/phases/65.1-phase-65-code-review-remediation/65.1-CONTEXT.md (D-21)
  </read_first>
  <files>test/integration/test_load_extension_twice_idempotent.py</files>
  <action>
Replace the Plan 01 skip-stub body. The test:

1. Keeps the PEP-723 header from the stub (Plan 01 created it with `dependencies = ["duckdb==1.5.2"]`).
2. Imports `duckdb`, `tempfile`, `os`, `pytest`. Sets up the `sys.path.insert + from test_ducklake_helpers import get_ext_dir, get_extension_path` boilerplate matching `test_concurrent_reads_per_call_conn.py`.
3. Defines `def test_load_extension_twice_idempotent():`. Body:
   a. `tempfile.TemporaryDirectory()` → `db_path = os.path.join(tmp, "test.duckdb")`.
   b. `conn = duckdb.connect(db_path, config={"allow_unsigned_extensions": "true", "extension_directory": EXT_DIR})`.
   c. `conn.execute(f"FORCE INSTALL '{EXT_PATH}'")`.
   d. `conn.execute("LOAD semantic_views")` — first load; assert no exception.
   e. `conn.execute("LOAD semantic_views")` — second load; assert no exception (D-21 idempotence).
   f. Sentinel — exercise a DDL statement that goes through the parser hook:
      - `conn.execute("CREATE TABLE t1 (id INTEGER)")`
      - `conn.execute("CREATE SEMANTIC VIEW v_after_double_load TABLES (t1 AS t1) METRICS (t1.cnt AS count(*))")`
      - `result = conn.execute("SELECT cnt FROM semantic_view('v_after_double_load', dimensions := [], metrics := ['cnt'])").fetchall()`
      - Assert `result == [(0,)]` (empty table, count is 0).
   g. Bonus assertion (best-effort): query for any duckdb_extensions() loaded count or other observable signal of single-registration. PATTERNS notes there's no public API for parser-extension list — the practical signal is "second LOAD does not throw AND DDL after second LOAD still works", which Steps d-f cover. Document this in a comment.
4. `if __name__ == "__main__": pytest.main([__file__, "-v"])`.

Note: this test cannot DIRECTLY observe the parser-extension list (no public API), so it relies on the contract "no exception + sentinel DDL works" as the regression signal. Pre-fix, the second LOAD itself does NOT throw (DuckDB doesn't surface the double-register as a user error); the symptom is internal list bloat which isn't user-visible. The test thus pins the BEHAVIOUR contract (no regression in LOAD-twice scenarios) rather than directly observing the leak. A regression where the list bloat causes a downstream parser failure would surface as Step f failing — that's the practical guard.
  </action>
  <verify>
    <automated>uv run test/integration/test_load_extension_twice_idempotent.py &gt; /tmp/claude/p12_t2_run.log 2>&amp;1; RC=$?; tail -40 /tmp/claude/p12_t2_run.log; test $RC -eq 0</automated>
  </verify>
  <acceptance_criteria>
    - File no longer contains `pytest.skip(...)` for the main test body
    - One top-level `def test_load_extension_twice_idempotent()` function
    - Body issues `LOAD semantic_views` exactly twice with no exception expectation
    - Sentinel DDL block (CREATE TABLE + CREATE SEMANTIC VIEW + SELECT FROM semantic_view) executes after the second LOAD without error
    - `uv run test/integration/test_load_extension_twice_idempotent.py` exits 0
  </acceptance_criteria>
  <done>WR-09 regression guard live: LOAD-twice + post-LOAD DDL exercised by Python integration test.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 3 (B-07 fix): Add structural verification test for parser-extension list dedup</name>
  <behavior>
    - A Rust integration test `tests/parser_hook_idempotent.rs::parser_hook_register_is_idempotent` invokes `sv_register_parser_hooks` twice against the same `duckdb_database` handle.
    - After both calls, an instrumentation FFI helper reports that the parser-extension list contains exactly ONE entry matching the `sv_parser_override` function pointer (not two).
    - This test would FAIL on the pre-Task-1 binary (the dedup check would not exist, and the count would be 2) and PASSES post-Task-1.
    - The behavioural integration test (Task 2) and this structural test together provide both pre/post-fix discriminating evidence.
  </behavior>
  <read_first>
    - cpp/src/shim.cpp post-Task-1 (sv_register_parser_hooks with the dedup check)
    - cpp/src/shim.hpp (FFI declaration pattern; mirror the WR-02 error_buf shape for the new helper)
    - tests/registration_error_surfaces.rs (template for top-level Rust integration test calling shim FFI; established by Plan 02b)
    - tests/no_long_lived_conn.rs (template for the simplest tests/*.rs layout)
    - .planning/phases/65.1-phase-65-code-review-remediation/65.1-CONTEXT.md (D-21)
    - cpp/include/duckdb.hpp around the `DBConfig::GetCallbackManager()` and `ExtensionCallbackManager::ParserExtensions()` declarations (to confirm the iterator's public visibility for the helper)
  </read_first>
  <files>cpp/src/shim.cpp, cpp/src/shim.hpp, tests/parser_hook_idempotent.rs</files>
  <action>
**B-07 PLAN-CHECKER FIX:** Task 2's integration test is behavioural only — both pre-fix and post-fix binaries pass it because DuckDB does not surface double-registration as a user error. This task adds a STRUCTURAL verification test that DOES distinguish.

**Decision (explicit per plan-checker requirement):** Choose **option (a) — public instrumentation FFI helper**. We expose a new public helper `sv_count_parser_extensions(db, name_substr, error_buf, error_buf_len) -> int32_t` in `cpp/src/shim.cpp` + `cpp/src/shim.hpp`. The helper iterates `DBConfig::GetCallbackManager().ParserExtensions()` and counts entries whose `parser_override` function pointer equals `sv_parser_override` (file-static C++ function, same TU — function-pointer equality is portable per RESEARCH.md Assumption A2).

We CHOSE option (a) over the alternatives because:
- Option (b) "keep behavioural-only with manual annotation" was the original silent failure (B-07).
- Option (c) "private `__sv_debug_*` helper gated behind cfg(test)" is awkward to gate cleanly in a C++ shim and would require conditional-compilation plumbing for marginal benefit. The helper is harmless even in production binaries (read-only iteration).

This decision is documented here AND should be cited in the helper's docstring.

**Sub-task A — Add the helper to shim.cpp + shim.hpp:**

1. In `cpp/src/shim.hpp`, declare:
   ```
   // Phase 65.1 Plan 12 (D-21, B-07 plan-checker fix): structural verification
   // of WR-09 parser-hook dedup. Returns the count of parser-extensions in the
   // given DBConfig whose `parser_override` function pointer matches the
   // file-static sv_parser_override symbol. Returns -1 on failure (with the
   // underlying message in error_buf, per the WR-02 ABI convention).
   //
   // We chose to expose this as a PUBLIC helper (option (a) per plan-checker)
   // rather than gating behind cfg-test plumbing — read-only iteration is
   // harmless in production binaries.
   int32_t sv_count_parser_extensions(
       duckdb_database db_handle,
       char *error_buf, size_t error_buf_len);
   ```

2. In `cpp/src/shim.cpp`, implement:
   ```
   int32_t sv_count_parser_extensions(
       duckdb_database db_handle,
       char *error_buf, size_t error_buf_len) {
       try {
           if (db_handle == nullptr) {
               if (error_buf && error_buf_len > 0) {
                   snprintf(error_buf, error_buf_len, "sv_count_parser_extensions: null db_handle");
               }
               return -1;
           }
           auto *wrapper = reinterpret_cast<duckdb::DatabaseWrapper *>(db_handle->internal_ptr);
           auto &db = *wrapper->database->instance;
           auto &config = DBConfig::GetConfig(db);
           auto &cbmgr = config.GetCallbackManager();
           int32_t count = 0;
           for (auto &existing : cbmgr.ParserExtensions()) {
               if (existing.parser_override == sv_parser_override) {
                   ++count;
               }
           }
           return count;
       } catch (const std::exception &e) {
           if (error_buf && error_buf_len > 0) {
               snprintf(error_buf, error_buf_len, "sv_count_parser_extensions failed: %s", e.what());
           }
           return -1;
       } catch (...) {
           if (error_buf && error_buf_len > 0) {
               snprintf(error_buf, error_buf_len, "sv_count_parser_extensions failed: unknown C++ exception");
           }
           return -1;
       }
   }
   ```

3. Place the implementation near `sv_register_parser_hooks` so the two stay co-located. Cite Phase 65.1 D-21 and the B-07 plan-checker fix in the doc comment.

**Sub-task B — Add the Rust integration test:**

Create `tests/parser_hook_idempotent.rs` (top-level integration test, no `mod` wrapper — same shape as `tests/no_long_lived_conn.rs`):

```rust
//\! Phase 65.1 Plan 12 Task 3 (B-07 plan-checker fix) — STRUCTURAL verification
//\! of WR-09 parser-hook idempotence. The behavioural integration test in
//\! test/integration/test_load_extension_twice_idempotent.py passes on both
//\! pre-fix and post-fix binaries; this test discriminates by observing the
//\! parser-extension list count via the sv_count_parser_extensions helper.

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;

extern "C" {
    fn sv_register_parser_hooks(
        db_handle: libduckdb_sys::duckdb_database,
        // ... mirror the post-Plan-02b extern shape including error_buf trailing pair ...
        error_buf: *mut c_char,
        error_buf_len: usize,
    ) -> bool;

    fn sv_count_parser_extensions(
        db_handle: libduckdb_sys::duckdb_database,
        error_buf: *mut c_char,
        error_buf_len: usize,
    ) -> i32;
}

#[test]
fn parser_hook_register_is_idempotent() {
    let mut db: libduckdb_sys::duckdb_database = ptr::null_mut();
    let rc = unsafe { libduckdb_sys::duckdb_open(ptr::null(), &mut db) };
    assert_eq\!(rc, libduckdb_sys::DuckDBSuccess);

    let mut error_buf = [0u8; 1024];

    // First registration
    let ok1 = unsafe {
        sv_register_parser_hooks(db, error_buf.as_mut_ptr().cast::<c_char>(), error_buf.len())
    };
    let msg1 = unsafe { CStr::from_ptr(error_buf.as_ptr().cast::<c_char>()) }
        .to_string_lossy().into_owned();
    assert\!(ok1, "first registration must succeed; error_buf: {msg1}");

    // Second registration — must be idempotent (no-op)
    error_buf.fill(0);
    let ok2 = unsafe {
        sv_register_parser_hooks(db, error_buf.as_mut_ptr().cast::<c_char>(), error_buf.len())
    };
    let msg2 = unsafe { CStr::from_ptr(error_buf.as_ptr().cast::<c_char>()) }
        .to_string_lossy().into_owned();
    assert\!(ok2, "second registration must succeed; error_buf: {msg2}");

    // STRUCTURAL assertion — only one entry should exist
    error_buf.fill(0);
    let count = unsafe {
        sv_count_parser_extensions(db, error_buf.as_mut_ptr().cast::<c_char>(), error_buf.len())
    };
    let msg3 = unsafe { CStr::from_ptr(error_buf.as_ptr().cast::<c_char>()) }
        .to_string_lossy().into_owned();
    assert_eq\!(count, 1, "expected exactly 1 sv_parser_override entry after two LOADs; got {count}; helper error_buf: {msg3}");

    unsafe { libduckdb_sys::duckdb_close(&mut db) };
}
```

The EXACT `sv_register_parser_hooks` extern signature MUST match what Plan 02b's `src/lib.rs` extern carries (after the WR-02 trailing-pair update). Read `src/lib.rs` to confirm and mirror.

This test FAILS on the pre-Task-1 binary (the dedup check is absent, so `count == 2`). It PASSES post-Task-1.
  </action>
  <verify>
    <automated>just build &gt; /tmp/claude/p12_t3_build.log 2>&amp;1; RC=$?; tail -30 /tmp/claude/p12_t3_build.log; test $RC -eq 0 &amp;&amp; cargo test --test parser_hook_idempotent &gt; /tmp/claude/p12_t3_test.log 2>&amp;1; RC2=$?; tail -30 /tmp/claude/p12_t3_test.log; test $RC2 -eq 0 &amp;&amp; grep -q "sv_count_parser_extensions" cpp/src/shim.hpp &amp;&amp; grep -q "sv_count_parser_extensions" cpp/src/shim.cpp</automated>
  </verify>
  <acceptance_criteria>
    - `cpp/src/shim.hpp` declares `int32_t sv_count_parser_extensions(...)` with the WR-02-style error_buf trailing pair
    - `cpp/src/shim.cpp` implements the helper, iterating `cbmgr.ParserExtensions()` and counting matches against `sv_parser_override`
    - The helper's doc comment names the option-(a) decision rationale per B-07 plan-checker fix
    - `tests/parser_hook_idempotent.rs` exists with one `#[test] fn parser_hook_register_is_idempotent`
    - Test invokes `sv_register_parser_hooks` exactly twice, then asserts `sv_count_parser_extensions` returns 1
    - `cargo test --test parser_hook_idempotent` exits 0 with `1 passed`
    - Both `just build` and `just test-all` exit 0
  </acceptance_criteria>
  <done>B-07 fix landed: structural verification test discriminates pre/post-fix binaries via the chosen public instrumentation FFI (option a documented in code).</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Repeat `LOAD semantic_views` calls within same process | Accumulating parser-extension hooks across LOADs |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-65.1-12-01 | Denial of service | Unbounded parser-hook accumulation across repeat-LOAD | mitigate | Iterator-based dedup before Register (D-21); leak path closed for same-process scenario |
| T-65.1-12-02 | Tampering / Race | Concurrent-thread LOAD race between iterator check + Register | accept | Documented as Pitfall 3 — out of scope for v0.10.0; user would need to explicitly LOAD from two threads simultaneously to hit, atypical for DuckDB extension API |
</threat_model>

<verification>
  <automated>just test-all &gt; /tmp/claude/p12_all.log 2>&amp;1; RC=$?; tail -100 /tmp/claude/p12_all.log; test $RC -eq 0 &amp;&amp; just ci &gt; /tmp/claude/p12_ci.log 2>&amp;1; RC2=$?; tail -40 /tmp/claude/p12_ci.log; test $RC2 -eq 0</automated>
</verification>

<success_criteria>
- `sv_register_parser_hooks` iterates existing parser extensions and skips Register on match
- `SemanticViewsParserInfo` allocation guarded inside the not-already-registered branch
- Integration test `test_load_extension_twice_idempotent.py` exercises double-LOAD + sentinel DDL
- `just test-all` + `just ci` both green
</success_criteria>

<output>
Create `.planning/phases/65.1-phase-65-code-review-remediation/65.1-12-SUMMARY.md` when done.
</output>
