Hierarchical Needs with upward_bundles#

Use upward_bundles when a documentation bundle owns Needs at a lower architectural level and those Needs refer to Needs owned by a higher level. The declaration is the validation interface of the bundle; it is separate from the bundles declarations that decide where documentation is mounted.

This How-To uses the SCORE metamodel and the following ownership model:

  • the module’s docs() source owns the external-to-the-components feat_req and the module architecture Need;

  • a component is a docs_bundle and owns a comp and a comp_req;

  • a subcomponent is another docs_bundle and owns its own comp and comp_req;

  • both component requirements derive from the module-level feature requirement.

Here, external means external to the component bundle’s ownership. It does not necessarily mean a different Bazel module or repository.

Declare the hierarchy#

The module and its component bundles can be declared in one Bazel package:

load("//:docs.bzl", "docs", "docs_bundle")

docs_bundle(
    name = "component",
    source_dir = "component",
    metamodel = "//src/extensions/score_metamodel:metamodel_yaml",
    # The module's own source bundle is generated by docs().
    upward_bundles = [":docs_source_bundle"],
)

docs_bundle(
    name = "subcomponent",
    source_dir = "subcomponent",
    metamodel = "//src/extensions/score_metamodel:metamodel_yaml",
    # The module is inherited transitively through component.
    upward_bundles = [":component"],
)

docs(
    source_dir = "module",
    project = "Example module",
    project_url = "https://example.invalid/module",
    metamodel = "//src/extensions/score_metamodel:metamodel_yaml",
    bundles = [
        {"bundle": ":component", "mount_at": "component"},
        {
            "bundle": ":subcomponent",
            "mount_at": "component/subcomponent",
        },
    ],
)

upward_bundles points from the child to its parent. The parent does not mount the child through this attribute. Mounting remains a separate, downward bundles relationship in docs().

The same attribute is available on docs() when the module’s own source bundle is itself below another documentation bundle:

docs(
    source_dir = "docs",
    upward_bundles = ["@platform_docs//:docs_bundle"],
    bundles = [
        {"bundle": ":component", "mount_at": "component"},
    ],
)

This makes the ancestor export available both to the module’s private source bundle and to the final composed needs_json build. The generated :docs_source_bundle remains the upward parent for bundles declared in the same module. The legacy :_docs_source_bundle label is still accepted, but new consumers should use the stable public name.

The generated :docs_source_bundle is the module’s own source-level bundle and is available as the host’s upward hierarchy anchor. This allows hierarchical bundles to be declared in another Bazel package. Its reusable Needs export is available as :docs_source_bundle_needs_upward. The public :docs_bundle target is the composed documentation tree and must not be used as the upward parent when it already mounts the child; that would introduce a dependency cycle.

If the module uses a custom conf.py and its feature requirement lives in module/index.rst, add the module name to required_in_id so the three-part SCORE requirement ID remains meaningful after the source directory is stripped:

extensions = ["score_sphinx_bundle"]
version = "0.0.0"
required_in_id = ["module"]

Write the Needs#

The module source can own the feature and module Needs:

.. feat:: Seat heating
   :id: feat__seat_heating
   :version: 1
   :security: NO
   :safety: QM
   :status: valid

.. feat_req:: Seat heating availability
   :id: feat_req__module__seat_heating
   :version: 1
   :reqtype: Functional
   :security: NO
   :safety: QM
   :status: valid
   :valid_from: v1.0
   :satisfied_by: feat__seat_heating

   The seat heating feature is available to the vehicle user.

.. mod:: Seat heating module
   :id: mod__seat_heating_module
   :version: 1
   :includes: comp__seat_heating_controller

The component bundle can refer to the module-level feat_req while owning its own requirement and architecture element:

.. comp:: Seat heating controller
   :id: comp__seat_heating_controller
   :version: 1
   :security: NO
   :safety: QM
   :status: valid
   :belongs_to: feat__seat_heating
   :consists_of: comp__seat_heating_sensor

.. comp_req:: Controller temperature control
   :id: comp_req__component__temperature_control
   :version: 1
   :reqtype: Functional
   :security: NO
   :safety: QM
   :status: valid
   :derived_from: feat_req__module__seat_heating
   :satisfied_by: comp__seat_heating_controller

   The controller regulates the requested heating level.

The subcomponent follows the same SCORE pattern. Its derived_from link tests the transitive closure: subcomponent declares only component, while component declares the module source bundle.

Verify the composed graph#

Build the normal Needs target from the module package:

$ bazel build //path/to/module:needs_json

The resulting graph must contain the module-level feat_req, the component and subcomponent comp_req Needs, and their resolved derived_from links. The same fixture should also be rendered with bazel run //path/to/module:docs to verify the final mounted document paths.

Bundle-local exports#

For every source-bearing docs_bundle the macro also creates <name>_needs_local and <name>_needs_upward. The first parses only the bundle’s own sources. The second is the reusable export: it merges the local Needs with the direct upward_bundles exports, whose files already contain their own upward closure. This makes the target useful as an explicit input to another bundle while keeping ownership local.

The public needs_json target still evaluates the complete composed source tree. It remains the right target for final backlinks, mounted document paths, global checks, metrics, and rendered documentation.

There is no separate <name>_needs_downward target: downward composition is owned by the host docs() target and is exposed as its public needs_json target.

For the separation of composition and Needs ownership, see the architecture concept.