Skip to content

DR: how to implement complex templates? #764

Description

@AlexanderLanin

What follows is an AI generated comparison


Model-complete Sphinx-Needs reports: decision and investigation

Phase 1 – Problem statement and decision criteria

Decision to make

Choose an architecture for a model-driven verification report that has both:

  1. the complete, resolved Sphinx-Needs model (including cross-worker and
    external Needs); and
  2. ordinary reStructuredText sections, anchors, and local table-of-contents
    entries in the same build.

The decision is not whether a Jinja helper should be a function or a
precomputed value. That choice cannot change when the model is available. The
actual question is whether the report becomes normal source before Sphinx
reads the document, or whether upstream Sphinx/Sphinx-Needs grows a supported
post-collection report-and-ToC API.

The current implementation is evidence and a temporary workaround, not an
accepted design.

Decision criteria

The selected design must:

  • retain parallel reading for the ordinary documentation build;
  • use graph links, not Need-ID conventions or hardcoded component lists;
  • resolve external Needs and calculated backlink fields from the complete
    model;
  • provide a real hierarchy to Sphinx and the theme, rather than a second HTML
    navigation menu;
  • have explicit inputs and dependencies, deterministic output, and a clear
    incremental-build story; and
  • use public, versioned APIs at the integration boundary. Dependency-private
    state may only be isolated behind a separately tested compatibility adapter.

Central phase conflict to resolve

There is no currently documented extension event that satisfies both timing
requirements in one ordinary Sphinx read pass:

  • doctree-read and Sphinx's ToC collection happen while individual documents
    are read; a parallel worker does not yet have the integrated Need model.
  • env-updated runs after all documents and worker environments are merged,
    but its supported return value merely marks documents for writing; it does
    not schedule a second source read.
  • Sphinx-Needs documents get_needs_view(app) as a read-only view of fully
    resolved
    Needs for the write phase. Calling it may resolve and lock the
    Need data, so it is intentionally not a read-phase template API.

Consequently, a report rendered after collection can use the complete model,
but cannot become ordinary read-phase sections and ToC entries through a
public API. A report rendered while reading can become normal sections, but
cannot safely depend on the complete model. The present manual purge and
re-read crosses this boundary, which explains both why it works and why it is
fragile.

Relevant contracts: Sphinx event callbacks,
Sphinx build-environment API,
Sphinx-Needs get_needs_view,
and Sphinx-Needs builder/export configuration.

The two concrete PR candidates

The immediate decision is primarily between two implemented approaches, not
between abstract designs. They must be compared directly before either gains
more features:

  • PR #730 introduces
    .. module-verification-report::. It creates the report outline during the
    normal read phase from an explicit :components: list, emits normal section
    nodes beside a generated mod_ver_report Need, and leaves data tables to
    late Sphinx-Needs filters. It is option K.
  • PR #763 introduces
    score_sphinx_needs_templates and mod_ver_report_tiny.need. It follows
    Need links inside a Need template, then manually purges and rereads marked
    report documents after environment merge; it patches rubric entries into
    env.tocs. It is option G.
Decision criterion PR #730 / option K PR #763 / option G
Report outline Explicit :components: list; static at read time Discovered from Need links after merge
Graph-driven completeness No; omitted linked components are not detected Yes in the second pass, subject to the complete integrated model
Normal sections and local ToC Yes; sections are normal document children No; flat rubric-derived env.tocs patch
Parallel-read design Public, ordinary read-phase directive Parallel read plus private main-process reread
External-Need handling Tables can resolve external IDs; outline still needs them listed Available only if present in the post-merge model
Main source of truth Directive options and naming conventions Need graph, but with lifecycle-private machinery
Critical risk Manual scope can drift from graph; IDs/titles/filters use conventions Re-entrant lifecycle, private APIs, and non-semantic ToC state

Neither PR fulfils every stated criterion. PR #730 is the cleaner Sphinx
integration when a reviewable, manually declared report scope is acceptable.
PR #763 is closer to graph-driven discovery, but its reread/ToC mechanism is a
temporary compatibility risk, not a stable extension architecture. If both
graph discovery and standard navigation remain non-negotiable, use A: retain
PR #730's normal-section renderer if useful, but generate its outline from a
complete manifest rather than from :components:.

Concrete use case

Running:

bazel run //:docs

must generate:

_build/baselibs_report.html

The source of the report is:

src/needs_templates/mod_ver_report_tiny.need

The desired result is the same report content and navigation as the reference
page:

https://eclipse-score.github.io/baselibs/pr-491/reporting/module_verification_report.html

The development build currently emits warnings from score_baselibs; those
warnings are deliberately outside the scope of this problem and should not be
used as evidence that the template architecture is wrong.

The document currently contains one report Need in
docs/baselibs_report.rst:

.. mod_ver_report:: Report for Test Module
   :id: mod_vrep__test_mod
   :belongs_to: mod__baselibs
   :template: mod_ver_report_tiny

The real input is an integrated Need model containing the module, its feature,
components, requirements, architecture items, tests, and work products. Many
of those Needs come from mounted or external repositories.

Desired behavior

The report must be model-driven

The template must follow Need links and derive its sections from the Need
graph. In particular:

  • component IDs must not be copied into the template as a manually maintained
    list;
  • the component order should follow the order of the relevant Need links;
  • the feature and component data should be resolved from Need relationships,
    not reconstructed from ID naming conventions where a relationship exists;
  • filters should be able to use the generated backlink fields, for example
    includes_back, belongs_to_back, or satisfied_by_back;
  • an external Need such as feat__baselibs is a real, known Need in the
    integrated build. The implementation must not make it appear absent merely
    because it was collected in another worker or loaded at another lifecycle
    stage.

