# v0.7.0 Coding Task Plan

| Field | Value |
|------|-----|
| Version | 0.7.0 |
| Status | FINAL |
| Created | 2026-05-17 |
| Source Spec | kiro/specs/27-v070_prod/spec.md |
| Source Design | kiro/specs/27-v070_prod/design.md |
| Source Tasks | kiro/specs/27-v070_prod/tasks.md |
| Source Impl Design | docs/project/0.7.0-implementation-design.md |
| AGENTS.md Version | 0.6.7 |

---

## 1. Coding Task Plan Overview

This document translates the v0.7.0 spec (40 REQ IDs, 5 Workstreams, ~100 subtasks) into an
executable coding task plan with PR breakdown, per-task detail, CI/CD integration, risk mitigation,
and timeline analysis.

**Total scope**: 15 PRs, 7 Phase-0 tasks (A01-A07), 6 Phase-1 tasks (B01-B06), 6 Phase-2 tasks
(C01-C06), 5 Phase-3 tasks (D01-D05), 6 Phase-4 tasks (E01-E06), 5 Phase-5 tasks (F01-F05).

**Core principles** (inherited from AGENTS.md and agent-handoff.md):

1. Pre-output Checklist validated before every file write.
2. NGX_AGAIN is suspend-and-resume, never treat as success.
3. Body must not be sent before headers.
4. New FFI struct fields are tail-append only.
5. C side must use `*_init()` / `*_cleanup()` helpers, never literal init.
6. Delivery counter ≠ Decision counter; delivery only on downstream NGX_OK.
7. New reason code/metric/config: define → implement → test → docs → validator.
8. Coverage ≥ 80% (aggregate), ≥ 90% (critical).
9. No Python E2E files (Rule 37).
10. Shell scripts macOS bash 3.2 compatible.

---

## 2. PR Breakdown and Dependency Graph

### PR Dependency Graph

```
PR1 ──► PR2 ──► PR3 ──► PR4 ──► PR5 ──► PR6
 │                                        │
 └────────────────► PR7 ──► PR8 ──► PR9   │
                    │                      │
                    └──► PR10 ──► PR11    │
                                       │   │
                    PR12 ──────────────┘   │
                    PR13 ──────────────┘   │
                    PR14 ──────────────┘   │
                    └──────► PR15 ◄────────┘
```

### PR Table

| PR | Title | Included Tasks | Preceding PRs | Est. Lines | Review Focus | Verify Command |
|----|-------|---------------|--------------|------------|--------------|----------------|
| PR1 | Fail-open replay + chunk preservation | A01 (A01.1-A01.14) | — | 500-800 | Rule 1,2,38; pending chain; delivery vs decision | `make test-nginx-unit-streaming && make test-nginx-unit && make verify-chunked-native-e2e-smoke && make coverage-c` |
| PR2 | Output ordering | A02 (A02.1-A02.7) | PR1 | 200-350 | header-before-body; alloc-before-send; idempotent send_header | `make test-nginx-unit && make test-nginx-integration && make coverage-c` |
| PR3 | Bounded decompression + error categories | A03+A04 (A03.1-A03.13, A04.1-A04.7) | PR2 | 400-600 | budget; DecompressionError enum; FFI mapping; per-category metrics | `make test-rust && make test-rust-fuzz-smoke && make test-nginx-unit && make coverage-rust && make coverage-c` |
| PR4 | Accept negotiation | A05 (A05.1-A05.12) | PR3 | 300-500 | q-value; table-driven test; RFC 7231 | `make test-rust && make test-nginx-unit && make test-e2e-rust && make coverage-rust` |
| PR5 | Parse budget + timeout | A06 (A06.1-A06.10) | PR4 | 300-450 | budget; timeout; new reason codes; metrics | `make test-rust-streaming && make test-nginx-unit-streaming && make coverage-c && make coverage-rust` |
| PR6 | FFI ABI checks | A07 (A07.1-A07.10) | PR5 | 200-350 | cbindgen; static_assert; layout test; helpers | `make build && make check-headers && make test-rust && make test-nginx-unit && make harness-check-full` |
| PR7 | Rust-first migration contract | B01 (B01.1-B01.5) | PR6 | 150-250 | docs; ADR; audit | `make docs-check && make harness-check` |
| PR8 | Conditional request/ETag | B02 (B02.1-B02.8) | PR7 | 300-450 | If-None-Match; weak ETag; FFIConditionalResult | `make test-rust && make test-nginx-unit && make test-e2e-rust && make coverage-rust` |
| PR9 | URL/Host validation | B03 (B03.1-B03.9) | PR7 | 200-350 | control char; X-Forwarded-*; link escape | `make test-rust && make test-rust-fuzz-smoke && make coverage-rust` |
| PR10 | Header plan + decision engine | B04+B05 (B04.1-B04.6, B05.1-B05.8) | PR8, PR9 | 500-800 | plan atomicity; pure function; reason code coverage | `make test-rust && make test-nginx-unit && make coverage-rust && make coverage-c` |
| PR11 | Reason-code/metrics normalization | B06 (B06.1-B06.9) | PR10 | 300-500 | single source; delivery ≠ decision; renderer sync | `make test-rust && make test-nginx-unit && make release-gates-check && make coverage-rust && make coverage-c` |
| PR12 | Package distribution | C01-C06 (C01.1-C06.5) | PR11 | 600-1000 | matrix; DEB/RPM build; GPG sign; docs | `make docs-check && make release-gates-check` |
| PR13 | K8s/Ingress | D01-D05 (D01.1-D05.4) | PR12 | 400-700 | manifest; helm; smoke; F5 assessment | `helm lint charts/nginx-markdown && kubectl apply --dry-run=client -f examples/kubernetes/manifest/` |
| PR14 | Operability + validators | E01-E06 (E01.1-E06.6) | PR6 | 800-1200 | diagnostics; dynconf dry-run/LKG/rollback; validators; gates | `make test-nginx-unit && make docs-check && make harness-check-full && make release-gates-check-strict` |
| PR15 | Final validation | F01-F05 (F01.1-F05.5) | PR11, PR12, PR13, PR14 | 100-200 | coverage; harness; version | `make harness-check-full && make docs-check && make release-gates-check-strict && make coverage-c && make coverage-rust` |

### Rollback Strategy

| PR | Risk | Rollback Method |
|----|------|-----------------|
| PR1 | Pending chain behavior change | `git revert` — C module only; no FFI ABI change |
| PR2 | Output ordering refactoring | `git revert` — C module only; no FFI ABI change |
| PR3 | FFI struct tail-append (MarkdownOptions) | `git revert` — ABI is backward-compatible (tail append); old code ignores new fields |
| PR4 | New FFI struct FFIAcceptResult | `git revert` — new struct, no existing consumers |
| PR5 | FFI struct tail-append (parse_timeout, parser_budget) | `git revert` — same as PR3 |
| PR6 | CI step + static_assert + helpers | `git revert` — CI change; revert CI config too |
| PR7 | Documentation only | `git revert` — no code change |
| PR8 | New FFI struct FFIConditionalResult | `git revert` — same as PR4 |
| PR9 | New FFI struct FFIUrlResult | `git revert` — same as PR4 |
| PR10 | New FFI structs FFIHeaderPlan, FFIDecisionResult | `git revert` — same as PR4 |
| PR11 | Reason-code single source; C constants removed | `git revert` — restore C constants; revert Rust enum |
| PR12 | Packaging files + CI workflows | `git revert` — no production code change |
| PR13 | K8s examples + Helm chart | `git revert` — no production code change |
| PR14 | Diagnostics + dynconf + validators | `git revert` — new C files; revert removes them |
| PR15 | Version bump + final checks | `git revert` — version bump revert |

---

## 3. Phase 0: P0 Runtime Correctness — Coding Tasks

### TASK-A01: Fail-open Replay and Chunk Preservation

_Requirement: REQ-0700-CORRECTNESS-001 | Risk Pack: runtime-streaming | Rules: 1, 2, 38 | PR: PR1_

#### A01.1: Analyze replay buffer append-failure return semantics

- [ ] **Description**: Audit all `ngx_http_markdown_replay_*` call sites in buffer code. Document each path where replay buffer init or append can return `NGX_ERROR`. Map each to its `precommit_error` assignment. Add `/* REPLAY-FAILURE-PATH: ... */` annotation comments.
- **Files**: `components/nginx-module/src/ngx_http_markdown_buffer.c`
- **Changes**: Comment annotations documenting all failure paths
- **Precondition**: None
- **Test**: Code review (annotation coverage)
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: All replay_* call sites annotated with failure path and precommit_error mapping

#### A01.2: Pending chain persistence on NGX_AGAIN

- [ ] **Description**: In `ngx_http_markdown_body_filter`, when `ngx_output_chain()` returns `NGX_AGAIN`, save `r->out` (the pending chain) to `ctx->pending_chain`. On next invocation (resume), check `ctx->pending_chain != NULL`; if so, prepend it to the new output. **Critical**: Never overwrite `ctx->pending_chain` with newly produced output. Only append new chain links to the tail. Add `pending_chain` and `pending_chain_active` fields to request context struct.
- **Files**: `components/nginx-module/src/ngx_http_markdown_filter_module.c`, `components/nginx-module/src/ngx_http_markdown_filter_module.h`
- **Changes**: New fields in `ngx_http_markdown_request_ctx_t`: `pending_chain`, `pending_chain_active`; modify body_filter NGX_AGAIN path
- **Precondition**: A01.1
- **Test**: C unit test (A01.8)
- **Docs**: Update `docs/features/DECISION_CHAIN.md`
- **Harness**: runtime-streaming; routing-manifest no change (paths already covered)
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: NGX_AGAIN path preserves pending chain; resume replays full chain; no data loss

#### A01.3: Last_buf protection

- [ ] **Description**: When `ctx->pending_chain_active` is true, walk the pending chain to find any `buf->last_buf == 1`. If found and downstream has not consumed it yet, do NOT set `last_buf=1` on any new output buffer. Only set `last_buf=1` after downstream returns `NGX_OK` for the pending chain.
- **Files**: `components/nginx-module/src/ngx_http_markdown_filter_module.c`
- **Changes**: Add last_buf guard logic in body_filter output path
- **Precondition**: A01.2
- **Test**: C unit test (A01.8)
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: last_buf never set while pending chain unconsumed

#### A01.4: Failopen_completed dedup finalization

- [ ] **Description**: Add `failopen_completed` flag to request context. In every fail-open path before calling `ngx_http_finalize_request()`, check `ctx->failopen_completed`; if already set, return (skip). Otherwise, set to 1 and finalize.
- **Files**: `components/nginx-module/src/ngx_http_markdown_error.c`, `components/nginx-module/src/ngx_http_markdown_filter_module.h`
- **Changes**: New bitfield `failopen_completed:1` in ctx; guard in all fail-open paths
- **Precondition**: A01.2
- **Test**: C unit test (A01.10)
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit`
- **Completion**: `ngx_http_finalize_request` called exactly once per request in fail-open path

#### A01.5: Delivery vs Decision counter separation

- [ ] **Description**: Add `markdown_decisions_total` and `markdown_delivery_total` SHM fields. Decision counter incremented in every decision path (CONVERT, SKIP, FAIL-OPEN). Delivery counter incremented ONLY after `ngx_output_chain()` returns `NGX_OK`. Invariant: `delivery_total ≤ decisions_total`. Add both to Prometheus renderer output.
- **Files**: `components/nginx-module/src/ngx_http_markdown_metrics_impl.h`, `components/nginx-module/src/ngx_http_markdown_prometheus_impl.h`, `components/nginx-module/src/ngx_http_markdown_filter_module.c`
- **Changes**: New SHM fields; new Prometheus output lines; increment logic in body_filter
- **Precondition**: A01.2
- **Test**: C unit test (A01.12)
- **Docs**: Update `docs/guides/prometheus-metrics.md`
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit && make coverage-c`
- **Completion**: Both counters exposed in /metrics; delivery ≤ decisions invariant holds

#### A01.6: Max-size failure transparent passthrough

- [ ] **Description**: When `markdown_max_size` is exceeded mid-stream, set `ctx->conversion_abandoned = 1`, increment decision counter. Do NOT discard subsequent upstream chunks; pass them through transparently. On last_buf, finalize as pass-through with original content-type.
- **Files**: `components/nginx-module/src/ngx_http_markdown_filter_module.c`
- **Changes**: Add `conversion_abandoned` flag; modify body_filter to pass-through subsequent chunks after max-size failure
- **Precondition**: A01.2
- **Test**: C unit test (A01.11)
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: Subsequent chunks delivered after max-size failure; no data loss

