KB-22AC rev 2

dot-iu-cutter v0.2 — Phase α DDL Verification Plan (rev 2, 2026-05-15)

19 min read Revision 2
dieu44-trien-khaidot-iu-cutterv0.2phase-alphaddl-verificationno-executionrevision-22026-05-15

dot-iu-cutter v0.2 — Phase α DDL Verification Plan

document_path: knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-ddl-verification-plan-2026-05-15.md
revision: r2
supersedes: r1 (2026-05-15)
date: 2026-05-15
revised: 2026-05-16
author: Agent (Claude Code CLI, Opus 4.7 1M)
phase: v0.2 — Phase α DDL post-application verification PLAN (planning only)
no_execution: TRUE
no_verification_queries_run_in_this_doc: TRUE

§0 — Revision 2 Changelog

trigger: GPT REVISION_REQUIRED on DDL draft r1 (PG fast-default pitfall).

verification_expectation_status:
  The r1 PASS targets in this plan were ALREADY correct:
    V-α-6 : public.tac_logical_unit.authority  → draft=86, no enacted/runtime/NULL
    V-α-8 : sandbox_tac.logical_unit.authority → NULL × 76
  r1's DDL would NOT have produced these via the intended path: ADD COLUMN
  DEFAULT 'draft' would have made all 86 public rows read 'draft' WITHOUT the
  BR-4 mapping ever running, and would have made the 76 sandbox rows read
  'draft' instead of NULL — i.e. V-α-8 would have FAILED under r1.

r2_changes_to_this_plan:
  - revision r1 → r2
  - §2.1 / §2.2: added a mechanism note (authority added WITHOUT default, then
    SET DEFAULT in a later step; catalog column_default still resolves to
    'draft'::text, so the EXPECTED OUTPUT of these queries is UNCHANGED)
  - §2.6 V-α-6: made the {draft=86, enacted=0, runtime=0, null=0} expectation
    fully explicit per GPT, with an r2 note that this is now achieved by the
    Step 3 backfill (not by a column default)
  - §2.8 V-α-8: added an r2 note that NULL × 76 is now genuinely guaranteed
    because the sandbox column is added WITHOUT default and no UPDATE runs
  - step-reference fix: alias indexes are DDL draft r2 "Step 9" (was r1 "Step 8");
    the backfill is DDL draft r2 "Step 3" (UNCHANGED — pre-backfill checks still valid)

no_verification_expectation_VALUES_changed: TRUE (only the mechanism narrative)

§1 — Purpose

Define the verification queries and pass/fail criteria that MUST be evaluated after the Phase α DDL is applied (either in dry-run or production). This plan is written so the verifier can re-run the queries verbatim without ambiguity. Nothing is executed in this document.

The plan applies in two contexts:

  1. dry-run env (after the Phase α DDL applies inside the fresh dry-run sibling env)
  2. production env (after the eventual production execution of Phase α DDL under explicit prompt)

In production, the queries are the closing gates of the production command-review package's verification phase.


§2 — Verification Queries (read-only; NOT executed here)

2.1 V-α-1 — New columns exist on public.tac_logical_unit

-- expected: 2 rows (authority, canonical_address_format_version)
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema='public'
  AND table_name='tac_logical_unit'
  AND column_name IN ('authority', 'canonical_address_format_version')
ORDER BY column_name;

Expected result:

authority                          | text | YES | 'draft'::text
canonical_address_format_version   | text | NO  | 'canonical-address-v1'::text

PASS condition: exactly 2 rows; authority is nullable with DEFAULT 'draft'; canonical_address_format_version is NOT NULL with DEFAULT 'canonical-address-v1'.

r2 mechanism note: authority's column_default reads 'draft'::text because DDL draft r2 Step 4 applies ALTER COLUMN authority SET DEFAULT 'draft' after the Step 3 backfill. The catalog default is recorded the same way it would be under r1, so this query's expected output is unchanged. The difference is purely when the default is attached (after backfill, affecting future rows only) — invisible to information_schema.columns. The distribution queries V-α-6 / V-α-8 are what actually distinguish a correct r2 run from the defective r1 behaviour.

2.2 V-α-2 — Mirror columns exist on sandbox_tac.logical_unit

-- expected: 2 rows
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema='sandbox_tac'
  AND table_name='logical_unit'
  AND column_name IN ('authority', 'canonical_address_format_version')
ORDER BY column_name;

Expected result: identical shape to §2.1.

PASS condition: same as §2.1.

r2 mechanism note: identical reasoning to §2.1 — sandbox authority gets its 'draft'::text default from DDL draft r2 Step 7 (ALTER COLUMN … SET DEFAULT), applied with no preceding backfill. The catalog default shape is unchanged from r1; the row-level distribution (V-α-8) is where the correctness shows.