The template currently has a linked_needs(need_id, link_name) helper. It
follows outgoing links and returns NeedItem objects. It is useful for an
ordinary, read-phase template, but it is not a solution for a model-complete
report: changing it into a precomputed attribute cannot move the model into
the earlier read phase.

The report must be a normal Sphinx page

The generated report should have:

  • the same visible report content as the reference;
  • working links to the actual Needs and documents;
  • the usual theme navigation, including the local “On this page” navigation;
  • navigation entries for dynamic component headings;
  • real, working anchors for those entries;
  • a meaningful heading hierarchy rather than a flat, manually assembled HTML
    menu.

An actual navigable menu is a requirement, not an optional enhancement. It
should use the normal Sphinx/theme navigation and must contain links to the
generated report sections, including sections for dynamically linked
components.

The report is currently rendered inside a Sphinx-Needs Need node. This is a
compatibility detail, not a selection criterion: any replacement must preserve
the report Need's metadata, links, targets, and normal Sphinx-Needs processing
and must regression-test dependent layouts/extracts. The visible report body
may move outside the node when that produces normal document structure.

Parallelism and build behavior are non-negotiable

Normal Sphinx parallel reading must remain enabled for the complete docs
build. Disabling parallelism globally makes the build too slow and is not an
acceptable solution.

Serializing the processing of this one report, or running a dedicated final
render step for it, is acceptable if that is required to obtain the complete
Need model and a correct menu. What is not acceptable is serializing the
entire documentation build: the hundreds of unrelated pages must continue to
be read and processed in parallel.

The solution should also preserve:

  • incremental builds;
  • deterministic output;
  • safe environment merging between workers;
  • HTML builds without breaking other builders or ordinary Need templates;
  • a clean process model without hidden cross-build global state.

Phase 2 – Established evidence and current implementations

Relevant lifecycle facts

The following behavior was observed while investigating the problem:

  1. Sphinx reads source documents in parallel workers.
  2. A Sphinx-Needs template is expanded while the source document is being
    read. At that point, the worker may not have all Needs from other workers.
  3. Sphinx-Needs creates a Need node and parses template output into it.
    Its _create_need_node call uses match_titles=False for Need content.
  4. The worker environments are merged later.
  5. External Needs are known to the build and do not produce an unknown-directive
    or unknown-Need build error, but their availability at the exact template
    render point is still a central question.
  6. Sphinx's normal ToC collector runs on doctree-read. It does not treat
    arbitrary content nested in a Sphinx-Needs Need as ordinary page sections.
  7. Sphinx-Needs resolves backlinks during later Need post-processing. The
    _back fields are therefore useful to later Need filters, but their
    availability during the first template expansion must not be assumed.
  8. app.env does not exist during extension setup(). It exists by
    builder-inited, but storing it in module-level state is a lifecycle
    workaround, not a satisfactory model API.

The key architectural question is whether the report should be rendered only
after the complete model exists, instead of rendering once during source
reading and then repairing the result later.

What has been tried or considered

1. Hardcoded component lists

The first report shape contained component-specific content or IDs directly in
the template.

This produces predictable output, but violates the main requirement: the
report must follow the integrated Need model and automatically adapt to the
linked components. It was rejected.

2. A dynamic template function

The template can call a helper such as:

{% set components = linked_needs(module_id, "includes") %}
{% for component in components %}
{{ component["title"] }}
{% endfor %}

This removes the hardcoded component list and is conceptually attractive.
However, a function called during the worker's first source read sees only the
part of the model available in that worker. It does not solve the timing
problem by itself.

There was also a question whether the function should be replaced by a
precomputed template attribute. That could make templates simpler, but it
still requires a well-defined point at which the complete model is available
and a supported way to inject the prepared data into a Need template.

3. A plain function in the render context

A plain function was initially considered for needs_render_context.
Sphinx serializes render configuration while using parallel readers, so the
callable and its captured state must be pickleable. The current code uses a
top-level _LinkedNeeds callable class for this reason.

That solves serialization of the callable, but it does not make the model
complete at first render time. It is an implementation workaround, not the
fundamental solution.

4. Disable parallelism globally

This would make all Needs available in one process and could hide the timing
problem.

It is unacceptable because it slows down the entire documentation build.
Parallelism should be disabled for no more than an explicitly isolated
operation, if it must be disabled at all; preferably it should not be disabled
for this feature.

5. Render once, then re-read selected pages after environment merge

The current extension uses the marker:

score: render-after-needs-collection

For templates containing the marker, it currently:

  1. allows the normal parallel source read;
  2. waits for env-updated, after worker environments have been merged;
  3. finds the documents containing Needs using marked templates;
  4. purges and clears those documents;
  5. calls app.builder.read_doc(docname) again in the main process.

This keeps global parallelism enabled and does produce the dynamic report.
However, it is a fragile second-pass workaround:

  • it re-enters Sphinx's source-reading lifecycle manually;
  • it emits env-purge-doc and calls env.clear_doc directly;
  • it depends on event ordering and on private details of how Sphinx-Needs
    stores Need data;
  • it renders and parses the marked page twice;
  • it needs special handling to avoid running after Need post-processing;
  • it is currently limited to HTML;
  • it makes it difficult to reason about other extensions that react to the
    first read or to purge events;
  • it may be incorrect for multiple levels of dependent templates or other
    generated data.

This is the best-performing workaround investigated so far, but it is exactly
the kind of lifecycle hack that should be reviewed or replaced.

6. Set the template environment at extension startup

The helper needs an environment to resolve Need IDs. Setting it during
setup() does not work because Sphinx has not created app.env yet. The
current code listens to builder-inited and stores the environment in a
module-level variable.

This was verified as a lifecycle limitation. It is not a good long-term API:
the state is implicit, process-local, and disconnected from the callable's
arguments. A better solution should receive the model through an explicit
supported context or defer rendering until the model is available.

