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-laworg (C1–C4, epic #89), rendering the reporting letter deterministically (no LLM in the body): - Declarative since 2026-07-09: the catalog lives inauthoring/specs/tomorrow-law-reporting-letter.json+tomorrow-law-inter-vivos.json(Texts, Risk Blocks, Snippets with orderedsnippet_items, Sections, Fields, andtemplate.snippet_bindings— the spec-format extension this conversion added).fixtures/jbrk.pykeeps only the tax-math compute hook + post-compile Risk Block approvals. - Section II =from_snippet→body(every Text across both snippets, numbered by a runningstep_numinphase_orderorder); Section III =from_snippet→analysis(approved Risk Blocks only — thedraftis withheld). Section I =templateover Fields. - Framework gap fixed:instantiate_templatenow deep-copiesdocument_snippetsbindings, 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 G2tax_mathcomputed-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: JBRKtax_mathpure 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 (thecomponent_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_jsonis 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 Blocks — s84_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]).
Snippets — post_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¶
instantiate_template("Reporting Letter")→ a new instance, Sections I/II/III copied, Fields copied with defaults.- Set the file's Fields (from the mocked graph):
Opco_name,effective_date, share descriptions, balances; computed Fields (capital_gain,Extraction_amount) supplied by thetax_mathhook (G2). - Bind one Snippet:
document_snippets(snippet=post_mortem_pipeline, ordinal=0, entity_bindings={opco→OpcoLtd, transferor→Estate_of_X, newco→Newco}). - Render:
- 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 lonedraftblock is excluded). - 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 }} …".
- Section III (grouped → risk_block): five approved analyses in order.
Document.state='under-review'→ lawyer reviews →finalized. A generation snapshot pins all unit versions + Field values + output.- 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)