2.3 V-α-3 — Alias table exists

-- expected: 1 row
SELECT count(*)
FROM information_schema.tables
WHERE table_schema='cutter_governance'
  AND table_name='canonical_address_alias'
  AND table_type='BASE TABLE';

Expected result: 1.

PASS condition: exactly 1.

2.4 V-α-4 — Alias table columns

-- expected: 9 rows in correct order
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema='cutter_governance'
  AND table_name='canonical_address_alias'
ORDER BY ordinal_position;

Expected columns (9):

alias_id          | uuid        | NO  | gen_random_uuid()
target_unit_id    | uuid        | NO  | (none)
alias_text        | text        | NO  | (none)
alias_kind        | text        | NO  | (none)
valid_from        | timestamptz | NO  | now()
valid_until       | timestamptz | YES | (none)
created_by        | text        | NO  | (none)
rationale         | text        | YES | (none)
scenario_ref      | text        | YES | (none)

PASS condition: exactly 9 columns in the expected order with the expected nullability/default shape.

2.5 V-α-5 — Alias table indexes

-- expected: 5 indexes (1 PK + 4 btree)
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname='cutter_governance'
  AND tablename='canonical_address_alias'
ORDER BY indexname;

Expected indexes:

canonical_address_alias_pkey
idx_alias_alias_kind
idx_alias_alias_text
idx_alias_target_unit_id
idx_alias_validity_window

PASS condition: exactly 5 indexes present; PK is on alias_id; the 4 btree indexes match the columns specified in DDL draft r2 Step 9 (was r1 Step 8).

2.6 V-α-6 — public.tac_logical_unit.authority distribution

SELECT authority, count(*) AS n
FROM public.tac_logical_unit
GROUP BY authority
ORDER BY authority NULLS FIRST;

Expected result (production state after the Step 3 backfill):

authority      | n
---------------|----
draft          | 86

Equivalent explicit breakdown (the GPT r2 verification expectation):

draft    = 86
enacted  = 0
runtime  = 0
null     = 0

PASS conditions:

  • exactly 1 row in the grouped result
  • authority='draft' with n=86
  • no NULL row (null = 0) — proves the Step 3 backfill actually fired
  • no 'enacted' row (enacted = 0) and no 'runtime' row (runtime = 0) — consistent with the current dataset being 100% lifecycle_status='draft_only'
  • if the production dataset ever contains active/retired rows (currently zero), those map to 'enacted' per BR-4; re-run and re-baseline before treating as failure

r2 note: under DDL draft r1 this query would still have shown draft=86, but for the wrong reason — the ADD COLUMN default, not the BR-4 mapping. The null = 0 and enacted = 0 assertions only become meaningful under r2, where Step 2 adds the column with all rows NULL and Step 3 performs the actual lifecycle mapping. This query is the primary evidence that the r2 correction works.

2.7 V-α-7 — public.tac_logical_unit.canonical_address_format_version distribution

SELECT canonical_address_format_version, count(*) AS n
FROM public.tac_logical_unit
GROUP BY canonical_address_format_version
ORDER BY canonical_address_format_version;

Expected result:

canonical-address-v1 | 86

PASS condition: exactly 1 row; value 'canonical-address-v1'; n=86.

r2 note: unchanged from r1. canonical_address_format_version is intentionally added NOT NULL DEFAULT 'canonical-address-v1' (DDL draft r2 Step 1), so the column-add-time fast-default backfilling all 86 rows is the desired behaviour for this column — the opposite of the authority requirement. GPT explicitly retained this as an accepted part.

2.8 V-α-8 — sandbox_tac.logical_unit.authority distribution (no backfill expected)

SELECT authority, count(*) AS n
FROM sandbox_tac.logical_unit
GROUP BY authority
ORDER BY authority NULLS FIRST;

Expected result:

(NULL)  | 76

(SQL aggregation typically renders the NULL group as an empty cell or (null) indicator.)

PASS conditions:

  • exactly 1 row
  • authority IS NULL with n=76
  • NO 'draft' rows — sandbox is NOT backfilled in Phase α; DDL draft r2 Step 6 adds the column WITHOUT a default, and Step 7's SET DEFAULT affects only FUTURE inserts, so all 76 existing sandbox rows remain genuinely NULL
  • intentional per BR-7 Option II + GPT Q4 decision

r2 note — this is the query r1 would have FAILED. Under DDL draft r1 (ADD COLUMN authority text DEFAULT 'draft'), all 76 sandbox rows would have read 'draft', producing draft=76, null=0 — directly contradicting the sandbox expectation. The r2 add-without-default correction is what makes null=76 actually true. Treat any non-NULL sandbox authority value here as a hard failure (rollback trigger T-3-class).