7. Extract the template code into a dedicated extension

Template configuration and runtime behavior had grown inside
score_sphinx_bundle. It was moved to:

src/extensions/score_sphinx_needs_templates/

This is a good organizational change, but it does not solve rendering timing.
The extension now contains the template folder configuration, the link helper,
the post-merge second pass, and the navigation workaround. Its public README
documents behavior; implementation details are kept in code comments.

8. Use ordinary RST section headings inside the Need template

This would be the cleanest route for normal Sphinx navigation if it worked.
It does not work directly because Sphinx-Needs parses Need content with
match_titles=False. Section syntax inside the template is therefore not
registered as ordinary Sphinx sections.

9. Move generated content out of the Need node

One possible way to recover ordinary Sphinx sections is to extract the
template-generated children from the Need and insert them into the containing
document section.

This risks changing the Need's rendering, layout, metadata handling, and
Sphinx-Needs processing. It also changes the semantic ownership of the report
content. It has not been accepted.

10. Add a second HTML menu from the template

A template could emit a manually generated <nav> with anchors to the report
sections.

This duplicates the theme's navigation, is builder-specific, and would need
to reproduce theme behavior and styling. It is not an acceptable substitute
for normal Sphinx ToC integration.

11. Add generated entries directly to env.tocs

The current extension has a further workaround. After Sphinx's ToC collector
has run, it:

  • finds direct rubric nodes in marked Need templates;
  • assigns generated IDs such as baselibs-report-json;
  • creates nodes.reference entries;
  • appends them to the document's existing env.tocs tree.

This makes the theme's local “On this page” menu appear and keeps the report
HTML itself unchanged. It also picks up dynamic component headings.

The drawbacks are significant:

  • the entries are flat because rubric nodes do not express section levels;
  • it relies on the report using direct rubrics as a private navigation
    convention;
  • IDs are assigned after parsing instead of being normal section targets;
  • the solution reproduces part of Sphinx's internal ToC representation;
  • nested headings, custom templates, non-HTML builders, and richer inline
    titles need additional decisions;
  • the menu can diverge from the actual semantic document hierarchy.

This fixes the visible missing menu for the current report, but it is a
particularly clear example of a workaround that should be replaced by a
proper extension point if one exists.

12. Add a custom template heading/directive API

Another possible direction is a custom directive or node that explicitly
declares a generated heading, its anchor, and its level. The extension could
then render it and integrate it with the ToC.

This would make hierarchy explicit and could avoid guessing from rubric text,
but it adds a new template language and still leaves the central question:
when should the directive be evaluated relative to Need collection and
post-processing? It has not been implemented.

Current implementation snapshot

The current code is therefore a combination of:

parallel source read
        |
        v
initial Need-template rendering (possibly incomplete model)
        |
        v
worker environment merge
        |
        v
manual purge + second read for marked pages
        |
        v
rubric IDs and env.tocs patch for local navigation

Relevant files:

  • src/extensions/score_sphinx_needs_templates/__init__.py
  • src/extensions/score_sphinx_needs_templates/README.md
  • src/needs_templates/mod_ver_report_tiny.need
  • docs/baselibs_report.rst

The current build can generate the report and the local menu. The command
still exits non-zero because the development build treats the known
score_baselibs warnings as errors. That warning behavior is unrelated to the
architecture question.

Phase 3 – Solution space and critical analysis

Solution proposals

Comparative assessment

The options below deliberately separate two axes that are often conflated:
when the graph is computed, and where the visible report becomes document
structure. A check mark means the property follows from the architecture; an
asterisk means that it is possible only with an upstream API that does not
exist today.

Option Concrete implementation Complete resolved model Normal Sphinx ToC Parallel ordinary docs Recommended use
A. Generated source from report manifest Yes, with explicit integrated-model contract Yes Yes Default local solution
B. Generated reports bundle Yes Yes Yes Many or separately owned reports
C. Stand-alone graph compiler Yes Yes Yes Reusable report platform
D. Preload model in every worker Potentially No with the current template structure No clean way Reject
E. Reader ordering / report serialization No clean way Potentially No Reject
F. Upstream report + ToC API Yes Yes* Yes Strategic upstream investment
G. Current second pass ★ PR #763 Yes in practice Partial, private patch Yes, except selected reread Temporary bridge
H. Write-phase node replacement Yes No Yes Builder-neutral late fragment only
I. html-collect-pages page Yes No Yes Separate HTML export only
J. Relax the normal-ToC requirement Yes Theme-specific only Yes Product decision, not an implementation fix
K. Hybrid directive ★ PR #730 Data widgets: yes; outline: no Yes Yes Only if explicit component list is accepted

A. Recommended: generate normal report source in a separate model stage

Make the report body a generated .rst source file, not the rendered content
of a Need template. Preserve the report Need as a small, explicit metadata
Need on the page, but put the model-derived report body immediately after it
with an .. include:: of the generated file. This retains the report Need's
ID, fields, links, and Sphinx-Needs processing while making the report
headings normal document sections.

The build has two dependency-aware stages:

Need sources + mounted/external Need inputs
        |
        v
parallel model-collection action -> versioned report manifest
        |
        v
report renderer -> generated .rst with sections and Need references
        |
        v
normal parallel Sphinx build -> ToC, links, supported builders

The collection action must have an explicit integrated-model contract. The
repository's //:needs_json is not automatically such a contract: the
Sphinx-Needs builder filter normally excludes external Needs. Choose one of
these deliberate designs:

  1. configure a report collector to export the required external Needs and
    resolved fields, with an ID-collision and external-URL policy; or
  2. consume local :needs_json_file and every external needs.json as declared
    inputs and perform a specified merge, prefix, condition, and backlink
    resolution step in the manifest producer.

The manifest must be typed and versioned rather than leaking a Sphinx
environment pickle or private dictionaries. It must interpret exports produced
with needs_json_remove_defaults=True against the matching schema, so an
absent field is not accidentally treated as None; it must also preserve the
Sphinx-Needs semantics of conditional links before traversal. The renderer
follows the chosen, documented link order, validates every required Need, and
emits standard section syntax and :need: references. A missing linked Need
is a generation error with the source report and link field in its diagnostic;
it must not render as an empty section.

This is the only option that fulfils the complete-model and normal-ToC
requirements using today's public phase boundaries. Its costs are an explicit
intermediate artifact and a second, parallelisable build action. In Bazel,
those costs are normally acceptable: the generated source is cacheable and
the action inputs precisely describe when a report must change.

The graph collection must be acyclic. Concretely, replace the current complex
template with a non-dynamic report-Need stub in the collection source. The
stub supplies the report ID and link fields, but not its generated body. The
manifest action then produces the body consumed by the final documentation
action. Do not make :needs_json depend on a final HTML build that itself
depends on the generated report; that would create a Bazel/Sphinx cycle and
make incremental behaviour opaque.

Required compatibility check. The report Need remains the authoritative
Need object. Verify that its fields, links, targets, layout, extracts, and
consumer-facing references remain correct after the visual body becomes normal
document content. This is a regression obligation for every option, not a
criterion that chooses between A and F.

The semantic check must include builders and styling. Existing report content
may contain HTML-specific raw markup or CSS assumptions; moving it into normal
RST only preserves non-HTML builders when the generator uses builder-neutral
nodes/markup or supplies an intentional representation for each builder.

B. Variant of A: materialise reports as a generated documentation bundle

When reports are numerous, generated report source should be packaged as a
data-only docs_bundle and mounted into the final documentation source tree.
Each report becomes an ordinary generated .rst page beneath a well-defined
mount point. The bundle contract must provide an attachable entry document:
either generate an index.rst with its own toctree or set entry_doc to the
generated report page. The final Sphinx build then reads it as normal source,
so sections, targets, search indexing, and all supported builders work without
special theme code.

This variant has useful ownership boundaries: the report generator owns the
derived sources, while the consuming documentation project owns placement and
ToC attachment. It also permits a report package to be tested from its
manifest without running the complete site. The repository's docs_bundle
and docs(data=...) mechanisms are designed to stage generated/supporting
files; use the former when the report must travel with a mount.

It adds rules and staging paths, so it is excessive for exactly one small
report. It is preferable to ad-hoc source-tree writes: generated files must
be Bazel outputs, never be written back to docs/ during a build. Cross
references must be emitted in the final project's Sphinx source context, not
resolved while the bundle is generated; otherwise links to local Needs cannot
be validated by the final build.

This is primarily an own-page packaging model. A generated file mounted in a
data-only bundle is not automatically a reliable .. include:: immediately
after a Need in a host document: the include path must resolve identically for
workspace runs and sandboxed builds. Treat inline inclusion as a separately
tested adapter, not as an implied property of the bundle mechanism.

C. Alternative input architecture: a stand-alone canonical graph compiler

Instead of obtaining the manifest through a seed Sphinx build, define a
canonical, validated Need-graph IR and compile it from Need source and external
needs.json inputs. Both the report generator and, eventually, Sphinx-Needs
could consume that IR. The report renderer is then a conventional deterministic
compiler: graph IR plus report specification in, .rst and diagnostics out.

This is attractive if reports become a product area used outside Sphinx (for
example PDF, dashboards, or compliance exports), because traversal semantics,
ordering, and diagnostics have one owner. It also makes Bazel dependencies
very explicit and avoids using a Sphinx builder merely as a model exporter.

It is not a cheap replacement for proposal A. Parsing Need directives,
dynamic fields, variants, constraints, external imports, and backlink rules
outside Sphinx-Needs would duplicate a large semantic surface and risks two
different models. Adopt it only if the project commits to making this IR the
single authoritative model with conformance tests against Sphinx-Needs.
Otherwise, use Sphinx-Needs' exported model as the manifest source in A.

D. Rejected: give every parallel reader the complete model up front

At first sight, a pre-read collector that builds the Need graph and distributes
it to every worker seems to preserve the current template API: the workers
would render templates with a complete linked_needs context, and Sphinx
would collect the resulting headings normally. It does not form a clean
architecture in this project.

Local Needs are only known after their source documents are parsed. Building
the graph before that point therefore either implements a second parser for
Sphinx-Needs syntax (the duplication problem in C), or reads all documents
once before the normal reader. The latter is a two-stage build in disguise and
must be a separate Bazel action to have reliable invalidation and environment
merging. Passing a mutable BuildEnvironment to worker render context is also
not a public API and produces stale, pickle, and process-isolation hazards.

This option is acceptable only if a canonical external graph compiler already
exists; then it collapses into C followed by A. It must not be implemented as
a hidden global, worker-local cache or a custom pre-scan of .rst files.

E. Rejected: influence reader order or serialize the report document

Another apparent shortcut is to read all ordinary documents in parallel and
read report documents last, serially. Sphinx does provide the public
env-before-read-docs hook to reorder the initial document list, but that is
not a dependency barrier: with parallel reading Sphinx still partitions the
list into worker chunks, and worker environments are merged only after those
chunks complete. A nominally late report document therefore cannot see local
Needs from other workers without manually triggering the same merge/read
operations used by the current workaround. External Needs may already be
loaded at this point; the unresolved part is the locally read model.

Global -j 1 has the same logical effect but makes the entire documentation
set serial and violates the performance requirement. Isolating one document
in a separate Sphinx invocation is viable, but that is precisely the explicit
model-and-source pipeline in A, not an in-process ordering trick.