#### A01.7: r->buffered semantic correctness

- [ ] **Description**: Clear `r->buffered` only when all pending chain has been consumed (downstream NGX_OK) and no more body data is expected. Never clear unconditionally on filter entry.
- **Files**: `components/nginx-module/src/ngx_http_markdown_filter_module.c`
- **Changes**: Conditional `r->buffered` clearing logic
- **Precondition**: A01.2
- **Test**: C unit test (A01.8 covers via streaming test)
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: `r->buffered` stays set during NGX_AGAIN pending; cleared only on completion

#### A01.8: C unit test — NGX_AGAIN path pending chain and resume

- [ ] **Description**: Write C unit test that mocks downstream returning NGX_AGAIN twice, verifies pending_chain preserved and data complete on resume. Test r->buffered stays set during NGX_AGAIN.
- **Files**: `components/nginx-module/tests/unit/body_filter_streaming_test.c`
- **Changes**: New test file or extend existing
- **Precondition**: A01.2, A01.3, A01.7
- **Test**: This IS the test
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: Test passes; pending chain data verified byte-for-byte

#### A01.9: C unit test — replay buffer failure → precommit_error

- [ ] **Description**: Write C unit test that mocks pool exhaustion causing replay buffer init or append to fail. Verify `precommit_error=1` and log contains "replay buffer" keyword.
- **Files**: `components/nginx-module/tests/unit/replay_buffer_test.c`
- **Changes**: New test file
- **Precondition**: A01.1
- **Test**: This IS the test
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit`
- **Completion**: Test passes; precommit_error set; log keyword verified

#### A01.10: C unit test — failopen_completed prevents duplicate finalize

- [ ] **Description**: Write C unit test verifying `ngx_http_finalize_request` called exactly once even when multiple fail-open paths are triggered.
- **Files**: `components/nginx-module/tests/unit/failopen_dedup_test.c`
- **Changes**: New test file
- **Precondition**: A01.4
- **Test**: This IS the test
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit`
- **Completion**: finalize called exactly once; failopen_completed flag verified

#### A01.11: C unit test — middle chunk max-size failure passthrough

- [ ] **Description**: Write C unit test: 3 chunks, chunk 2 triggers max-size failure, verify chunks 1 and 3 delivered as-is.
- **Files**: `components/nginx-module/tests/unit/body_filter_streaming_test.c`
- **Changes**: Extend existing or new test
- **Precondition**: A01.6
- **Test**: This IS the test
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: Chunks 1 and 3 delivered; chunk 2 triggers fail-open

#### A01.12: C unit test — delivery counter only on NGX_OK

- [ ] **Description**: Write C unit test: fail-open scenario verifies decisions=1, delivery=0. Normal conversion verifies decisions=1, delivery=1.
- **Files**: `components/nginx-module/tests/unit/metrics_delivery_test.c`
- **Changes**: New test file
- **Precondition**: A01.5
- **Test**: This IS the test
- **Docs**: None
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: Counter values match expected; invariant delivery ≤ decisions holds

#### A01.13: E2E test — chunked replay scenario

- [ ] **Description**: Write Rust E2E test (Rule 37: no Python) that sends chunked request, forces NGX_AGAIN, verifies response integrity.
- **Files**: `tools/e2e-harness/src/scenarios/chunked_replay.rs`
- **Changes**: New E2E scenario
- **Precondition**: A01.2, A01.3
- **Test**: This IS the test
- **Docs**: None
- **Harness**: runtime-streaming, e2e-migration
- **Verify**: `make verify-chunked-native-e2e-smoke`
- **Completion**: E2E passes; chunked replay verified end-to-end

#### A01.14: Update DECISION_CHAIN.md

- [ ] **Description**: Update `docs/features/DECISION_CHAIN.md` with new reason codes (REPLAY_BUFFER_ERROR), failopen_completed state machine, and delivery vs decision counter semantics.
- **Files**: `docs/features/DECISION_CHAIN.md`
- **Changes**: New sections for replay buffer, failopen_completed, counter semantics
- **Precondition**: A01.4, A01.5
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: Document accurately reflects new behavior

---

### TASK-A02: Safe Conversion Output Ordering

_Requirement: REQ-0700-CORRECTNESS-002 | Risk Pack: nginx-protocol-safety | PR: PR2_

#### A02.1: Alloc-before-send refactoring

- [ ] **Description**: Restructure the conversion success path in body filter: (1) Allocate body buffer (ngx_palloc), (2) Allocate chain link, (3) Copy FFI markdown to pool buffer, (4) Set buffer properties, (5) Prepare header mutations, (6) Call ngx_http_send_header() exactly once, (7) Send body chain. Steps 1-5 must all succeed before step 6.
- **Files**: `components/nginx-module/src/ngx_http_markdown_conversion_impl.h`
- **Changes**: Restructure conversion success path; add allocation failure → fail-open goto
- **Precondition**: PR1 (A01)
- **Test**: C unit test (A02.6)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit`
- **Completion**: Body buffer allocation precedes header send; allocation failure → fail-open

#### A02.2: Rust FFI output → pool copy before header send

- [ ] **Description**: After FFI returns markdown buffer, allocate pool buffer and copy BEFORE any header operations. This ensures header mutation sees a fully prepared output.
- **Files**: `components/nginx-module/src/ngx_http_markdown_conversion_impl.h`
- **Changes**: Reorder: pool alloc → copy → then header ops
- **Precondition**: A02.1
- **Test**: C unit test (A02.6)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit`
- **Completion**: No path where header is sent before body buffer is ready

#### A02.3: Header mutation only after output resources ready

- [ ] **Description**: Move all header mutations (Content-Type, ETag, Vary) to after allocation and copy steps. Same restructured flow as A02.1.
- **Files**: `components/nginx-module/src/ngx_http_markdown_conversion_impl.h`, `components/nginx-module/src/ngx_http_markdown_headers_impl.h`
- **Changes**: Reorder header mutation calls
- **Precondition**: A02.1
- **Test**: C unit test (A02.6)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit`
- **Completion**: Header mutations execute only after allocation success

#### A02.4: ngx_http_send_header called once (idempotency)

- [ ] **Description**: Add `headers_sent` flag to request context. In header_filter and body_filter, check flag before calling `ngx_http_send_header()`. Set flag after first call. Body filter entry skips send_header if already sent.
- **Files**: `components/nginx-module/src/ngx_http_markdown_headers_impl.h`, `components/nginx-module/src/ngx_http_markdown_filter_module.h`
- **Changes**: New `headers_sent:1` bitfield; guard in send_header calls
- **Precondition**: A02.1
- **Test**: C unit test (A02.7)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit`
- **Completion**: `ngx_http_send_header` called exactly once per request

#### A02.5: Allocation failure documentation

- [ ] **Description**: Document fault-injection approach: `NGX_MARKDOWN_TEST_ALLOC_FAIL=1` env var triggers `ngx_palloc` failure injection in test builds. Document how to test alloc-failure paths.
- **Files**: `docs/testing/FAULT_INJECTION.md` (new) or section in existing testing doc
- **Changes**: New documentation
- **Precondition**: A02.1
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: Fault-injection method documented

#### A02.6: C unit test — header before body

- [ ] **Description**: Write C unit test verifying send_header call precedes body chain send. Verify allocation failure in body buffer → fail-open pass-through, no partial body sent.
- **Files**: `components/nginx-module/tests/unit/output_ordering_test.c`
- **Changes**: New test file
- **Precondition**: A02.1, A02.2, A02.3
- **Test**: This IS the test
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit`
- **Completion**: send_header called before body; alloc failure → no partial body

#### A02.7: C unit test — header send idempotency

- [ ] **Description**: Write C unit test: multiple filter entries → `ngx_http_send_header` called exactly once.
- **Files**: `components/nginx-module/tests/unit/output_ordering_test.c`
- **Changes**: Extend existing test
- **Precondition**: A02.4
- **Test**: This IS the test
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit`
- **Completion**: send_header call count == 1

---

### TASK-A03: Rust Bounded Decompression

_Requirement: REQ-0700-CORRECTNESS-003 | Risk Pack: ffi-boundary | Rules: 3, 32 | PR: PR3_

#### A03.1: Replace input_size*10 with explicit decompression_budget

- [ ] **Description**: Replace `input_size * 10` fixed inflation assumption with explicit `decompression_budget` parameter in Rust `BoundedDecompressor`. Default: `input_len * 20` if budget==0.
- **Files**: `components/rust-converter/src/decompress/bounded.rs` (new)
- **Changes**: New `BoundedDecompressor` struct with budget field; per-chunk budget check
- **Precondition**: PR2 (A02)
- **Test**: Rust unit test (A03.8, A03.9)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: No `input_size * 10` pattern remains; budget read from config

#### A03.2: Bounded growing decompression implementation

- [ ] **Description**: Implement `BoundedDecompressor::decompress()` that decompresses chunk-by-chunk, checking output size against budget after each chunk. If `output.len() > budget`, return `BudgetExceeded`.
- **Files**: `components/rust-converter/src/decompress/bounded.rs`
- **Changes**: Implementation of decompress method with budget enforcement
- **Precondition**: A03.1
- **Test**: Rust unit test (A03.8, A03.9)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make test-rust-fuzz-smoke`
- **Completion**: Decompress stops at budget; returns BudgetExceeded

#### A03.3: DecompressionError enum

- [ ] **Description**: Define `DecompressionError` repr(C) enum: `OK=0, BudgetExceeded=1, FormatError=2, TruncatedInput=3, IoError=4`. New file `decompress/error.rs`.
- **Files**: `components/rust-converter/src/decompress/error.rs` (new), `components/rust-converter/src/decompress/mod.rs` (new or extend)
- **Changes**: New enum with repr(C)
- **Precondition**: A03.1
- **Test**: Rust unit test (A04.5)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Enum defined with all variants; repr(C) confirmed

#### A03.4: Auxiliary buffer cleanup on all exit paths

- [ ] **Description**: Ensure all `match` arms and `?` propagation in `BoundedDecompressor::decompress` drop auxiliary buffers. Verify with test that checks `Drop` was called.
- **Files**: `components/rust-converter/src/decompress/bounded.rs`
- **Changes**: Verify ownership model; add Drop tracking test
- **Precondition**: A03.2
- **Test**: Rust unit test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: No memory leak on any decompression exit path

#### A03.5: FFI — add decompression_budget to MarkdownOptions

- [ ] **Description**: Tail-append `decompression_budget: u64` field to `MarkdownOptions` repr(C) struct. Update `markdown_options_init()` helper to set default 0.
- **Files**: `components/rust-converter/src/ffi/options.rs`, `components/nginx-module/src/ngx_http_markdown_ffi_helpers.c`
- **Changes**: New field in MarkdownOptions (tail-append); update init helper
- **Precondition**: A03.1
- **Test**: Rust layout test (A07.9); C static_assert (A07.10)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make test-nginx-unit && make check-headers`
- **Completion**: Field added to tail; helper sets default; ABI backward-compatible

#### A03.6: FFI — add FFIDecompResult struct