2.9 V-α-9 — sandbox_tac.logical_unit.canonical_address_format_version distribution

SELECT canonical_address_format_version, count(*) AS n
FROM sandbox_tac.logical_unit
GROUP BY canonical_address_format_version
ORDER BY canonical_address_format_version;

Expected result:

canonical-address-v1 | 76

PASS condition: exactly 1 row; value 'canonical-address-v1'; n=76 (DEFAULT applied at column-add time across all 76 sandbox rows — intentional mirror of Step 1; unchanged from r1).

2.10 V-α-10 — Alias table row count = 0

SELECT count(*) AS n
FROM cutter_governance.canonical_address_alias;

Expected: 0.

PASS condition: alias table is empty (Phase α creates it with no rows; writers are Phase β).

2.11 V-α-11 — Existing canonical_address column unchanged

-- column metadata unchanged
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema='public'
  AND table_name='tac_logical_unit'
  AND column_name='canonical_address';

Expected result:

canonical_address | text | NO | (none)

PASS condition: column still text NOT NULL; no default added; UNIQUE constraint still present (verifiable via V-α-12 below).

2.12 V-α-12 — UNIQUE constraint on canonical_address still in place

SELECT conname, pg_get_constraintdef(c.oid)
FROM pg_constraint c
JOIN pg_class t ON t.oid=c.conrelid
WHERE t.relname='tac_logical_unit' AND conname='tac_logical_unit_canonical_address_key';

Expected: 1 row showing UNIQUE (canonical_address).

PASS condition: UNIQUE constraint intact.

2.13 V-α-13 — v0.1 cutter_governance tables unchanged

-- exactly 6 tables in cutter_governance after Phase α
-- (5 from v0.1 + 1 new alias table)
SELECT relname, n_live_tup
FROM pg_stat_user_tables
WHERE schemaname='cutter_governance'
ORDER BY relname;

Expected result (post-Phase-α):

canonical_address_alias       | 0
cut_change_set                | 0
cut_change_set_affected_row   | 0
decision_backlog_entry        | 0
dot_pair_signature            | 0
verify_result                 | 0

PASS conditions:

  • exactly 6 tables
  • all 6 have n_live_tup = 0
  • the original 5 v0.1 tables are present with names exactly as shipped in v0.1 (no rename / no drop)

2.14 V-α-14 — Row count of public.tac_logical_unit unchanged

SELECT count(*) AS rows_total,
       count(canonical_address) AS canonical_non_null,
       count(DISTINCT canonical_address) AS distinct_canonical
FROM public.tac_logical_unit;

Expected result:

86 | 86 | 86

PASS condition: row count unchanged from pre-Phase-α state (86); canonical_address still 100% non-null; still 100% distinct. (The Step 3 backfill UPDATEs in place; it does not change the row count.)

2.15 V-α-15 — Row count of sandbox_tac.logical_unit unchanged

SELECT count(*) FROM sandbox_tac.logical_unit;

Expected: 76.

PASS condition: row count unchanged from pre-Phase-α state.

2.16 V-α-16 — Schema diff between pre-Phase-α snapshot and post-Phase-α schema

# Compare CREATE TABLE / ALTER TABLE / CREATE INDEX / CREATE SCHEMA additions.
diff \
  <(grep -E '^(CREATE TABLE|CREATE SCHEMA|CREATE INDEX|ALTER TABLE)' <pre_snapshot.sql>  | sort) \
  <(grep -E '^(CREATE TABLE|CREATE SCHEMA|CREATE INDEX|ALTER TABLE)' <post_snapshot.sql> | sort)

Expected diff: ADDITIONS ONLY, exclusively within:

  • public.tac_logical_unit (the 2 new columns + the ALTER COLUMN authority SET DEFAULT — all additive on this table)
  • sandbox_tac.logical_unit (the 2 new mirror columns + the ALTER COLUMN authority SET DEFAULT)
  • cutter_governance.canonical_address_alias (1 CREATE TABLE + 4 explicit CREATE INDEX + PG-generated PK/comment metadata)
  • comments via COMMENT ON

PASS conditions:

  • NO removed lines
  • NO ALTER lines outside public.tac_logical_unit / sandbox_tac.logical_unit (additive only on those tables; existing columns unchanged)
  • NO touched lines on any other production table

r2 note: a pg_dump of public.tac_logical_unit after r2 renders authority with DEFAULT 'draft'::text in the recreated CREATE TABLE (pg_dump always materialises the current default regardless of when it was attached). This is expected and additive — the SET DEFAULT is not a separate destructive line.


These are the Q-CHK-1/2/3 checks recommended in the fn_tac_birth_gate_lu inspection doc. They run before DDL draft r2 Step 3 (the backfill UPDATE) to confirm trigger re-validation will not fail. (Step 3 is unchanged between r1 and r2 — these checks remain valid as authored.)