F. Strategic: request a supported upstream report feature

Propose a Sphinx-Needs feature for model-complete reports, rather than growing
another repository-local lifecycle workaround. The upstream API must cover two
separate products:

  • a post-collection renderer supplied with an immutable, complete
    NeedsView, including external Needs and backlinks; and
  • a way to contribute a structured section outline (title, stable target,
    level, and document) before Sphinx builds the document ToC, or a Sphinx
    upstream API that allows the same contribution later without editing
    env.tocs.

An API that only adds a Jinja context, a doctree-resolved hook, or an HTML
visitor is insufficient: it can produce content but cannot retrospectively
make it a normal source-level hierarchy. Likewise, a Sphinx-Needs-only
post-render hook cannot meet the ToC requirement unless it comes with an
explicit, supported ToC contract.

It is not an immediate delivery path: it needs an upstream design,
compatibility version, and tests for parallel reading, external Needs,
incremental builds, and non-HTML builders. Until such an API exists, proposal
A is the clean local solution.

G. PR #763: transitional containment with a second pass

The existing marked-template second pass may remain only as a time-boxed
bridge while proposal A is implemented or an upstream feature is evaluated.
Its contract should be deliberately narrow:

  • only named report documents may opt in; ordinary Need templates remain
    read-phase templates;
  • the second read happens once, after merge and before Need post-processing;
  • model traversal is read-only and errors on absent graph targets;
  • all use of SphinxNeedsData, manual purging, read_doc, and env.tocs is
    isolated in one adapter with pinned Sphinx/Sphinx-Needs compatibility tests;
  • it is exercised with -j 1 and -j > 1, a clean build and an incremental
    rebuild, plus an external linked Need; and
  • it has an owner and removal criterion.

This option deliberately does not become the long-term architecture. It
still relies on re-entering a lifecycle that Sphinx exposes no public event to
repeat, and its ToC patch is structurally flat.

It is also listener-order-sensitive: another earlier env-updated listener
can resolve and lock the Need view, causing this handler to skip its reread.
The builder's direct read_doc and environment clear_doc calls are not
public extension APIs, and the module-global template environment is
process/app state rather than environment-owned state. These are compatibility
risks even when the current event sequence happens to work.

H. Rejected for the stated requirements: write-phase node replacement

A custom placeholder node could be populated in doctree-resolved from
get_needs_view(app). It can be a builder-neutral late fragment when it emits
only already-supported Docutils nodes with no new Sphinx-level semantics. It
is not a solution here:
reference resolution and the normal ToC were already processed in the read
phase, and post-hoc sections have no supported way to join that hierarchy.
It must also record reverse Need-to-report dependencies and return affected
report documents for rewriting; otherwise an incremental build can retain a
stale late fragment after a linked Need changes.

I. Rejected for normal documentation: publish a late HTML page

Sphinx's html-collect-pages event can add an HTML page after the model is
complete. A page context can be built from get_needs_view(app), so it is a
reasonable export mechanism for a self-contained dashboard, downloadable
report, or machine-oriented visualization.

It is not an ordinary Sphinx document: it is HTML-builder specific, has no
source document for the normal ToC collector, and cannot naturally participate
in source-level cross-reference resolution, non-HTML builders, or the report
Need's normal document semantics. It can receive the theme's global context and look
visually integrated, but it has no ordinary local ToC or active toctree
position; search indexing, object inventory, and linkcheck do not gain this
virtual page automatically. Relative URLs for nested page names and HTML
escaping are also the extension's responsibility. Keep this option only for
an explicitly separate HTML deliverable.

J. Product alternative: relax the normal-ToC requirement explicitly

If the product decision is that only HTML output matters and standard local
navigation is out of scope, retain a custom report navigation model: a
semantic report-heading node, stable anchors, and a theme adapter that renders
its tree. This is better than scraping rubric nodes because titles and levels
are explicit and testable.

It is still not Sphinx's normal ToC. It requires a versioned adapter for the
chosen theme (PyData in this repository) and separate behaviour for every
builder, and it keeps the
complete-model timing problem unless combined with G or a write-phase render.
Therefore this is a conscious requirement trade-off, not an alternative that
meets the requirements at the top of this document. It should be selected only
with product approval that standard local navigation and non-HTML parity are
out of scope.

K. PR #730: static report outline with late Sphinx-Needs widgets

PR #730 implements
another viable shape: a .. module-verification-report:: directive renders a
temporary plain Docutils container during the normal read phase, using
nested_parse_with_titles, and returns its children into the surrounding
document. Its generated reStructuredText therefore contains ordinary section
headings (and normal document navigation) plus a generated mod_ver_report
Need. The data-heavy portions are emitted as needtable and needpie
directives; Sphinx-Needs resolves their filters only after Need collection. The
generated Need and report body are siblings, not a report body nested inside a
Need. A build-finished consistency check verifies only that each listed
component belongs to the module's includes links and, when set, that the
requested feature belongs to each listed component.

This is a sound solution for a static report outline with dynamic table
contents
. It keeps ordinary parallel reading, uses normal Sphinx headings,
and avoids the current manual reread and env.tocs patch. It is therefore
substantially cleaner than G for consumers willing to declare their report
structure.

It does not meet this investigation's model-driven-outline requirement:
:components: is mandatory and is a manually maintained list, component
titles are derived from ID prefixes instead of Need fields, and the directive
does not discover the component order from the module's includes links. Its
check does not detect a component that is present in includes but omitted
from :components:; it only rejects listed components that are not linked.
Even a stronger late check could not make an omitted component appear in the
already-read ToC. The report body also sits beside a generated report Need,
rather than inside that Need node.

