KB-3354

DIEU-32 NULL Heading Body — Root-Cause Investigation Report (INTENTIONAL_TAC_HEADING_CONTAINER + TAC_TO_IU_MODEL_GAP)

19 min read Revision 1
p3dphase5c2r2dieu32root-causeheading-null-bodyreadonlyinvestigationno-mutation2026-05-14

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.md Verdict: PASS — primary INTENTIONAL_TAC_HEADING_CONTAINER, secondary TAC_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 modelsection_type='heading' rows with children > 0 carry no semantic body. The only difference is the literal representation: D28's insert package used body='' (empty string); D32's insert package used body=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 from title, 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 have children > 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's body=NULL is a representation choice within the same modeling pattern, not a defect.

Secondary: TAC_TO_IU_MODEL_GAP

  • fn_iu_create source (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=0NULL).
  • 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

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 := title reproduces 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=true records that the source had no body.
  • content_profile.src_content_hash continues 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:

  1. 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).
  2. section_type='heading' AND body IS NULL — recommended general form.
  3. body IS NULL regardless 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_source after 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 + children view.
  • 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 via parent_or_container_ref (or identity_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'.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/reports/dieu32-null-heading-body-root-cause-investigation-report.md