- [ ] **Description**: New `FFIDecompResult` repr(C) struct: `output: *mut u8`, `output_len: usize`, `error: DecompressionError`, `decompressed_size: usize`. Add `markdown_decompress_bounded` FFI function and `markdown_ffidecomp_result_release` helper.
- **Files**: `components/rust-converter/src/ffi/decomp.rs` (new), `components/nginx-module/src/ngx_http_markdown_ffi_helpers.c`
- **Changes**: New struct; new FFI function; new release helper
- **Precondition**: A03.2, A03.3
- **Test**: Rust unit test; C unit test (A03.11)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make test-nginx-unit && make check-headers`
- **Completion**: FFI function callable from C; release helper frees Rust-owned memory

#### A03.7: C — add markdown_decompression_budget directive

- [ ] **Description**: Add NGINX config directive `markdown_decompression_budget <size>;` with default 0. Context: http, server, location. Wire to MarkdownOptions.decompression_budget.
- **Files**: `components/nginx-module/src/ngx_http_markdown_config_directives_impl.h`, `components/nginx-module/src/ngx_http_markdown_config_handlers_impl.h`
- **Changes**: New directive entry; handler parsing size value; merge logic
- **Precondition**: A03.5
- **Test**: C unit test; E2E config test
- **Docs**: Update `docs/guides/CONFIGURATION.md`
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: Directive parsed; value stored; merge works across contexts

#### A03.8: Rust unit test — compression ratio within budget

- [ ] **Description**: Test: compress at 15x ratio with budget 20x → succeeds. Test: compress at 5x ratio → succeeds.
- **Files**: `components/rust-converter/tests/bounded_decompress_test.rs` (new)
- **Changes**: New test file
- **Precondition**: A03.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Test passes; decompress succeeds for ratios within budget

#### A03.9: Rust unit test — budget exceeded

- [ ] **Description**: Test: compress at 25x ratio with budget 20x → `BudgetExceeded` returned. Verify no more data written after budget hit.
- **Files**: `components/rust-converter/tests/bounded_decompress_test.rs`
- **Changes**: Extend test
- **Precondition**: A03.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: BudgetExceeded returned; output size ≤ budget

#### A03.10: Rust fuzz test — malicious/super-budget compressed input

- [ ] **Description**: Add fuzz target for `decompress_bounded` that feeds arbitrary byte sequences. Verify no panic; returns error for invalid input.
- **Files**: `components/rust-converter/fuzz/fuzz_decompress_bounded.rs` (new)
- **Changes**: New fuzz target
- **Precondition**: A03.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust-fuzz-smoke`
- **Completion**: Fuzz runs without panic for 5s

#### A03.11: C unit test — FFI bounded decompress pass-through and budget exceeded

- [ ] **Description**: C unit test: call `markdown_decompress_bounded` with budget-exceeding input → verify fail-open pass-through. Normal input → verify successful decompress.
- **Files**: `components/nginx-module/tests/unit/bounded_decompress_test.c` (new)
- **Changes**: New test file
- **Precondition**: A03.6
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-nginx-unit`
- **Completion**: Budget exceeded → fail-open; normal → decompress success

#### A03.12: Metrics — per-category decompression counters

- [ ] **Description**: Add SHM fields and Prometheus output for: `decompression_budget_exceeded_total`, `decompression_format_error_total`, `decompression_truncated_input_total`, `decompression_io_error_total`. Each incremented by specific error category, NOT aggregated into `conversion_error_total`.
- **Files**: `components/nginx-module/src/ngx_http_markdown_metrics_impl.h`, `components/nginx-module/src/ngx_http_markdown_prometheus_impl.h`
- **Changes**: New SHM fields; new Prometheus output; increment in error paths
- **Precondition**: A03.3, A03.6
- **Test**: C unit test
- **Docs**: Update `docs/guides/prometheus-metrics.md`
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit && make coverage-c`
- **Completion**: All 4 counters exposed; each incremented by correct error category

#### A03.13: Update configuration and decompression docs

- [ ] **Description**: Update `docs/guides/CONFIGURATION.md` with `markdown_decompression_budget` directive. Update `docs/features/DECOMPRESSION.md` with bounded decompression model and error categories.
- **Files**: `docs/guides/CONFIGURATION.md`, `docs/features/DECOMPRESSION.md`
- **Changes**: New directive documentation; bounded model explanation
- **Precondition**: A03.7
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: Docs accurately describe new feature

---

### TASK-A04: Decompression Error Category Preservation

_Requirement: REQ-0700-CORRECTNESS-004 | Risk Pack: ffi-boundary | Rule: 15 | PR: PR3_

#### A04.1: Complete FFI error code mapping

- [ ] **Description**: Ensure each `DecompressionError` variant maps to a unique C FFI constant: `FFI_DECOMPRESSION_OK=0, FFI_DECOMPRESSION_BUDGET_EXCEEDED=1, FFI_DECOMPRESSION_FORMAT_ERROR=2, FFI_DECOMPRESSION_TRUNCATED_INPUT=3, FFI_DECOMPRESSION_IO_ERROR=4`. Add constants to C header.
- **Files**: `components/rust-converter/src/decompress/error.rs`, `components/nginx-module/src/markdown_converter.h`
- **Changes**: C #define constants matching Rust enum values
- **Precondition**: A03.3
- **Test**: Rust unit test (A04.5)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make check-headers`
- **Completion**: Rust variant count == C define count; values match

#### A04.2: Per-category Prometheus counters on C side

- [ ] **Description**: C side must increment the specific counter matching the error category (from FFIDecompResult.error), NOT a generic `conversion_error_total`.
- **Files**: `components/nginx-module/src/ngx_http_markdown_conversion_impl.h`
- **Changes**: Switch statement on error category → increment specific counter
- **Precondition**: A03.12, A04.1
- **Test**: C unit test (A04.6)
- **Docs**: None
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: Each error category increments its own counter

#### A04.3: Log with full error category information

- [ ] **Description**: Replace generic "decompression failed" log with category-specific: `ngx_log_error(NGX_LOG_ERR, ..., "markdown decompression failed: category=%s", decomp_error_name(result.error))`.
- **Files**: `components/nginx-module/src/ngx_http_markdown_conversion_impl.h`
- **Changes**: Category name lookup function; specific log message
- **Precondition**: A04.1
- **Test**: C unit test (verify log content)
- **Docs**: None
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: Log contains full error category name

#### A04.4: Document decompression error categories

- [ ] **Description**: Document all decompression error categories and their semantics in `docs/features/DECOMPRESSION.md`.
- **Files**: `docs/features/DECOMPRESSION.md`
- **Changes**: Error category table with trigger conditions and semantics
- **Precondition**: A04.1
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: All 4 error categories documented with trigger and semantics

#### A04.5: Rust unit test — each error variant returns correct FFI code

- [ ] **Description**: Test each `DecompressionError` variant maps to correct C FFI constant value.
- **Files**: `components/rust-converter/tests/decompress_error_test.rs` (new)
- **Changes**: New test file
- **Precondition**: A04.1
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: All variants map correctly

#### A04.6: C unit test — resource-limit counter increments correctly

- [ ] **Description**: Test: budget-exceeded failure increments `decompression_budget_exceeded_total`, NOT `decompression_format_error_total`.
- **Files**: `components/nginx-module/tests/unit/bounded_decompress_test.c`
- **Changes**: Extend test
- **Precondition**: A04.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: Correct counter incremented for each error category

#### A04.7: FFI mapping closure test

- [ ] **Description**: Test that Rust enum variant count equals C #define count. No unmapped variants.
- **Files**: `components/rust-converter/tests/ffi_closure_test.rs` (new), `components/nginx-module/tests/unit/ffi_closure_test.c` (new)
- **Changes**: New test files
- **Precondition**: A04.1
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make test-nginx-unit`
- **Completion**: Rust variant count == C define count; all values mapped

---

### TASK-A05: Rust Accept Negotiation

_Requirement: REQ-0700-CORRECTNESS-005 | Risk Pack: nginx-protocol-safety | PR: PR4_

#### A05.1: Rust Accept header parser

- [ ] **Description**: Implement Accept header parser: split by `,`, trim, split each by `;`, extract `q=` parameter. Return `Vec<(mime_type, q_value)>`.
- **Files**: `components/rust-converter/src/negotiator/accept.rs` (new), `components/rust-converter/src/negotiator/mod.rs` (new)
- **Changes**: New parser function
- **Precondition**: PR3 (A03, A04)
- **Test**: Rust table-driven test (A05.10)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: Accept header correctly parsed to (mime, q) pairs

#### A05.2: Q-value precise comparison for available variants

- [ ] **Description**: Filter parsed Accept entries to `AVAILABLE_VARIANTS` (text/markdown, text/html). Default q=1.0 for missing q parameter. Compare markdown_q vs html_q.
- **Files**: `components/rust-converter/src/negotiator/accept.rs`
- **Changes**: Comparison logic; filtering to available variants
- **Precondition**: A05.1
- **Test**: Rust table-driven test (A05.10)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: Higher q-value variant selected correctly

#### A05.3: Ignore non-relevant high-priority MIME types

- [ ] **Description**: Ignore MIME types not in AVAILABLE_VARIANTS (e.g., application/json) regardless of q-value, unless all available variants have q=0.
- **Files**: `components/rust-converter/src/negotiator/accept.rs`
- **Changes**: Filtering logic
- **Precondition**: A05.2
- **Test**: Rust table-driven test (A05.10)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: Non-relevant types ignored; markdown/html only considered

#### A05.4: text/markdown;q=0 explicit rejection

- [ ] **Description**: If `text/markdown;q=0`, return `Reject` decision. Link not converted.
- **Files**: `components/rust-converter/src/negotiator/accept.rs`
- **Changes**: Reject path in comparison
- **Precondition**: A05.2
- **Test**: Rust test (q=0 → Reject)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: q=0 → Reject decision returned

#### A05.5: Case-insensitive MIME type and q-parameter

- [ ] **Description**: MIME type comparison is case-insensitive. q-parameter key `q=` is case-insensitive.
- **Files**: `components/rust-converter/src/negotiator/accept.rs`
- **Changes**: `.to_ascii_lowercase()` on MIME types
- **Precondition**: A05.1
- **Test**: Rust test (TEXT/MARKDOWN → Convert)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: Case-insensitive matching works

#### A05.6: markdown_on_wildcard config controls */* behavior

- [ ] **Description**: When Accept contains `*/*`, behavior depends on `markdown_on_wildcard` config: 0=Skip, 1=Convert.
- **Files**: `components/rust-converter/src/negotiator/accept.rs`
- **Changes**: Wildcard handling with on_wildcard parameter
- **Precondition**: A05.2
- **Test**: Rust test (wildcard cases)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: */* behavior controlled by config

#### A05.7: Malformed Accept header stable fallback

- [ ] **Description**: Malformed q-value → treat as q=1.0 with warning. Empty/invalid parts → skip. Overall: never panic on malformed input.
- **Files**: `components/rust-converter/src/negotiator/accept.rs`
- **Changes**: Error-tolerant parsing; never panic
- **Precondition**: A05.1
- **Test**: Rust edge-case test (A05.11)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: No panic on malformed Accept; stable fallback behavior

#### A05.8: FFI — FFIAcceptResult struct

- [ ] **Description**: New `FFIAcceptResult` repr(C) struct: `decision: AcceptDecision`, `markdown_q: f32`, `html_q: f32`, `on_wildcard: u8`. Add `markdown_negotiate_accept` FFI function and `markdown_ffiaccept_result_release` helper.
- **Files**: `components/rust-converter/src/ffi/accept.rs` (new), `components/nginx-module/src/ngx_http_markdown_ffi_helpers.c`
- **Changes**: New struct; new FFI function; new release helper
- **Precondition**: A05.1-A05.7
- **Test**: Rust FFI test; C integration test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make check-headers`
- **Completion**: FFI function callable; result accessible from C

#### A05.9: C — integrate Accept negotiation into header_filter

- [ ] **Description**: Replace existing C-side Accept parsing in header_filter with FFI call to `markdown_negotiate_accept`. Use `FFIAcceptResult.decision` to drive conversion/skip.
- **Files**: `components/nginx-module/src/ngx_http_markdown_accept.c`, `components/nginx-module/src/ngx_http_markdown_header_filter.c`
- **Changes**: Replace C Accept logic with FFI call; handle FFIAcceptResult
- **Precondition**: A05.8
- **Test**: E2E test (A05.12)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit && make test-e2e-rust`
- **Completion**: C-side Accept parsing replaced; FFI drives decision

#### A05.10: Rust table-driven test — key Accept header scenarios

- [ ] **Description**: Table-driven test covering all cases from design.md §8.2: `text/markdown`, `text/html`, `text/markdown;q=0.9, text/html;q=0.8`, `application/json, text/markdown;q=0.9`, `text/markdown;q=0`, `*/*`, missing header, case variations, space-tolerant parsing.
- **Files**: `components/rust-converter/tests/accept_negotiation_test.rs` (new)
- **Changes**: New test file with table-driven cases
- **Precondition**: A05.1-A05.7
- **Test**: This IS the test
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: All 10+ test cases pass

#### A05.11: Rust edge-case test — empty/multi/overlong/illegal headers

- [ ] **Description**: Test: empty header, multi-value header, overlong header (>8KB), illegal q-value, null bytes.
- **Files**: `components/rust-converter/tests/accept_negotiation_test.rs`
- **Changes**: Extend test
- **Precondition**: A05.7
- **Test**: This IS the test
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-rust`
- **Completion**: All edge cases handled without panic

#### A05.12: E2E test — Accept negotiation end-to-end

