DIEU-32 NULL Heading Body — Root-Cause Investigation Report (INTENTIONAL_TAC_HEADING_CONTAINER + TAC_TO_IU_MODEL_GAP)
DIEU-32 NULL Heading Body — Root-Cause Investigation Report
Date (UTC): 2026-05-14 Author: Claude Opus 4.7 (1M, xhigh) Mode: READ-ONLY / NO MUTATION Controlling prompt:
knowledge/dev/laws/dieu44-trien-khai/prompts/agent-dieu32-null-heading-root-cause-investigation-2026-05-14.mdVerdict: PASS — primaryINTENTIONAL_TAC_HEADING_CONTAINER, secondaryTAC_TO_IU_MODEL_GAP. DIEU-32 is not a defective law. Policy:POLICY_SYNTHESIZE_TITLE_FOR_HEADING_NULL_BODY(governed, deterministic, body-source-marked).
0. Hard boundaries — all honored
| Boundary | Honored |
|---|---|
| No DB writes / DDL / DML / TAC update / IU create-update-delete | ✅ |
No fn_iu_create / no mutating fn invocation |
✅ |
| No content repair / no KB source patch | ✅ |
| No code / UI / vector work / no migration retry | ✅ |
| Only SELECTs + reading KB | ✅ |
no_mutation_performed = true.
1. Live TAC state for DIEU-32 (all 23 rows)
Full row dump with body_is_null, body_char, section_type, children count, parent. Sorted by render_order:
| ro | canonical_address | section_type | body | title (first 60) | parent | children |
|---|---|---|---|---|---|---|
| 0 | D38-DIEU32-ROOT |
heading | NULL | ĐIỀU 32: LUẬT PHÊ DUYỆT — v1.1 BAN HÀNH |
(root) | 10 |
| 1 | D38-DIEU32-S0 |
paragraph | len=329 | Preamble |
ROOT | 0 |
| 2 | D38-DIEU32-S1 |
paragraph | len=196 | §1. Mục đích |
ROOT | 0 |
| 3 | D38-DIEU32-S2 |
heading | NULL | §2. Nguyên tắc |
ROOT | 4 |
| 4 | D38-DIEU32-S2-P1 |
principle | len=93 | §2.1 DOT 100% |
S2 | 0 |
| 5 | D38-DIEU32-S2-P2 |
principle | len=144 | §2.2 Đề xuất phải đứng được một mình |
S2 | 0 |
| 6 | D38-DIEU32-S2-P3 |
principle | len=130 | §2.3 Qua PG — theo đúng luồng thiết kế |
S2 | 0 |
| 7 | D38-DIEU32-S2-P4 |
paragraph | len=117 | §2.4 Phạm vi áp dụng |
S2 | 0 |
| 8 | D38-DIEU32-S3 |
heading | NULL | §3. Schema approval |
ROOT | 5 |
| 9 | D38-DIEU32-S3-P1 |
technical_spec | len=546 | §3.1 Split code khỏi payload |
S3 | 0 |
| 10 | D38-DIEU32-S3-P2 |
technical_spec | len=453 | §3.2 2 bảng type |
S3 | 0 |
| 11 | D38-DIEU32-S3-P3 |
technical_spec | len=410 | §3.3 Seed chuẩn |
S3 | 0 |
| 12 | D38-DIEU32-S3-P4 |
governance_process | len=143 | §3.4 Register Collection Protocol |
S3 | 0 |
| 13 | D38-DIEU32-S3-P5 |
process | len=600 | §3.5 Sequencing migration bắt buộc |
S3 | 0 |
| 14 | D38-DIEU32-S4 |
heading | NULL | §4. Quorum approvals |
ROOT | 3 |
| 15 | D38-DIEU32-S4-P1 |
technical_spec | len=225 | §4.1 Bảng \apr_approvals`` |
S4 | 0 |
| 16 | D38-DIEU32-S4-P2 |
technical_spec | len=220 | §4.2 Trigger quorum |
S4 | 0 |
| 17 | D38-DIEU32-S4-P3 |
checklist | len=217 | §4.3 Hardening rules |
S4 | 0 |
| 18 | D38-DIEU32-S5 |
paragraph | len=206 | §5. DOT hỗ trợ APR |
ROOT | 0 |
| 19 | D38-DIEU32-S6 |
process | len=171 | §6. Lifecycle |
ROOT | 0 |
| 20 | D38-DIEU32-S7 |
governance_process | len=145 | §7. Reserved / unimplemented handlers |
ROOT | 0 |
| 21 | D38-DIEU32-S8 |
checklist | len=167 | §8. Thước đo |
ROOT | 0 |
| 22 | D38-DIEU32-S9 |
changelog | len=327 | CHANGELOG |
ROOT | 0 |
Observation: The 4 NULL-body rows (ROOT, S2, S3, S4) are exactly and only the rows that satisfy section_type='heading' AND children > 0. No other row of any type or any sub-structure has NULL body. This is a clean, deterministic pattern: pure container headings carry no body, leaves carry body.
current_tac_rows = 23, null_body_rows = 4, null_body_rows_all_heading = true.
2. Cross-publication heading comparison (live)
To distinguish "intentional D32 modeling" from "D32-specific bug", compare every section_type='heading' row across the 3 publications:
| publication | canonical_address | section_type | body | children |
|---|---|---|---|---|
| DIEU-28 | D38-DIEU28-ROOT | heading | '' (len=0, empty string) |
12 |
| DIEU-28 | D38-DIEU28-S1 | heading | '' |
2 |
| DIEU-28 | D38-DIEU28-S2 | heading | '' |
5 |
| DIEU-28 | D38-DIEU28-S3 | heading | '' |
5 |
| DIEU-28 | D38-DIEU28-S8 | heading | '' |
2 |
| DIEU-32 | D38-DIEU32-ROOT | heading | NULL | 10 |
| DIEU-32 | D38-DIEU32-S2 | heading | NULL | 4 |
| DIEU-32 | D38-DIEU32-S3 | heading | NULL | 5 |
| DIEU-32 | D38-DIEU32-S4 | heading | NULL | 3 |
| DIEU-35 | D38-DIEU35-S0 | heading | len=1285 (non-empty) | 0 (leaf) |
Key cross-publication pattern (decisive):
- D28 + D32 use the same container-heading model —
section_type='heading'rows withchildren > 0carry no semantic body. The only difference is the literal representation: D28's insert package usedbody=''(empty string); D32's insert package usedbody=NULL. Both encode the same semantic ("title is the visible content; body content lives in children"). TAC's render layer treats both identically (P10B-1C conversion rule renders headings fromtitle, not body). - D35 has only one
section_type='heading'row, and it is a LEAF (children=0) with substantive body. The label "heading" there is being used for a single non-container row whose body holds the actual content. This is not a container.
So section_type='heading' is overloaded: it can be either a container (children>0, no semantic body) or a leaf (children=0, body holds the content). The container/leaf split is what determines whether body is required.
historical_roundtrip_zero_drift_confirmed = true (P10B-1C explicit).
null_body_rows_all_heading = true (all 4 NULL rows in D32 are section_type='heading' with children > 0).
3. Historical intent — segmentation candidate + P10B-1C execute report
3.1 Segmentation candidate (p10b-1a-d32-segmentation-candidate-2026-04-29.md)
Explicit tree from the segmentation:
ROOT (heading)
├── S0 Preamble (paragraph)
├── S1 §1. Mục đích (paragraph)
├── S2 §2. Nguyên tắc (heading → 4 children)
├── S3 §3. Schema approval (heading → 5 children)
├── S4 §4. Quorum (heading → 3 children)
├── S5 §5. DOT hỗ trợ APR (paragraph)
├── S6 §6. Lifecycle (process)
├── S7 §7. Reserved handlers (governance_process)
├── S8 §8. Thước đo (checklist)
└── S9 CHANGELOG (changelog)
The four (heading → N children) rows are exactly the four NULL-body rows. The segmentation deliberately classified them as containers because the original law has no paragraph body at those levels.
3.2 P10B-1C execute/render report (2026-04-29) — round-trip 0 drift
Quoting verbatim from p10b-1c-d32-execute-report-2026-04-29.md:
Conversion rules used:
- ROOT →
# {title}- S0 (Preamble) → body only (blockquote already included)
- Depth-1 heading/paragraph →
## {title}then body- Depth-2 →
### {title}then body- …
- Diff
-u original reconstructed: empty diff.- Normalized comparison: IDENTICAL (5601 chars both sides).
- ✅ Round-trip 0 content drift confirmed.
The render explicitly uses title to render the ROOT and depth-1 headings. The body is consulted, but for the four container headings the body is NULL/absent and the rendered output relies entirely on the title. Round-trip drift = 0; no content is missing.
current_render_matches_original = true (confirmed by P10B-1C; verified live by inspection of original law and live container rows in §1).
4. Original law comparison
knowledge/dev/laws/dieu32-approval-law.md is available (5,601 chars). Looking at the four container-heading sections in the original:
| Original markdown | Has any body content at that level? |
|---|---|
# ĐIỀU 32: LUẬT PHÊ DUYỆT — v1.1 BAN HÀNH followed by blockquote of v1.1 BAN HÀNH banner, then ---, then ## §1. Mục đích |
Banner is captured by S0=Preamble (paragraph, len=329, contains the blockquote). ROOT itself has no body — title is the visible content. |
## §2. Nguyên tắc immediately followed by ### §2.1 DOT 100% |
No body between heading and first child. |
## §3. Schema approval immediately followed by ### §3.1 Split code khỏi payload |
No body between heading and first child. |
## §4. Quorum approvals immediately followed by ### §4.1 Bảng \apr_approvals`` |
No body between heading and first child. |
Confirmed live: a SELECT on §2 + its 4 children shows the heading row has body=(null) and the four children carry the substantive content (§2.1 = 93 chars, §2.2 = 144 chars, §2.3 = 130 chars, §2.4 = 117 chars). Same for §3 and §4.
Data-loss check: none. Every paragraph, principle, technical_spec, process, governance_process, checklist, changelog row in the original law has a matching non-null body in TAC. The 4 NULL bodies correspond exactly to the four pure-container headings, and the original law has no body content at those levels.
original_law_available = true, current_render_matches_original = true, data_loss_detected = false.
5. Root-cause classification
Primary: INTENTIONAL_TAC_HEADING_CONTAINER
- All 4 NULL-body rows are
section_type='heading'AND havechildren > 0. - The original law contains no body content at those four levels — only a heading line followed by sub-sections.
- The TAC segmentation explicitly modelled them as containers (
heading → N children). - The TAC render layer correctly renders them from
title(P10B-1C conversion rules). - Round-trip drift = 0 over 5,601 chars (P10B-1C diff empty + normalized identical).
- Cross-publication evidence: D28 uses the same container-heading model with
body=''(empty string, semantically equivalent). D32'sbody=NULLis a representation choice within the same modeling pattern, not a defect.
Secondary: TAC_TO_IU_MODEL_GAP
fn_iu_createsource (verified earlier):IF p_body IS NULL THEN RAISE EXCEPTION 'body required'; END IF;— rejects NULL but accepts empty string.- D28 passed migration because its 5 heading containers used
body=''(length=0≠NULL). - D32 was blocked because its 4 heading containers used
body=NULL— same semantics, different representation. - The IU contract requires non-NULL body, but TAC permits NULL for the same modeling purpose. The gap is in IU's body-not-NULL invariant vs TAC's lax representation of "no body" (NULL or '').
Ruled out
TAC_INSERT_PACKAGE_BUG— ruled out. The insert package authored a deterministic, internally consistent model where every container heading has no body. The choice of NULL over '' is a representation preference, not a bug, because TAC accepts both and the render output is identical (P10B-1C 0-drift). A "bug" claim would require evidence that the package was supposed to write text and silently failed; the original law has no text at those levels, so there was nothing to write.DATA_LOSS_OR_DRIFT— ruled out by P10B-1C 0-drift report + direct read of the original law + live row dump. No content is missing.UNKNOWN_NEEDS_MORE_EVIDENCE— not applicable; evidence is decisive.
root_cause_classification = INTENTIONAL_TAC_HEADING_CONTAINER (primary) + TAC_TO_IU_MODEL_GAP (secondary).
6. Policy recommendation
Recommended: POLICY_SYNTHESIZE_TITLE_FOR_HEADING_NULL_BODY
Rule (proposed for cutter v0.1):
IF source.section_type = 'heading' AND source.body IS NULL THEN
iu_body := source.title
identity_profile.body_source := 'synthesized_from_title_due_to_tac_heading_null_body'
content_profile.body_source := 'synthesized_from_title_due_to_tac_heading_null_body'
content_profile.src_body_was_null := true
content_profile.src_title := source.title
content_profile.src_unit_version_id := source.id
content_profile.src_content_hash := source.content_hash -- still preserved as provenance
ELSE
iu_body := source.body
content_profile.body_source := 'preserved_from_tac_unit_version_body'
ENDIF
Why this preserves the law correctly
- The original law has no body text at the four container-heading levels; the visible content of "§2. Nguyên tắc" is the title. Writing
iu_body := titlereproduces the visible heading text exactly, with no fabrication. - Round-trip integrity is preserved: when the IU layer renders these rows,
# {title}(or## {title}) produces the same output as TAC's render did at 0-drift in P10B-1C. - No content is invented — only a representation conversion ("title becomes body because IU's body is the unit's textual representation, and the unit's textual representation in TAC was the title").
Does it violate source preservation?
Not at the semantic level (visible content of the heading is preserved verbatim). At the byte level it diverges from unit_version.body = tac_unit_version.body for these 4 rows. To handle this cleanly:
- The provenance flag
content_profile.body_source='synthesized_from_title_due_to_tac_heading_null_body'makes the divergence explicit and auditable. content_profile.src_body_was_null=truerecords that the source had no body.content_profile.src_content_hashcontinues to carry the original TAC hash as provenance.- Future reverse migration / reconciliation can detect synthesized rows and reverse the rule.
How V-3 should be adjusted
V-3a (IU-side hash consistency) and V-3c (TAC hash provenance) remain unchanged.
V-3b (body preservation) becomes conditional:
V-3b' (conditional body preservation):
IF content_profile.body_source = 'preserved_from_tac_unit_version_body' THEN
require iu.unit_version.body = tac_unit_version.body (byte-equality)
ELSIF content_profile.body_source = 'synthesized_from_title_due_to_tac_heading_null_body' THEN
require iu.unit_version.body = tac_logical_unit.title
AND content_profile.src_body_was_null = true
AND identity_profile.body_source matches
AND source.section_type = 'heading'
ELSE
FAIL — unknown body_source policy
ENDIF
v3_policy_patch_required = true and synthesize_title_allowed = true.
General rule, not D32-specific
The rule should be general for section_type='heading' AND source.body IS NULL. Reasons:
- It captures the modeling pattern, not a single-publication accident.
- D28 already happens to be compatible (its headings use
'', not NULL, so the synthesize branch never fires). - D35 has only a leaf-style heading (children=0), so the synthesize branch never fires; its body remains the source body.
- Future publications using either NULL or '' for container headings are handled uniformly.
Conservative variants, in order of strictness:
section_type='heading' AND children>0 AND body IS NULL— strictest; would only fire on container headings. Safe but slightly less general (rejects heading-leaf with NULL body, but that's a malformed source).section_type='heading' AND body IS NULL— recommended general form.body IS NULLregardless of section_type — too broad; would mask defects in other section types.
Recommend variant 2 with a soft-flag log when children=0 and body is NULL on a heading row (suggests defect).
Risk to future automated cutter (Cắt luật A)
- LOW: rule is deterministic, side-effect-free, fully marked with provenance.
- The cutter can encode this as a single conditional during the per-row payload preparation.
- The conditional V-3b' is purely declarative and can be implemented as one extra SELECT against
content_profile.body_sourceafter migration. - The rule is reversible: any synthesized row can be detected by the provenance flag, and the original (NULL) body in TAC is unchanged.
- No risk of cascading misclassification: container-heading detection is unambiguous given the live
section_type+childrenview.
Alternatives considered (not recommended)
POLICY_FIX_TAC_UPSTREAM_BODY_FIRST— would require TAC writes (currently forbidden) and would not preserve historical TAC representation; it also assumes one of NULL vs '' is "right" when both encode the same intent.POLICY_PATCH_FN_IU_CREATE_ALLOW_NULL_HEADING— would relax IU's body-not-NULL invariant globally, which is a regression in IU contract strength; the model gap should be resolved at the cutter, not in IU schema/function.POLICY_SKIP_HEADING_ROWS— would lose tree structure; container headings carry the hierarchy viaparent_or_container_ref(oridentity_profile.tac_hierarchy) and dropping them corrupts the rendering tree.POLICY_BLOCK_AND_ESCALATE— already the current state; not actionable; defers a clear modeling decision.
7. DIEU-32 retry recommendation
Once POLICY_SYNTHESIZE_TITLE_FOR_HEADING_NULL_BODY and the conditional V-3b' are approved:
- DIEU-32 retry is recommended with the synthesize policy applied to the 4 container-heading rows (
D38-DIEU32-ROOT,D38-DIEU32-S2,D38-DIEU32-S3,D38-DIEU32-S4). - All other 19 rows pass unchanged with
body_source='preserved_from_tac_unit_version_body'. - Expected outcome: 23/23 IU + 23/23 UV + 23/23 birth committed, no source mutation, all gates V-1..V-7 (with patched V-3) and V-8..V-10 PASS.
dieu32_retry_recommended = true, fix_tac_upstream_required = false.
8. Required final response fields
dieu32_root_cause_investigation_status=PASS
no_mutation_performed=true
current_tac_rows=23
null_body_rows=4
null_body_rows_all_heading=true
historical_roundtrip_zero_drift_confirmed=true
original_law_available=true
current_render_matches_original=true
root_cause_classification=INTENTIONAL_TAC_HEADING_CONTAINER (secondary: TAC_TO_IU_MODEL_GAP)
recommended_policy=POLICY_SYNTHESIZE_TITLE_FOR_HEADING_NULL_BODY
synthesize_title_allowed=true
fix_tac_upstream_required=false
data_loss_detected=false
dieu32_retry_recommended=true
v3_policy_patch_required=true
report_path=knowledge/dev/laws/dieu44-trien-khai/reports/dieu32-null-heading-body-root-cause-investigation-report.md
next_recommended_action=GPT_REVIEW_ROOT_CAUSE_THEN_DECIDE_DIEU32_RETRY_POLICY
DIEU-32 NULL Heading Body Root-Cause Investigation | 2026-05-14 | Claude Opus 4.7 xhigh | READ-ONLY | No mutation. Verdict: INTENTIONAL_TAC_HEADING_CONTAINER + TAC_TO_IU_MODEL_GAP. Recommended policy: synthesize title for heading NULL body, with explicit provenance and conditional V-3b'.