# Plan Review Panel — ops-20-statements-m4

**Plan**: `.claude/plans/ops-20-statements-m4.plan.md` · **Plan version**: `sha256:5bbcc3e800e902e88e9810aa8cb6f8782e27ba4e0cadae583c990edd8e3e8a93`
**Verdict**: `divergent` via `multi-agent`
**Quorum**: 4/3 responses · 4 distinct roles (of 4 fielded) · passed=false
**Layers**: L1 converged · L2 divergent · L3 not fired
**Halted at**: `5.2e`

> Reason: L2 quorum not satisfied: 11 blocking finding(s): architect/CRITICAL, architect/CRITICAL, architect/HIGH, architect/HIGH

## Findings

| Perspective | Severity | Claim | Evidence |
|---|---|---|---|
| architect | CRITICAL | Plan cites `ops/statements/check-statements.mjs:1132~1133` as containing `CENSUS_ID` · `CENSUS_FENCE` constant pairs to mirror for the carryover design pattern | Line 274 of plan states: '\| Naming \| `ops/statements/check-statements.mjs:1132` \| `CENSUS_ID` · `CENSUS_FENCE` 상수 쌍(1132~1133)'. When read, lines 1132-1133 contain `const samePath = ...` and `const IS_ENTRY = ...` logic, not any CENSUS constants. Grep for CENSUS in check-statements.mjs returns zero matches. |
| architect | CRITICAL | Plan's Design Decision D1 and 'Patterns to Mirror' section assume M3 created a `--census` command-line handler and census-related functions that can be mirrored for the `--carryover` implementation | M3 plan line 285-286 shows usage: `node ops/statements/check-statements.mjs --census > ...`. Current main() function in check-statements.mjs (lines 1113-1127) shows only three supported commands: --list-tokens, --fixture-tree, and --check. No --census command exists. Grep for '--census' in check-statements.mjs returns zero matches. |
| architect | HIGH | Plan's 'Patterns to Mirror' section (line 278-282) claims to cite existing patterns at `ops/statements/check-statements.mjs:1093` for `emit` enum pattern and `ops/cells/check-cells.mjs:1575` for similar constants. The statements.mjs line numbers are inaccurate, and if patterns don't exist as described, the plan's foundation of 'mirroring proven patterns' rather than inventing new abstractions is broken | Plan line 278 states logging pattern at `:1093` references `for (const r of SKIP_REASONS)` emit. When read, line 1093 is a blank closing line of a const assignment. Grep for 'SKIP_REASONS' and 'emit(' in check-statements.mjs returns zero matches. The check-cells.mjs patterns do exist (line 1575 has `CK_CENSUS_FENCE`), but the statements.mjs analog doesn't, breaking the mirror premise. |
| architect | HIGH | Plan's Task 2 assumes `buildCarryover(root)` can be designed by mirroring `buildCensus` from M3, but Design Decision D11 states 'Task 4 가 판정 표를 적는다' (Task 4 will write the judgment table). This creates a dependency ordering problem: how can Task 2 build the carryover machinery without knowing the table format that Task 4 will define? | Task 2 (line 320) describes building carryover output format while Task 4 (line 338-343) is described as creating the table structure. The plan says at D11 (line 260-263): 'census 의 D11 과 같다. 값을 손으로 적으면...' but it doesn't establish whether Task 2's buildCarryover function already assumes Task 4's table schema exists. Task 2 Validate says 'rc 0' without defining what the 37-row table structure is first. |
| test | MEDIUM | The plan specifies that 4 new tokens (OPS_INV_FINDINGS_CARRYOVER, OPS_INV_NUMBER_TOKEN_SOURCE, OPS_INV_NUMBER_SIGN_BOUNDARY, OPS_INV_HOST_USER_PATH) will be added to the code | Plan line 176-183 (D6) lists the 4 new tokens. However, none of the 13 Tasks (lines 313-414) explicitly state where these tokens get added to the TOKENS array in check-statements.mjs:32-71. The Validation block (line 599) assumes `--list-tokens` will output these tokens, and lines 600-602 test for their presence, but the implementation path for adding them to TOKENS is implicit, not explicit. This creates a gap between plan assertion and testable requirement. |
| test | MEDIUM | Design decision D13 (lines 200-214) claims the regex must be exactly `[^\|]*` not `(.*)` because the latter would incorrectly accept 4-column malformed rows | The plan explains at lines 632-633 of Validation that if the regex was changed to `(.*)`, the `carryover-row-malformed` case 'should pass when it shouldn't'. However, the actual Validation bash code (lines 629-634) only tests the normal case (regex is `[^\|]*` and case fails). The contrapositive test described in the plan commentary is not implemented in the actual bash validation. If someone changed the regex to `(.*)`, the current Validation would not catch it during normal execution. |
| test | LOW | Task 1's Validate section (line 317) states it will check six baseline metrics against the §Summary table | The Validation block at lines 442-477 implements this check by parsing the plan document itself. However, this is a self-referential test: it reads baseline values from the plan's §Summary (which states 143 statements, 127 stated, 16 inferred, 132 rows_covered, 84 rows_skipped, 27 findings_census_rows) and verifies the current code output matches. While I verified these numbers do match the current state, the plan provides no independent verification that these baseline numbers are actually correct initially. The test can detect if baseline drifts, but relies on the plan's documented figures being accurate. |
| test | LOW | The plan claims Task 5 will create a `### A1 도출 영향 관측` section documenting which records moved due to number token source restrictions | Task 5 description (lines 345-350) says to record moved record IDs in the F-OPS-20-M4 section. However, Task 5 doesn't create the F-OPS-20-M4 section itself - Task 4 does (line 338). This creates an implicit task ordering dependency: Task 4 must complete before Task 5's implementation can add to that section. While the task order in the plan is sequential, this dependency isn't explicitly documented as a prerequisite. |
| invariant | CRITICAL | Multiple tasks defer their gate verification to Task 10, creating a skip predicate where intermediate tasks (4, 6, 8, 9) with no gates can pass their Validates while earlier deferred gates are unproven | Plan line 336 (Task 3 Gate): 'Task 10 이 초록이 되기 전에는 이 Task 를 완료로 적지 않는다', line 350 (Task 5 Gate): same statement, line 364 (Task 7 Gate): same statement. But Task 4 line 343: '- **Gate**: (없음)', Task 6 line 357: '- **Gate**: (없음)', Task 8 line 371: '- **Gate**: (없음)', Task 9 line 378: '- **Gate**: (없음)'. This ordering allows Tasks 4, 6, 8, 9 to run their Validates and succeed while the earlier deferred gates remain unevaluated, violating HALT vs degrade boundary. |
| invariant | HIGH | The emit position check (carryover_rows and carryover_open before verdict) is claimed in Task 3 Validate but not actually verified until Validation step 6b, which only runs after all tasks complete. Tasks 4-9 can pass their Validates while emit is in wrong position | Plan line 333-335: Task 3 says emit is critical and 'Task 4~8 의 Validate 가 없는 줄을 `grep` 하며 조용히 통과한다' if emit is late. But Task 3 Validate at line 335 claims to check this, yet neither Task 3 nor 4-9 actually verify the line ordering. Verification only happens at Validation step 6b (lines 534-541), which runs after all tasks. This is a **skip predicate**. |
| invariant | HIGH | Wire field carryover_ids_sha256 is vulnerable to self-corroboration: if the hash calculation is wrong in buildCarryover (Task 2), the re-derivation in Validation step 5b will produce the same wrong value and both will match. The gap from Task 2 to Validation step 5b is unprotected | Plan lines 500-505 (Validation step 5b): explicitly admits '같은 생산자의 산출끼리 비교하므로, sha 계산이 틀리면 양쪽이 똑같이 틀린 값을 들고 통과한다 — 이 저장소가 "wire 필드는 자기를 검증 못 한다" 로 이미 아는 형태다.' This acknowledges the defect but does not prevent it during task execution. The 'answer' (re-derivation) only runs at the very end. |
| invariant | HIGH | Baseline drift at Task 1 creates unresolved dependency: if §Summary values drift from live report, Task 1 stops with BASELINE_DRIFT, but the plan says recovery requires 'construtor가 손으로' (implementer manually) updating plan and re-anchoring receipt. This creates a potential receipt anchor erosion if manual recovery is deferred or skipped | Plan line 318 (Task 1 Gate): 'BASELINE_DRIFT 로 **멈추기만** 한다 — 계획서 정수 갱신과 receipt 재앵커는 구현자가 손으로 하는 별개 행위'. Line 859 Risk: '복구가 계획서 편집이라 `plan_hash` 가 움직여 receipt 가 stale 이 된다'. The gate stops but provides no mechanism to enforce that the receipt is re-anchored or to detect if it's not. |

## Refutation attempted

| Perspective | Verdict | What was attacked |
|---|---|---|
| architect | fail | Attacked the plan's architectural foundation by verifying: 1. **Citation accuracy**: Searched for and read the exact lines cited in "Patterns to Mirror" section — found that specific line numbers (1132-1133, 1093) do not contain the code patterns the plan claims they do. 2. **Pattern existence**: Grepped check-statements.mjs for census-related code patterns (CENSUS, --census, buildCensus, emit, SKIP_REASONS) that M3 supposedly created and M4 claims to mirror — found zero matches, indicating either M3 was not fully implemented or the patterns are mislocated. 3. **Abstraction boundary**: Verified that M3 plan claims to create `--census` command (line 285 of M3 plan) but that command doesn't exist in current main() function (lines 1113-1127), which only supports --list-tokens, --fixture-tree, and --check. 4. **Design dependency chain**: Examined whether Tasks 2-4 have correct ordering when Task 2 supposedly builds abstraction based on patterns that either don't exist yet (M3 incomplete) or citations are wrong (lines misidentified). The plan fails its own "Patterns to Mirror" architectural principle because the patterns it cites are either not present in the codebase or are located at different lines than specified. |
| security | pass | Attempted to find trust boundary violations, bypass paths for user path detection pattern, CR normalization inconsistencies causing security drift, partial-state trust issues in carryover verdict field. Tested concrete hostile scenarios: UNC paths, relative paths with Users/ component, placeholder notation False-positive cases, environment variable forms, Unix-style paths. Verified pattern matching correctness for case variations and separator styles on Windows. Checked that regex `[a-z0-9_-]` with `-i` flag correctly matches actual usernames while rejecting ellipsis notation. Could not construct a scenario where the pattern would miss actual Windows absolute paths with disclosable usernames or incorrectly reject legitimate placeholder documentation. |
| test | fail | Searched for the 4 new tokens in check-statements.mjs TOKENS array (lines 32-71) — they don't exist yet, which is expected. Verified that none of the 13 Tasks explicitly state where they should be added. Examined the Validation block's bash code (lines 629-634) which tests carryover-row-malformed case but doesn't implement the contrapositive test described in plan commentary. Verified baseline metrics (143 statements, 127 stated/16 inferred) match current state but found no independent verification of their correctness in the test. Confirmed Task 4 creates F-OPS-20-M4 section and Task 5 adds A1 subsection, creating implicit ordering. Checked that makeFixtureTree already exists at line 2027, so Task 9 has adequate reference implementation. |
| invariant | fail | I systematically attacked the plan through the invariant erosion lens: (1) Skip predicates - traced task gate dependency graph and found Tasks 4, 6, 8, 9 have no gates while Tasks 3, 5, 7 defer to Task 10, creating a window where intermediate tasks can pass without earlier gates being proven. (2) Gate ordering - verified that emit position verification is deferred to Validation step 6b, which runs after all tasks, allowing Tasks 4-9 to succeed with wrong emit placement. (3) Wire field verification - examined carryover_ids_sha256 and confirmed it's vulnerable to same-producer self-corroboration until step 5b at the very end. (4) Anchoring - traced baseline drift recovery path and found it requires manual action with no enforcement mechanism. (5) HALT vs degrade - verified that Tasks 4-9 can degrade silently (pass Validates) while earlier gates fail to block. |

## Measurement

<!-- Written by plan-review/cli.js record on EVERY exit path, pass or halt.
     Machine-readable; do not hand-edit. A null field means the axis was
     not observed, never that it was zero. -->

```json
{
  "verdict": "divergent",
  "source": "multi-agent",
  "layers": {
    "l1": "converged",
    "l2": "divergent",
    "l3": "not fired"
  },
  "quorum": {
    "responded": 4,
    "required": 3,
    "roles": 4,
    "of": 4,
    "passed": false
  },
  "wall_clock_ms": 345715,
  "halt_stage": "5.2e",
  "backlog_appended": null,
  "backlog_skipped_nonblocking": null,
  "granted": 4,
  "reviewed_plan_hash": "sha256:5bbcc3e800e902e88e9810aa8cb6f8782e27ba4e0cadae583c990edd8e3e8a93",
  "plan_path": ".claude/plans/ops-20-statements-m4.plan.md",
  "recorded_at": "2026-08-31T02:47:42.511Z"
}
```
