Reports Engine¶
Verified — minor divergences from the code — 1 finding(s) · 19d408be · 2026-08-25
All canonical_operations, function signatures, file paths, error classes, route paths, and integration behaviour (StreamingResponse, dependency check, split Mermaid/SVG/PDF pipeline) match backend/reports/*.py and backend/api/api_reports_router.py exactly. One small internal inconsistency found in the compiled doc's own prose.
Divergences from the code — details
| Sev | Where | Doc says | Code does | Evidence |
|---|---|---|---|---|
| LOW | Report Types table, row 'Reference Whitelist' | The Report Types table states the Reference Whitelist XLSX has '5 sheets'. | reference_whitelist_xlsx.render() builds a Workbook with six sheets in order (Cover, ROOT, FUNCTIONAL SYSTEMS, TECHNICAL SYSTEMS, COMPONENTS, About) -- the doc's own Core Operations section correctly says 'six sheets' two sections later, contradicting the table. | backend/reports/reference_whitelist_xlsx.py:175-189 |
Layer: api
# Reports Engine
Overview — Server-Side Report Generation¶
The Reports Engine generates all downloadable reports in EIDOS Explorer. It is responsible for four distinct output artefacts, each with its own data-collection and rendering module pair.
The engine uses a split pipeline for the two diagram-based reports (System Breakdown and System Context), consistent with ADR-007 Approach C:
- The server generates Mermaid source (a plain text string) on a GET request. IAM filtering has already been applied: the Mermaid source contains only nodes the authenticated caller may see.
- The browser renders the Mermaid source to an SVG using
mermaid.js(withhtmlLabels: false). - The browser POSTs the rendered SVG back to the server's
/pdfendpoint, which sanitises and converts the SVG to a PDF usingsvglib+ ReportLab Platypus and streams the bytes back.
The Reference Architecture PDF and the Reference Whitelist XLSX are fully server-side: the server reads the loader graph, builds a data model, and renders the final file in a single GET request with no client-side rendering step.
Report Types¶
| Report Name | Output Format | Description | Triggering Endpoint |
|---|---|---|---|
| Project Reference Architecture | PDF (A4 portrait, multi-page) | Formal IEC-81346 reference architecture document: aspects, roots, building blocks, naming patterns, scope | GET /api/reports/reference-architecture |
| Reference Whitelist | XLSX (5 sheets) | ISO 81346-12 whitelist of allowed Functional and Technical System designations grouped by class letter | GET /api/reports/reference-whitelist.xlsx |
| Functional System Breakdown (source) | JSON with mermaid_src |
IAM-filtered Mermaid flowchart or treeView source for browser rendering; multiple palette/layout modes | GET /api/reports/system-breakdown |
| Functional System Breakdown (PDF) | PDF (A4 landscape) | Browser-rendered SVG assembled into a print-ready PDF via svglib + ReportLab | POST /api/reports/system-breakdown/pdf |
| System Context & Interface Diagram (source) | JSON with mermaid_src |
1-hop associative neighbour diagram centred on a selected system node; IIS interface annotations included | GET /api/reports/system-context |
| System Context & Interface Diagram (PDF) | PDF (A4 landscape) | Browser-rendered SVG assembled into PDF with context-specific visual transforms | POST /api/reports/system-context/pdf |
| Documentation 360° | PDF (A4 portrait, document list) | Per-node documentation context with provenance and clickable source links; optional subtree | POST /api/reports/documentation-360/pdf |
Documentation 360°¶
Unlike the other five report types, Documentation 360° is fully server-side
synchronous (like the Reference Architecture PDF and Reference Whitelist —
no split Mermaid/SVG pipeline) and is not a diagram: it is a document list,
one heading + one table per analysed node, with rulebook-governed provenance
for every entry. It has its own canonical engine, separate from the reports
module proper — see the Documentation 360 Engine
page for the occurrence model, the documentContext rulebook category, the
two-batch fetch orchestration, and the IAM invariants.
Core Operations¶
collect -- Reference Architecture data collection¶
# backend/reports/ref_arch_data.py
def collect(
lang: str = 'en',
project_name: str = '',
customer_name: str = '',
secondary_domains: dict[str, str] | None = None,
service_token: str = '',
) -> RefArchModel:
Reads the full loader graph via persistence.eidos_loader._load() and constructs a RefArchModel. For each tree in the loader's trees list it derives an AspectDef (type, label, prefix, root node, hierarchy predicate, level count) and collects BlockDef records for nodes up to depth 4. It also resolves all relation rules into RuleRef records and generates up to five example designation strings.
When secondary_domains is provided (a {domain_key: base_url} dict) the function fetches /tree and /nodes from each secondary backend over HTTP using a service token and folds those aspects, roots, and building blocks into the model alongside the primary domain.
| Param | Type | Description |
|---|---|---|
lang |
str |
'en' or 'da'; selects localisation for the rendered PDF |
project_name |
str |
Shown in cover and page headers; falls back to DISPLAY_NAME env var |
customer_name |
str |
Shown in footer |
secondary_domains |
dict[str, str] or None |
Additional backend URLs keyed by domain name |
service_token |
str |
Bearer token forwarded in X-Service-Token to secondary backends |
Returns a RefArchModel dataclass. The route calls ref_arch_pdf.render(model) on the result.
ref_arch_pdf.render -- Reference Architecture PDF renderer¶
# backend/reports/ref_arch_pdf.py
def render(model: RefArchModel) -> bytes:
Builds a multi-page A4 PDF using ReportLab Platypus. Document structure: cover page (project metadata, statistics chips), table of contents, executive summary, applied principles section (five IEC-81346 aspects described), structural overview table, reference roots table, per-domain building-block tables (up to three hierarchy levels with breadcrumb headers), naming patterns, scope statement, and a source-data appendix.
All visible strings are localised through the _S lookup table ('en' / 'da'). Fonts default to Inter (loaded from backend/fonts/); Helvetica is the fallback when the font files are absent.
Returns raw PDF bytes. The route wraps them in a StreamingResponse with Content-Disposition: attachment.
build -- Reference Whitelist data collection¶
# backend/reports/reference_whitelist.py
def build(
project_name: str = "",
customer_name: str = "",
generated_at: str | None = None,
) -> WhitelistModel:
Reads the loader graph and separates nodes by productType. Tree-root nodes are collected into a flat ROOT list (one row per aspect root, in tree-definition order). All remaining Functional System and Technical System nodes are grouped by the first alphabetic character of their label after stripping the aspect prefix character, sorted by ISO 81346-12 class letter. The class header string for each group is resolved from the iso81346_12_classes catalogue module. Component nodes are deliberately excluded.
Raises ReferenceWhitelistError if the loader returns no node map.
| Param | Type | Description |
|---|---|---|
project_name |
str |
Cover sheet metadata |
customer_name |
str |
Cover sheet metadata |
generated_at |
str or None |
ISO date string; defaults to today UTC |
reference_whitelist_xlsx.render -- Whitelist XLSX renderer¶
# backend/reports/reference_whitelist_xlsx.py
def render(model: WhitelistModel) -> bytes:
Creates an openpyxl Workbook with six sheets in order: Cover, ROOT, FUNCTIONAL SYSTEMS, TECHNICAL SYSTEMS, COMPONENTS, About. Each data sheet has a colour-coded tab, frozen header row, and fixed column widths (28, 60, 30 characters). Group class sub-headers are written as merged three-column rows in a lighter shade of the sheet theme colour. All cells use wrap_text=True, vertical="top" alignment. Returns the workbook as raw XLSX bytes.
build_mermaid_source -- Functional System Breakdown Mermaid generator¶
# backend/reports/functional_system_breakdown.py
def build_mermaid_source(
visible_tree: list[dict],
palette_mode: PaletteMode,
anchor_node_id: Optional[str] = None,
node_types: NodeTypesMode = "func-tech",
show_product_type: bool = False,
direction: str = "TB",
) -> str:
Converts an IAM-filtered visible tree into a Mermaid flowchart source string. The caller supplies the output of build_visible_tree; this function performs no IAM resolution itself.
| Param | Type | Description |
|---|---|---|
visible_tree |
list[dict] |
IAM-filtered tree; each node has id, label, restricted, productType, children |
palette_mode |
'depth' or 'category' |
'depth': HSL gradient, one stop per nesting level. 'category': saturated hue per functional role |
anchor_node_id |
Optional[str] |
If set, only the subtree rooted at this node is rendered |
node_types |
'all' or 'func-tech' or 'tech-comp' |
'func-tech' drops Components; 'tech-comp' lifts Functional Systems so Technical Systems become roots |
show_product_type |
bool |
Appends productType as a third label line on every node |
direction |
str |
Mermaid flowchart direction: TB, LR, BT, or RL |
Maximum nesting depth is 8 (MAX_NESTING_DEPTH). Nodes at the cap are collapsed into a leaf placeholder. At each depth level, sibling nodes are split into containers (nodes with visible children) and leaves and rendered in separate row-chunked grids connected by invisible ~~~ bridge edges, preventing the layout engine from interleaving large subgraphs with small leaf nodes.
Raises AnchorIsComponentError if anchor_node_id resolves to a Component node. Raises ValueError if anchor_node_id is not found in the tree.
build_treeview_source -- System Breakdown treeView generator¶
# backend/reports/functional_system_breakdown.py
def build_treeview_source(
visible_tree: list[dict],
anchor_node_id: Optional[str] = None,
show_product_type: bool = False,
) -> str:
Generates Mermaid treeView-beta (v11.14+) source. Structure is encoded purely by indentation; each node is a quoted string combining label, description, and optionally productType separated by em-dashes. Used when the frontend selects style=treeview on the system-breakdown endpoint.
Raises AnchorIsComponentError and ValueError under the same conditions as build_mermaid_source.
resolve_system_neighbours -- System Context neighbour resolution¶
# backend/reports/system_context.py
def resolve_system_neighbours(
*,
anchor_id: str,
nodes: dict,
rels_by_node: dict,
parent_of: dict,
children_of: dict,
) -> list[dict]:
Resolves the 1-hop associative neighbour set of a Technical/Functional System anchor by traversing through its component closure. The ISO-81346 model places connectivity relations on Component nodes inside a Technical System rather than on the system itself. This function builds the anchor's component closure (all Component descendants, stopping at any nested system), then follows all non-hierarchy relations from every node in that closure to their far endpoints, resolving each endpoint up the hierarchy to its owning Technical or Functional System.
| Param | Type | Description |
|---|---|---|
anchor_id |
str |
Focus system node id |
nodes |
dict |
{id: {productType, label, ...}} from the loader |
rels_by_node |
dict |
{id: [{from, to, predicate}, ...]} -- both endpoints indexed |
parent_of |
dict |
{child_id: parent_id} |
children_of |
dict |
{parent_id: [child_id, ...]} |
Returns a list of dicts with keys id, predicate, direction ('in' or 'out'), via_source, and via_target. The via_source/via_target pair identifies the actual relation endpoints so the caller can key an IIS interface lookup on the real edge. Hierarchy predicates (hasPart, hasSubLocation, hasSubDiscipline, hasSubType, hasSubSignal) are excluded.
build_system_context_source -- System Context Mermaid generator¶
# backend/reports/system_context.py
def build_system_context_source(
*,
anchor_id: str,
anchor_label: str,
anchor_description: str = "",
anchor_product_type: Optional[str] = None,
neighbours: Iterable[NeighbourEdge],
lang: Lang = "en",
) -> str:
Builds a flowchart LR Mermaid source centred on the anchor node. The anchor receives the focus class (dark slate with thick border). Each neighbour is coloured by its owning Functional System group: neighbours from the same FS group share a palette slot assigned in first-seen order. Restricted neighbours render as a grey italic placeholder with no real label or description (IAM rule 09-R27).
Edge arrows are typed by predicate: safety predicates produce dashed arrows (-.->), bidirectional predicates produce double-headed arrows (<-->), all others use solid arrows. IIS interface records add a third edge-label line (IIS . <criticality_class> . <status>) when the interface key is present on the NeighbourEdge.
Raises AnchorIsComponentError if anchor_product_type is 'Component'.
sanitize_svg -- SVG pre-processing for PDF assembly¶
# backend/reports/system_breakdown_pdf.py
def sanitize_svg(svg: str) -> tuple[str, int]:
Two transforms applied before svglib parses the Mermaid SVG:
- Rewrites
stroke-dasharray:0tostroke-dasharray:none(ADR-007 section 13). Mermaid emits this value for.edge-pattern-solid; svglib rejects zero-length dash cycles. - Calls
flatten_multiline_textto expand multi-line node labels.
Returns (sanitized_svg, dash_fixes_count).
flatten_multiline_text -- Multi-line SVG label normalisation¶
# backend/reports/system_breakdown_pdf.py
def flatten_multiline_text(svg: str) -> tuple[str, int]:
Parses the SVG with defusedxml (XXE-safe), walks every <text> element that contains two or more text-outer-tspan outer line wrappers, and replaces it with one <text> per line positioned at an absolute y value. Line positions are centred on the node box's vertical midpoint by reading the nearest ancestor translate(x, y) lift. Single-line <text> elements are left untouched. Returns (flattened_svg, lines_flattened_count). On any parse failure returns the original SVG unchanged with count 0.
system_breakdown_pdf.render -- Breakdown PDF assembler¶
# backend/reports/system_breakdown_pdf.py
def render(
svg: str,
*,
project_name: str = "",
customer_name: str = "",
palette: str = "depth",
scope: str = "full",
anchor_label: Optional[str] = None,
anchor_description: Optional[str] = None,
lang: str = "en",
node_types: str = "func-tech",
) -> tuple[bytes, dict]:
Validates the SVG payload (non-empty, under the 2 MB cap), sanitises it, converts it to a ReportLab Drawing via svglib.svg2rlg, scales it to fit the A4 landscape drawable area (margins: side 1.5 cm, top 2.5 cm, bottom 1.7 cm), and assembles a single-page PDF using ReportLab Platypus BaseDocTemplate.
Scale is min(avail_w / natural_w, avail_h / natural_h, 1.0) -- never upscales. Post-scale text size is checked against the 9 pt legibility threshold (ADR-007 section 12); if it falls below, a structured warning is logged but the PDF is still returned.
| Param | Type | Description |
|---|---|---|
svg |
str |
Client-rendered Mermaid SVG |
project_name |
str |
Right-aligned in the page header |
customer_name |
str |
Left-aligned in the footer |
scope |
'full' or 'subtree' |
Affects the title string only |
anchor_label |
Optional[str] |
Shown in the title when scope='subtree' |
anchor_description |
Optional[str] |
Centred subtitle under the title |
lang |
'en' or 'da' |
Localises the title template |
node_types |
str |
Carried through for audit logging only |
Returns (pdf_bytes, report) where report is a dict with keys dash_fixes, scale, natural_w, natural_h, effective_pt, legible, svg_shape, and generated_at.
Raises InvalidSVGError (maps to HTTP 400) when the payload is empty, over the size cap, unparseable, or produces a zero-size drawing.
system_context_pdf.render -- System Context PDF assembler¶
# backend/reports/system_context_pdf.py
def render(
svg: str,
*,
project_name: str = "",
customer_name: str = "",
anchor_label: Optional[str] = None,
anchor_description: Optional[str] = None,
lang: str = "en",
) -> tuple[bytes, dict]:
Same structure as system_breakdown_pdf.render with two additional context-specific SVG transforms applied after sanitize_svg:
_transparent_edge_label_backgrounds: makes Mermaid greyrgba(232,232,232)edge-label background rects non-painting by injecting an inlinestyle="fill:none;stroke:none"(wins the SVG cascade over the embedded stylesheet rule)._round_node_corners: addsrx="4" ry="4"to node box<rect>elements for rounded corners.
Returns the same (pdf_bytes, report) tuple as the breakdown assembler.
Data Structures¶
RefArchModel¶
Returned by collect(), consumed by ref_arch_pdf.render().
@dataclass
class RefArchModel:
project_name: str
customer_name: str
generated_at: str # ISO date string
lang: str # 'en' or 'da'
aspects: list[AspectDef]
roots: list[RootDef]
building_blocks: list[BlockDef]
relation_rules: list[RuleRef]
predicates: list[PredicateDef]
propagation_rules: list[dict]
examples: list[ExampleDef]
source_file: str
node_count: int
relation_count: int
rule_count: int
WhitelistModel¶
Returned by build(), consumed by reference_whitelist_xlsx.render().
@dataclass
class WhitelistModel:
project_name: str
customer_name: str
generated_at: str
standard_label: str # e.g. "DS/ISO 81346-12:2018"
component_standard: str # e.g. "IEC/EN 81346-2:2009, Table 3"
root_rows: list[WhitelistRow]
functional_groups: list[WhitelistGroup]
technical_groups: list[WhitelistGroup]
NeighbourEdge¶
Input to build_system_context_source(). Built by the server route after IAM resolution.
class NeighbourEdge(TypedDict, total=False):
id: str # neighbour node id
label: str # "" when restricted=True
description: str
productType: Optional[str]
restricted: bool
predicate: str
direction: str # "out" (anchor -> neighbour) or "in" (neighbour -> anchor)
interface: Optional[dict] # {criticality_class, status} -- never an id
Error Handling¶
| Error | Module | HTTP mapping | Trigger |
|---|---|---|---|
InvalidSVGError(ValueError) |
system_breakdown_pdf, system_context_pdf |
400 | Empty payload, over 2 MB cap, unparseable XML, zero-size drawing |
AnchorIsComponentError(ValueError) |
functional_system_breakdown, system_context |
400 | Anchor node has productType == "Component" |
ValueError (generic) |
functional_system_breakdown |
400 | anchor_node_id not found in visible tree |
ReferenceWhitelistError(Exception) |
reference_whitelist |
500 | Loader returned no node map |
Legibility warnings (body text below 9 pt post-scale) and portrait-SVG-on-landscape-paper warnings are logged via structlog as structured warnings but do not prevent the PDF from being returned.
Integration¶
How the frontend triggers report generation¶
All report requests originate from the browser. Routes are registered in backend/server.py.
Reference Architecture PDF: the browser navigates to or fetches GET /api/reports/reference-architecture?lang=en. The server calls collect() then ref_arch_pdf.render() and streams the bytes back with Content-Disposition: attachment.
Reference Whitelist XLSX: the browser navigates to GET /api/reports/reference-whitelist.xlsx. The server calls build() then reference_whitelist_xlsx.render() and streams the XLSX bytes.
System Breakdown and System Context (split pipeline):
- The browser sends
GET /api/reports/system-breakdown?palette=depth&scope=full&node_types=func-tech(or/system-context?anchor_node_id=<uuid>). The server applies IAM, builds the Mermaid source, and returns JSON includingmermaid_src. - The browser renders the Mermaid string to an SVG using
mermaid.jswithhtmlLabels: false. - The browser POSTs the SVG to
POST /api/reports/system-breakdown/pdf(or/system-context/pdf) as a JSON body withsvg,project_name,customer_name, and other parameters. - The server calls
sanitize_svg, then the appropriaterender()function, and streams the resulting PDF bytes back.
How the generated file is returned to the browser¶
All report endpoints use FastAPI StreamingResponse:
return StreamingResponse(
io.BytesIO(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
For the XLSX endpoint the media_type is application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.
The PDF-assembly endpoints additionally return diagnostic response headers from the render report:
X-Render-Scale-- the scale factor applied to the SVGX-Render-Pt-- effective post-scale body text size in pointsX-Render-Legible--"true"or"false"relative to the 9 pt threshold
Dependency check at startup¶
Server startup validates that defusedxml, svglib, and reportlab are importable. Missing dependencies are logged as structured errors naming the affected endpoints; the server continues to start so that non-report functionality remains available.