Skip to content

feat(roboledger): text-block disclosure binding (Document → Nonnumeric fact)#889

Merged
jfrench9 merged 6 commits into
mainfrom
feature/text-block-binding
Jul 19, 2026
Merged

feat(roboledger): text-block disclosure binding (Document → Nonnumeric fact)#889
jfrench9 merged 6 commits into
mainfrom
feature/text-block-binding

Conversation

@jfrench9

Copy link
Copy Markdown
Member

Summary

Second half of the text-block-facts increment (report-engine M3, on top of #888): narrative disclosures become first-class facts, authored from the existing Document system.

  • bind-text-block operation (REST + MCP): binds a platform Document — or one section by slug — to a disclosure element as a Nonnumeric fact in a standing 'disclosure' FactSet with document provenance (document_id + section_id + full-sha256 content_hash as the drift signal). Re-binding the same element + period replaces the fact and refreshes the hash. Cross-database by provenance reference (Documents live in the platform DB); the lookup is graph-scoped.
  • Report snapshots: report builds copy in-window standing bindings into per-report FactSets carrying the document provenance verbatim, so filed reports are immutable against later document edits; regeneration clears snapshots while standing sets survive.
  • Picker hardening: disclosure picking is now scoped to the report's taxonomy closure and requires a member (to-endpoint) fact hit.
  • Rendering: text-block CAPs dispatch to a narrative envelope builder (rows carry text_value); numeric notes keep the grid path. GraphQL derives the new fields automatically.
  • Bundles: disclosure envelopes (numeric and text notes) now ride the downloadable flavors; JSON-LD emits rs:value string literals with contentType/factType and no unit/decimals for Nonnumeric facts; XBRL keeps stripping disclosures with a belt-and-braces Nonnumeric filter.
  • Demo: Driftline authors a "Significant Accounting Policies" note and binds its three policy documents.

No schema migration (rides #888's 0021).

Testing

  • Full tests/ without maxfail: 11,105 passed (two remaining failures are the known pre-existing graph_api ordering flake — passes in isolation).
  • Showcase demo end-to-end on the local stack: three documents bound into one standing FactSet; report snapshot with document provenance; GraphQL envelope carries the markdown in textValue (facts + rendering rows); JSON-LD carries the narratives with no numericValue/unit and both disclosure envelopes; materialized graph has Nonnumeric fact nodes with no unit edges; XBRL still Arelle-valid, SHACL conforms; matching-period re-bind returns replaced=true against the original standing set, different period creates a separate binding.
  • Note: the demo's narrative rows print via the published SDK, which drops the new field until the Python client is regenerated (already queued).

jfrench9 added 6 commits July 19, 2026 01:07
Add the 'document' provenance arm (document_id + section_id +
content_hash as the drift signal) to the FactProvenance union, the
BindTextBlockRequest/Response models, and the non-numeric fields on
FactLite (text_value/fact_type/content_type; value becomes optional)
and RenderingRowLite (text_value). fact_to_lite maps them through;
GraphQL derives the new fields automatically.
…act)

Bind a platform Document (or one section, by markdown-parser slug) to a
disclosure element as a Nonnumeric text-block fact in a standing
factset_type='disclosure' FactSet stamped with document provenance.
Cross-database by design: Documents live in the platform DB, so the
linkage is the provenance reference, never an FK, and the lookup is
graph-scoped so another graph's document id is a plain miss. Re-binding
the same element+period replaces the fact and refreshes the content
hash; the bind is also the sprint's only text authoring surface, so it
stamps element.item_type='text_block' when unset.

Hand-written router endpoint + MCP tool (both sessions needed — the
registrar runner passes neither the platform session nor graph_id).
Harden the fact-driven disclosure picker: scope to the report's
taxonomy closure (downward-recursive on parent_taxonomy_id, threaded
from the already-resolved report taxonomy) and require a MEMBER
(to-endpoint) fact hit — a total-only fact no longer renders a note
with an empty, structurally unfailable breakdown.

Report builds now snapshot standing text-block bindings: the latest
factset_type='disclosure' set per text-block structure inside the
report window is copied into a report FactSet carrying the document
provenance verbatim, so a filed report keeps the narrative as bound at
generation time even if the document is later edited; regeneration's
DELETE-by-report_id clears snapshots while standing sets survive.

Cross-graph share carries the non-numeric fact columns through the
copy (without them a shared narrative violates the value-shape CHECK).
Text-block CAPs render narrative rows, not a numeric grid: a new
builder shares every envelope atom with the statement path and swaps
only the rendering projection — one row per bound Nonnumeric fact
carrying text_value. disclosure.build_envelope dispatches by CAP:
TEXT_BLOCK_CAPS route here, every other disclosure CAP keeps the
statement family's grid path. A bound-but-arcless note still renders
(the FactSet is proof of content); a bare registry row stays None.
…t arms

build_report_bundle now appends a disclosure envelope for every picked
disclosure structure (numeric roll_up notes and text-block notes), so
notes ride the downloadable flavors instead of only the read API.
BundleFact grows the non-numeric arm (text_value/fact_type/
content_type; value and unit_ref optional); units are minted for
numeric facts only. The JSON-LD encoder emits rs:value string literals
(+ rs:contentType, rs:factType) for Nonnumeric facts with no
numericValue/unit/decimals — SHACL shapes already admit the shape —
and the canonical context gains the three terms. The XBRL emitter
keeps stripping disclosures and adds a belt-and-braces filter so a
Nonnumeric fact can never reach the numeric formatter.
…wcase demo

The Driftline demo authors a 'Significant Accounting Policies' note
(text_block CAP, one concept per policy document) and binds the three
uploaded policy documents to it via bind-text-block before materialize,
so the narrative facts reach the graph in the same run and the annual
report snapshots them. upload_policies now returns title->document_id
(the PG id — not the OpenSearch-side document_id) for the bind step,
and the verify step prints narrative row heads alongside numeric rows.
@jfrench9
jfrench9 merged commit 7a5084a into main Jul 19, 2026
6 checks passed
@jfrench9
jfrench9 deleted the feature/text-block-binding branch July 19, 2026 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant