Skip to content

fix: exclude nested bundle mount roots - #784

Merged
AlexanderLanin merged 3 commits into
mainfrom
fix/781-structural-mount-excludes
Sep 3, 2026
Merged

fix: exclude nested bundle mount roots#784
AlexanderLanin merged 3 commits into
mainfrom
fix/781-structural-mount-excludes

Conversation

@AlexanderLanin

@AlexanderLanin AlexanderLanin commented Sep 1, 2026

Copy link
Copy Markdown
Member

Why this is needed

When a directory-backed documentation bundle is mounted below another Sphinx source tree, the same documents can be discovered twice: once by the containing directory walk and once by the bundle mount. That creates duplicate document names and Sphinx-Needs IDs, and makes nested module/component layouts unreliable. The ownership boundary must be based on directories rather than the manifest's current file list so files created during live preview remain discoverable.

What this PR achieves

This fixes #781 by making each directory-mounted bundle own its physical source subtree while excluding nested bundle roots from containing walks. For directory mounts, every document is therefore discovered by exactly one Sphinx source: the primary tree or its owning bundle mount.

Changes

  • Add structural exclusions for bundle roots below the application's primary source directory.
  • Add structural exclusions for child bundle roots below a parent directory mount.
  • Preserve directory walking for directory mounts, including files created during live preview.
  • Keep explicit docs_bundle(srcs = [...]) mounts in file-list mode.
  • Extend the existing reference integration with module/component packages nested below each module's docs directory, covering both legacy data and modern external_needs APIs.
  • Document the source-ownership boundaries and runtime behavior.

Known gap

Explicit docs_bundle(srcs = [...]) mounts are intended for generated documentation sources outside the primary source tree and remain in file-list mode. They are not included in the structural directory-exclusion calculation. If a workspace source file is explicitly mounted from below the primary source tree or another directory mount, it can still be discovered twice; handling that case with exact-file exclusions is left for a follow-up.

The regression coverage verifies the directory exclusion rules directly and renders the nested reference integration, including the module-level and integration-level component paths. Existing subdirectory-bundle coverage continues to verify nested bundle composition without duplicate Needs.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

License Check Results

🚀 The license check job ran with the Bazel command:

bazel run --lockfile_mode=error //src:license-check

Status: ⚠️ Needs Review

Click to expand output
[License Check Output]
Extracting Bazel installation...
Starting local Bazel server (8.6.0) and connecting to it...
INFO: Invocation ID: 5f42996f-d071-4521-a4bd-a19eee45ad2b
Computing main repo mapping: 
Loading: 
Loading: 0 packages loaded
Loading: 0 packages loaded
Loading: 0 packages loaded
    currently loading: src
WARNING: Target pattern parsing failed.
ERROR: Skipping '//src:license-check': no such target '//src:license-check': target 'license-check' not declared in package 'src' defined by /home/runner/work/docs-as-code/docs-as-code/src/BUILD
ERROR: no such target '//src:license-check': target 'license-check' not declared in package 'src' defined by /home/runner/work/docs-as-code/docs-as-code/src/BUILD
INFO: Elapsed time: 5.747s
INFO: 0 processes.
ERROR: Build did NOT complete successfully
ERROR: Build failed. Not running target

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Documentation preview for this pull request is available at:
pr-784: https://eclipse-score.github.io/docs-as-code/pr-784/

@AlexanderLanin
AlexanderLanin force-pushed the fix/781-structural-mount-excludes branch 2 times, most recently from 05ae1e4 to 56d8ef9 Compare September 2, 2026 21:51
@AlexanderLanin
AlexanderLanin requested a balanced review from Copilot September 2, 2026 21:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Explicit file-list mounts can still be discovered twice, violating the single-owner invariant.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds directory-based ownership boundaries to prevent duplicate Sphinx discovery for nested documentation bundles.

Changes:

  • Excludes nested bundle roots from primary and parent directory walks.
  • Adds nested-package regression coverage and documentation.
  • Preserves explicit file-list mounting.

Critical issue: Explicit file-list mounts are omitted from ownership exclusions, allowing containing directory walks to rediscover their files and cause duplicate document names or Need IDs.

