P10A-2A — D35 Insert/Render Package (Read-Only Generation, 2026-04-29)
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:
directusis 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 > 0tac_logical_unit.sort_order >= 0tac_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_processhas 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);
8.2 Footer excerpt (last 8 lines)
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_statusstaysproposed, AND- All
tac_unit_version.lifecycle_statusstaydraft(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 commitperformed (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