- [ ] **Description**: Rust E2E test: send request with various Accept headers, verify response Content-Type and body format.
- **Files**: `tools/e2e-harness/src/scenarios/accept_negotiation.rs` (new)
- **Changes**: New E2E scenario
- **Precondition**: A05.9
- **Test**: This IS the test
- **Docs**: None
- **Harness**: nginx-protocol-safety, e2e-migration
- **Verify**: `make test-e2e-rust`
- **Completion**: E2E passes for all Accept negotiation scenarios

---

### TASK-A06: Parse Timeout and Parser Budget

_Requirement: REQ-0700-CORRECTNESS-006 | Risk Pack: runtime-streaming | Rules: 3, 7, 17 | PR: PR5_

#### A06.1: Parse timeout implementation

- [ ] **Description**: Implement `parse_timeout` using `ngx_event_add_timer` on request event. If timer fires before FFI returns, set `ctx->parse_timed_out = 1`. After FFI returns, check flag → fail-open with `PARSE_TIMEOUT`. Default: 30s.
- **Files**: `components/nginx-module/src/ngx_http_markdown_parse_budget.c` (new), `components/nginx-module/src/ngx_http_markdown_parse_budget.h` (new)
- **Changes**: New timer-based timeout mechanism
- **Precondition**: PR4 (A05)
- **Test**: C unit test (A06.7)
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: Parse timeout fires correctly; fail-open with PARSE_TIMEOUT

#### A06.2: Parser memory budget implementation

- [ ] **Description**: Pass `parser_memory_budget` to Rust FFI as part of `MarkdownOptions` (tail-append). Rust side tracks allocated bytes during parsing. If exceeds budget → return `PARSE_BUDGET_EXCEEDED`. Default: 64MB.
- **Files**: `components/rust-converter/src/ffi/options.rs`, `components/rust-converter/src/parser/budget.rs` (new)
- **Changes**: New budget fields in MarkdownOptions; Rust budget tracking
- **Precondition**: A06.1
- **Test**: C unit test (A06.8); Rust test (A06.9)
- **Docs**: None
- **Harness**: runtime-streaming, ffi-boundary
- **Verify**: `make test-rust && make test-nginx-unit-streaming`
- **Completion**: Budget enforced; PARSE_BUDGET_EXCEEDED on over-allocation

#### A06.3: Document parser interrupt feasibility

- [ ] **Description**: Document that Rust HTML parser does not support mid-parse interruption. Alternative: depth limit + node count limit as proxy budget. If true timeout needed, consider separate task with watchdog (future work).
- **Files**: `docs/features/PARSE_BUDGET.md` (new)
- **Changes**: New documentation
- **Precondition**: A06.2
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: Feasibility documented with alternatives

#### A06.4: Distinguish PARSE_TIMEOUT and PARSE_BUDGET_EXCEEDED error codes

- [ ] **Description**: Define two distinct reason codes: `PARSE_TIMEOUT` (8) and `PARSE_BUDGET_EXCEEDED` (9). Ensure they trigger different fail-open messages and metrics.
- **Files**: `components/rust-converter/src/decision/reason_code.rs` (extend), `components/nginx-module/src/markdown_converter.h`
- **Changes**: Two new reason code constants
- **Precondition**: A06.1, A06.2
- **Test**: C unit test (A06.7, A06.8)
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-rust && make test-nginx-unit`
- **Completion**: Two distinct reason codes with distinct metrics

#### A06.5: C — add markdown_parse_timeout and markdown_parser_budget directives

- [ ] **Description**: Add two NGINX config directives: `markdown_parse_timeout <seconds>;` (default 30s) and `markdown_parser_budget <bytes>;` (default 64m). Context: http, server, location.
- **Files**: `components/nginx-module/src/ngx_http_markdown_config_directives_impl.h`, `components/nginx-module/src/ngx_http_markdown_config_handlers_impl.h`
- **Changes**: Two new directive entries with handlers
- **Precondition**: A06.1, A06.2
- **Test**: C unit test
- **Docs**: Update `docs/guides/CONFIGURATION.md`
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: Both directives parsed and wired to MarkdownOptions

#### A06.6: Metrics — parse_timeouts_total and parse_budget_exceeded_total

- [ ] **Description**: Add SHM fields and Prometheus output for `parse_timeouts_total` and `parse_budget_exceeded_total`.
- **Files**: `components/nginx-module/src/ngx_http_markdown_metrics_impl.h`, `components/nginx-module/src/ngx_http_markdown_prometheus_impl.h`
- **Changes**: New SHM fields; new Prometheus output; increment in timeout/budget paths
- **Precondition**: A06.4
- **Test**: C unit test (A06.7, A06.8)
- **Docs**: Update `docs/guides/prometheus-metrics.md`
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: Both metrics exposed and incremented correctly

#### A06.7: C unit test — timeout triggers PARSE_TIMEOUT

- [ ] **Description**: Test: parse timeout fires → PARSE_TIMEOUT reason code + `parse_timeouts_total` incremented + fail-open.
- **Files**: `components/nginx-module/tests/unit/parse_budget_test.c` (new)
- **Changes**: New test file
- **Precondition**: A06.1, A06.4, A06.6
- **Test**: This IS the test
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: Timeout → correct reason code + metric

#### A06.8: C unit test — budget exceeded triggers PARSE_BUDGET_EXCEEDED

- [ ] **Description**: Test: parser allocates beyond budget → PARSE_BUDGET_EXCEEDED + `parse_budget_exceeded_total` incremented + fail-open.
- **Files**: `components/nginx-module/tests/unit/parse_budget_test.c`
- **Changes**: Extend test
- **Precondition**: A06.2, A06.4, A06.6
- **Test**: This IS the test
- **Docs**: None
- **Harness**: runtime-streaming
- **Verify**: `make test-nginx-unit-streaming`
- **Completion**: Budget exceeded → correct reason code + metric

#### A06.9: Rust test — heavy input parsing

- [ ] **Description**: Test: deep nesting (100+ levels), many nodes (100K+). Verify budget enforcement works.
- **Files**: `components/rust-converter/tests/parse_budget_test.rs` (new)
- **Changes**: New test file
- **Precondition**: A06.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Heavy input handled; budget enforced

#### A06.10: Update CONFIGURATION.md

- [ ] **Description**: Document `markdown_parse_timeout` and `markdown_parser_budget` directives with defaults and value ranges.
- **Files**: `docs/guides/CONFIGURATION.md`
- **Changes**: Two new directive sections
- **Precondition**: A06.5
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: Both directives documented

---

### TASK-A07: FFI ABI Layout and Header Drift Check

_Requirement: REQ-0700-CORRECTNESS-007 | Risk Pack: ffi-boundary | Rule: 15 | PR: PR6_

#### A07.1: CI — cbindgen header diff check step

- [ ] **Description**: Add GitHub Actions CI step: build Rust → cbindgen → diff output vs checked-in header. Non-zero exit → FAIL, block PR merge.
- **Files**: `.github/workflows/ci.yml`
- **Changes**: New CI step
- **Precondition**: PR5 (A06)
- **Test**: CI runs on PR; intentional header change → CI fails
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: CI step passes
- **Completion**: CI catches header drift

#### A07.2: C static_assert for struct layout

- [ ] **Description**: Add `ngx_static_assert` for `sizeof(MarkdownOptions)` and `offsetof` for critical fields (decompression_budget, parse_timeout_ms, parser_memory_budget) in C header.
- **Files**: `components/nginx-module/src/markdown_converter.h`
- **Changes**: Static assertions at end of header
- **Precondition**: A03.5, A06.2
- **Test**: C compile test (A07.10)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make build`
- **Completion**: Compile fails if layout drifts

#### A07.3: Rust layout tests

- [ ] **Description**: Add Rust `#[test]` for `size_of::<MarkdownOptions>()`, `align_of`, and `offset_of` for each field. Must match expected values.
- **Files**: `components/rust-converter/tests/layout_tests.rs` (new)
- **Changes**: New test file
- **Precondition**: A03.5, A06.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Layout test passes; matches expected sizes and offsets

#### A07.4: FFI struct helper constructors

- [ ] **Description**: Implement `markdown_options_init()` and `markdown_options_cleanup()` C helpers. Set all defaults. Document: C code MUST use these, never literal init like `MarkdownOptions opts = {0}`.
- **Files**: `components/nginx-module/src/ngx_http_markdown_ffi_helpers.c` (new or extend), `components/nginx-module/src/ngx_http_markdown_ffi_helpers.h`
- **Changes**: New init/cleanup functions for all FFI structs
- **Precondition**: A03.5, A03.6
- **Test**: C unit test
- **Docs**: Document in `docs/architecture/FFI_MIGRATION_CONTRACT.md`
- **Harness**: ffi-boundary
- **Verify**: `make test-nginx-unit`
- **Completion**: All FFI structs have init/cleanup helpers

#### A07.5: Document zero/default initialization strategy

- [ ] **Description**: Document that `*_init()` helpers zero-init and set defaults. C code must use helpers; literal init is forbidden.
- **Files**: `docs/architecture/FFI_MIGRATION_CONTRACT.md`
- **Changes**: New section on init strategy
- **Precondition**: A07.4
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: Strategy documented

#### A07.6: Copied-header sync check

- [ ] **Description**: Ensure `make check-headers` verifies both C header copies are identical.
- **Files**: `Makefile` (check-headers target already exists; verify it covers both copies)
- **Changes**: Verify existing target or enhance
- **Precondition**: None
- **Test**: `make check-headers`
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make check-headers`
- **Completion**: Both header copies verified identical

#### A07.7: Document ABI compatibility assumptions

- [ ] **Description**: Document: tail-append only for new fields, repr(C) required, feature-gated fields must not change layout, major version bump required for field reorder.
- **Files**: `docs/architecture/FFI_MIGRATION_CONTRACT.md`
- **Changes**: ABI compatibility section
- **Precondition**: A07.4
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: ABI assumptions documented

#### A07.8: CI test — header drift detection

- [ ] **Description**: Verify CI step from A07.1 actually blocks on drift. Test: make intentional change to Rust struct, run CI, verify FAIL.
- **Files**: `.github/workflows/ci.yml`
- **Changes**: Verify CI step (manual test or CI dry-run)
- **Precondition**: A07.1
- **Test**: Manual verification
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: CI step blocks on drift
- **Completion**: Header drift → CI FAIL

#### A07.9: Rust test — layout tests pass

- [ ] **Description**: Run layout tests from A07.3 and verify they pass with current struct definitions.
- **Files**: `components/rust-converter/tests/layout_tests.rs`
- **Changes**: Ensure test runs
- **Precondition**: A07.3
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Layout test passes

#### A07.10: C test — static_assert passes

- [ ] **Description**: Verify `make build` succeeds with static_assert checks from A07.2.
- **Files**: `components/nginx-module/src/markdown_converter.h`
- **Changes**: Verify compile
- **Precondition**: A07.2
- **Test**: `make build`
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make build`
- **Completion**: Build succeeds with static_assert

---

## 4. Phase 1: Rust-first Architecture — Coding Tasks

### TASK-B01: C/Rust Boundary Strategy and Migration Contract

_Requirement: REQ-0700-RUST-001 | Risk Pack: ffi-boundary | Rule: 15 | PR: PR7_

#### B01.1: Create FFI_MIGRATION_CONTRACT.md

- [ ] **Description**: Create `docs/architecture/FFI_MIGRATION_CONTRACT.md` with table of all FFI functions: name, ownership (C/Rust/Shared), migration status, compatibility, added-in-version. Populate with existing + 0.7.0 new functions.
- **Files**: `docs/architecture/FFI_MIGRATION_CONTRACT.md` (new)
- **Changes**: New documentation file
- **Precondition**: PR6 (A07)
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: All FFI functions documented with migration status

#### B01.2: Audit C code for Rust-scope logic