File summaries
File Description
src/tests/docs_bzl/test_subdirectory_bundle.py Updates nested-bundle coverage.
src/tests/docs_bzl/test_reference_integration.py Tests nested components.
src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/embedded/content/index.rst Updates fixture content.
src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/embedded/BUILD Defines the nested bundle.
src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD Relocates the embedded bundle fixture.
src/tests/docs_bzl/scenarios/reference_integration/modern_module/docs/components/component/index.rst Updates modern component content.
src/tests/docs_bzl/scenarios/reference_integration/modern_module/docs/components/component/BUILD Defines the modern component package.
src/tests/docs_bzl/scenarios/reference_integration/modern_module/BUILD Mounts the modern component.
src/tests/docs_bzl/scenarios/reference_integration/legacy_module/docs/components/component/index.rst Adds legacy component documentation.
src/tests/docs_bzl/scenarios/reference_integration/legacy_module/docs/components/component/BUILD Defines the legacy component package.
src/tests/docs_bzl/scenarios/reference_integration/legacy_module/BUILD Mounts the legacy component.
src/tests/docs_bzl/scenarios/reference_integration/BUILD Describes the nested integration layout.
src/extensions/score_mounts/tests/test_excludes.py Tests exclusion generation and serialization.
src/extensions/score_mounts/__init__.py Implements structural exclusions, but omits file-list mounts from ownership processing.
src/extensions/docs/mounts_internals.rst Documents internal exclusion behavior.
docs/concepts/mounts/index.rst Documents source ownership boundaries.
Review details
  • Files reviewed: 15/16 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/extensions/score_mounts/__init__.py
Comment thread src/extensions/score_mounts/__init__.py Outdated
Comment on lines +234 to +271
def _primary_mount_excludes(
source_dir: Path,
source_mounts: list[tuple[MountSpec, Path]],
) -> list[str]:
"""Return primary-walk exclusions for mounts below the app source root.

Without these patterns, Sphinx's normal walk of ``source_dir`` would also
discover documents that ``sphinx_mounts`` is about to register at the bundle's
``mount_at`` location. The mounted directory must be the sole owner of those
documents.
"""
patterns: set[str] = set()
for _, mount_dir in source_mounts:
pattern = _nested_mount_pattern(source_dir, mount_dir)
if pattern is not None:
patterns.add(pattern)
return sorted(patterns)


def _nested_mount_excludes(
parent_index: int,
source_mounts: list[tuple[MountSpec, Path]],
) -> list[str]:
"""Return all descendant mount roots excluded from one directory mount.

The result includes direct and deeper descendants. Excluding every descendant
makes the ownership boundary independent of manifest order: each nested mount
receives its own documents, while the containing mount keeps the rest.
"""
_, parent_dir = source_mounts[parent_index]
patterns: set[str] = set()
for child_index, (_, child_dir) in enumerate(source_mounts):
if child_index == parent_index:
continue
pattern = _nested_mount_pattern(parent_dir, child_dir)
if pattern is not None:
patterns.add(pattern)
return sorted(patterns)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if this could be done in one, so we save some iterations here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I combined the primary and nested exclusion calculation into one precomputation. The runtime mount loop now reuses the calculated results instead of rescanning the complete mount list for every parent.

Comment thread src/extensions/score_mounts/__init__.py Outdated
_make_mount_entry(
walk_dir,
spec,
tuple(_nested_mount_excludes(index, source_mounts)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only use this nested_mount_excludes here, so why not make the return type a tuple directly, instead of a list?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The per-mount exclusion results are now returned as tuples directly, so the runtime code no longer needs to convert the result with tuple(...).

@MaximilianSoerenPollak MaximilianSoerenPollak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My issues with the code have been addressed.
From my side this is alright now.

@AlexanderLanin
AlexanderLanin merged commit 7468750 into main Sep 3, 2026
24 checks passed
@AlexanderLanin
AlexanderLanin deleted the fix/781-structural-mount-excludes branch September 3, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

fix: respect Bazel package boundaries when mounting docs bundles

3 participants