Source code for jamb.publish.document

"""The in-memory document model handed to the Quarto renderer.

This module turns the requirement :class:`~jamb.core.models.Item` objects and
their traceability graph into a flat, ordered, render-agnostic structure. It
holds no formatting logic and never touches the filesystem or the Quarto
binary, which makes it fully deterministic and unit-testable on its own.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal

if TYPE_CHECKING:
    from jamb.core.models import Item, TraceabilityGraph

ItemType = Literal["requirement", "info", "heading"]


[docs] @dataclass(frozen=True) class RenderItem: """A single requirement item prepared for rendering. Attributes: uid: The item identifier, used verbatim as its anchor. type: The item type, which selects how the body is rendered. heading_text: The text shown on the item's heading line. body: The item body text (empty string when the item has none). level: Heading depth for ``heading`` items; ``None`` otherwise. parent_links: UIDs this item traces up to. child_links: UIDs that trace down to this item, restricted to the items present in the rendered set. """ uid: str type: ItemType heading_text: str body: str level: int | None parent_links: tuple[str, ...] child_links: tuple[str, ...]
[docs] @dataclass(frozen=True) class RenderSection: """A group of items sharing a document prefix.""" prefix: str items: tuple[RenderItem, ...]
[docs] @dataclass(frozen=True) class PublishDocument: """A complete document ready to be rendered to any output format. Attributes: title: The document title shown on the title block. subtitle: Optional metadata line shown under the title (e.g. document id, version, date, status), or ``None`` to omit. sections: Document sections in hierarchy order. total_items: The number of items across all sections. include_links: Whether parent and child link references render. known_uids: Every UID in the document, used to decide whether a parent link renders as an anchor or as plain text. """ title: str subtitle: str | None sections: tuple[RenderSection, ...] total_items: int include_links: bool known_uids: frozenset[str]
[docs] def build_publish_document( items: list[Item], title: str, *, subtitle: str | None = None, include_links: bool = True, document_order: list[str] | None = None, graph: TraceabilityGraph | None = None, ) -> PublishDocument: """Assemble a :class:`PublishDocument` from requirement items. Items are ordered by document hierarchy, then document prefix, then UID, and grouped into one section per prefix. Args: items: The items to publish. title: The document title. subtitle: Optional metadata line shown under the title. include_links: Whether to carry parent and child link references. document_order: Document prefixes in hierarchy order; used to sort sections. Prefixes outside the list sort last, alphabetically. graph: The traceability graph, used to resolve child (reverse) links. Returns: The assembled document. """ known_uids = frozenset(item.uid for item in items) if document_order: order_index = {prefix: i for i, prefix in enumerate(document_order)} fallback = len(document_order) else: order_index = {} fallback = 0 def sort_key(item: Item) -> tuple[int, str, str]: return (order_index.get(item.document_prefix, fallback), item.document_prefix, item.uid) sections: list[RenderSection] = [] current_prefix: str | None = None current_items: list[RenderItem] = [] for item in sorted(items, key=sort_key): if item.document_prefix != current_prefix: if current_prefix is not None: sections.append(RenderSection(current_prefix, tuple(current_items))) current_prefix = item.document_prefix current_items = [] heading_text = f"{item.uid}: {item.header}" if item.header else item.uid if graph is not None: child_links = tuple(c for c in graph.item_children.get(item.uid, []) if c in known_uids) else: child_links = () current_items.append( RenderItem( uid=item.uid, type=item.type, heading_text=heading_text, body=item.text or "", level=item.level, parent_links=tuple(item.links), child_links=child_links, ) ) if current_prefix is not None: sections.append(RenderSection(current_prefix, tuple(current_items))) return PublishDocument( title=title, subtitle=subtitle, sections=tuple(sections), total_items=len(items), include_links=include_links, known_uids=known_uids, )