- [ ] **Description**: Scan all C files for: Accept parsing, ETag matching, URL validation, header plan construction, decision logic. Mark each with `/* RUST-MIGRATION-CANDIDATE: ... */` comment.
- **Files**: `components/nginx-module/src/*.c`, `components/nginx-module/src/*.h`
- **Changes**: Annotation comments
- **Precondition**: PR6 (A07)
- **Test**: Code review (annotation coverage)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-nginx-unit`
- **Completion**: All Rust-scope logic in C annotated

#### B01.3: Migration priority list

- [ ] **Description**: Create prioritized list from `0.7.0-rust-first-migration-plan.md` §3: P0 (Accept, BoundedDecomp, ErrorCategories) → P1 (ETag, URL, HeaderPlan, DecisionEngine, ReasonCode) → P2 (ConfigValidation).
- **Files**: `docs/architecture/FFI_MIGRATION_CONTRACT.md`
- **Changes**: Priority section
- **Precondition**: B01.1
- **Test**: `make docs-check`
- **Docs**: Extend B01.1 doc
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: Priority list documented

#### B01.4: Update ADR-0001 / Create ADR-0010

- [ ] **Description**: Create ADR-0010: Rust-first C/Rust Boundary Evolution for v0.7.0. Document rationale, boundary rules, migration status.
- **Files**: `docs/architecture/ADR/0010-rust-first-boundary-evolution.md` (new)
- **Changes**: New ADR
- **Precondition**: B01.1
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: ADR created

#### B01.5: CI — migration contract linter (optional)

- [ ] **Description**: Optional: Add CI linter that flags C-side code matching Rust-scope patterns (Accept parsing, decision logic). Can be a grep-based check or Clippy-like tool.
- **Files**: `tools/harness/check_rust_first_contract.py` (new)
- **Changes**: New linter script
- **Precondition**: B01.2
- **Test**: Script runs; flags violations
- **Docs**: None
- **Harness**: harness-remediation
- **Verify**: `python3 tools/harness/check_rust_first_contract.py`
- **Completion**: Linter detects C-side Rust-scope logic

---

### TASK-B02: Rust Conditional Request/ETag

_Requirement: REQ-0700-RUST-002 | Risk Pack: ffi-boundary | Rules: 15, 27 | PR: PR8_

#### B02.1: Rust If-None-Match parsing

- [ ] **Description**: Implement If-None-Match parser: parse ETag list, support strong and weak comparison (RFC 7232 §2.3.2). Return `FFIConditionalResult` with `is_not_modified` flag.
- **Files**: `components/rust-converter/src/conditional/etag.rs` (new), `components/rust-converter/src/conditional/mod.rs` (new)
- **Changes**: New ETag parser and comparison logic
- **Precondition**: PR7 (B01)
- **Test**: Rust unit test (B02.6)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: If-None-Match parsed; strong/weak comparison works

#### B02.2: Rust If-Modified-Since parsing

- [ ] **Description**: Parse HTTP-date format (RFC 7231 §7.1.1.1). Compare with Last-Modified timestamp.
- **Files**: `components/rust-converter/src/conditional/modified.rs` (new)
- **Changes**: New date parser and comparison
- **Precondition**: B02.1
- **Test**: Rust unit test (B02.7)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: If-Modified-Since compared correctly

#### B02.3: Conversion-generated weak ETag (W/"hash")

- [ ] **Description**: After conversion, compute SHA-256 of markdown output → `W/"<hex_prefix_16>"`. Return via `FFIConditionalResult.new_etag`.
- **Files**: `components/rust-converter/src/conditional/etag.rs`
- **Changes**: ETag generation from hash
- **Precondition**: B02.1
- **Test**: Rust unit test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Valid W/"hash" ETag generated

#### B02.4: FFI — FFIConditionalResult struct

- [ ] **Description**: New `FFIConditionalResult` repr(C) struct: `is_not_modified: u8`, `new_etag: *mut c_char`, `new_etag_len: usize`, `reason_code: u32`. Add `markdown_check_conditional` FFI function and `markdown_fficonditional_result_release` helper.
- **Files**: `components/rust-converter/src/ffi/conditional.rs` (new), `components/nginx-module/src/ngx_http_markdown_ffi_helpers.c`
- **Changes**: New struct; FFI function; release helper
- **Precondition**: B02.1, B02.2, B02.3
- **Test**: Rust FFI test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make check-headers`
- **Completion**: FFI function callable from C

#### B02.5: C — integrate conditional request into decision chain

- [ ] **Description**: Replace C-side conditional logic with FFI call. C still calls `ngx_http_finalize_request(r, NGX_HTTP_NOT_MODIFIED)` for 304.
- **Files**: `components/nginx-module/src/ngx_http_markdown_conditional.c`
- **Changes**: Replace C logic with FFI call
- **Precondition**: B02.4
- **Test**: E2E test (B02.8)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit && make test-e2e-rust`
- **Completion**: Conditional request handled via Rust FFI; 304 works

#### B02.6: Rust unit test — ETag strong/weak comparison

- [ ] **Description**: Test: strong comparison (exact match), weak comparison (opaque-tag match), W/"abc" vs W/"abc", W/"abc" vs "abc".
- **Files**: `components/rust-converter/tests/etag_test.rs` (new)
- **Changes**: New test file
- **Precondition**: B02.1
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: All ETag comparison cases pass

#### B02.7: Rust unit test — If-Modified-Since time boundaries

- [ ] **Description**: Test: exact match, 1s before, 1s after, invalid date format.
- **Files**: `components/rust-converter/tests/modified_test.rs` (new)
- **Changes**: New test file
- **Precondition**: B02.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Time boundary cases pass

#### B02.8: E2E test — conditional request 304 verification

- [ ] **Description**: Rust E2E test: send If-None-Match, verify 304 response. Send If-Modified-Since, verify 304.
- **Files**: `tools/e2e-harness/src/scenarios/conditional_requests.rs` (new)
- **Changes**: New E2E scenario
- **Precondition**: B02.5
- **Test**: This IS the test
- **Docs**: None
- **Harness**: nginx-protocol-safety, e2e-migration
- **Verify**: `make test-e2e-rust`
- **Completion**: 304 returned for matching conditional requests

---

### TASK-B03: Rust URL and Forwarded Host Validation

_Requirement: REQ-0700-RUST-003 | Risk Pack: ffi-boundary | Rule: 27 | PR: PR9_

#### B03.1: URL/Host control character rejection

- [ ] **Description**: Implement `validate_url()`: reject URLs containing C0 control chars (U+0000-U+001F except HT=0x09, LF=0x0A, CR=0x0D). Return `UrlValidationError` with specific error type.
- **Files**: `components/rust-converter/src/validation/url.rs` (new), `components/rust-converter/src/validation/mod.rs` (new)
- **Changes**: New validation function
- **Precondition**: PR7 (B01)
- **Test**: Rust unit test (B03.6)
- **Docs**: None
- **Harness**: ffi-boundary, output-safety
- **Verify**: `make test-rust`
- **Completion**: Control chars rejected; valid URLs pass

#### B03.2: X-Forwarded-Host/Proto parsing

- [ ] **Description**: Parse X-Forwarded-Host and X-Forwarded-Proto headers. Construct base URL: `{proto}://{host}`. Return as `FFIUrlResult.base_url`.
- **Files**: `components/rust-converter/src/validation/forwarded.rs` (new)
- **Changes**: New parsing and URL construction function
- **Precondition**: B03.1
- **Test**: Rust unit test (B03.7)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Base URL correctly constructed from forwarded headers

#### B03.3: Link emission Markdown escape

- [ ] **Description**: Escape `]` → `%5D`, `[` → `%5B` in link labels and destinations. Prevent Markdown injection via link text.
- **Files**: `components/rust-converter/src/validation/escape.rs` (new)
- **Changes**: New escape function
- **Precondition**: B03.1
- **Test**: Rust unit test (B03.8)
- **Docs**: None
- **Harness**: output-safety
- **Verify**: `make test-rust`
- **Completion**: Link labels/destinations escaped; injection prevented

#### B03.4: Reject URL with control char → link not emitted

- [ ] **Description**: When URL validation fails due to control chars, the link must not be emitted in output. Integrate with streaming emitter.
- **Files**: `components/rust-converter/src/streaming/emitter.rs`
- **Changes**: Guard link emission on URL validation result
- **Precondition**: B03.1, B03.3
- **Test**: Rust unit test
- **Docs**: None
- **Harness**: output-safety
- **Verify**: `make test-rust`
- **Completion**: Invalid URLs → no link in output

#### B03.5: FFI — expose URL validation and base URL construction