Accordingly, K is not a replacement for A under the stated criteria. It is a
strong alternative if the product explicitly accepts :components: as the
authoritative report-outline declaration and treats graph validation—not graph
discovery—as sufficient. In that case, adopt PR #730's directive directly and
add tests for a mismatching component list, an external Need in a needtable,
and section/anchor output under parallel reading.

Critical-risk analysis and evidence required

The following analysis is deliberately more demanding than a feature list. A
passing happy-path demo is not sufficient evidence for any option: the risks
are primarily invalidation, model semantics, phase ordering, and long-term API
ownership.

A. Generated source from a report manifest

The major risk is a hidden bootstrap cycle. The seed needs_json build must
read a report-Need stub but must not require the final generated report body;
the final documentation build may consume the body. If the seed and final
build have different Sphinx config, defines, mounts, or external-Needs inputs,
dynamic fields, variants, link targets, and visibility can disagree even when
both builds individually succeed. The manifest must therefore record a
fingerprint of every semantics-affecting input and fail when the final build
uses an incompatible configuration.

The word "complete" cannot be delegated to the existing :needs_json target
without a contract. Its default Sphinx-Needs builder filter excludes external
Needs, and its configured removal of default-valued fields makes an omitted
JSON field semantically different from an explicit None. The manifest
producer must either export the integrated model with an intentional external
Need filter, or merge the local export and every external JSON input itself.
It must apply the matching schema defaults and conditional-link semantics,
including any configured ID-prefix and URL policy.

Graph ordering requires special care. The requirement is source-link order,
but Sphinx-Needs may normalise link collections for deterministic processing.
The manifest contract must explicitly state which order it exports and test it
with non-alphabetical linked components. If the exported model no longer
retains declared order, the producer must export that order as a first-class
field; sorting IDs in the renderer is not an equivalent substitute.

There is also an incremental-build limit: if the renderer consumes one complete
needs.json, Bazel will normally execute it whenever that input changes,
including unrelated Need changes. The realistic target is a byte-identical
generated report and no invalidation of the final HTML when its transitive
fingerprint is unchanged—not necessarily zero renderer executions. Achieving
finer action-level invalidation needs the model producer to emit per-report
shards or a dependency index, which is an optimisation to justify with build
measurements. Generated-source diagnostics must map failures back to the
report ID and contributing Need IDs, otherwise authors receive only opaque
line numbers in a Bazel output tree.

Evidence before implementation: inspect one complete exported model for
external Needs, resolved backlinks, variants, and link order; define a schema
and compatibility fingerprint; prove seed/final equality for the actual report
dependencies (including builder-affecting settings such as variants and
mounts); and write clean/incremental/parallel tests that compare generated
source, not merely HTML screenshots.

B. Generated reports bundle

This inherits all risks from A and adds source-placement risk. A data-only
bundle is not automatically proof that its .rst files are discovered by the
final Sphinx source tree, mounted once, and attached to the intended toctree.
The bundle rules, mount manifest, and sphinx_docs source staging must agree
for both bazel run and sandboxed bazel build; a path that works only from a
workspace checkout is not sufficient.

A pure-data bundle also needs an explicit entry contract. Its default entry is
index, so a generator that emits only module-report.rst has no reliable
attachment point unless it creates an index/toctree stub or sets entry_doc
accordingly. Files are mounted as a directory, not inserted beside a host
Need; inline inclusion is a separate cross-mode path-resolution problem.

Ownership can also obscure dependencies: the report-producing package may use
Need IDs from the consuming package, while the consumer owns the final
cross-reference resolution. Keep the generated body free of resolved URLs and
emit only source-level Need references. Verify collision handling for generated
docnames, labels, anchors, static assets, and multiple mounted report bundles.

Evidence before implementation: an end-to-end fixture with two mounts,
one report, a local cross-reference, and an external Need; run it both through
the interactive target and the sandboxed needs_json/HTML targets.

C. Stand-alone canonical graph compiler

Its critical risk is semantic fork, not implementation size alone. Sphinx-
Needs has behaviour for dynamic values, filters, parts, imports, variants,
constraints, and backlinks. A compiler that approximates even one of those
rules can produce a report that contradicts Sphinx's own Need tables. Version
pinning does not solve this: every upgrade creates a compatibility surface
that must be revalidated.

The design is credible only if the graph IR becomes an owned product with an
explicit language specification and differential tests against Sphinx-Needs
for representative models, including errors. Otherwise the compiler becomes a
second undocumented parser whose apparent determinism hides correctness drift.

Evidence before investment: name a non-Sphinx consumer that cannot use A,
define IR ownership and upgrade policy, and demonstrate differential agreement
on imports, backlinks, conditional links, and variants. Without these, reject
C on lifecycle cost alone.

D. Preload the model in every reader

The central failure mode is two inconsistent models: a pre-scan model used for
headings and Sphinx-Needs' later model used for tables and validation. A small
source change can then affect only one model, particularly with includes,
collections, mounted sources, configuration-dependent options, or external
Needs. Transferring the model to workers also creates a large serialisation and
memory cost, and mutable shared state invalidates the extension's parallel-safe
claim.

Even a hypothetical complete preloaded model solves only data availability.
With the current template render location, output remains inside a
Sphinx-Needs Need node and is parsed with titles disabled; it cannot become
normal document sections or ToC entries. Solving that requires a separate
structural design such as A, K, or an upstream API.

No amount of locking fixes this because the ordering is wrong: the ordinary
reader has not yet established the data it is supposed to preload. The only
safe implementation is an independent prior action with a stable artifact,
which reduces D to A or C.

Decision: reject unless C has already been adopted; do not prototype an
in-process pre-scan or module-global worker cache.

E. Reader ordering or report serialisation

