From fad36ef9de6227460aa6bd44707c9cb671e3918f Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Mon, 15 Jun 2026 16:14:31 -0500 Subject: [PATCH 1/4] fix(qa-gate): terminate no-FAIL diagnosis phase instead of looping (#655 sub-defect B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The QA remediation gate had no clean terminal state for a phase that legitimately has zero FAIL checks and zero code changes — a read-only diagnosis / documentation-disposition round. `source_verification_missing` was exempted for `input_mode=none` only at `_gate_stage=done`, but a round must clear the `verify` stage to reach `done`. So such a round was flagged `qa_gate_source_verification_missing=true` at `verify` and routed `REMEDIATION_REQUIRED` forever; the only escape observed in the field was manually advancing `verify -> done`. Add a stage-agnostic exemption that fires only when the round genuinely has nothing to remediate, using a precise predicate (not the corroboration- dependent METADATA_ONLY_ROUND, which reads non-metadata edits as metadata-only when no git evidence is available): - input_mode=none and no source verification path, AND - zero fail-classifications and zero registry known issues, AND - the recorded files are all planning metadata, AND - the phase's authoritative verification resolves on disk and is a clean PASS with zero FAIL rows and zero pre-existing/known issues (new helper `phase_baseline_is_clean`). Rounds that carry fail-classifications, carry known issues, recorded any non-metadata edit, have no phase verification on disk (a continuation round that lost its source), or whose phase verification carries pre-existing issues do NOT satisfy the predicate and still fail closed. Scope: addresses sub-defect B only. Sub-defect A (change-evidence under planning_tracking=manual) does not reproduce on current main — plan-amendment evidence is corroborated under both tracking modes and CHANGE_EVIDENCE_ UNAVAILABLE only routes REMEDIATION when ROUND_CODE_FIX_COUNT>0. Sub-defect C (deviation_override / accepted-deviation disposition) is a separate concern. Tests: - qa-result-gate.bats: the no-classification metadata-only round on a clean baseline now PROCEEDs (was the loop); a non-metadata edit on a clean baseline still fails closed. Existing fail-closed cases (missing phase verification, missing prior-round verification, carried known issues, classified process-exceptions) remain REMEDIATION_REQUIRED. Refs #655. Co-Authored-By: Claude Opus 4.8 --- scripts/qa-result-gate.sh | 59 +++++++++++++++++++++++++++++++++++++++ tests/qa-result-gate.bats | 55 ++++++++++++++++++++++++++++++++++-- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/scripts/qa-result-gate.sh b/scripts/qa-result-gate.sh index b18f2a7f..d58ef093 100755 --- a/scripts/qa-result-gate.sh +++ b/scripts/qa-result-gate.sh @@ -1088,6 +1088,41 @@ classification_ids_cover_source_fail_ids() { return 0 } +# Detect a clean phase-level terminal baseline (issue #655): the phase's +# authoritative verification resolves on disk, is a PASS result, lists zero FAIL +# rows, and carries zero pre-existing/known issues. This identifies a no-FAIL +# disposition phase (diagnosis / documentation) that has genuinely nothing left +# to remediate, distinguishing it from a continuation round that simply lost its +# source verification (no phase verification on disk) or a phase that still +# carries unresolved pre-existing issues. +phase_baseline_is_clean() { + local phase_dir="${1:-}" phase_v result preexist + [ -n "$phase_dir" ] || return 1 + phase_v=$(bash "$SCRIPT_DIR/resolve-verification-path.sh" phase "$phase_dir" 2>/dev/null || true) + [ -n "$phase_v" ] && [ -r "$phase_v" ] || return 1 + result=$(extract_frontmatter_scalar_value "$phase_v" result) + case "$result" in + PASS|pass|Pass) ;; + *) return 1 ;; + esac + [ "$(count_fail_rows_in_verification "$phase_v")" -eq 0 ] 2>/dev/null || return 1 + preexist=$(awk -F'|' ' + function trim(v){ gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); return v } + /^## Pre-existing Issues/ { found=1; next } + found && /^## / { exit } + found && /^\|/ { + if ($0 ~ /^\|[[:space:]-]+(\|[[:space:]-]+)+\|?[[:space:]]*$/) next + t=trim($2); f=trim($3) + if (tolower(t)=="test" && tolower(f)=="file") next + if (t=="") next + count++ + } + END { print count+0 } + ' "$phase_v" 2>/dev/null) + [ "${preexist:-0}" -eq 0 ] 2>/dev/null || return 1 + return 0 +} + extract_frontmatter_json_object_array() { local file_path="${1:-}" local key_name="${2:-}" @@ -1980,6 +2015,30 @@ if [ "$IN_REMEDIATION" = "true" ] && [ "$SUMMARY_SCOPE_DIR" != "$PHASE_DIR" ]; t ROUND_SOURCE_VERIFICATION_MISSING="false" fi + # No-FAIL documentation / plan-amendment disposition (issue #655 sub-defect B): + # a round that classifies nothing (zero fail-classifications), carries no + # known issues, recorded only planning-metadata files, and has no source + # verification path has, by design, no FAIL source to anchor against — there + # is nothing to remediate. Exempt it from the source-missing check at ANY + # stage so the verify stage does not deadlock and force an unbounded re-run + # (the round could otherwise escape only by manually advancing verify -> done). + # + # The discriminator is the RECORDED files (not METADATA_ONLY_ROUND, which is + # corroboration-dependent and reads non-metadata edits as metadata-only when no + # git evidence is available) plus a clean phase-level baseline. A round that + # carries fail-classifications or known issues, recorded any non-metadata file, + # or whose phase verification is absent / not-PASS / carries FAIL rows or + # pre-existing issues does NOT satisfy this and still fails closed. + if [ "${ROUND_INPUT_MODE:-none}" = "none" ] \ + && [ -z "$SOURCE_VERIFICATION_PATH" ] \ + && [ "${ROUND_CLASSIFICATION_TYPE_COUNT:-0}" -eq 0 ] 2>/dev/null \ + && [ "${KNOWN_ISSUES_COUNT:-0}" -eq 0 ] 2>/dev/null \ + && [ -n "$ROUND_RECORDED_STRUCTURAL_PATHS" ] \ + && ! paths_include_non_metadata "$PHASE_DIR" <<< "$ROUND_RECORDED_STRUCTURAL_PATHS" \ + && phase_baseline_is_clean "$PHASE_DIR"; then + ROUND_SOURCE_VERIFICATION_MISSING="false" + fi + if paths_include_process_exception_evidence "$PHASE_DIR" <<< "$ROUND_ALL_RECORDED_PATHS"; then ROUND_PROCESS_EXCEPTION_EVIDENCE_VALID=true elif [ "$METADATA_ONLY_ROUND" = "true" ] \ diff --git a/tests/qa-result-gate.bats b/tests/qa-result-gate.bats index 240addda..11c7497b 100644 --- a/tests/qa-result-gate.bats +++ b/tests/qa-result-gate.bats @@ -2231,7 +2231,15 @@ VERIF [[ "$output" == *"qa_gate_routing=PROCEED_TO_UAT"* ]] } -@test "metadata-only round with missing fail_classifications → REMEDIATION_REQUIRED" { +# Issue #655 (sub-defect B): a no-classification, metadata-only round on a phase +# whose authoritative verification is a clean PASS (0 FAIL, 0 pre-existing) has +# genuinely nothing to remediate, so it must terminate (PROCEED_TO_UAT) rather +# than deadlock at the verify stage on source_verification_missing. A historical +# phase-root SUMMARY deviation does not force remediation — phase-root deviations +# are already treated as historical during remediation (they are not counted in +# the round-scoped deviation override). Previously this routed REMEDIATION_REQUIRED +# via the stage-asymmetric source-missing check; that was the #655 loop. +@test "no-classification metadata-only round on a clean-baseline phase → PROCEED_TO_UAT (issue #655)" { create_verif "write-verification.sh" "PASS" create_summary_with_yaml_deviations "01-01" "Historical orchestration issue" @@ -2244,7 +2252,7 @@ VERIF cat > "$PHASE_DIR/remediation/qa/round-01/R01-PLAN.md" <<'PLAN' --- round: 01 -title: Missing classifications should fail closed +title: No-FAIL documentation disposition --- PLAN cat > "$PHASE_DIR/remediation/qa/round-01/R01-VERIFICATION.md" <<'VERIF' @@ -2263,6 +2271,49 @@ VERIF run bash "$SCRIPT" "$PHASE_DIR" [ "$status" -eq 0 ] + [[ "$output" != *"qa_gate_source_verification_missing=true"* ]] + [[ "$output" == *"qa_gate_routing=PROCEED_TO_UAT"* ]] +} + +# Issue #655 (sub-defect B) — fail-closed companions: the source-missing exemption +# above must NOT fire when the phase baseline is not clean. Two guards: +# 1. no phase-level verification on disk (a continuation round that lost its source) +# 2. the phase verification carries pre-existing/known issues +# are covered by the round-02 and known-issues fail-closed tests below; here we pin +# that a round which records a NON-metadata edit still fails closed even on a clean +# baseline (it claims real work with no source verification to anchor it). +@test "no-classification round recording a non-metadata edit still fails closed (issue #655)" { + create_verif "write-verification.sh" "PASS" + + mkdir -p "$PHASE_DIR/remediation/qa/round-01" + printf 'stage=verify\nround=01\n' > "$PHASE_DIR/remediation/qa/.qa-remediation-stage" + + create_round_summary_with_files "$PHASE_DIR/remediation/qa/round-01" "01" \ + ' - "src/Feature.swift"' + + cat > "$PHASE_DIR/remediation/qa/round-01/R01-PLAN.md" <<'PLAN' +--- +round: 01 +title: Non-metadata edit without a source must fail closed +--- +PLAN + cat > "$PHASE_DIR/remediation/qa/round-01/R01-VERIFICATION.md" <<'VERIF' +--- +writer: write-verification.sh +result: PASS +plans_verified: + - R01 +--- +## Checks +| ID | Category | Description | Status | Evidence | +|----|----------|-------------|--------|----------| +| MH-01 | must_have | Code edited | PASS | Done | +VERIF + + run bash "$SCRIPT" "$PHASE_DIR" + + [ "$status" -eq 0 ] + [[ "$output" == *"qa_gate_source_verification_missing=true"* ]] [[ "$output" == *"qa_gate_routing=REMEDIATION_REQUIRED"* ]] } From 723224afac7b3173525c0db1cfd869016b2df91a Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Mon, 15 Jun 2026 17:17:42 -0500 Subject: [PATCH 2/4] fix(qa-gate): address QA round 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the findings from the round-1 QA review (read-only adversarial review of the sub-defect B fix): - phase_baseline_is_clean pre-existing-issues counter miscounted GitHub-style colon alignment rows and non-canonical header rows (would silently defeat the fix for those formats) — replaced with the header-state-machine pattern (skip alignment rows via [[:space:]:|-], treat the first non-separator row as the header regardless of titles). - phase_baseline_is_clean result check was narrower than the gate's authoritative RESULT parser — now normalizes with toupper() and falls back to the legacy `status:` field. - gate-level coverage for the reachable phase_baseline_is_clean fail-closed branches (absent phase verification, non-PASS result, pre-existing issues); removed a misconceived test for the count_fail==0 branch, which is unreachable through the gate because a phase verification with FAIL rows makes qa-remediation-state derive input_mode=verification (documented as defense-in-depth instead). Co-Authored-By: Claude Opus 4.8 --- scripts/qa-result-gate.sh | 24 ++++++------ tests/qa-result-gate.bats | 81 +++++++++++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/scripts/qa-result-gate.sh b/scripts/qa-result-gate.sh index d58ef093..f1b6dae2 100755 --- a/scripts/qa-result-gate.sh +++ b/scripts/qa-result-gate.sh @@ -1100,21 +1100,23 @@ phase_baseline_is_clean() { [ -n "$phase_dir" ] || return 1 phase_v=$(bash "$SCRIPT_DIR/resolve-verification-path.sh" phase "$phase_dir" 2>/dev/null || true) [ -n "$phase_v" ] && [ -r "$phase_v" ] || return 1 + # Normalize like the gate's authoritative RESULT parser: case-insensitive, + # with a fallback to the legacy `status:` frontmatter field when `result:` is + # absent, so the same on-disk verification is judged consistently here. result=$(extract_frontmatter_scalar_value "$phase_v" result) - case "$result" in - PASS|pass|Pass) ;; - *) return 1 ;; - esac + [ -n "$result" ] || result=$(extract_frontmatter_scalar_value "$phase_v" status) + result=$(printf '%s' "$result" | tr '[:lower:]' '[:upper:]') + [ "$result" = "PASS" ] || return 1 [ "$(count_fail_rows_in_verification "$phase_v")" -eq 0 ] 2>/dev/null || return 1 - preexist=$(awk -F'|' ' - function trim(v){ gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); return v } - /^## Pre-existing Issues/ { found=1; next } + preexist=$(awk ' + /^## Pre-existing Issues/ { found=1; header_done=0; next } found && /^## / { exit } found && /^\|/ { - if ($0 ~ /^\|[[:space:]-]+(\|[[:space:]-]+)+\|?[[:space:]]*$/) next - t=trim($2); f=trim($3) - if (tolower(t)=="test" && tolower(f)=="file") next - if (t=="") next + # Skip GitHub-style alignment separator rows (dashes, optional colons). + if ($0 ~ /^\|[[:space:]:|-]+$/) next + # The first non-separator table row is the column header, whatever its + # column titles are; every subsequent row is a counted data row. + if (!header_done) { header_done=1; next } count++ } END { print count+0 } diff --git a/tests/qa-result-gate.bats b/tests/qa-result-gate.bats index 11c7497b..8515a637 100644 --- a/tests/qa-result-gate.bats +++ b/tests/qa-result-gate.bats @@ -2275,13 +2275,17 @@ VERIF [[ "$output" == *"qa_gate_routing=PROCEED_TO_UAT"* ]] } -# Issue #655 (sub-defect B) — fail-closed companions: the source-missing exemption -# above must NOT fire when the phase baseline is not clean. Two guards: -# 1. no phase-level verification on disk (a continuation round that lost its source) -# 2. the phase verification carries pre-existing/known issues -# are covered by the round-02 and known-issues fail-closed tests below; here we pin -# that a round which records a NON-metadata edit still fails closed even on a clean -# baseline (it claims real work with no source verification to anchor it). +# Issue #655 (sub-defect B) — fail-closed companions. The source-missing +# exemption must fire ONLY when every guard passes; the tests below each satisfy +# the earlier guards (input_mode=none, 0 classifications, 0 known issues) and +# then trip exactly one remaining guard, so each branch is exercised live: +# - this test: a recorded NON-metadata edit (claims real work, no source) +# - the three tests after it: the reachable false-return branches of +# phase_baseline_is_clean (absent phase verification, non-PASS result, +# pre-existing issues). Its count_fail==0 guard is defense-in-depth and is +# not independently reachable through the gate: a phase verification with +# FAIL rows makes qa-remediation-state derive input_mode=verification, so the +# exemption's input_mode=none precondition already excludes that case. @test "no-classification round recording a non-metadata edit still fails closed (issue #655)" { create_verif "write-verification.sh" "PASS" @@ -2317,6 +2321,69 @@ VERIF [[ "$output" == *"qa_gate_routing=REMEDIATION_REQUIRED"* ]] } +# Helper: a metadata-only, no-classification round that clears every guard up to +# phase_baseline_is_clean. Callers set the phase verification to vary the baseline. +_seed_clean_guard_round() { + mkdir -p "$PHASE_DIR/remediation/qa/round-01" + printf 'stage=verify\nround=01\n' > "$PHASE_DIR/remediation/qa/.qa-remediation-stage" + create_round_summary_with_files "$PHASE_DIR/remediation/qa/round-01" "01" \ + ' - "01-test-phase/remediation/qa/round-01/R01-SUMMARY.md"' + cat > "$PHASE_DIR/remediation/qa/round-01/R01-PLAN.md" <<'PLAN' +--- +round: 01 +title: No-FAIL documentation disposition +--- +PLAN + cat > "$PHASE_DIR/remediation/qa/round-01/R01-VERIFICATION.md" <<'VERIF' +--- +writer: write-verification.sh +result: PASS +plans_verified: + - R01 +--- +## Checks +| ID | Category | Description | Status | Evidence | +|----|----------|-------------|--------|----------| +| MH-01 | must_have | Documented | PASS | Done | +VERIF +} + +@test "phase_baseline_is_clean branch: no phase verification on disk → fails closed (issue #655)" { + _seed_clean_guard_round + # No phase-level VERIFICATION.md exists (a continuation round that lost its source). + + run bash "$SCRIPT" "$PHASE_DIR" + + [ "$status" -eq 0 ] + [[ "$output" == *"qa_gate_source_verification_missing=true"* ]] + [[ "$output" == *"qa_gate_routing=REMEDIATION_REQUIRED"* ]] +} + +@test "phase_baseline_is_clean branch: phase verification result is not PASS → fails closed (issue #655)" { + create_verif "write-verification.sh" "FAIL" + _seed_clean_guard_round + + run bash "$SCRIPT" "$PHASE_DIR" + + [ "$status" -eq 0 ] + [[ "$output" == *"qa_gate_source_verification_missing=true"* ]] + [[ "$output" == *"qa_gate_routing=REMEDIATION_REQUIRED"* ]] +} + +@test "phase_baseline_is_clean branch: phase verification carries a pre-existing issue → fails closed (issue #655)" { + create_verif "write-verification.sh" "PASS" "## Pre-existing Issues +| Test | File | Error | +|------|------|-------| +| SomeTest | src/foo.swift | flaky boundary case |" + _seed_clean_guard_round + + run bash "$SCRIPT" "$PHASE_DIR" + + [ "$status" -eq 0 ] + [[ "$output" == *"qa_gate_source_verification_missing=true"* ]] + [[ "$output" == *"qa_gate_routing=REMEDIATION_REQUIRED"* ]] +} + @test "metadata-only round with inline fail_classifications process-exception → PROCEED_TO_UAT" { create_source_fail_verif "DEV-0101-COMMIT" "Historical process exception still needs classification" create_summary_with_yaml_deviations "01-01" "Historical batched commit" From 60684e8803c4eac66ae3cb5229f80ff0950cf0bb Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Mon, 15 Jun 2026 17:29:14 -0500 Subject: [PATCH 3/4] fix(qa-gate): address QA round 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 QA review came back clean — no blockers or major issues. The round-1 fixes hold; 187/187 bats pass, shellcheck clean. One minor, pre-existing, non-regressing observation noted and deferred: `count_fail_rows_in_verification` keys on a literal "Status" column header, so a self-contradictory verification (frontmatter result: PASS but a FAIL row under a differently-named header) is read as 0 FAILs. This is the established gate-wide parser (not introduced here), real write-verification.sh output always uses the "Status" header, and the direction is benign — out of scope for #655. Co-Authored-By: Claude Opus 4.8 From b1b935cbd18c77518e49f1d83fc33e808143e8a6 Mon Sep 17 00:00:00 2001 From: Shane McCarron Date: Mon, 15 Jun 2026 17:32:29 -0500 Subject: [PATCH 4/4] fix(qa-gate): address QA round 3 Round 3 QA review surfaced two minor, fail-safe findings; both addressed: - Finding 2 (placeholder pre-existing table): `phase_baseline_is_clean` counted a placeholder "None"/"N/A" row in a `## Pre-existing Issues` table as a real carried issue, so a clean no-FAIL phase that renders an explicit empty table failed closed. The counter now skips placeholder rows (None / N/A / "no ... issues"), matching the placeholder-skipping convention used elsewhere in the gate. Fail-safe direction either way (could only have caused a spurious remediation, never a false PROCEED). - Finding 1 (round-02+ coverage gap): all prior #655 tests used round-01, leaving the new exemption's round>1 source-resolution path unverified. Added a round-02 no-classification clean-baseline test asserting PROCEED_TO_UAT, locking in the deliberate disposition broadening. 189/189 bats pass; shellcheck + bash -n clean. Co-Authored-By: Claude Opus 4.8 --- scripts/qa-result-gate.sh | 7 ++++ tests/qa-result-gate.bats | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/scripts/qa-result-gate.sh b/scripts/qa-result-gate.sh index f1b6dae2..de322fac 100755 --- a/scripts/qa-result-gate.sh +++ b/scripts/qa-result-gate.sh @@ -1117,6 +1117,13 @@ phase_baseline_is_clean() { # The first non-separator table row is the column header, whatever its # column titles are; every subsequent row is a counted data row. if (!header_done) { header_done=1; next } + # Skip placeholder rows (e.g. "| None | - | - |") so a clean phase that + # renders an explicit empty pre-existing table still reads as clean. + split($0, _cells, "|") + _c = _cells[2] + gsub(/^[[:space:]]+|[[:space:]]+$/, "", _c) + _lc = tolower(_c) + if (_lc == "" || _lc == "none" || _lc == "n/a" || _lc == "na" || _lc ~ /^none[.,; ]/ || _lc ~ /^no .*(issue|pre-existing)/) next count++ } END { print count+0 } diff --git a/tests/qa-result-gate.bats b/tests/qa-result-gate.bats index 8515a637..560f3ad2 100644 --- a/tests/qa-result-gate.bats +++ b/tests/qa-result-gate.bats @@ -2384,6 +2384,73 @@ VERIF [[ "$output" == *"qa_gate_routing=REMEDIATION_REQUIRED"* ]] } +# Issue #655 (round 3, finding 2): a placeholder Pre-existing Issues table (a +# single "None"/"N/A" row) is an explicit empty table, not a carried issue, so +# `phase_baseline_is_clean` must treat the phase as clean — otherwise a clean +# no-FAIL phase could spuriously fail closed. +@test "phase_baseline_is_clean treats a placeholder pre-existing-issues row as clean → PROCEED_TO_UAT (issue #655)" { + create_verif "write-verification.sh" "PASS" "## Pre-existing Issues +| Test | File | Error | +|------|------|-------| +| None | - | - |" + _seed_clean_guard_round + + run bash "$SCRIPT" "$PHASE_DIR" + + [ "$status" -eq 0 ] + [[ "$output" != *"qa_gate_source_verification_missing=true"* ]] + [[ "$output" == *"qa_gate_routing=PROCEED_TO_UAT"* ]] +} + +# Issue #655 (round 3, finding 1): lock in the deliberate broadening for round-02+. +# A round-02 no-FAIL disposition (input_mode=none, zero classifications, metadata-only +# recorded files) on a clean phase baseline terminates rather than looping — the same +# no-FAIL semantics as round-01, exercised through the round>1 source-resolution path. +@test "round-02 no-classification disposition on a clean baseline → PROCEED_TO_UAT (issue #655)" { + create_verif "write-verification.sh" "PASS" + mkdir -p "$PHASE_DIR/remediation/qa/round-01" "$PHASE_DIR/remediation/qa/round-02" + printf 'stage=verify\nround=02\n' > "$PHASE_DIR/remediation/qa/.qa-remediation-stage" + # Prior round-01 verification exists (a normal continuation), PASS/clean. + cat > "$PHASE_DIR/remediation/qa/round-01/R01-VERIFICATION.md" <<'VERIF' +--- +writer: write-verification.sh +result: PASS +plans_verified: + - R01 +--- +## Checks +| ID | Category | Description | Status | Evidence | +|----|----------|-------------|--------|----------| +| MH-01 | must_have | Documented | PASS | Done | +VERIF + create_round_summary_with_files "$PHASE_DIR/remediation/qa/round-02" "02" \ + ' - "01-test-phase/remediation/qa/round-02/R02-SUMMARY.md"' + cat > "$PHASE_DIR/remediation/qa/round-02/R02-PLAN.md" <<'PLAN' +--- +round: 02 +title: No-FAIL documentation disposition +--- +PLAN + cat > "$PHASE_DIR/remediation/qa/round-02/R02-VERIFICATION.md" <<'VERIF' +--- +writer: write-verification.sh +result: PASS +plans_verified: + - R02 +--- +## Checks +| ID | Category | Description | Status | Evidence | +|----|----------|-------------|--------|----------| +| MH-01 | must_have | Documented | PASS | Done | +VERIF + + run bash "$SCRIPT" "$PHASE_DIR" + + [ "$status" -eq 0 ] + [[ "$output" != *"qa_gate_source_verification_missing=true"* ]] + [[ "$output" == *"qa_gate_routing=PROCEED_TO_UAT"* ]] +} + @test "metadata-only round with inline fail_classifications process-exception → PROCEED_TO_UAT" { create_source_fail_verif "DEV-0101-COMMIT" "Historical process exception still needs classification" create_summary_with_yaml_deviations "01-01" "Historical batched commit"