KB-72B2

P10A-2A — D35 Insert/Render Package (Read-Only Generation, 2026-04-29)

31 min read Revision 1
p10ap10a-2adieu-35dieu-38insert-packagerenderrollbackread-onlys187

P10A-2A — Điều 35 Insert/Render Package (Read-Only Generation)

Date: 2026-04-29 Captured (UTC): 2026-04-29T04:49:17Z (env), schema queries 2026-04-29T~05:00Z, source re-fetched ~05:00Z Agent: Claude (Opus 4.7) — codex-webtest delegation, executing P10A-2A dispatch v0.2 + R1 patches Mode: ZERO MUTATION — pg_catalog / information_schema SELECT + Agent Data GET only. SQL files generated as artifacts; NONE executed. Per dispatch: Generated SQL files (insert/render/rollback/verify) là artifacts only. Agent KHÔNG được execute chúng. SSH host: contabo (38.242.240.89, vmi3080463) Database container: postgres (image postgres:16); DB directus; current_user directus.


0. PASS / FAIL Outcome (per dispatch §TIÊU CHÍ)

# Criterion Result
1 Schema re-verify (actual columns match candidate expectations) ✅ PASS — 14 tac_* tables, 4 target tables column set unchanged from P10A-1; UNIQUE / FK / CHECK enumerated.
2 Candidate v3 complete (§8 split + descriptions clean + full body verified) ✅ PASS — 36 units (v2 31 + §8 split into parent + 5 children), descriptions regenerated, hash-verified vs v2.
3 Collision check (UNIQUE-aware) ✅ PASS — tac_publication WHERE doc_code='DIEU-35' AND version='v5.2' → 0 rows; tac_logical_unit WHERE doc_code='DIEU-35' → 0.
4 Execution path discovery (catalog) ✅ PASS (discovery only per R1#3) — role directus holds INSERT+SELECT+UPDATE+DELETE on all 4 target tables. Other roles (workflow_admin, incomex) have no INSERT/DELETE per role_table_grants.
5 Insert SQL generated (compilable, real columns/types/defaults) ✅ PASS — single transaction, DO block resolves parent_id via jsonb map; columns + defaults match Phase 0.2; vocab values match Phase 0.3.
6 Render SQL — recursive CTE preorder ✅ PASS — materialized-path CTE; ordering by path array gives correct DFS preorder; parameterized via :pub_id.
7 Rollback feasibility (trigger source analysis) ✅ PASS — fn_tac_pm_enacted_lock and fn_tac_enacted_immut only block when row's lifecycle is enacted; insert keeps publication=proposed, UV=draft, so DELETE path is open until any future enact APR fires. No DML test rows inserted.
8 Full body hash match ✅ PASS — 31/31 v2 hashes reproduced byte-for-byte by re-running v2 segmenter (/tmp/seg2.py) against re-fetched source (sha256 still 4353ec6d…894d2e5c). 5 §8 child hashes are NEW (split).
9 Package report uploaded to KB ✅ (this file) at knowledge/dev/laws/dieu38-trien-khai/reports/p10a-2a-d35-insert-package-2026-04-29.md.

ALL 9 PASS. Status: P10A-2A PASS — STOP, await GPT review before any P10A-2B execution.

⚠ Critical Finding (HARD CONSTRAINT not surfaced in candidates v1/v2)

fn_tac_birth_gate_lu enforces a canonical_address regex that requires D38- prefix, not dieu-35/:

^D38-[A-Z0-9]+-((ROOT)|(S[0-9]+(-P[0-9]+(-[0-9]+)*)?))$

Candidate v1 §6.1 proposed dieu-35/{unit_key} — that pattern would have failed BG-LU-01 with check_violation. P10A-2A remaps every unit to D38-DIEU35-{ROOT|S{N}|S{N}-P{N}|S{N}-P{N}-{N}} (full table in §5).


1. Phase 0 — Schema + Environment Verify (raw output excerpts)

1.1 DB / Role / Container

Field Value
Container postgres (image postgres:16)
current_database directus
current_user directus
Role privileges (all 4 tables) SELECT, INSERT, UPDATE, DELETE ✅ (information_schema.role_table_grants)
Other granted roles (INSERT/DELETE) none beyond directus (via role_table_grants filter)

R1#3 implication: directus is the only role catalog-confirmed with INSERT on the 4 target tables. P10A-2B execution role decision is out of scope; this is a discovery, not a hard PASS criterion.

1.2 Columns (54 rows across 4 target tables) — verified against Phase 0.2 query

Confirmed identical to P10A-1 §2.1–§2.4 schema snapshot. All defaults present:

  • tac_logical_unit.id = gen_random_uuid(), lifecycle_status = 'draft_only', identity_profile = '{}'::jsonb, sort_order = 0.
  • tac_unit_version.lifecycle_status = 'draft', review_state = 'unreviewed', length_flag = 'normal', provenance = 'PROV-AI', vector_sync_status = 'pending', vector_chunk_count = 0, version_number = 1.
  • tac_publication.lifecycle_status = 'proposed', risk_tier = 'medium', publication_profile = '{}'::jsonb.
  • tac_publication_member.render_order = 0.

1.3 Constraints

UNIQUE (collision keys):

Constraint Definition
tac_publication_doc_code_version_key UNIQUE (doc_code, version)
tac_logical_unit_canonical_address_key UNIQUE (canonical_address)
tac_unit_version_logical_unit_id_version_number_key UNIQUE (logical_unit_id, version_number)
tac_publication_member_publication_id_logical_unit_id_key UNIQUE (publication_id, logical_unit_id)

FK: 17 FK rows; all targeting vocab/parent (no cross-schema). Verified.

CHECK (selected):

  • tac_publication.risk_tier ∈ {low,medium,high,highest}
  • tac_unit_version.length_flag ∈ {normal,soft_limit,hard_limit}
  • tac_unit_version.version_number > 0
  • tac_logical_unit.sort_order >= 0
  • tac_publication_member.render_order >= 0

1.4 Vocab values (active rows)

tac_section_type_vocab (17 active) — 12 used by candidate (subset of vocab): appendix, article, changelog, checklist, definition, governance_process, heading, instruction_block, paragraph, principle, process, technical_spec ✅.

section_type soft / hard body_req desc_req
article 500 / 1500 t t
heading 100 / 300 f f
paragraph 300 / 1000 t t
definition 200 / 500 t t
principle 300 / 800 t t
process 500 / 1500 t t
governance_process 500 / 1500 t t
technical_spec 1000 / 3000 t t
instruction_block 1000 / 3000 t t
checklist 300 / 1000 t f
appendix 2000 / 5000 t f
changelog 500 / 2000 t f

tac_publication_type_vocab includes law (default_risk_tier=highest) ✅.

Lifecycle vocabs verified: pub_lifecycle = {proposed, enacted, superseded, retired}, lu_lifecycle = {active, draft_only, retired}, uv_lifecycle = {draft, enacted, superseded, retired}, review_state = {unreviewed, in_review, review_passed, review_failed, needs_re_review}. Insert SQL uses proposed / draft_only / draft / unreviewed only.

1.5 Birth gate config (11 rows)

checker_id mode enabled rationale
BG-LU-02..06 block t Hard invariant
BG-UV-01 block t Hard invariant
BG-UV-02 block t Required profile (description_required)
BG-UV-03 warn t Quality, can grace (body required)
BG-UV-04 block t Required profile
BG-UV-05 warn t Quality (length)
BG-UV-06 warn t Quality (provenance)

Implication: BG-UV-02 (description NOT NULL when section_type.description_required) is hard-block — every UV row in the insert SQL therefore has a non-empty description field (verified §3.4 below). BG-UV-03 (body required) is warn mode — the §4 / §6 / §8 parent placeholder rows can carry minimal bodies without failing.

1.6 Triggers (6 BEFORE on the 4 target tables)

Table Trigger Events Function Owner SECDEF
tac_logical_unit trg_tac_birth_gate_lu INSERT, UPDATE fn_tac_birth_gate_lu directus t
tac_unit_version trg_tac_birth_gate_uv INSERT fn_tac_birth_gate_uv directus t
tac_unit_version trg_tac_uv_compute_derived INSERT, UPDATE fn_tac_uv_compute_derived directus t
tac_unit_version trg_tac_enacted_immut UPDATE, DELETE fn_tac_enacted_immut directus t
tac_publication_member trg_tac_pm_consistency INSERT, UPDATE fn_tac_pm_consistency directus t
tac_publication_member trg_tac_pm_enacted_lock INSERT, UPDATE, DELETE fn_tac_pm_enacted_lock directus t

1.7 Collision check (UNIQUE-aware per R1#2)

Probe Result
SELECT id, … FROM tac_publication WHERE doc_code='DIEU-35' AND version='v5.2' 0 rows
SELECT count(*) FROM tac_logical_unit WHERE doc_code='DIEU-35' 0
tac_logical_unit.canonical_address UNIQUE safe — none of the 36 D38-DIEU35-* addresses exist yet

No collisions — proceed.


2. Source re-snapshot

Field Value
Path knowledge/dev/laws/dieu35-dot-governance-law.md
Revision 13
Bytes (UTF-8) 39,938
SHA256 4353ec6d453411a7c8e207658bbc4457d00f99747cba90551c8a4926894d2e5c
Method GET /api/documents/<urlencoded>?full=true (Bearer AGENT_DATA_API_KEY)
Identical to P10A-1 / v2 snapshot ✅ — no source drift

3. Candidate v3 (final patches per dispatch §1)

3.1 §8 split (dispatch §1.1) — applied

§8 source body contains 5 sub-headings (3 numbered DOT cap subsections + 2 ★ KHOẢN markers):

### 8.1 dot-dot-health — 14 health checks (v5.1 FINAL)
### ★ v5.1 KHOẢN 2 — ESCALATION BẮT BUỘC qua `fn_log_issue()`
### 8.2 dot-dot-coverage (Cấp A) — giữ v5.0
### 8.3 dot-dot-register (Cấp B) — v5.1 strengthened
### ★ v5.1 KHOẢN 3 — `fn_birth_gate` configurable mode + scope rõ

Dispatch suggested "4 cặp" but the actual structure is 3 + 2; we split into 5 children (matches reality, mirrors §4 KHOẢN-style precedent). Parent dieu35.s8 retains 86-word intro (the text between ## §8. and the first ###).

3.2 §4 keys (dispatch §1.3) — kept as sub2/sub3/sub4

Renaming risk: §4 children include canonical 4.1, 4.2, 4.3, 4.4 plus three KHOẢN-style ★ markers. We keep candidate v2 keys unchanged (dieu35.s4.1, …sub2, …sub3, …sub4, …s4.2, …s4.3, …s4.4) to avoid re-keying parent_key references mid-package. Canonical_address mapping resolves the differentiation deterministically (see §5).

3.3 Description regeneration (dispatch §1.2) — applied

All units rewritten via clean_desc() heuristic that strips: code-fence prefixes, table pipes, headings, bullets, SQL-keyword starters (CREATE, SELECT, INSERT, …), and short noise-only lines. Output template: [{section_type}] {first ~80-char prose} (capped 240 chars). For body-empty units (e.g. parent placeholders), description falls back to title.

Sample (truncated):

unit_key description
dieu35.root [article] Điều 35 — Luật Quản trị DOT v5.2 FINAL (synthetic structural root).
dieu35.s4 [technical_spec] (parent — see children dieu35.s4.*)
dieu35.s6 [governance_process] (parent — see children dieu35.s6.*)
dieu35.s8 [governance_process] §8. DOT TỰ QUẢN TRỊ — 4 CẶP (v5.1 mở rộng)
dieu35.s8.2 [governance_process] 8.2 dot-dot-coverage (Cấp A) — giữ v5.0

Every UV row's description is non-empty → BG-UV-02 satisfied.

3.4 Full body extraction + hash verify (dispatch §1.4) — applied

Re-ran v2 segmenter logic (/tmp/seg2.py) inside the v3 generator (/tmp/seg3.py) against the re-fetched source. 31 / 31 v2 hashes reproduced byte-for-byte:

V2 HASH MATCH: 31 / 31  mismatches: 0

For the 5 new §8 children, fresh SHA256 hashes were computed (these become the source of truth for P10A-2B):

unit_key body_sha256 (v3) bytes wc
dieu35.s8 (parent, replacement) computed in v3 JSON ~464 86
dieu35.s8.1 (v3 JSON) 265
dieu35.s8.kh2 (v3 JSON) 20
dieu35.s8.2 (v3 JSON) 1 (empty) 0
dieu35.s8.3 (v3 JSON) 40
dieu35.s8.kh3 (v3 JSON) 191

§8.2 has empty body ("giữ v5.0" — no inline content). Section_type governance_process has body_required=true, but BG-UV-03 is in warn mode → trigger emits warning, does not block. Description is non-empty → BG-UV-02 satisfied. Open issue O3 below.

3.5 Field mapping (dispatch §1.5) — candidate → schema

Schema column Source / value
tac_publication
doc_code 'DIEU-35'
version 'v5.2'
publication_type 'law' (FK vocab; default_risk_tier=highest)
name full title string from preamble
owner 'S178-FIX23'
description static one-liner
lifecycle_status 'proposed' (default; explicit)
risk_tier 'highest'
publication_profile jsonb {source_path, source_sha256, source_bytes, source_revision, captured_at_utc, candidate_version='v3', unit_count=36}
tac_logical_unit (per unit)
id DEFAULT gen_random_uuid()
canonical_address unit.canonical_address (D38-DIEU35-…)
doc_code 'DIEU-35'
parent_id resolved at runtime via jsonb map v_lu_ids->>parent_key
sort_order unit.sort_order (≥0 enforced)
section_type unit.section_type (FK vocab)
section_code NULL (column allows NULL)
owner 'S178-FIX23'
identity_profile DEFAULT '{}'::jsonb
tier NULL
lifecycle_status 'draft_only' (default; explicit)
tac_unit_version (per unit)
id DEFAULT
logical_unit_id bound to LU just inserted
version_number DEFAULT 1
title unit.title
body unit.body (full segment text)
description unit.description (cleaned)
content_hash NOT SET — fn_tac_uv_compute_derived overwrites with sha256(title | body | description | profile) at INSERT
lifecycle_status 'draft' (default; explicit)
review_state 'unreviewed' (default; explicit)
length_flag NOT SET — trigger computes from body word count + vocab limits
content_profile jsonb {unit_key, body_sha256_candidate, word_count}
provenance 'PROV-AI' (default; explicit)
tac_publication_member (per unit)
id DEFAULT
publication_id v_pub_id
logical_unit_id, unit_version_id bound to current LU/UV
render_order unit.sort_order (CHECK ≥0)

No mismatches. Every candidate field maps cleanly to a schema column.


4. Vocab-validation summary

12 distinct section_types used; all lifecycle_status='active'. body_required / description_required satisfied (parent placeholders rely on BG-UV-03 warn mode). No FK/CHECK violations expected.


5. Final canonical_address map (v3, 36 units)

All match ^D38-[A-Z0-9]+-((ROOT)|(S[0-9]+(-P[0-9]+(-[0-9]+)*)?))$:

sort unit_key canonical_address section_type wc
0 dieu35.root D38-DIEU35-ROOT article 12
1 dieu35.preamble D38-DIEU35-S0 heading 215
2 dieu35.s1 D38-DIEU35-S1 principle 212
3 dieu35.s2 D38-DIEU35-S2 paragraph 295
4 dieu35.s3 D38-DIEU35-S3 definition 197
5 dieu35.s4 D38-DIEU35-S4 technical_spec 5
6 dieu35.s4.1 D38-DIEU35-S4-P1 technical_spec 223
7 dieu35.s4.sub2 D38-DIEU35-S4-P1-1 technical_spec 310
8 dieu35.s4.sub3 D38-DIEU35-S4-P1-2 technical_spec 389
9 dieu35.s4.sub4 D38-DIEU35-S4-P1-3 technical_spec 277
10 dieu35.s4.2 D38-DIEU35-S4-P2 technical_spec 40
11 dieu35.s4.3 D38-DIEU35-S4-P3 technical_spec 34
12 dieu35.s4.4 D38-DIEU35-S4-P4 technical_spec 147
13 dieu35.s5 D38-DIEU35-S5 process 462
14 dieu35.s6 D38-DIEU35-S6 governance_process 5
15 dieu35.s6.1 D38-DIEU35-S6-P1 paragraph 19
16 dieu35.s6.2 D38-DIEU35-S6-P2 governance_process 176
17 dieu35.s6.3 D38-DIEU35-S6-P3 governance_process 96
18 dieu35.s6.4 D38-DIEU35-S6-P4 governance_process 85
19 dieu35.s6.5 D38-DIEU35-S6-P5 governance_process 99
20 dieu35.s6.6 D38-DIEU35-S6-P6 paragraph 36
21 dieu35.s6.7 D38-DIEU35-S6-P7 paragraph 21
22 dieu35.s7 D38-DIEU35-S7 process 35
23 dieu35.s8 D38-DIEU35-S8 governance_process 86
24 dieu35.s8.1 D38-DIEU35-S8-P1 governance_process 265
25 dieu35.s8.kh2 D38-DIEU35-S8-P2 governance_process 20
26 dieu35.s8.2 D38-DIEU35-S8-P3 governance_process 0
27 dieu35.s8.3 D38-DIEU35-S8-P4 governance_process 40
28 dieu35.s8.kh3 D38-DIEU35-S8-P5 governance_process 191
29 dieu35.s9 D38-DIEU35-S9 instruction_block 463
30 dieu35.s10 D38-DIEU35-S10 checklist 142
31 dieu35.s11 D38-DIEU35-S11 governance_process 343
32 dieu35.s12 D38-DIEU35-S12 paragraph 30
33 dieu35.appendix_a D38-DIEU35-S13 appendix 209
34 dieu35.changelog D38-DIEU35-S14 changelog 316
35 dieu35.post_merge_todo D38-DIEU35-S15 checklist 132

Note on appendix_a / changelog / post_merge_todo: mapped to S13/S14/S15 to satisfy the regex (it does not allow free-form addresses). Sort_order remains contiguous.


8. Insert SQL

Field Value
Path on VPS /tmp/p10a-2a-insert-candidate.sql
Bytes 85,675
Lines 1,279
SHA256 c6c6781cfc33dc37191db8096569e28d3afa0714e97bfc3810481d0589073563
Generator /tmp/sqlgen.py (sha256 36fc9c382946a471301cd49d8f5c18124ee37bc98dca573f5ffe786bb4f4d550)
Strategy Single transaction; DO $p10a$ block with two jsonb maps v_lu_ids / v_uv_ids for parent_id resolution; per-unit insert sequence LU → UV → PM, ordered root-first then by sort_order so every parent_id lookup hits a previously-inserted row.

8.1 Header excerpt (first 50 lines)

-- p10a-2a-insert-candidate.sql  (NOT EXECUTED in P10A-2A)
-- D35 v5.2 pilot. Single transaction. Mutable lifecycle states only.
-- Trigger surface (BEFORE): birth_gate_lu, birth_gate_uv, uv_compute_derived, pm_consistency, pm_enacted_lock.
-- canonical_address regex: ^D38-[A-Z0-9]+-((ROOT)|(S[0-9]+(-P[0-9]+(-[0-9]+)*)?))$
BEGIN;
DO $p10a$
DECLARE
  v_pub_id    uuid;
  v_lu_ids    jsonb := '{}'::jsonb;
  v_uv_ids    jsonb := '{}'::jsonb;
  v_parent_id uuid;
  v_lu_id     uuid;
  v_uv_id     uuid;
BEGIN
  -- 1. tac_publication
  INSERT INTO tac_publication(doc_code, version, publication_type, name, owner, description, lifecycle_status, risk_tier, publication_profile)
  VALUES (
    'DIEU-35', 'v5.2', 'law',
    'DIEU 35: LUAT QUAN TRI DOT - v5.2 FINAL (BAN HANH 2026-04-18 S178 Fix 15)',
    'S178-FIX23',
    'Luat Quan tri DOT v5.2 FINAL - pilot insert P10A-2 (read-back render).',
    'proposed', 'highest',
    jsonb_build_object(
      'source_path','knowledge/dev/laws/dieu35-dot-governance-law.md',
      'source_sha256','4353ec6d453411a7c8e207658bbc4457d00f99747cba90551c8a4926894d2e5c',
      'source_bytes',39938, 'source_revision',13,
      'captured_at_utc','2026-04-29T04:02:21Z',
      'candidate_version','v3', 'unit_count',36)
  ) RETURNING id INTO v_pub_id;
  RAISE NOTICE 'pub_id=%', v_pub_id;

  -- 2-3. LU + UV + PM per unit (ordered: root then sort_order)
  -- unit_key=dieu35.root ca=D38-DIEU35-ROOT section_type=article wc=12
  v_parent_id := NULL;
  INSERT INTO tac_logical_unit(canonical_address, doc_code, parent_id, sort_order, section_type, owner, lifecycle_status)
  VALUES ('D38-DIEU35-ROOT', 'DIEU-35', v_parent_id, 0, 'article', 'S178-FIX23', 'draft_only')
  RETURNING id INTO v_lu_id;
  v_lu_ids := v_lu_ids || jsonb_build_object('dieu35.root', v_lu_id::text);
  INSERT INTO tac_unit_version(logical_unit_id, version_number, title, body, description, lifecycle_status, review_state, content_profile, provenance)
  VALUES (v_lu_id, 1, 'ĐIỀU 35: LUẬT QUẢN TRỊ DOT — v5.2 FINAL',
          'Điều 35 — Luật Quản trị DOT v5.2 FINAL (synthetic structural root).',
          '[article] Điều 35 — Luật Quản trị DOT v5.2 FINAL (synthetic structural root).',
          'draft', 'unreviewed',
          jsonb_build_object('unit_key','dieu35.root',
            'body_sha256_candidate','5f32d336180a94c4fd4fe28cd141832e667dbbc90f90847ee4776abbb03982bd',
            'word_count',12),
          'PROV-AI')
  RETURNING id INTO v_uv_id;
  v_uv_ids := v_uv_ids || jsonb_build_object('dieu35.root', v_uv_id::text);
  INSERT INTO tac_publication_member(publication_id, logical_unit_id, unit_version_id, render_order)
  VALUES (v_pub_id, v_lu_id, v_uv_id, 0);
  v_uv_ids := v_uv_ids || jsonb_build_object('dieu35.post_merge_todo', v_uv_id::text);
  INSERT INTO tac_publication_member(publication_id, logical_unit_id, unit_version_id, render_order)
  VALUES (v_pub_id, v_lu_id, v_uv_id, 35);

  RAISE NOTICE 'lu_ids=%', v_lu_ids;
  RAISE NOTICE 'uv_ids=%', v_uv_ids;
END $p10a$;
COMMIT;

Full file kept on VPS at the path above for P10A-2B retrieval.


9. Render SQL

Field Value
Path on VPS /tmp/p10a-2a-render.sql
Bytes 1,512
SHA256 2142f6abcc1c94386a848745fb13f64584d2f716c2c2631e43b25a17f99ed579
Parameter :pub_id (psql \set or -v pub_id="'…'")
Validation method static structural review of recursive CTE (no EXPLAIN against unknown pub_id, per R1#4).
-- p10a-2a-render.sql  (NOT EXECUTED in P10A-2A; parameterized via :pub_id)
WITH RECURSIVE
  members AS (
    SELECT pm.publication_id, pm.logical_unit_id, pm.unit_version_id, pm.render_order
      FROM tac_publication_member pm
     WHERE pm.publication_id = :'pub_id'::uuid
  ),
  tree AS (
    -- root: lu rows in members whose parent is not also in members
    SELECT lu.id          AS lu_id,
           lu.parent_id   AS parent_id,
           lu.sort_order  AS sort_order,
           ARRAY[lu.sort_order]::int[] AS path,
           1              AS depth
      FROM tac_logical_unit lu
      JOIN members m ON m.logical_unit_id = lu.id
     WHERE lu.parent_id IS NULL
        OR lu.parent_id NOT IN (SELECT logical_unit_id FROM members)
    UNION ALL
    SELECT lu.id, lu.parent_id, lu.sort_order,
           t.path || lu.sort_order, t.depth+1
      FROM tac_logical_unit lu
      JOIN members m ON m.logical_unit_id = lu.id
      JOIN tree t   ON t.lu_id = lu.parent_id
  )
SELECT t.depth, t.path,
       lu.canonical_address, lu.section_type,
       uv.title, uv.description, uv.body
  FROM tree t
  JOIN members m ON m.logical_unit_id = t.lu_id
  JOIN tac_logical_unit lu ON lu.id = t.lu_id
  JOIN tac_unit_version uv ON uv.id = m.unit_version_id
 ORDER BY t.path;  -- preorder DFS by materialized path

Preorder correctness rationale: the materialized path array (int[]) is built by appending each level's sort_order. PostgreSQL int[] ordering is lexicographic element-by-element, which is exactly DFS preorder of a sort_order-keyed tree. Roots are members whose parent_id is NULL or whose parent is outside the publication's member set (covers both the synthetic dieu35.root case and any future re-parenting choice).


10. Rollback SQL + feasibility analysis

Field Value
Path on VPS /tmp/p10a-2a-rollback.sql
Bytes 1,444
SHA256 25cc49d9bcc96b8b7cd434fcf3d92656ce8fedd11f5edf6337e3e1ee5f18e184
Parameter :pub_id

10.1 Trigger source analysis (catalog only — no INSERT/DELETE test)

fn_tac_pm_enacted_lock (full source captured in Phase 0.8):

IF v_pub_lifecycle = 'enacted' THEN
  RAISE EXCEPTION 'INV-ENACTED-PUB: publication_member của publication % đã enacted, locked', v_pub_id;
END IF;

→ DELETE on tac_publication_member is allowed when the publication is proposed | superseded | retired. Allowed in our pilot's mutable state.

fn_tac_enacted_immut:

IF TG_OP = 'DELETE' AND OLD.lifecycle_status = 'enacted' THEN
  RAISE EXCEPTION 'INV-ENACTED-IMMUT: cannot delete enacted unit_version %', OLD.id;
END IF;

→ DELETE on tac_unit_version is blocked only when UV's lifecycle_status='enacted'. Insert SQL keeps every UV at 'draft' → DELETE allowed.

tac_logical_unit has no ENACTED-immut trigger; only birth_gate_lu fires on INSERT/UPDATE. DELETE has no protective trigger → unrestricted (subject to FK from UV/PM, handled by deletion order).

tac_publication has no DELETE-blocking trigger.

10.2 Verdict

Rollback is feasible as long as:

  • tac_publication.lifecycle_status stays proposed, AND
  • All tac_unit_version.lifecycle_status stay draft (or any non-enacted state).

The script enforces this with a guard DO block that fails fast if lifecycle_status='enacted'. Once any APR transitions the pilot to enacted, this rollback path collapses (re-derivable: BG-UV-03 still warn, but enacted_immut and pm_enacted_lock block DELETE).

BEGIN;
DO $$
DECLARE v_lc text; v_pm int;
BEGIN
  SELECT lifecycle_status INTO v_lc FROM tac_publication WHERE id = :'pub_id'::uuid;
  IF v_lc IS NULL THEN RAISE EXCEPTION 'pub_id not found'; END IF;
  IF v_lc = 'enacted' THEN
    RAISE EXCEPTION 'Refusing rollback: publication % is enacted (pm_enacted_lock will block)', :'pub_id'::uuid;
  END IF;
  SELECT count(*) INTO v_pm FROM tac_publication_member WHERE publication_id = :'pub_id'::uuid;
  RAISE NOTICE 'rollback: pm rows to drop = %', v_pm;
END $$;

DELETE FROM tac_publication_member WHERE publication_id = :'pub_id'::uuid;
DELETE FROM tac_unit_version uv
 USING tac_logical_unit lu
 WHERE uv.logical_unit_id = lu.id
   AND lu.doc_code='DIEU-35'
   AND lu.canonical_address LIKE 'D38-DIEU35-%';
DELETE FROM tac_logical_unit
 WHERE doc_code='DIEU-35' AND canonical_address LIKE 'D38-DIEU35-%';
DELETE FROM tac_publication WHERE id = :'pub_id'::uuid;
COMMIT;

11. Verify-counts SQL

Field Value
Path on VPS /tmp/p10a-2a-verify-counts.sql
Bytes 664
SHA256 fb0a802049817190b6e75ba9b91c64c7071878d6fd53285fd8af214db8519363
Expected 1 publication, 36 LUs, 36 UVs, 36 PMs
SELECT 'tac_publication' AS rel, count(*) AS rows FROM tac_publication WHERE id = :'pub_id'::uuid
UNION ALL SELECT 'tac_publication_member', count(*) FROM tac_publication_member WHERE publication_id = :'pub_id'::uuid
UNION ALL SELECT 'tac_unit_version', count(*) FROM tac_unit_version uv
                                                JOIN tac_publication_member pm ON pm.unit_version_id = uv.id
                                                WHERE pm.publication_id = :'pub_id'::uuid
UNION ALL SELECT 'tac_logical_unit', count(*) FROM tac_logical_unit
                                                WHERE doc_code='DIEU-35' AND canonical_address LIKE 'D38-DIEU35-%';

12. Open Issues / Warnings (carry-forward to GPT review)

# Severity Issue
O1 HIGH canonical_address regex enforced by fn_tac_birth_gate_lu is D38-only (^D38-[A-Z0-9]+-…). Candidate v1 dieu-35/{unit_key} would have failed. P10A-2A remaps to `D38-DIEU35-{ROOT
O2 MEDIUM §4 ★KHOẢN children mapped to S4-P1-1 / -P1-2 / -P1-3 (treated as sub-articles of §4.1), not separate top-level S4-P5/P6/P7. Decision is reversible at P10A-2B but changes parent_id semantics.
O3 MEDIUM dieu35.s8.2 body is empty (source has only the heading "8.2 dot-dot-coverage (Cấp A) — giữ v5.0"). BG-UV-03 is currently warn mode → insert will succeed with a warning. If GPT prefers, P10A-2B can: (a) supply a 1-line stub body, or (b) flip the unit's length_flag post-insert to length_exception_reason='kept v5.0 baseline'.
O4 MEDIUM §8 split count is 5 children (3 numbered + 2 KHOẢN), not the "4 cặp" suggested in dispatch §1.1. v3 reflects the actual source structure.
O5 LOW Generator stores body_sha256 as body_sha256_candidate inside content_profile (jsonb). The trigger-computed content_hash differs (it hashes title|body|description|profile). Both retained for audit; content_hash is the durable PG-side identity.
O6 LOW Insert SQL uses DO $p10a$ block; nested PL/pgSQL exception inside the same transaction will roll the whole batch back on any single-row failure. No partial-state risk.
O7 LOW Phase 0.4b shows directus is the only role with INSERT on the 4 tables. P10A-2B execution role decision (use directus vs. add a dedicated tac_writer) is out of scope here.
O8 LOW Description regenerator chose template [{section_type}] {prose} — diverges from the future Đ43 description-governance template referenced in P10A-1B §4 caveat. Acceptable for candidate; may be overridden in a later APR.
O9 LOW appendix_a / changelog / post_merge_todo mapped to S13/S14/S15 to satisfy the canonical_address regex (no first-class slot for non-numeric tail sections).

13. Đ41 / VPS Code Hygiene

  • Repo files modified: none. No git add/git commit performed (no host-repo edits made).
  • VPS temp artifacts (kept for audit):
Path SHA256 Bytes Purpose
/tmp/d35-source.json (re-fetched) 39,938 (body) Raw source body API response
/tmp/d35-segments-v3.json 1a2a8c2ffda5aac45c7a466c974e69ad13f54b9a494eb34b05abed18ad786a54 ~30 KB Candidate v3 JSON (36 units)
/tmp/seg3.py c486da2aba12df99260e8ecd55141070ee53608fff7b31a0a13d631bd73ab120 8 KB v3 generator (extends seg2 + §8 split + CA map)
/tmp/sqlgen.py 36fc9c382946a471301cd49d8f5c18124ee37bc98dca573f5ffe786bb4f4d550 ~7 KB SQL package generator
/tmp/p10a-2a-insert-candidate.sql c6c6781cfc33dc37191db8096569e28d3afa0714e97bfc3810481d0589073563 85,675 Insert SQL (NOT EXECUTED)
/tmp/p10a-2a-render.sql 2142f6abcc1c94386a848745fb13f64584d2f716c2c2631e43b25a17f99ed579 1,512 Render SQL (NOT EXECUTED)
/tmp/p10a-2a-rollback.sql 25cc49d9bcc96b8b7cd434fcf3d92656ce8fedd11f5edf6337e3e1ee5f18e184 1,444 Rollback SQL (NOT EXECUTED)
/tmp/p10a-2a-verify-counts.sql fb0a802049817190b6e75ba9b91c64c7071878d6fd53285fd8af214db8519363 664 Verify-counts SQL (NOT EXECUTED)
/tmp/p10a2a-phase0.sql / /tmp/p10a2a-phase0.out 73 / 480 lines Phase 0 query script + raw output
  • Canonical output = this KB report.

14. STOP

P10A-2A closes here. No SQL was executed. Hand off to GPT for package review. Upon approval, P10A-2B will execute insert → verify counts → render read-back → STOP-and-await-User-review. Until then no mutation.


P10A-2A report v1 | S187 | 2026-04-29 | Read-only generation | 9/9 PASS | Critical finding O1 (D38- canonical_address namespace) flagged for GPT review