This assumes that document order can make worker data visible. It cannot:
env-before-read-docs can reorder Sphinx's initial document list, but parallel
reading still partitions it into worker chunks and Sphinx merges their
environments only after the read phase, not between selected documents. A
custom serial report chunk would additionally have to reproduce purging,
dependency tracking, collector events, and error handling, turning the
extension into a partial Sphinx builder. External Needs may be available
before reading; local Needs from other workers are not.

Serialising the complete build masks the model issue but imposes a permanent
performance regression. Serialising an isolated, separate Sphinx action is
valid only when its output is treated as an artifact, which again is A.

Decision: reject as an in-process design; measure it only if considering
the explicit seed action of A.

F. Upstream report-and-ToC API

The difficult question is API ownership across two projects. Sphinx-Needs can
own a resolved Need view, but Sphinx owns source reading, doctrees, and ToC
collection. A feature that promises post-collection content but delegates ToC
integration to consumers would merely standardise the current workaround.
Conversely, a Sphinx change must define persistence, invalidation, translation,
single-page builders, and interactions with other collectors.

An upstream proposal should contain a minimal contract, not a request for a
generic callback: immutable resolved model input; declarative structured
outline; stable generated-source identity; explicit dependency reporting; and
documented behaviour for every supported builder. It also needs a migration
path for cached environments and a version floor for dependent projects.

The proposal must additionally choose a real phase mechanism. The current
env-updated contract can mark documents for writing, not re-enter source
reading and ToC collection. Thus either Sphinx must own a declarative second
collection iteration—with purging, re-reading, collector execution,
invalidation, and convergence limits—or the generated source/manifest must be
available before the first reader pass. Sphinx-Needs alone cannot add normal
sections after Sphinx's collection phase.

Evidence before commitment: maintainer agreement that this belongs in the
respective project, an accepted API sketch covering ToC timing, and a supported
fallback for existing Sphinx versions. Until then, it has schedule risk with
no bounded delivery date.

G. PR #763 current reread and env.tocs patch

The current mechanism is vulnerable to re-entrancy: env-updated is a normal
post-read event, but the handler purges and reads documents again from inside
it. Other extensions may have already recorded read-phase data, while
Sphinx-Needs may be about to post-process or lock Needs. A guard that prevents
an infinite loop does not make those extensions' state reversible. The handler
also declares parallel-read safety even though the rerender is intentionally
main-process and builder-specific.

There is a concrete hidden order dependency: an earlier env-updated listener
may call get_needs_view() and lock post-processing, after which the current
handler returns without rerendering. Direct clear_doc and read_doc calls
also bypass parts of the normal Builder.read() protocol, while the
module-global template environment is cross-app/process state rather than
environment-owned data. A build that happens to pass once therefore does not
establish extension compatibility.

The ToC patch has a separate correctness defect: rubrics carry no section
level, and direct mutation of env.tocs/toc_num_entries copies collector
internals. Anchor collision handling, translated titles, incremental doctree
caching, nested reports, and non-HTML builders are all unproven. A successful
HTML page demonstrates only the happy path.

Containment requirements: pin Sphinx and Sphinx-Needs versions, add an
adapter boundary and a removal date, and test event traces as well as output
for clean and incremental -j 1/parallel builds. Include an intentionally
early env-updated view resolver, two reports, template changes, removed
reports, and two Sphinx apps in one interpreter. Do not add new report types
or features to this mechanism.

H. Write-phase node replacement

This option has a hard phase barrier. By doctree-resolved, Sphinx has applied
post-transforms and resolved references for the document being written. New
nodes may render, but newly created pending references, citations, sections,
and ToC relationships do not receive the same normal processing unless the
extension reimplements or reruns private transforms. Persisting the mutation
would also blur the distinction between the cached read doctree and a
builder-specific write doctree.

It is safe only when replacement emits self-contained nodes with no new
Sphinx-level structure or references. This is narrower than the report.

It also runs only for documents selected for writing. A correct incremental
implementation needs an environment-persisted, parallel-merge-safe reverse
map from each traversed Need to report documents and must mark those documents
for rewrite when the Need changes. Without it, a clean build may be correct
while an incremental build retains stale late content.

Decision: retain only as a documented pattern for simple late fragments;
do not use it for the report page.

I. Late HTML page via html-collect-pages

The page has no source identity, so standard document-level facilities are
absent by design: no ToC collector entry, no source dependency graph, and no
normal resolution of RST cross-references. It can use the theme's global
context and look integrated, but it has no ordinary local ToC, active toctree
position, automatic search-index entry, inventory entry, or linkcheck
coverage. Relative URLs, anchors, escaping, and accessibility behaviour become
extension responsibilities. A static rendered page may look correct while
linkcheck, non-HTML builders, and theme upgrades silently diverge.

This is viable for a separately branded export whose links are deliberately
plain URLs and whose accessibility/navigation are tested as an HTML product.
It must not be presented as a normal Sphinx report page.

J. Theme-specific report navigation

This replaces a stable Sphinx semantic contract with a private theme contract.
The extension must define heading nesting, unique anchors, keyboard behaviour,
ARIA semantics, mobile layout, active-section tracking, translations, and
theme upgrades. It also has to decide what non-HTML builders and alternate
themes receive. Simply copying the visible theme menu is neither accessible
nor maintainable integration.

PyData is the active repository theme, so any adapter must target its actual
sidebar/scroll behaviour, not a generic or Furo-shaped menu. It must also
declare reverse dependencies from Need-graph changes to the report page;
write-phase/theme-context navigation otherwise becomes stale on incremental
builds even if a clean build is correct.

The approach is justified only if a report is explicitly an HTML application
rather than documentation. It requires product ownership for the navigation
component and visual/accessibility regression tests; without that, reject it.

K. PR #730 hybrid directive

