Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Bench evidence-validation tests verify repository-history invariants
# (retained-run ancestry / reviewed squash relations) that need the
# real commit graph, not a depth-1 checkout.
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
Expand All @@ -34,8 +39,14 @@ jobs:
- name: Run repository checks
run: python scripts/check_repo.py

- name: Check documentation local links
run: python tools/check_markdown_links.py

- name: Run full project test suite
run: python -m pytest tests -q

- name: Run bench unit/contract suites (isolated per bench)
run: python tools/run_bench_unit_tests.py

- name: Run smoke tests
run: python scripts/smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,4 @@ This ADR does not:
- [Gate C review](../product-line-engineering/gate-c-product-line-scope-review.md)
- [Product-line semantic kernel](../../textual-notation-of-model/packages/methods/de4sdv/de4sdv_product_line.sysml)
- [Standalone reference product](../../model-based-product-line-engineering/product-models/aebs_autoware_reference_product.sysml)
- [AAOS-integrated configured member](../../textual-notation-of-model/packages/features/middleware/mw_variability_configuration.sysml)
- [AAOS-integrated configured member](../../textual-notation-of-model/packages/features/middleware/middleware_variability_configuration.sysml)
8 changes: 8 additions & 0 deletions docs/naming/naming-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,14 @@ Artifact-aware rules (small and explicit, no heuristic parser):
templates), the historical 009C–009I implementation-plan record, and the
naming QA/manifest docs (which quote unregistered forms as
counterexamples). The conventions doc itself stays fully governed.
- **Git-ignored bench runtime workspaces**
(`implementation/*/workspace/...` that `git check-ignore` confirms is
ignored): vendored upstream source checkouts and build outputs produced
by bench prepare/build scripts. Their identifiers follow upstream naming
and are runtime material, not committed governed data. The exemption is
behavioral, not a blanket path rule — any tracked file under such a
directory stays governed — so a clean CI checkout and a developer
checkout with a built workspace behave identically.
- **Non-governed look-alikes**: hash-algorithm names (`SHA-256`, `SHA-1`),
GitHub line anchors (`#L743-L754`), charset fragments, mixed-case prose
(`SERVER-IPv4`, `AI-Ready`), external project names (`S-CORE`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ runtime evidence.
| Planned reference member | Vehicle Platform Integration Mode | Existing supporting model evidence |
|---|---|---|
| Standalone Autoware AEBS Reference Member | Standalone | [Standalone reference product](../../model-based-product-line-engineering/product-models/aebs_autoware_reference_product.sysml) |
| AAOS-Integrated Autoware AEBS Reference Member | AAOS Integrated | [Middleware configured member](../../textual-notation-of-model/packages/features/middleware/mw_variability_configuration.sysml) |
| AAOS-Integrated Autoware AEBS Reference Member | AAOS Integrated | [Middleware configured member](../../textual-notation-of-model/packages/features/middleware/middleware_variability_configuration.sysml) |

The supporting artifacts remain evidence of local definitions and maturity.
They do not replace the governed membership decision.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ compliance, or homologation claim is made.
Video bytes are not tracked in Git. The three continuous raw recordings are
held in the maintainer archive under
`2026-09-06/p188-exact-head-108bfa9/`; their checksums are recorded in
[`external-media.yaml`](../../../external-media.yaml). Per-state stills are
[`external-media.yaml`](../../external-media.yaml). Per-state stills are
retained here as frame-level evidence.

## Per-run record contents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,25 @@ def test_squash_delivered_reviewed_head_accepts_retained_run_ancestor(self) -> N
"reviewed_head": "871ef95bbdf3b865d5761d692065674fc0b4e196",
"delivery_commit": "81e043386251118b302bafbed91922f8fa821522",
}
missing = [
name
for name, revision in (
("retained_run_head", relation["retained_run_head"]),
("reviewed_head", relation["reviewed_head"]),
)
if subprocess.run(
["git", "-C", repository, "cat-file", "-e", f"{revision}^{{commit}}"],
capture_output=True,
).returncode
!= 0
]
if missing:
self.skipTest(
"requires the squashed PR #66 commit objects "
f"({', '.join(missing)}) which exist only in clones that "
"fetched the pull refs; run this test from a full developer "
"clone or fetch refs/pull/66/head"
)
live_head = subprocess.check_output(
["git", "-C", repository, "rev-parse", "HEAD"], text=True
).strip()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ def test_identity_changes_for_authoritative_input_and_ignores_pycache(tmp_path:
def test_runtime_verifier_accepts_live_inheritance_and_rejects_mismatch(
tmp_path: Path,
) -> None:
inherited_setup = INHERITED / "workspace/install/setup.bash"
if not inherited_setup.is_file():
pytest.skip(
"requires the git-ignored inherited 009A workspace build output "
f"({inherited_setup}); built by the bench prepare/build flow, "
"not by a source checkout"
)
command = [
sys.executable,
str(BENCH / "scripts/verify_runtime.py"),
Expand Down
35 changes: 35 additions & 0 deletions scripts/check_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

from pathlib import Path
import re
import subprocess
import sys

try:
Expand Down Expand Up @@ -137,6 +138,17 @@
# section 11 documents this narrowing).
_GOVERNED_SUFFIXES = {".sysml", ".yaml", ".yml", ".md"}

# Ignored upstream/runtime workspaces under implementation benches: these
# directories hold vendored upstream source checkouts (Autoware) and build
# outputs produced by prepare/build scripts. They are git-ignored runtime
# material whose identifiers follow upstream naming, not the DE4SDV
# registry. Scanning them would fail CI whenever a developer has a local
# workspace checkout, regardless of committed content. The committed
# repository surface is still fully governed.
_IGNORABLE_RUNTIME_PARTS = {
"workspace", # bench runtime checkouts/build trees (git-ignored)
}

# ---------------------------------------------------------------------------
# Identifier registry (docs/naming/naming-conventions.md section 5).
#
Expand Down Expand Up @@ -359,9 +371,32 @@ def _is_exempt_from_id_scan(path: Path) -> bool:
return True
if any(part in _EXEMPT_ID_PATH_PARTS for part in parts):
return True
# Git-ignored bench runtime workspaces (vendored upstream checkouts and
# build outputs) are outside the governed committed surface. Any tracked
# file under a bench workspace remains scanned.
if any(part in _IGNORABLE_RUNTIME_PARTS for part in parts):
return _is_ignored_runtime_path(path)
return any(rel.startswith(prefix) for prefix in _EXEMPT_PATH_PREFIXES)


def _is_ignored_runtime_path(path: Path, repository: Path | None = None) -> bool:
"""True only when git actually ignores the path (tracked files stay governed).

``repository`` defaults to the DE4SDV root; tests may pass an isolated
fixture repository to exercise both sides of the rule deterministically.
"""
repository = ROOT if repository is None else repository
try:
result = subprocess.run(
["git", "check-ignore", "-q", str(path.relative_to(repository))],
cwd=repository,
capture_output=True,
)
except (OSError, ValueError):
return False
return result.returncode == 0


# Technical tokens that merely look like ID-shaped but are external
# vocabulary or URL fragments (never governed identifiers). The URL-pattern
# guard strips GitHub line-anchor suffixes such as `#L743-L754` before
Expand Down
224 changes: 224 additions & 0 deletions tests/test_check_markdown_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
"""Behavioral tests for documentation local-link validation."""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path
from unittest import mock

ROOT = Path(__file__).resolve().parents[1]
TOOL = ROOT / "tools" / "check_markdown_links.py"


def _load_tool():
import importlib.util

spec = importlib.util.spec_from_file_location("check_markdown_links", TOOL)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def test_committed_markdown_local_links_resolve() -> None:
"""The committed corpus passes against the real tracked surface."""
completed = subprocess.run(
[sys.executable, str(TOOL)], cwd=ROOT, capture_output=True, text=True
)
assert completed.returncode == 0, completed.stdout + completed.stderr
assert "Markdown local-link check passed" in completed.stdout


import contextlib


@contextlib.contextmanager
def _scenario(tmp_path: Path, files: dict[str, str], tracked: set[str]):
module = _load_tool()
for relative, content in files.items():
destination = tmp_path / relative
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(content, encoding="utf-8")
original_root = module.ROOT
try:
module.ROOT = tmp_path
errors = module.broken_links(
[name for name in files if name.endswith(".md")],
tracked=frozenset(tracked),
)
yield errors
finally:
module.ROOT = original_root


def test_tracked_relative_file_passes(tmp_path: Path) -> None:
with _scenario(
tmp_path,
{
"docs/doc.md": "[target](./target.md)\n",
"docs/target.md": "exists\n",
},
{"docs/doc.md", "docs/target.md"},
) as errors:
assert errors == [], errors


def test_missing_file_fails(tmp_path: Path) -> None:
with _scenario(
tmp_path,
{"docs/doc.md": "[missing](./no-such-file.md)\n"},
{"docs/doc.md"},
) as errors:
assert errors == ["docs/doc.md:1: broken local link -> ./no-such-file.md (resolved docs/no-such-file.md is not tracked repository content)"], errors


def test_existing_but_untracked_file_fails(tmp_path: Path) -> None:
"""A local file that exists but is not tracked must not satisfy a link."""
with _scenario(
tmp_path,
{
"docs/doc.md": "[target](./untracked.md)\n",
"docs/untracked.md": "exists locally, not tracked\n",
},
{"docs/doc.md"}, # untracked.md deliberately absent from the surface
) as errors:
assert len(errors) == 1, errors
assert "untracked.md" in errors[0]
assert "not tracked repository content" in errors[0]


def test_repository_root_escape_fails(tmp_path: Path) -> None:
"""A link escaping the repo fails even when the outside file exists."""
outside = tmp_path / "outside-repository-file.md"
outside.write_text("beyond the root\n", encoding="utf-8")
with _scenario(
tmp_path,
{
"docs/doc.md": "[escape](../../../../outside-repository-file.md)\n",
},
{"docs/doc.md", "outside-repository-file.md"},
) as errors:
assert len(errors) == 1, errors
assert "escapes repository root" in errors[0]


def test_url_decoded_paths_continue_to_work(tmp_path: Path) -> None:
with _scenario(
tmp_path,
{
"docs/my doc.md": "exists\n",
"docs/doc.md": "[decoded](my%20doc.md)\n",
},
{"docs/my doc.md", "docs/doc.md"},
) as errors:
assert errors == [], errors


def test_fragment_and_query_stripping(tmp_path: Path) -> None:
with _scenario(
tmp_path,
{
"docs/target.md": "exists\n",
"docs/doc.md": (
"[fragment](./target.md#section)\n"
"[query](./target.md?download=1)\n"
"[both](./target.md?download=1#section)\n"
),
},
{"docs/target.md", "docs/doc.md"},
) as errors:
assert errors == [], errors


def test_tracked_directory_link_passes_and_empty_directory_fails(
tmp_path: Path,
) -> None:
"""Directory links resolve when tracked content lies beneath them."""
with _scenario(
tmp_path,
{
"docs/doc.md": "[dir](./sub/)\n",
"docs/sub/file.md": "tracked content beneath sub\n",
},
{"docs/doc.md", "docs/sub/file.md"},
) as errors:
assert errors == [], errors
with _scenario(
tmp_path,
{"docs/doc.md": "[dir](./empty/)\n"},
{"docs/doc.md"},
) as errors:
assert len(errors) == 1, errors
assert "no tracked content beneath" in errors[0]


def test_tracked_but_deleted_file_fails(tmp_path: Path) -> None:
"""An indexed-but-deleted (unstaged deletion) target must fail."""
module = _load_tool()
doc = tmp_path / "docs/doc.md"
target = tmp_path / "docs/target.md"
doc.parent.mkdir(parents=True)
doc.write_text("[target](./target.md)\n", encoding="utf-8")
target.write_text("exists\n", encoding="utf-8")
original_root = module.ROOT
try:
module.ROOT = tmp_path
errors = module.broken_links(
["docs/doc.md"], tracked=frozenset({"docs/doc.md", "docs/target.md"})
)
assert errors == [], errors
target.unlink() # unstaged deletion of the tracked target
errors = module.broken_links(
["docs/doc.md"], tracked=frozenset({"docs/doc.md", "docs/target.md"})
)
finally:
module.ROOT = original_root
assert len(errors) == 1, errors
assert "missing from the working tree" in errors[0]


def test_single_quoted_title_target_still_checked(tmp_path: Path) -> None:
"""Single-quoted titles are inline links, not extractor escapes."""
with _scenario(
tmp_path,
{"docs/doc.md": "[missing](./no-such.md \'optional title\')\n"},
{"docs/doc.md"},
) as errors:
assert len(errors) == 1, errors
assert "no-such.md" in errors[0]


def test_angle_destination_target_still_checked(tmp_path: Path) -> None:
"""Angle-bracket destinations are inline links too."""
with _scenario(
tmp_path,
{"docs/doc.md": "[missing](<./no-such.md>)\n"},
{"docs/doc.md"},
) as errors:
assert len(errors) == 1, errors
assert "no-such.md" in errors[0]


def test_git_inventory_failure_fails_closed(tmp_path: Path, capsys) -> None:
"""A failed git inventory must fail the check, not pass vacuously."""
module = _load_tool()
with mock.patch.object(
module.subprocess,
"run",
return_value=subprocess.CompletedProcess(
["git"], 128, b"", b"fatal: not a git repository"
),
), mock.patch.object(sys, "argv", ["check_markdown_links.py"]):
assert module.main() == 1
assert "git ls-files failed" in capsys.readouterr().err


def test_root_directory_link_counts_all_tracked_files(tmp_path: Path) -> None:
"""./ must not depend on tracked dotfiles existing."""
with _scenario(
tmp_path,
{"README.md": "[root](./)\n", "src/code.py": "x\n"},
{"README.md", "src/code.py"},
) as errors:
assert errors == [], errors
Loading
Loading