Skip to content

TDD - JBRK Legal OS Reporting Letter (DocGen configuration)

status: implemented
date: 2026-06-08
solution: jbrk
consumes: docgen framework
release: releases/r2-framework-production-features
primary-source: ../../reference/JBRK_Legal_OS_Info_Guide.docx

✅ Implementation status (seeded, renders through the real engine)

JBRK is implemented + seeded in the tomorrow-law org (C1–C4, epic #89), rendering the reporting letter deterministically (no LLM in the body): - Declarative since 2026-07-09: the catalog lives in authoring/specs/tomorrow-law-reporting-letter.json + tomorrow-law-inter-vivos.json (Texts, Risk Blocks, Snippets with ordered snippet_items, Sections, Fields, and template.snippet_bindings — the spec-format extension this conversion added). fixtures/jbrk.py keeps only the tax-math compute hook + post-compile Risk Block approvals. - Section II = from_snippetbody (every Text across both snippets, numbered by a running step_num in phase_order order); Section III = from_snippetanalysis (approved Risk Blocks only — the draft is withheld). Section I = template over Fields. - Framework gap fixed: instantiate_template now deep-copies document_snippets bindings, so a composition reporting letter instantiates with its Section II/III content. - As-built note vs the design below: the seed uses per-document Fields + per-binding entity_bindings for the file facts (not the G2 tax_math computed-fields path, which remains the production story). Verified live in the demo + tests/integration/test_jbrk_seeded.py (all-7 assertions) + tests/unit/test_jbrk_fixture.py.

What this is. The technical design that proves the generic DocGen framework can be configured (not forked) to produce JBRK's tax-reorg reporting letter. It maps every JBRK construct onto a DocGen R2 primitive, gives fixtures + a worked example (post-mortem pipeline), and ends with a framework-coverage table and an explicit misfits / gaps section. DocGen vocabulary throughout (JBRK origin terms noted once in [JB-T-vocab]).

Builds on: ../../releases/r2-framework-production-features/r2-tdd.md ([T2-composition], [T2-render], [T2-entity-versions], [T2-entity-generation-snapshot]), ../../releases/r1-framework-v0-core/r1-tdd.md (sections.render_mode, Document.state), and ../../explanation/phoenix-uc2-analysis.md (G1/G2/G3).


Vocabulary mapping [JB-T-vocab]

Decided for this project. JBRK's four-layer architecture renames onto DocGen primitives; we use the DocGen name everywhere after this table.

JBRK origin term DocGen primitive DocGen schema home
Atom (transaction-step template: heading + narrative + bullets, Jinja, context-independent {% if %}) Text (structured) texts ([T2-entity-texts])
Molecule (MoleculeSchema(atom_sequence, risk_blocks, entity_roles, phase_order)) Snippet (composition) snippets + snippet_items ([T2-entity-snippets], [T2-entity-snippet_items])
Risk Block (Section III analysis, lawyer-approved) Risk Block (approval-gated) risk_blocks ([T2-entity-risk_blocks])
Variable (flat dict from the graph adapter) Field (+ computed via G2) fields ([D-15]) + computed-fields hook (G2)
MoleculeSchema.phase_order Snippet phase_order snippets.phase_order
MoleculeSchema.entity_roles + per-file binding Snippet entity_roles_json + document_snippets.entity_bindings_json [T2-entity-document_snippets]
{{step_num}} running counter render-provided step_num over the flattened stream [T2-render]

Document template [JB-T-template]

The reporting letter is a DocGen document template (documents.is_template=true, [D-13]) with three Sections. instantiate_template deep-copies it per file; the instance binds Snippets and supplies the file's Fields.

Section (title) render_mode What it renders
Section I — Background Facts template (or a narrowly-scoped custom_render summarizing approved facts only) the fact narrative from the Field dict; no analysis
Section II — Transaction Steps & Tax Consequences template, grouped-by-type → text every Text item across all bound Snippets, (phase_order, ordinal)-ordered, numbered by step_num
Section III — Strategic Risk Commentary template, grouped-by-type → risk_block every approved Risk Block item across all bound Snippets, same ordering

Section II and III are the heart of the genericity proof: each is a single template-mode section whose output_template consumes the renderer's flattened, phase+ordinal-ordered stream of snippet_items ([T2-render]), filtered by item_type. The grouping ("all Texts first, then all Risk Blocks, across Snippets") is the grouped-by-type assembly mode — a per-snippet expansion would interleave steps and risks incorrectly, which is exactly the JBRK reporting-letter shape the assembly mode exists for.

Why grouped-by-type, concretely

A file binding estate_freeze (phase 0) + post_mortem_pipeline (phase 10) must read:

  • Section II: freeze steps (1, 2, …) then pipeline steps (…, N) — all transaction steps together, in execution order, one running counter.
  • Section III: freeze risks then pipeline risks — all analysis together.

Inline (per-snippet) assembly would produce [freeze steps][freeze risks][pipeline steps][pipeline risks] — wrong for a reporting letter. Grouped-by-type produces [all steps][all risks] — correct. The section template picks grouped; step_num is the document-level running index (1..N over the text stream).


Snippet → snippet_items mapping (worked: post_mortem_pipeline) [JB-T-snippet]

JBRK's MoleculeSchema for the post-mortem pipeline:

MOLECULES["post_mortem_pipeline"] = MoleculeSchema(
    id="post_mortem_pipeline",
    atom_sequence=["incorporate_newco", "s85_transfer", "tax_funding_dividend", "surplus_extraction"],
    risk_blocks=["s84_1_non_share_consideration", "s84_2_deemed_dividend",
                 "s55_2_part_iv", "s164_6_loss_carryback", "gaar"],
    entity_roles={"opco": "corporation", "transferor": "individual", "newco": "corporation"},
    phase_order=10,
)

maps to a DocGen snippets row + ordered snippet_items rows:

snippets column value
key post_mortem_pipeline
name / description "Post-Mortem Pipeline" / "Standard post-mortem pipeline reorganization"
entity_roles_json {"opco":"corporation","transferor":"individual","newco":"corporation"}
phase_order 10
snippet_items ordinal item_type item_key
1 text incorporate_newco
2 text s85_transfer
3 text tax_funding_dividend
4 text surplus_extraction
5 risk_block s84_1_non_share_consideration
6 risk_block s84_2_deemed_dividend
7 risk_block s55_2_part_iv
8 risk_block s164_6_loss_carryback
9 risk_block gaar

atom_sequence and risk_blocks collapse into one ordered snippet_items table discriminated by item_type ([T2-entity-snippet_items]). Render emits the text items as Section II and the risk_block items as Section III — and grouped-by-type assembly collates these across all Snippets bound to the document.


Primitive mapping [JB-T-primitives]

Texts (JBRK atoms) — Section II [JB-T-text]

Each transaction-step Text uses the structured columns from [T2-entity-texts]: heading_template, narrative_template, bullets_json, entity_roles_used. Context-independent by rule — variants come from {% if %}, never hardcoded.

# texts row: key = "s85_transfer"
heading_template: "Transaction #{{ step_num }} -- Transfer of Shares Under Subsection 85(1)"
narrative_template: |
  Effective {{ effective_date }}, the {{ role.transferor.title }} transferred the
  {{ subject_shares }} of {{ role.opco.name }} to {{ role.newco.name }} in
  consideration for {{ consideration_desc }}.
  {% if is_post_mortem %}This step occurs after the deemed disposition on death,
  establishing the high outside basis the pipeline relies on.{% endif %}
bullets_json:
  - "The elected amount under subsection 85(1) is {{ Extraction_amount }}, equal to the
     adjusted cost base of the transferred shares."
  - "The parties will file a joint election on Form T2057 on or before {{ t2057_due_date }}."
  - "The resulting capital gain is {{ capital_gain }}."
entity_roles_used: ["opco", "transferor", "newco"]

This Text exercises two framework gaps:

  • G1 (structured/iterable render data){% if is_post_mortem %} conditionals and, where a step has a variable number of sub-items (e.g. iterating over multiple transferred share classes), {% for %} over a list-valued Field / gathered row. The sandboxed Jinja env must permit {% if %} / {% for %} / attribute access. (Same primitive Phoenix P3 needs.)
  • G2 (computed fields){{ Extraction_amount }}, {{ capital_gain }} are computed, not stored: JBRK tax_math pure functions (s85.py, …) registered as a DocGen computed-fields hook whose outputs merge into the render scope. {{ step_num }} is render-provided (the running counter), not a Field.

Risk Blocks + approval gate — Section III [JB-T-risk]

# risk_blocks row: key = "s84_2_deemed_dividend"
heading: "Subsection 84(2) -- Deemed Dividend Risk"
statutory_ref: "s.84(2)"
applies_to: "post_mortem_pipeline"
status: "approved"        # draft -> approved; only approved renders in production
template: |
  The principal risk to the pipeline is that the CRA recharacterizes the surplus
  extraction of {{ Extraction_amount }} as a deemed dividend under subsection 84(2)...
  {% if opco_cda > 0 %}The available capital dividend account balance of {{ opco_cda }}
  partially shelters this exposure.{% endif %}

The status gate ([T2-entity-risk_blocks]) is the enforceable form of "every word approved": the templated render in [T2-render] renders only approved Risk Blocks. A draft block is excluded (or DRAFT-tagged) — [JB-success-3].

Snippet composition + phase_order + entity_roles [JB-T-compose]

Covered above ([JB-T-snippet]). phase_order sorts multi-pattern documents (freeze 0 → crystallization 1 → pipeline 10); entity_roles_json declares the role→entity-type contract the document binding must satisfy.

document_snippets binding (role → entity) [JB-T-binding]

Per file, the instance declares which patterns apply and binds roles to real graph entities:

document_snippets column example value
document_id the file's reporting-letter instance
snippet_id post_mortem_pipeline
ordinal 0 (tiebreak within a phase_order)
entity_bindings_json {"opco":"corp:OpcoLtd","transferor":"ind:Estate_of_X","newco":"corp:Newco"}

Render resolves bindings into the Jinja scope so {{ role.opco.name }} reads the bound corporation ([T2-render]).

Computed Fields — G2 (tax_math) [JB-T-g2]

JBRK's tax_math/ is the canonical instance of DocGen's computed-fields hook. Solution code registers pure functions compute(graph) -> {capital_gain, Extraction_amount, deemed_dividend, safe_income, ...}; the framework merges outputs into the render scope (a computed.* namespace or below-fields precedence per G2). Framework owns the merge; JBRK owns the formulas (one module per ITA provision, each unit-tested — JBRK ships 57 tax_math tests). Deterministic + unit-testable, which is what keeps Section II/III reproducible.

Templated render assembly (grouped-by-type) [JB-T-render]

Section II/III are render_mode='template' sections (r1-tdd.md). The renderer ([T2-render]) loads document_snippets ordered by (snippets.phase_order, document_snippets.ordinal), expands each Snippet's snippet_items, renders each unit (Text heading/narrative/bullets, or approved Risk Block) via the sandboxed Jinja env against the Field dict + resolved bindings, and exposes a flattened phase+ordinal-ordered stream carrying item_type, owning snippet, and a document-level step_num (1..N over the text items). Section II's template selects grouped-by-type → text; Section III selects grouped-by-type → risk_block. Fully deterministic, no LLM — the audit chain is structural (each rendered span traces to one approved unit + its bindings).

Version history + generation snapshot — the audit spine [JB-T-audit]

  • Version history ([T2-entity-versions]). Every Text, Risk Block, Snippet, Section, Field, and Document is snapshot-on-write. A Risk Block's draft→approved promotion is a new version; "show me the exact Risk Block wording on the date we approved it" is one query.
  • Generation snapshot ([T2-entity-generation-snapshot]). Each letter generation pins the version of every consumed unit (the component_manifest_json), the resolved Field values (field_values_json), and the produced output (output_json). For JBRK's fully-templated path, re-rendering from the manifest reproduces the letter byte-for-byte (model_config_json is empty — no LLM). This is the regulated-firm defense: "on the filing date, this letter came from these exact approved unit versions and these facts." Satisfies [JB-success-1] and [JB-success-4].

Lawyer approval gate [JB-T-gate]

Two distinct gates, both first-class in the framework:

Gate Mechanism Enforced where
Per-unit Risk Block status draft→approved; production render refuses non-approved ([P2-feat-2], [T2-render]) render phase — a draft block never reaches a finalized letter
Document-level Document.state draft → under-review → finalized ([T-entity-2]) a "before execution docs" workflow gate: execution-document generation (T2057, agreements, resolutions) is downstream of finalized and refuses to run until the lawyer signs off the assembled letter

The document-level gate sits between assembly (DocGen render) and execution (JBRK solution step) — DocGen produces and locks the letter; the execution-doc generator (out of scope here) checks state='finalized' before proceeding. This is JBRK's "the engine never produces client-facing documents without lawyer sign-off."


Fixtures [JB-T-fixtures]

Minimal synthetic catalog (ingestion mocked per [JB-feat-inputs]):

Texts (atoms)incorporate_newco, s85_transfer, tax_funding_dividend, surplus_extraction, freeze_exchange_s86, crystallization_s85.

Risk Blockss84_1_non_share_consideration (approved), s84_2_deemed_dividend (approved), s55_2_part_iv (approved), s164_6_loss_carryback (approved), gaar (approved), plus one draft block (s246_1_benefit_conferral) to prove the gate ([JB-success-3]).

Snippetspost_mortem_pipeline (phase 10), inter_vivos_pipeline (phase 10), estate_freeze (phase 0), each with snippet_items per [JB-T-snippet].

Field dict + computed hook — a fixture graph flattened to ~60 scalar Fields plus a stub tax_math module returning fixed capital_gain / Extraction_amount / deemed_dividend (G2), so the templated path is deterministic and unit-testable.

Document template — "Reporting Letter" with Sections I/II/III per [JB-T-template].


Worked example [JB-T-example] — post-mortem pipeline reporting letter

  1. instantiate_template("Reporting Letter") → a new instance, Sections I/II/III copied, Fields copied with defaults.
  2. Set the file's Fields (from the mocked graph): Opco_name, effective_date, share descriptions, balances; computed Fields (capital_gain, Extraction_amount) supplied by the tax_math hook (G2).
  3. Bind one Snippet: document_snippets(snippet=post_mortem_pipeline, ordinal=0, entity_bindings={opco→OpcoLtd, transferor→Estate_of_X, newco→Newco}).
  4. Render:
  5. Renderer flattens snippet_items: texts [incorporate_newco, s85_transfer, tax_funding_dividend, surplus_extraction] (step_num 1–4), then risk_blocks [s84_1, s84_2, s55_2, s164_6, gaar] (the lone draft block is excluded).
  6. Section II (grouped → text): four numbered transaction steps, Jinja-rendered against Fields. "Transaction #2 -- Transfer of Shares Under Subsection 85(1) … The elected amount … is {{ Extraction_amount }} …".
  7. Section III (grouped → risk_block): five approved analyses in order.
  8. Document.state='under-review' → lawyer reviews → finalized. A generation snapshot pins all unit versions + Field values + output.
  9. Reproduce: re-render from the snapshot manifest → byte-identical letter ([JB-success-1]).

Multi-Snippet variant ([JB-success-2]): also bind estate_freeze (phase 0). Now Section II reads freeze steps first (step_num 1–2), then pipeline steps (3–6) — single running counter across both Snippets; Section III lists freeze risks then pipeline risks. This is the case that requires grouped-by-type + the document-level step_num.


Framework coverage [JB-T-coverage]

JBRK need DocGen primitive Status
Transaction-step template (heading/narrative/bullets, Jinja, context-independent) Text (structured) [T2-entity-texts] ✅ R2 primitive
{% if is_post_mortem %} conditional variants Jinja {% if %} in templates ✅ R2
{% for %} over variable sub-items (e.g. N share classes) structured/iterable render data 🟡 G1 — R2 framework gap (also Phoenix P3)
{{ step_num }} sequential numbering across all steps render-provided running counter over the flattened stream 🟡 part of [T2-render] — render behavior, see misfits
{{ capital_gain }}, {{ Extraction_amount }} (computed, not stored) computed-fields hook = tax_math 🟡 G2 — R2 framework gap
Variable dict (~60 stored values) Field [D-15] ✅ R1
Section-III analysis, lawyer-authored, approval-gated Risk Block + status gate [T2-entity-risk_blocks] ✅ R2 primitive
"Every word approved" enforcement render refuses non-approved Risk Blocks [T2-render] ✅ R2
Molecule = ordered atoms + risk blocks Snippet + snippet_items [T2-entity-snippet_items] ✅ R2
phase_order (freeze→crystallization→pipeline) snippets.phase_order ✅ R2
entity_roles + per-file role binding entity_roles_json + document_snippets.entity_bindings_json ✅ R2
Multi-molecule document multiple document_snippets rows ✅ R2
Section II = all atoms across molecules; III = all risk blocks across molecules grouped-by-type assembly mode [T2-render] 🟡 R2 render behavior — see misfits
Deterministic, no-LLM body render_mode='template' [T-render] ✅ R1 path, R2 composition
Audit: which unit versions produced the filed letter version history + generation snapshot [T2-entity-versions] / [T2-entity-generation-snapshot] ✅ R2
Document-level lawyer sign-off before execution docs Document.state finalized gate [T-entity-2] ✅ R1 (workflow gate is JBRK-side)
PDF→graph ingestion gather phase + DataTool 🟡 JBRK solution code (mocked here)
Error-detection / pre-generation readiness precondition gate 🟡 G3 — R2 gap, not built for this solution

Misfits / gaps [JB-T-misfits]

Everything JBRK needs maps to a DocGen primitive — with two qualifications and three framework gaps already tracked. Nothing requires inventing a new primitive beyond what R2 + the G1/G2/G3 list already commit to.

# Item Verdict
[JB-misfit-1] Cross-snippet grouped-by-type assembly + document-level step_num running counter. This is the one place JBRK forces new render behavior, not just catalog configuration. A per-snippet (inline) expansion interleaves steps and risks across molecules and numbers per-molecule — wrong for a reporting letter. The grouped-by-type mode and the flattened-stream step_num (1..N across all Snippets) are specified in [T2-render] but are render logic the framework must implement, not data the solution supplies. Already in R2 scope (no new schema — pure assembly over snippet_items.item_type + ordinal + phase_order), flagged here as the load-bearing framework dependency for [JB-success-2].
[JB-misfit-2] G2 computed Fields (tax_math). Section II/III interpolate computed tax values that are not stored Fields. DocGen has no native derived-value concept — it requires the computed-fields hook (G2). Framework owns the merge; JBRK owns the formulas. R2 gap, in build; without it, the templates can't render.
[JB-misfit-3] G1 structured/iterable render data. {% for %} over a list (e.g. multiple transferred share classes within one step) needs list/dict values in the Jinja scope + the sandboxed env permitting iteration/attribute access. R2 gap. Most JBRK steps are scalar {% if %}, so this is lower-pressure than for Phoenix P3 — but the same primitive serves both.
[JB-misfit-4] PDF→graph ingestion + computed-value extraction. Genuinely solution code, not framework — correctly out of the framework (mocked here). Not a gap; noted so the boundary is explicit. The framework provides gather/DataTool plumbing; JBRK provides the extractor + adapter.
[JB-misfit-5] Error-detection / pre-generation readiness (G3). JBRK's accountant-error checks and "don't generate until inputs are ready" overlap DocGen's precondition gate (G3) — a pre-render block distinct from the post-hoc verify phase. Not needed to produce the letter, so not built for this solution, but the same primitive would host JBRK's validation logic. R2 gap, tracked.

Conclusion. No new primitive is required. JBRK is expressible as a DocGen configuration on top of R2's composition model plus the already-committed gaps: G1 (structured render data), G2 (computed fields — the hard dependency, = tax_math), and the grouped-by-type assembly + step_num counter in [T2-render] (the one piece of genuinely new render behavior). Ingestion and error-detection are correctly solution-side.


Cross-references

  • PRD: ./prd.md
  • Primary source: ../../reference/JBRK_Legal_OS_Info_Guide.docx
  • DocGen R2 TDD: ../../releases/r2-framework-production-features/r2-tdd.md ([T2-composition], [T2-render], [T2-entity-versions], [T2-entity-generation-snapshot])
  • DocGen R1 TDD: ../../releases/r1-framework-v0-core/r1-tdd.md (render_mode, Document.state)
  • DocGen domain model: ../../reference/domain-model.md ([D-11], [D-12] walkthroughs 3+4, [D-15])
  • Gap analysis: ../../explanation/phoenix-uc2-analysis.md (G1/G2/G3)