- [ ] **Description**: Add `markdown_validate_url` FFI function returning `FFIUrlResult`. Add `markdown_ffiurl_result_release` helper.
- **Files**: `components/rust-converter/src/ffi/url.rs` (new), `components/nginx-module/src/ngx_http_markdown_ffi_helpers.c`
- **Changes**: New FFI function; new release helper
- **Precondition**: B03.1, B03.2
- **Test**: Rust FFI test; C integration test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make check-headers`
- **Completion**: FFI function callable from C

#### B03.6: Rust unit test — control character rejection

- [ ] **Description**: Test URLs with each C0 control char. Verify rejection for U+0000-U+001F (except HT/LF/CR). Verify pass for HT/LF/CR.
- **Files**: `components/rust-converter/tests/url_validation_test.rs` (new)
- **Changes**: New test file
- **Precondition**: B03.1
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: All control char cases handled correctly

#### B03.7: Rust unit test — X-Forwarded-* parsing

- [ ] **Description**: Test: single host, multiple hosts (first wins), missing proto defaults to http, missing host returns error.
- **Files**: `components/rust-converter/tests/forwarded_test.rs` (new)
- **Changes**: New test file
- **Precondition**: B03.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: X-Forwarded-* parsing cases pass

#### B03.8: Rust unit test — link escape injection defense

- [ ] **Description**: Test: link destination with `]`, `[`, backticks. Verify escaped output prevents Markdown injection.
- **Files**: `components/rust-converter/tests/escape_test.rs` (new)
- **Changes**: New test file
- **Precondition**: B03.3
- **Test**: This IS the test
- **Docs**: None
- **Harness**: output-safety
- **Verify**: `make test-rust`
- **Completion**: All injection attempts neutralized

#### B03.9: Rust fuzz test — URL validation malformed input

- [ ] **Description**: Fuzz target for `validate_url` with arbitrary bytes. Verify no panic.
- **Files**: `components/rust-converter/fuzz/fuzz_url_validation.rs` (new)
- **Changes**: New fuzz target
- **Precondition**: B03.1
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust-fuzz-smoke`
- **Completion**: Fuzz runs without panic for 5s

---

### TASK-B04: Rust Response Header Plan Prototype

_Requirement: REQ-0700-RUST-004 | Risk Pack: ffi-boundary | Rules: 15, 29 | PR: PR10_

#### B04.1: HeaderPlan struct

- [ ] **Description**: Implement `FFIHeaderPlan` (entries array, count) and `FFIHeaderEntry` (action: Set/Delete/Modify, name, value) repr(C) structs. Implement `HeaderAction` repr(C) enum.
- **Files**: `components/rust-converter/src/header/plan.rs` (new), `components/rust-converter/src/header/mod.rs` (new)
- **Changes**: New struct and enum definitions
- **Precondition**: PR8, PR9 (B02, B03)
- **Test**: Rust unit test (B04.5)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: HeaderPlan struct usable

#### B04.2: Plan construction for conversion scenarios

- [ ] **Description**: Build plan for conversion: Set Content-Type: text/markdown; charset=utf-8, Delete Content-Length, Set Transfer-Encoding: chunked, Set ETag (from B02), Set Vary: Accept. Build plan for pass-through: empty or minimal.
- **Files**: `components/rust-converter/src/header/plan.rs`
- **Changes**: Plan construction logic for both scenarios
- **Precondition**: B04.1
- **Test**: Rust unit test (B04.5)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Correct plan built for convert and pass-through

#### B04.3: FFI — FFIHeaderPlan struct

- [ ] **Description**: Add `markdown_build_header_plan` FFI function and `markdown_ffiheader_plan_release` helper.
- **Files**: `components/rust-converter/src/ffi/header_plan.rs` (new), `components/nginx-module/src/ngx_http_markdown_ffi_helpers.c`
- **Changes**: New FFI function; release helper
- **Precondition**: B04.2
- **Test**: Rust FFI test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make check-headers`
- **Completion**: FFI function callable from C

#### B04.4: C — header plan atomic application

- [ ] **Description**: Implement `markdown_apply_header_plan()`: validate all operations first, then apply all. If any fails → rollback (undo what was done). Note: NGINX header list doesn't support true rollback, so validate-first, apply-once.
- **Files**: `components/nginx-module/src/ngx_http_markdown_headers_impl.h`
- **Changes**: New function to apply header plan atomically
- **Precondition**: B04.3
- **Test**: C unit test (B04.6)
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit`
- **Completion**: All header mutations applied atomically

#### B04.5: Rust unit test — header plan construction

- [ ] **Description**: Test: conversion plan contains correct entries. Pass-through plan is minimal/empty.
- **Files**: `components/rust-converter/tests/header_plan_test.rs` (new)
- **Changes**: New test file
- **Precondition**: B04.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Plan entries match expected

#### B04.6: C unit test — header plan atomic application and rollback

- [ ] **Description**: Test: all entries applied successfully. Test: partial failure → rollback (or fail-open).
- **Files**: `components/nginx-module/tests/unit/header_plan_test.c` (new)
- **Changes**: New test file
- **Precondition**: B04.4
- **Test**: This IS the test
- **Docs**: None
- **Harness**: nginx-protocol-safety
- **Verify**: `make test-nginx-unit`
- **Completion**: Atomic application verified; rollback tested

---

### TASK-B05: Rust Decision Engine Phase 1

_Requirement: REQ-0700-RUST-005 | Risk Pack: ffi-boundary | Rules: 7, 23 | PR: PR10_

#### B05.1: DecisionContext struct

- [ ] **Description**: Define `DecisionContext` repr(C) struct: `accept_result`, `conditional_result`, `is_eligible`, `has_accept_header`, `error_policy`, `decompression_error`.
- **Files**: `components/rust-converter/src/decision/engine.rs` (new), `components/rust-converter/src/decision/mod.rs` (new)
- **Changes**: New context struct
- **Precondition**: B02, B03 (for accept_result, conditional_result types)
- **Test**: Rust unit test (B05.6)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Context struct defined

#### B05.2: make_decision pure function

- [ ] **Description**: Implement `markdown_make_decision(ctx: &DecisionContext) -> FFIDecisionResult` pure function. Same input always produces same output. No side effects.
- **Files**: `components/rust-converter/src/decision/engine.rs`
- **Changes**: Decision logic implementation
- **Precondition**: B05.1
- **Test**: Rust unit test (B05.6, B05.7)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Decision function is pure; idempotent

#### B05.3: Decision path coverage — all branches produce reason code

- [ ] **Description**: Every branch in `make_decision` must produce a `ReasonCode`: CONVERTED, SKIPPED_ACCEPT, SKIPPED_NO_ACCEPT, SKIPPED_CONDITIONAL, FAILED_DECOMPRESSION_*, PARSE_TIMEOUT, PARSE_BUDGET_EXCEEDED, REPLAY_BUFFER_ERROR, FFI_CALL_ERROR. No MISSING_REASON_CODE path.
- **Files**: `components/rust-converter/src/decision/engine.rs`, `components/rust-converter/src/decision/reason_code.rs`
- **Changes**: Ensure all branches covered
- **Precondition**: B05.2
- **Test**: Rust unit test (B05.7)
- **Docs**: None
- **Harness**: ffi-boundary, observability-metrics
- **Verify**: `make test-rust`
- **Completion**: Every decision path has reason code

#### B05.4: log_decision() callsite documentation

- [ ] **Description**: Document each reason code's `log_decision()` callsite (file:line where the decision is logged).
- **Files**: `docs/features/DECISION_CHAIN.md`
- **Changes**: Callsite mapping table
- **Precondition**: B05.3
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: All reason codes have documented callsites

#### B05.5: FFI — expose make_decision and get_reason_code_string

- [ ] **Description**: Add `markdown_make_decision` FFI function returning `FFIDecisionResult`. Add `markdown_get_reason_code_string` FFI function returning `*const c_char` (static, no free needed).
- **Files**: `components/rust-converter/src/ffi/decision.rs` (new), `components/nginx-module/src/ngx_http_markdown_ffi_helpers.c`
- **Changes**: New FFI functions
- **Precondition**: B05.2
- **Test**: Rust FFI test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust && make check-headers`
- **Completion**: Both FFI functions callable from C

#### B05.6: Rust unit test — idempotent decision

- [ ] **Description**: Test: same DecisionContext → same FFIDecisionResult. Run 100 times with same input.
- **Files**: `components/rust-converter/tests/decision_engine_test.rs` (new)
- **Changes**: New test file
- **Precondition**: B05.2
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Decision is idempotent

#### B05.7: Rust unit test — every path produces valid reason code

- [ ] **Description**: Test: enumerate all DecisionContext combinations → verify each produces a valid, defined ReasonCode (not MISSING).
- **Files**: `components/rust-converter/tests/decision_engine_test.rs`
- **Changes**: Extend test
- **Precondition**: B05.3
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary, observability-metrics
- **Verify**: `make test-rust`
- **Completion**: All decision paths have reason codes

#### B05.8: Update FFI_MIGRATION_CONTRACT.md

- [ ] **Description**: Add `markdown_make_decision` and `markdown_get_reason_code_string` to FFI migration contract table.
- **Files**: `docs/architecture/FFI_MIGRATION_CONTRACT.md`
- **Changes**: New entries in function table
- **Precondition**: B05.5
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: New FFI functions documented

---

### TASK-B06: Reason-code and Metrics Normalization

_Requirement: REQ-0700-RUST-006 | Risk Pack: ffi-boundary | Rules: 7, 8, 8b, 8c, 23 | PR: PR11_

#### B06.1: Rust enum as reason-code single source

- [ ] **Description**: Define `ReasonCode` repr(C) enum in Rust as the single source of truth. All values: Converted=0, SkippedAccept=1, SkippedNoAccept=2, SkippedConditional=3, FailedDecompressionBudget=4, FailedDecompressionFormat=5, FailedDecompressionTruncated=6, FailedDecompressionIo=7, ParseTimeout=8, ParseBudgetExceeded=9, ReplayBufferError=10, FfiCallError=11.
- **Files**: `components/rust-converter/src/decision/reason_code.rs` (new or extend)
- **Changes**: New enum definition
- **Precondition**: B05.3
- **Test**: Rust unit test (B06.8)
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: ReasonCode enum is single source of truth

#### B06.2: C side accesses reason codes via FFI only

- [ ] **Description**: Remove all `#define MARKDOWN_REASON_*` constants from C code. C side calls `markdown_get_reason_code_string(code)` for logging and metrics labels.
- **Files**: `components/nginx-module/src/markdown_converter.h`, `components/nginx-module/src/*.c`
- **Changes**: Remove C constants; replace with FFI calls
- **Precondition**: B06.1, B05.5
- **Test**: C unit test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-nginx-unit`
- **Completion**: No independent C reason code constants

#### B06.3: Prometheus metric name consistency

- [ ] **Description**: Verify every Prometheus metric name in Rust matches the Prometheus output format exactly. Add validator: `assert_eq!(rust_metric_name, prometheus_output_name)`.
- **Files**: `components/rust-converter/src/metrics/*.rs`, `tools/release/gates/validate_release_gates_070.py`
- **Changes**: Name alignment; validator
- **Precondition**: B06.1
- **Test**: Validator runs
- **Docs**: Update `docs/guides/prometheus-metrics.md`
- **Harness**: observability-metrics
- **Verify**: `make release-gates-check`
- **Completion**: All metric names consistent

#### B06.4: Separate delivery_counter and decision_counter Prometheus series

- [ ] **Description**: Expose `markdown_decisions_total{reason="..."}` and `markdown_delivery_total` as separate Prometheus time series. delivery only incremented on downstream NGX_OK.
- **Files**: `components/nginx-module/src/ngx_http_markdown_prometheus_impl.h`
- **Changes**: Separate output lines for both counters
- **Precondition**: A01.5
- **Test**: C unit test (B06.9)
- **Docs**: Update `docs/guides/prometheus-metrics.md`
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: Both series visible in /metrics

#### B06.5: Update all renderers (JSON, text, Prometheus)

- [ ] **Description**: Ensure JSON, text, and Prometheus renderers output both `decisions_total` and `delivery_total`.
- **Files**: `components/nginx-module/src/ngx_http_markdown_metrics_impl.h`
- **Changes**: Update all renderer paths
- **Precondition**: B06.4
- **Test**: C unit test
- **Docs**: None
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: All renderers output both counters

#### B06.6: Update prometheus-metrics.md

- [ ] **Description**: Document all new metrics with names, types, units, labels, and lifecycle.
- **Files**: `docs/guides/prometheus-metrics.md`
- **Changes**: New metric sections
- **Precondition**: B06.3, B06.4
- **Test**: `make docs-check`
- **Docs**: This IS the doc update
- **Harness**: docs-tooling-drift
- **Verify**: `make docs-check`
- **Completion**: All new metrics documented

#### B06.7: Add release-gate validator for new reason codes and metrics

- [ ] **Description**: Add validators in `tools/release/gates/validate_release_gates_070.py` that check: all Rust enum variants have C define, all metrics match Rust keys, all reason codes have docs.
- **Files**: `tools/release/gates/validate_release_gates_070.py` (new)
- **Changes**: New validator script
- **Precondition**: B06.1, B06.3
- **Test**: `make release-gates-check`
- **Docs**: None
- **Harness**: docs-tooling-drift
- **Verify**: `make release-gates-check`
- **Completion**: Validators cover all new surfaces

#### B06.8: Rust unit test — reason code enum closure

- [ ] **Description**: Test: all ReasonCode variants have unique values. Test: count matches expected. No duplicates.
- **Files**: `components/rust-converter/tests/reason_code_test.rs` (new)
- **Changes**: New test file
- **Precondition**: B06.1
- **Test**: This IS the test
- **Docs**: None
- **Harness**: ffi-boundary
- **Verify**: `make test-rust`
- **Completion**: Enum is closed and unique

#### B06.9: C unit test — delivery ≠ decision in fail-open scenario

- [ ] **Description**: Test: fail-open request → decisions=1, delivery=0. Normal request → decisions=1, delivery=1.
- **Files**: `components/nginx-module/tests/unit/metrics_delivery_test.c`
- **Changes**: Extend test from A01.12
- **Precondition**: B06.4
- **Test**: This IS the test
- **Docs**: None
- **Harness**: observability-metrics
- **Verify**: `make test-nginx-unit`
- **Completion**: delivery < decisions in fail-open; delivery == decisions in normal

---

## 5. Phase 2: Package Distribution — Coding Tasks

_Tasks C01-C06 are primarily packaging/CI/docs work. Each subtask follows the same format._

### TASK-C01: Package Distribution Strategy (C01.1-C01.4)

- [ ] **C01.1**: Create `docs/guides/PACKAGE_DISTRIBUTION.md` — channels, strategy, non-goals, ABI sensitivity, default-safe install
- [ ] **C01.2**: Add non-official repository disclaimer with self-hosted repo URL
- [ ] **C01.3**: Document NGINX dynamic module ABI sensitivity and version binding
- [ ] **C01.4**: Document default-safe installation (module installed but not loaded)

_Verify: `make docs-check` | Precondition: PR11 | PR: PR12_

### TASK-C02: Package Matrix and Metadata Model (C02.1-C02.4)

- [ ] **C02.1**: Create `packaging/matrix.yaml` — OS × arch × nginx_channel × libc (5 OS × 2 arch × 2 nginx = 20 combinations)
- [ ] **C02.2**: Define package metadata schema — name, version, arch, depends, description, license, maintainer
- [ ] **C02.3**: Add CI matrix validation step
- [ ] **C02.4**: Document build matrix and release matrix

_Verify: `make docs-check && CI matrix validate` | Precondition: C01 | PR: PR12_

### TASK-C03: DEB Package Build Pipeline (C03.1-C03.7)

- [ ] **C03.1**: Create `packaging/debian/` directory (control, postinst, postrm, conffiles, rules, changelog.Debian)
- [ ] **C03.2**: Implement DEB build script (dpkg-deb)
- [ ] **C03.3**: Implement NGINX ABI dependency declaration in control file
- [ ] **C03.4**: Implement postinst/postrm scripts (load_module snippet, conservative enable — commented out by default)
- [ ] **C03.5**: Create `.github/workflows/release-deb.yml` with full matrix strategy
- [ ] **C03.6**: Test: `dpkg-deb --info` and `--contents` validation
- [ ] **C03.7**: Test: `dpkg -i` install + `nginx -V` verification

_Verify: DEB smoke test | Precondition: C02 | PR: PR12 | Harness: packaging-distribution_

### TASK-C04: RPM Package Build Pipeline (C04.1-C04.6)

- [ ] **C04.1**: Create `packaging/rpm/` directory (SPECS, SOURCES)
- [ ] **C04.2**: Implement RPM SPEC file (BuildRequires, Requires, %post/%postun, %changelog)
- [ ] **C04.3**: Implement NGINX ABI dependency declaration
- [ ] **C04.4**: Create `.github/workflows/release-rpm.yml` with matrix strategy
- [ ] **C04.5**: Test: `rpm -qip` and `rpmbuild --specfile` validation
- [ ] **C04.6**: Test: `rpm -i` install + `nginx -V` verification

_Verify: RPM smoke test | Precondition: C02 | PR: PR12 | Harness: packaging-distribution_

### TASK-C05: APT/RPM Repository and GPG Signing (C05.1-C05.5)

- [ ] **C05.1**: Implement GPG signing pipeline (packages + repository metadata)
- [ ] **C05.2**: Implement APT repository structure (dists/<codename>/... per matrix)
- [ ] **C05.3**: Implement YUM repository structure (repodata/repomd.xml)
- [ ] **C05.4**: Document GPG key acquisition and key rotation process
- [ ] **C05.5**: Test: `gpg --verify` + `apt-get update` + `yum makecache`

_Verify: Signature verification | Precondition: C03, C04 | PR: PR12 | Harness: packaging-distribution_

### TASK-C06: Installation/Upgrade/Rollback/Troubleshooting Docs (C06.1-C06.5)

- [ ] **C06.1**: Create `docs/guides/PACKAGE_INSTALLATION.md` — per-OS install steps
- [ ] **C06.2**: Create post-install verification command docs (nginx -V + curl test)
- [ ] **C06.3**: Document upgrade flow (dpkg -i / rpm -U + nginx -s reload)
- [ ] **C06.4**: Document rollback flow (dpkg -i old / rpm -U --oldpackage)
- [ ] **C06.5**: Create troubleshooting guide (ABI mismatch, signature failure, module load failure)

_Verify: `make docs-check` | Precondition: C01 | PR: PR12 | Harness: docs-tooling-drift_

---

## 6. Phase 3: Kubernetes/Ingress — Coding Tasks

### TASK-D01: K8s/Ingress Strategy (D01.1-D01.3)

- [ ] **D01.1**: Create `docs/guides/KUBERNETES_DEPLOYMENT.md` — "reference example, not production-ready" disclaimer
- [ ] **D01.2**: List verified Ingress Controllers (OSS NGINX) and unverified (F5 — pending D04)
- [ ] **D01.3**: Document enable/verify/disable/rollback steps

_Verify: `make docs-check` | Precondition: PR12 | PR: PR13 | Harness: packaging-distribution_

### TASK-D02: Custom Image Build (D02.1-D02.4)

- [ ] **D02.1**: Create `examples/kubernetes/Dockerfile.ingress` — parameterized NGINX Ingress Controller base image
- [ ] **D02.2**: Implement module compilation and injection into Ingress Controller image
- [ ] **D02.3**: Test: `docker build` + `docker run --rm <image> nginx -V`
- [ ] **D02.4**: Document build steps and customization guide

_Verify: Docker build test | Precondition: D01 | PR: PR13 | Harness: packaging-distribution_

### TASK-D03: K8s Manifests and Helm Chart (D03.1-D03.4)

- [ ] **D03.1**: Create `examples/kubernetes/manifest/` — Deployment, ConfigMap, Service, Ingress, namespace
- [ ] **D03.2**: Extend `charts/nginx-markdown/` — update Chart.yaml and values.yaml for v0.7.0
- [ ] **D03.3**: Add ConfigMap module configuration example (including dynconf)
- [ ] **D03.4**: Test: `kubectl apply --dry-run=client` + `helm lint` + `helm template`

_Verify: `helm lint && kubectl apply --dry-run` | Precondition: D02 | PR: PR13 | Harness: packaging-distribution_

### TASK-D04: F5 NGINX Ingress Controller Feasibility (D04.1-D04.3)

- [ ] **D04.1**: Assess F5 NGINX Ingress Controller custom module injection method
- [ ] **D04.2**: Record feasibility conclusion, injection method, known limitations, alternatives
- [ ] **D04.3**: Create `docs/guides/F5_INGRESS_FEASIBILITY.md`

_Verify: `make docs-check` | Precondition: D01 | PR: PR13 | Harness: packaging-distribution_

### TASK-D05: K8s Smoke and E2E (D05.1-D05.4)

- [ ] **D05.1**: Create K8s smoke test script (conversion + Accept negotiation + metrics) — bash 3.2 compatible
- [ ] **D05.2**: Create E2E scenario script (deploy, config update, rolling upgrade, rollback, scale)
- [ ] **D05.3**: Test: smoke script execution → conversion returns Markdown, Accept negotiation correct, /metrics accessible
- [ ] **D05.4**: Document smoke and E2E execution methods

_Verify: Smoke + E2E scripts | Precondition: D03 | PR: PR13 | Harness: packaging-distribution_

---

## 7. Phase 4: Operability + Release Gates — Coding Tasks

### TASK-E01: Runtime Explainability and Diagnostics (E01.1-E01.7)

- [ ] **E01.1**: Create `components/nginx-module/src/ngx_http_markdown_diagnostics.c/.h` — diagnostics endpoint
- [ ] **E01.2**: Implement structured decision path log (accept_result, conditional_result, conversion_status, reason_code, duration_ms)
- [ ] **E01.3**: Implement `/nginx-markdown/diagnostics` endpoint (config_snapshot + recent_decisions + metrics_snapshot + dynconf_state)
- [ ] **E01.4**: Add `markdown_diagnostics on | off;` and `markdown_diagnostics_allow <CIDR>;` directives
- [ ] **E01.5**: Default: deny external access (explicit allow required)
- [ ] **E01.6**: C unit test: diagnostics endpoint output format and content verification
- [ ] **E01.7**: E2E test: diagnostics endpoint access and content verification

_Verify: `make test-nginx-unit && make test-nginx-integration && make coverage-c` | Precondition: PR6 | PR: PR14 | Harness: dynamic-config-hot-reload_

### TASK-E02: Dynconf Dry-run (E02.1-E02.6)

- [ ] **E02.1**: Implement `markdown_dynconf_dry_run on | off;` directive
- [ ] **E02.2**: Implement dry-run mode: validate new config but do not apply (do not replace active_snapshot)
- [ ] **E02.3**: Implement validation result with full details (line number, field, error reason) via `FFIValidationResult`
- [ ] **E02.4**: C unit test: dry-run validation success/failure scenarios
- [ ] **E02.5**: C unit test: dry-run does NOT replace active_snapshot
- [ ] **E02.6**: Update `docs/guides/CONFIGURATION.md`

_Verify: `make test-nginx-unit && make coverage-c` | Precondition: PR6 | PR: PR14 | Harness: dynamic-config-hot-reload | Rules: 34, 35_

### TASK-E03: Dynconf Current Snapshot Introspection (E03.1-E03.4)

- [ ] **E03.1**: Implement active_snapshot query mechanism (all config item names and current values) via diagnostics endpoint
- [ ] **E03.2**: Ensure header_filter entry bind-once semantics unchanged
- [ ] **E03.3**: C unit test: snapshot query returns complete config
- [ ] **E03.4**: C unit test: reload concurrent with request → snapshot consistency

_Verify: `make test-nginx-unit && make coverage-c` | Precondition: E01 | PR: PR14 | Harness: dynamic-config-hot-reload | Rules: 34, 35_

### TASK-E04: Dynconf Last-known-good and Rollback (E04.1-E04.8)

- [ ] **E04.1**: Implement last_known_good preservation: on successful reload, previous version → LKG
- [ ] **E04.2**: Implement manual rollback trigger: `active_snapshot ← last_known_good`; increment `dynconf_rollbacks_total` metric
- [ ] **E04.3**: Ensure `applied_mtime` only updated after reload success (Rule 35)
- [ ] **E04.4**: Ensure unknown dynconf key → NGX_ERROR (Rule 35)
- [ ] **E04.5**: C unit test: LKG preservation and rollback
- [ ] **E04.6**: C unit test: applied_mtime unchanged on validation failure
- [ ] **E04.7**: E2E test: reload + rollback scenario
- [ ] **E04.8**: Update `docs/guides/DYNAMIC_CONFIG.md`

_Verify: `make test-nginx-unit && make verify-streaming-failure-cache-e2e && make coverage-c` | Precondition: E02 | PR: PR14 | Harness: dynamic-config-hot-reload | Rule: 35_

### TASK-E05: Docs/Release Gates/Harness Alignment (E05.1-E05.12)

- [ ] **E05.1**: Create `tools/release/gates/validate_release_gates_070.py`
- [ ] **E05.2**: Update `docs/project/release-gates/` with 0.7.0 gate definitions
- [ ] **E05.3**: Update Makefile with `release-gates-check-070` target
- [ ] **E05.4**: Update README.md / README_zh-CN.md: version number, new features
- [ ] **E05.5**: Update `docs/guides/CONFIGURATION.md`: all new directives
- [ ] **E05.6**: Update `docs/guides/INSTALLATION.md`: package manager install path
- [ ] **E05.7**: Update `docs/architecture/SYSTEM_ARCHITECTURE.md`: new subsystems
- [ ] **E05.8**: Update `docs/guides/prometheus-metrics.md`: new metrics
- [ ] **E05.9**: Update AGENTS.md: new 0.7.0 rules and checklist items
- [ ] **E05.10**: Update `docs/harness/routing-manifest.json`: new risk pack and verification family (if any)
- [ ] **E05.11**: Update CHANGELOG.md [0.7.0] section
- [ ] **E05.12**: Update GitHub Actions workflow path filters

_Verify: `make docs-check && make release-gates-check-070 && make harness-check-full` | Precondition: All prior E tasks | PR: PR14 | Harness: docs-tooling-drift_

### TASK-E06: Validator Coverage (E06.1-E06.6)

- [ ] **E06.1**: Config validators: decompression_budget, parse_timeout, parser_memory_budget, markdown_diagnostics, markdown_dynconf_dry_run
- [ ] **E06.2**: Metric validators: decompression_*_total, parse_*_total, replay_buffer_errors_total
- [ ] **E06.3**: Reason-code validators: Rust enum + C define + docs + metrics four-way alignment
- [ ] **E06.4**: Package metadata validators: DEB control fields, RPM SPEC fields
- [ ] **E06.5**: K8s manifest validators: `helm lint` + `kubectl apply --dry-run`
- [ ] **E06.6**: Integrate all new validators into `release-gates-check-070`

_Verify: `make release-gates-check-strict` | Precondition: E05 | PR: PR14 | Harness: docs-tooling-drift_

---

## 8. Phase 5: Final Validation — Coding Tasks

### TASK-F01: FFI ABI Final Verification (F01.1-F01.4)

- [ ] **F01.1**: Confirm all new FFI struct fields appended to tail (no reorder)
- [ ] **F01.2**: Confirm both C header copies identical (`make check-headers`)
- [ ] **F01.3**: Confirm all helper constructors and cleanup helpers cover new fields
- [ ] **F01.4**: Run layout tests and static_assert (`make test-rust && make build`)

_Verify: `make build && make check-headers && make test-rust && make test-nginx-unit` | Precondition: All PR1-PR14 | PR: PR15_

### TASK-F02: Configuration Compatibility (F02.1-F02.3)

- [ ] **F02.1**: Config migration test: 0.6.x config runs under 0.7.0 with identical semantics
- [ ] **F02.2**: Verify new default values behave correctly (decompression_budget=0 → input*20, parse_timeout=30s, parser_budget=64m)
- [ ] **F02.3**: Verify deprecated directive grace period

_Verify: E2E compatibility test suite | Precondition: All PR1-PR14 | PR: PR15_

### TASK-F03: Coverage Gate (F03.1-F03.3)

- [ ] **F03.1**: Run `make coverage-c`, confirm aggregate ≥ 80%, critical ≥ 90%
- [ ] **F03.2**: Run `make coverage-rust`, confirm aggregate ≥ 80%, critical ≥ 90%
- [ ] **F03.3**: If any new `#[ignore]` tests exist, run `cargo test -- --ignored` at least once

_Verify: `make coverage-c && make coverage-rust` | Precondition: All PR1-PR14 | PR: PR15_

### TASK-F04: Harness Full Verification (F04.1-F04.3)

- [ ] **F04.1**: Run `make harness-check-full`, confirm PASS
- [ ] **F04.2**: Run `make docs-check`, confirm PASS
- [ ] **F04.3**: Run `make release-gates-check-strict`, confirm PASS

_Verify: `make harness-check-full && make docs-check && make release-gates-check-strict` | Precondition: All PR1-PR14 | PR: PR15_

### TASK-F05: Version and Changelog (F05.1-F05.5)

- [ ] **F05.1**: Update Cargo.toml version to 0.7.0
- [ ] **F05.2**: Update C header version macros
- [ ] **F05.3**: Update CHANGELOG.md [0.7.0] section
- [ ] **F05.4**: Update `docs/project/PROJECT_STATUS.md`
- [ ] **F05.5**: Update `docs/project/VERSION_PLANNING.md`

_Verify: `make docs-check && make release-gates-check-070` | Precondition: All PR1-PR14 | PR: PR15_

---

## 9. CI/CD Integration Plan

### New GitHub Actions Workflows

| Workflow | File | Purpose | Matrix | Trigger |
|----------|------|---------|--------|---------|
| DEB build | `.github/workflows/release-deb.yml` | Build DEB packages for full OS × arch × nginx matrix | 5 OS × 2 arch × 2 nginx = 20 jobs | Tag push, manual |
| RPM build | `.github/workflows/release-rpm.yml` | Build RPM packages for full OS × arch × nginx matrix | 2 RPM OS × 2 arch × 2 nginx = 8 jobs | Tag push, manual |
| FFI header drift | (add step to `.github/workflows/ci.yml`) | cbindgen header diff check | N/A | Every PR |
| Coverage gate | (add step to `.github/workflows/ci.yml`) | Enforce coverage ≥ 80%/90% | N/A | Every PR |
| Release gates 070 | (add step to `.github/workflows/ci.yml`) | `make release-gates-check-070` | N/A | Every PR |

### CI Matrix Strategy

```yaml
# release-deb.yml matrix
strategy:
  fail-fast: false
  matrix:
    include:
      - os: ubuntu-22.04, arch: amd64, nginx: stable, libc: glibc
      - os: ubuntu-22.04, arch: arm64, nginx: stable, libc: glibc
      - os: ubuntu-22.04, arch: amd64, nginx: mainline, libc: glibc
      - os: ubuntu-22.04, arch: arm64, nginx: mainline, libc: glibc
      - os: ubuntu-24.04, arch: amd64, nginx: stable, libc: glibc
      - os: ubuntu-24.04, arch: arm64, nginx: stable, libc: glibc
      - os: ubuntu-24.04, arch: amd64, nginx: mainline, libc: glibc
      - os: ubuntu-24.04, arch: arm64, nginx: mainline, libc: glibc
      - os: debian-12,   arch: amd64, nginx: stable, libc: glibc
      - os: debian-12,   arch: arm64, nginx: stable, libc: glibc
      - os: debian-12,   arch: amd64, nginx: mainline, libc: glibc
      - os: debian-12,   arch: arm64, nginx: mainline, libc: glibc

# release-rpm.yml matrix
strategy:
  fail-fast: false
  matrix:
    include:
      - os: almalinux-9,    arch: amd64, nginx: stable, libc: glibc
      - os: almalinux-9,    arch: arm64, nginx: stable, libc: glibc
      - os: almalinux-9,    arch: amd64, nginx: mainline, libc: glibc
      - os: almalinux-9,    arch: arm64, nginx: mainline, libc: glibc
      - os: amazon-linux-2023, arch: amd64, nginx: stable, libc: glibc
      - os: amazon-linux-2023, arch: arm64, nginx: stable, libc: glibc
      - os: amazon-linux-2023, arch: amd64, nginx: mainline, libc: glibc
      - os: amazon-linux-2023, arch: arm64, nginx: mainline, libc: glibc
```

### Coverage Gate Configuration

```yaml
# In ci.yml
- name: Coverage gate
  run: |
    make coverage-c
    make coverage-rust
    make coverage-gate
  env:
    COVERAGE_C_MIN_LINE: 80
    COVERAGE_C_MIN_FUNC: 80
    COVERAGE_RUST_MIN_LINE: 80
    COVERAGE_RUST_MIN_FUNC: 80
```

### Release Gate Automation

```yaml
# In ci.yml
- name: Release gates 0.7.0
  run: make release-gates-check-070

- name: FFI header drift check
  run: |
    make rust-lib
    make check-headers
```

---

## 10. Risk Mitigation Plan

| Risk ID | Risk | Primary Pack | Likelihood | Impact | Mitigation Strategy | Rollback | Monitoring |
|---------|------|-------------|-----------|--------|-------------------|---------|------------|
| R01 | Pending chain overwritten on NGX_AGAIN | runtime-streaming | Medium | High | A01.2-A01.3: explicit persistence + last_buf guard + unit test + E2E | git revert PR1 | `make test-nginx-unit-streaming` |
| R02 | Body sent before headers | nginx-protocol-safety | Low | High | A02.1-A02.4: alloc-before-send + headers_sent flag + unit test | git revert PR2 | `make test-nginx-unit` |
| R03 | Zip bomb via decompression | ffi-boundary | Medium | High | A03.1-A03.2: explicit budget + per-chunk check + fuzz test | git revert PR3 | `decompression_budget_exceeded_total` metric |
| R04 | Rust error category lost in FFI mapping | ffi-boundary | Medium | Medium | A04.1-A04.7: complete enum mapping + closure test + CI drift check | git revert PR3 | `make test-rust` (closure test) |
| R05 | Accept negotiation wrong MIME type | nginx-protocol-safety | Low | Medium | A05.1-A05.12: table-driven test + RFC compliance + E2E | git revert PR4 | `make test-e2e-rust` |
| R06 | Parser OOM on huge document | runtime-streaming | Low | High | A06.1-A06.2: parse timeout + memory budget + fail-open | git revert PR5 | `parse_timeouts_total` + `parse_budget_exceeded_total` |
| R07 | FFI struct layout drift → memory corruption | ffi-boundary | Low | Critical | A07.1-A07.10: cbindgen CI + static_assert + layout test + helpers | git revert PR6 | `make check-headers` (CI) |
| R08 | C-side new pure logic violates Rust-first | ffi-boundary | Medium | Medium | B01.2-B01.5: audit + migration contract + CI linter | git revert PR7 | `check_rust_first_contract.py` |
| R09 | DEB/RPM NGINX ABI mismatch | packaging-distribution | Medium | High | C02-C04: build matrix + ABI dependency + install smoke | Remove package from repo | Install smoke CI |
| R10 | Helm chart K8s API version incompatible | packaging-distribution | Low | Medium | D03.4: helm lint + kubectl dry-run | git revert PR13 | `helm lint` CI |
| R11 | Dynconf reload race with request | dynamic-config-hot-reload | Low | High | E03.2-E03.4: snapshot bind-once + consistency test | git revert PR14 | `make test-nginx-unit` |
| R12 | New reason code/metric missing validator | docs-tooling-drift | Medium | Medium | E06.1-E06.6: validators + release-gates-check-070 | git revert PR14 | `make release-gates-check-strict` |

---

## 11. Timeline and Critical Path

### Hard Dependencies

```
Phase 0 (A01-A07) → Phase 1 (B01-B06) → Phase 2 (C01-C06) → Phase 3 (D01-D05)
                                                                              ↓
Phase 0 (A01-A07) → Phase 4 (E01-E06) ──────────────────────────────────→ Phase 5 (F01-F05)
```

### Parallel Execution Groups

| Group | Can Run In Parallel | Estimated Time |
|-------|-------------------|----------------|
| Group 1 | Phase 0 tasks (A01-A07 are sequential within phase due to internal deps) | 2-3 weeks |
| Group 2a | Phase 1 (B01-B06) — sequential after Phase 0 | 2-3 weeks |
| Group 2b | Phase 4 (E01-E06) — can run in parallel with Phase 1 | 2-3 weeks |
| Group 3 | Phase 2 (C01-C06) — after Phase 1 | 1-2 weeks |
| Group 4 | Phase 3 (D01-D05) — after Phase 2 | 1 week |
| Group 5 | Phase 5 (F01-F05) — after all | 2-3 days |

### Critical Path

```
A01 → A02 → A03+A04 → A05 → A06 → A07 → B01 → B02 → B04+B05 → B06 → C01-C06 → D01-D05 → F01-F05
                                  ↘ B03 ↗
```

**Critical path length**: ~10-12 weeks (sequential)
**With parallelization**: ~7-9 weeks (Phase 4 parallel with Phase 1-3)

### Estimated Effort by Phase

| Phase | Tasks | Subtasks | Est. Time (sequential) | Est. Time (with parallel) |
|-------|-------|----------|----------------------|--------------------------|
| Phase 0 | 7 | 47 | 3-4 weeks | 3-4 weeks |
| Phase 1 | 6 | 35 | 2-3 weeks | 2-3 weeks |
| Phase 2 | 6 | 27 | 1-2 weeks | 1-2 weeks |
| Phase 3 | 5 | 14 | 1 week | 1 week |
| Phase 4 | 6 | 39 | 2-3 weeks | (parallel with 1-3) |
| Phase 5 | 5 | 17 | 2-3 days | 2-3 days |
| **Total** | **35** | **179** | **9-13 weeks** | **7-9 weeks** |

---

## 12. Task Dependency Graph (text-based)

```
A01.1 ──► A01.2 ──► A01.3 ──► A01.8
              │          │
              ├──► A01.4 ──► A01.10
              ├──► A01.5 ──► A01.12
              ├──► A01.6 ──► A01.11
              └──► A01.7

A01 ──► A02.1 ──► A02.2 ──► A02.3 ──► A02.6
              │                       │
              ├──► A02.4 ──► A02.7   │
              └──► A02.5             │

A02 ──► A03.1 ──► A03.2 ──► A03.8, A03.9
              │          │
              ├──► A03.3 ──► A04.1
              ├──► A03.4
              ├──► A03.5 ──► A03.7
              └──► A03.6 ──► A03.11

A03 ──► A04.1 ──► A04.2 ──► A04.6
              │          │
              ├──► A04.3    │
              ├──► A04.4    │
              ├──► A04.5    │
              └──► A04.7    │

A04 ──► A05.1 ──► A05.2 ──► A05.8 ──► A05.9 ──► A05.12
              │          │
              ├──► A05.3    ├──► A05.10
              ├──► A05.4    ├──► A05.11
              ├──► A05.5
              ├──► A05.6
              └──► A05.7

A05 ──► A06.1 ──► A06.2 ──► A06.7, A06.8
              │          │
              ├──► A06.3    ├──► A06.9
              ├──► A06.4
              ├──► A06.5
              └──► A06.6

A06 ──► A07.1 ──► A07.8
              │
              ├──► A07.2 ──► A07.10
              ├──► A07.3 ──► A07.9
              ├──► A07.4 ──► A07.5, A07.7
              └──► A07.6

A07 ──► B01.1 ──► B01.3 ──► B01.4
              │
              ├──► B01.2 ──► B01.5

B01 ──► B02.1 ──► B02.2 ──► B02.4 ──► B02.5 ──► B02.8
              │          │
              └──► B02.3    ├──► B02.6
                           └──► B02.7

B01 ──► B03.1 ──► B03.2 ──► B03.5
              │          │
              ├──► B03.3    ├──► B03.7
              ├──► B03.4    └──► B03.9
              ├──► B03.6
              └──► B03.8

B02+B03 ──► B04.1 ──► B04.2 ──► B04.3 ──► B04.4 ──► B04.6
                                     │
                                     └──► B04.5

B02+B03 ──► B05.1 ──► B05.2 ──► B05.3 ──► B05.5
                                     │
                                     ├──► B05.6
                                     ├──► B05.7
                                     └──► B05.4 ──► B05.8

B04+B05 ──► B06.1 ──► B06.2
              │
              ├──► B06.3 ──► B06.7
              ├──► B06.4 ──► B06.5
              ├──► B06.6
              ├──► B06.8
              └──► B06.9

B06 ──► C01 ──► C02 ──► C03 ──► C05
                            │
                            └──► C04 ──► C05

C05 ──► C06

C06 ──► D01 ──► D02 ──► D03 ──► D05
                            │
                            └──► D04

A07 ──► E01 ──► E02 ──► E03 ──► E04 ──► E05 ──► E06

All ──► F01
All ──► F02
All ──► F03
All ──► F04
All ──► F05
```

---

## 13. Document Updates

| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.7.0-draft | 2026-05-17 | spec-tasks-agent | Initial v0.7.0 coding task plan: 15 PRs, 179 subtasks, PR breakdown, per-task detail, CI/CD, risk mitigation, timeline |