3.1 Q-CHK-1 — section_type vocab activity

SELECT count(*) AS rows_with_inactive_section_type
FROM public.tac_logical_unit u
LEFT JOIN public.tac_section_type_vocab v
       ON v.code = u.section_type
WHERE coalesce(v.lifecycle_status, 'missing') != 'active';

Expected: 0.

PASS condition: every row's section_type is active in the vocab.

3.2 Q-CHK-2 — parent_id resolution + doc_code parity

SELECT count(*) AS rows_with_parent_problem
FROM public.tac_logical_unit child
LEFT JOIN public.tac_logical_unit parent
       ON parent.id = child.parent_id
WHERE child.parent_id IS NOT NULL
  AND (parent.id IS NULL OR parent.doc_code != child.doc_code);

Expected: 0.

PASS condition: every non-null parent resolves and shares doc_code.

3.3 Q-CHK-3 — canonical_address regex defensive recheck

SELECT count(*) AS rows_failing_regex
FROM public.tac_logical_unit
WHERE canonical_address !~ '^D38-[A-Z0-9]+-((ROOT)|(S[0-9]+(-P[0-9]+(-[0-9]+)*)?))$';

Expected: 0.

PASS condition: every row passes the canonical-address-v1 regex (matches the trigger's hardcoded regex).

If any of the three pre-backfill checks fail, the Phase α DDL must NOT proceed in that environment until the underlying data is fixed.


§4 — Failure-Stop Criteria (per verification query)

fail_action_on_ANY_failure_during_dry_run:
  - record evidence (query output + timestamp)
  - rollback the dry-run transaction
  - do NOT proceed to subsequent scenarios
  - escalate to GPT review

fail_action_on_ANY_failure_during_production_post_DDL_verification:
  - STOP immediately
  - capture all verification query outputs to the production artefact directory
  - execute the rollback per the rollback draft (separate file)
  - record an admin_fallback_log entry (manual; the runtime APR path is not Phase α scope)
  - notify Đ32 (standard path) + G-4 Custodian
  - do NOT self-retry

specific_pass_threshold:
  - V-α-1 .. V-α-16: ALL must PASS
  - Q-CHK-1, Q-CHK-2, Q-CHK-3: ALL must PASS (pre-backfill)
  - any single failure: STOP

production_specific_rollback_trigger:
  any_of:
    - V-α-1 / V-α-2 missing columns
    - V-α-3 / V-α-4 alias table malformed or missing
    - V-α-6 authority distribution not {'draft' × 86, no enacted/runtime/NULL}
    - V-α-7 format_version distribution not {'canonical-address-v1' × 86}
    - V-α-8 sandbox authority distribution not {NULL × 76}  (r2: a 'draft' value here means the column was wrongly given a column-add default — the exact r1 defect)
    - V-α-13 cutter_governance tables changed beyond the additive alias table
    - V-α-14 / V-α-15 row counts differ from pre-state
    - V-α-16 schema diff shows ANY non-additive change

§5 — Hard Boundaries

no_verification_query_executed_here: TRUE
no_DDL_executed: TRUE
no_mutation: TRUE
no_dry_run_started: TRUE
no_production_migration_allowed: TRUE
output_form: phase_alpha_verification_planning_only
revision: r2 (mechanism narrative updated; no expectation values changed)

§6 — Cross-References

fn_inspection:        knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-fn-tac-birth-gate-lu-inspection-2026-05-15.md
ddl_draft:            knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-ddl-draft-2026-05-15.sql.md  (r2)
rollback_draft:       knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-rollback-draft-2026-05-15.md  (r2)
authoring_report:     knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-ddl-authoring-report-2026-05-15.md  (r2)
dry_run_plan:         knowledge/dev/laws/dieu44-trien-khai/v0.2-design/dot-iu-cutter-v0.2-phase-alpha-dry-run-plan-2026-05-15.md
risk_review_plan:     knowledge/dev/laws/dieu44-trien-khai/v0.2-design/dot-iu-cutter-v0.2-phase-alpha-risk-review-plan-2026-05-15.md
v0_1_verification_pattern_(C-09a..e): knowledge/dev/laws/dieu44-trien-khai/execution/dot-iu-cutter-v0.1-p0-production-command-review-package-2026-05-15.md  (§2.9)
v0_1_production_execution_report: knowledge/dev/laws/dieu44-trien-khai/execution/dot-iu-cutter-v0.1-production-execution-report-2026-05-15.md  (§8 verification results pattern)

End of Phase α DDL verification plan (revision 2).

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.2-ddl-authoring/dot-iu-cutter-v0.2-phase-alpha-ddl-verification-plan-2026-05-15.md