K has a favourable phase split but a strong source-of-truth problem. The
directive's :components: list creates the section skeleton early enough for
the ToC, while needtable filters resolve their data later. If the list and
the graph disagree, the build-finished check reports only a listed component
that is absent from module.includes; it does not detect an included component
that is absent from the list. It is too late to create missing headings in
either case; under -W, users receive a failed build only for the directed
checks that exist, rather than an automatically corrected report. This may be
acceptable when the list is a deliberate reviewable report scope, but it is
not automatic graph coverage.

Its title and slug derivation are also naming-convention semantics. Renaming a
Need, using a non-standard external ID, or changing a title can leave headings
and data inconsistent. The directive should at least preserve explicit titles
in its declaration or consume them from a generated manifest. It must safely
quote IDs used in generated filter expressions and produce collision-resistant
anchors. In the reviewed PR, work-product selection additionally uses
normalised-ID substring matching rather than Need links, which can produce
false positives and false negatives; component overview sorting can also
diverge from the declared outline order.

Finally, nested_parse_with_titles makes heading level depend on the location
of the directive in the surrounding document. Tests must cover root and nested
placement, multiple reports in one document, references to generated anchors,
and the interaction of needtable with external Needs under parallel reading.
It must also test duplicate component/report IDs and normalised title-label
collisions, version-qualified component entries whose qualifier is discarded,
and safely quoted filter IDs. Coverage-file integration is another declared
build input; reading an LCOV path during directive execution without a tracked
dependency lets a changed file at the same path leave an incremental Sphinx
report stale and harms Bazel reproducibility.

Decision: K is a strong immediate option only when an explicit outline is
an intentional authoring contract. To evolve it toward graph discovery, feed
it the manifest from A; do not move graph traversal into its read-phase
directive.

Phase 4 – Validation and explicit exclusions

Acceptance tests for any selected design

  1. A module linked to two components generates two ordered component sections
    without their IDs appearing in the template or renderer configuration.
  2. An external feature such as feat__baselibs resolves through its graph
    link and produces a working Need reference.
  3. Backlink-dependent report content is evaluated only after backlinks are
    resolved.
  4. The local navigation contains nested, working anchors for the report and
    component sections, with no hand-authored HTML menu.
  5. -j 1 and parallel reading produce byte-identical report source and
    equivalent HTML.
  6. Changing an unrelated document leaves the generated report byte-identical
    and does not invalidate final report HTML; changing a transitive report
    Need or external-Needs input does. The renderer itself may still execute
    when it consumes a monolithic model export.
  7. HTML and one non-HTML builder complete without dependency-private API
    failures.
  8. A clean build produces the report on its first invocation; no second
    invocation, warmed Sphinx environment, or source-tree write is required.
  9. The Bazel action graph is acyclic: the model export does not consume the
    final generated report, and the final docs action consumes only declared
    generated outputs.
  10. A manifest with an unsupported schema or producer version fails with a
    clear diagnostic rather than producing a plausible but incorrect report.

Non-solutions to avoid

Any proposed solution should explicitly reject or justify the following:

  • disabling Sphinx parallelism for the whole docs build;
  • hardcoding component IDs in the .need template;
  • relying on a worker-local Need model being complete;
  • manually purging and re-reading arbitrary documents unless this is a
    documented, supported Sphinx extension pattern;
  • monkeypatching Sphinx-Needs internals without a strong compatibility plan;
  • rendering a duplicate HTML-only menu instead of integrating with the normal
    ToC;
  • inferring relationships from Need ID string conventions when graph links are
    available;
  • silently hiding missing Needs or falling back to an empty report when the
    model is incomplete.

Phase 5 – Recommendation and next decision

Decision guide

  • A — generated RST from the complete model: choose this. It first builds
    the complete Need graph, then generates an ordinary report page from it. It
    is the only local option that automatically follows the graph and gives
    Sphinx real headings and navigation.
  • B — generated report bundle: this is A packaged for many reports or for
    reports supplied by another documentation module. It is not a different
    model solution.
  • K / PR refactor(module-verification-report): split, tests, docs #730: choose it for the fastest clean Sphinx solution only when
    authors deliberately maintain the component list in :components:. It gives
    good navigation, but it does not discover components from the Need graph.
  • G / PR initial version #763: keep it only as a short-lived bridge. It discovers graph
    components, but does so by manually re-entering Sphinx's read lifecycle and
    by patching the ToC internals.
  • C and F: consider these only as larger product investments: C creates a
    new model platform; F requires an upstream Sphinx/Sphinx-Needs feature.
  • D, E, H, I, and J: do not select these for this report. Each gives up a
    required property or replaces it with private, builder-specific machinery.

Recommended decision

Choose A as the target architecture for the Baselibs report. Reuse the
normal-section rendering ideas from PR #730 where useful, but supply its
component list, titles, and order from a complete graph manifest rather than
from hand-written directive options. Do not extend PR #763 with further report
features; keep it only until A is available.

What to implement next

The next step is not a generic new platform and not ten proof-of-concepts. It
is one narrow, end-to-end delivery for baselibs_report:

  1. Replace the dynamic report template in the model-collection input with a
    simple report-Need stub containing its ID and graph links.
  2. Build one integrated report manifest that deliberately combines the
    local Need export and the required external Need exports, resolves the
    report's links/backlinks, and preserves the required component order.
  3. Render that manifest into one generated .rst report body with ordinary
    sections and :need: references; include or mount it in the final docs
    build next to the report Need.
  4. Run the Phase 4 acceptance tests. Once they pass, remove the PR initial version #763
    reread and env.tocs code for this report.

The manifest is intentionally small and report-specific at first: report ID,
ordered component/feature IDs, display titles, link targets, and input/schema
versions. It must not serialise Sphinx's environment or pre-render HTML. If a
second report later needs the same pipeline, promote the shared parts into B;
do not start with C.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions