From b756248e0154e6067f449f0d642398ea6efd8237 Mon Sep 17 00:00:00 2001 From: proboscis Date: Tue, 14 Jul 2026 17:21:28 +0900 Subject: [PATCH 1/3] Add failing ADR wiring self-check tests --- packages/doeff-adr/tests/test_wiring.py | 122 ++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 packages/doeff-adr/tests/test_wiring.py diff --git a/packages/doeff-adr/tests/test_wiring.py b/packages/doeff-adr/tests/test_wiring.py new file mode 100644 index 00000000..c227629b --- /dev/null +++ b/packages/doeff-adr/tests/test_wiring.py @@ -0,0 +1,122 @@ +"""Regression tests for executable ADR collection wiring.""" + +import subprocess +import sys +from collections.abc import Sequence + +import pytest + +pytest_plugins = ["pytester"] + + +def _make_executable_adr(pytester: pytest.Pytester, adr_id: str) -> None: + pytester.mkdir("docs") + pytester.mkdir("docs/adr") + pytester.makefile( + ".hy", + **{ + f"docs/adr/defadr_{adr_id.lower().replace('-', '_')}": f"""\ + (require doeff-adr.macros [defadr]) + + (defadr ADR-{adr_id} + :title "wiring fixture" + :status "proposed") + """, + }, + ) + + +def _make_smoke_test(pytester: pytest.Pytester) -> None: + pytester.mkdir("tests") + pytester.makefile(".py", **{"tests/test_smoke": "def test_smoke():\n assert True\n"}) + + +def _combined_output(result: pytest.RunResult) -> str: + return f"{result.stdout.str()}\n{result.stderr.str()}" + + +def test_strict_wiring_fails_when_defadr_is_outside_collection_scope( + pytester: pytest.Pytester, +) -> None: + pytester.makepyprojecttoml( + """\ + [tool.pytest.ini_options] + testpaths = ["tests"] + """ + ) + _make_smoke_test(pytester) + _make_executable_adr(pytester, "WIRING-RED") + + result: pytest.RunResult = pytester.runpytest("-q", "--doeff-adr-wiring=strict") + + assert result.ret != pytest.ExitCode.OK + output: str = _combined_output(result) + assert "doeff-adr wiring verification failed" in output + assert "docs/adr/defadr_wiring_red.hy" in output + + +def test_strict_wiring_passes_when_all_defadrs_are_collected( + pytester: pytest.Pytester, +) -> None: + pytester.makepyprojecttoml( + """\ + [tool.pytest.ini_options] + testpaths = ["tests", "docs/adr"] + """ + ) + _make_smoke_test(pytester) + _make_executable_adr(pytester, "WIRING-GREEN") + + result: pytest.RunResult = pytester.runpytest("-q", "--doeff-adr-wiring=strict") + + result.assert_outcomes(passed=2) + + +def test_default_wiring_mode_warns_without_failing( + pytester: pytest.Pytester, +) -> None: + pytester.makepyprojecttoml( + """\ + [tool.pytest.ini_options] + testpaths = ["tests"] + """ + ) + _make_smoke_test(pytester) + _make_executable_adr(pytester, "WIRING-WARN") + + result: pytest.RunResult = pytester.runpytest("-q") + + result.assert_outcomes(passed=1, warnings=1) + output: str = _combined_output(result) + assert "doeff-adr wiring verification warning" in output + assert "docs/adr/defadr_wiring_warn.hy" in output + + +def test_verify_wiring_cli_runs_strict_collection( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import doeff_adr.cli + + commands: list[Sequence[str]] = [] + + def run_command(command: Sequence[str], *, check: bool) -> subprocess.CompletedProcess[str]: + assert not check + commands.append(command) + return subprocess.CompletedProcess(command, returncode=0) + + monkeypatch.setattr(doeff_adr.cli.subprocess, "run", run_command) + + exit_code: int = doeff_adr.cli.main(["verify-wiring", "docs/adr"]) + + assert exit_code == 0 + assert commands == [ + [ + sys.executable, + "-m", + "pytest", + "--collect-only", + "-q", + "--doeff-adr-wiring=strict", + "docs/adr", + ] + ] From 80ab0e5b94a3b8f11b959ebcc02dc90ea2a17c6e Mon Sep 17 00:00:00 2001 From: proboscis Date: Tue, 14 Jul 2026 17:31:48 +0900 Subject: [PATCH 2/3] Add fail-closed ADR wiring verification --- .github/workflows/python-compatibility.yml | 6 + .../defadr_doeff_adr_001_wiring_selfcheck.hy | 47 ++++++++ packages/doeff-adr/README.md | 33 ++++++ packages/doeff-adr/pyproject.toml | 4 + packages/doeff-adr/src/doeff_adr/cli.py | 48 ++++++++ .../doeff-adr/src/doeff_adr/pytest_plugin.py | 103 +++++++++++++++++- .../doeff-adr/tests/test_defadr_macros.py | 23 ++++ packages/doeff-adr/tests/test_wiring.py | 14 ++- pyproject.toml | 1 + 9 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 docs/adr/defadr_doeff_adr_001_wiring_selfcheck.hy create mode 100644 packages/doeff-adr/src/doeff_adr/cli.py diff --git a/.github/workflows/python-compatibility.yml b/.github/workflows/python-compatibility.yml index 1f049310..d683f449 100644 --- a/.github/workflows/python-compatibility.yml +++ b/.github/workflows/python-compatibility.yml @@ -35,9 +35,15 @@ jobs: - name: Install dependencies run: uv sync --group dev --package doeff --python ${{ matrix.python-version }} + - name: Install Semgrep for executable ADR enforcement + run: uv tool install --python 3.12 semgrep + - name: Verify doeff-vm import run: uv run --package doeff --python ${{ matrix.python-version }} python -c "import doeff_vm; import doeff_vm.doeff_vm" + - name: Verify executable ADR collection wiring + run: uv run --package doeff --python ${{ matrix.python-version }} doeff-adr verify-wiring + - name: Run test suite run: uv run --package doeff --python ${{ matrix.python-version }} pytest -q diff --git a/docs/adr/defadr_doeff_adr_001_wiring_selfcheck.hy b/docs/adr/defadr_doeff_adr_001_wiring_selfcheck.hy new file mode 100644 index 00000000..76279fc6 --- /dev/null +++ b/docs/adr/defadr_doeff_adr_001_wiring_selfcheck.hy @@ -0,0 +1,47 @@ +;;; Executable ADR: defadr の存在と pytest collection の配線を自己検査する。 + +(require doeff-adr.macros [defadr defsemgrep rule law]) +(import doeff-adr.macros [fact interpretation counterexample]) + + +(defadr ADR-DOE-ADR-001 + :title "実行可能 ADR の存在と実際の pytest collection を全量照合する" + :status "accepted" + :scope ["doeff-adr" "pytest collection" "CI wiring"] + :problem + [(fact + "doeff・proboscis-ema・agent-control-plane の3リポジトリで defadr_*.hy が存在しても testpaths または CI 引数の外に置かれ、検査が無音で不活性化した。" + :evidence "docs/crystallization/erosion-audit-2026-07-02.md") + (fact + "pytest_collect_file は pytest が走査したファイルだけを受け取るため、collection root 外の defadr をプラグイン単独では従来発見できなかった。" + :evidence "packages/doeff-adr/src/doeff_adr/pytest_plugin.py")] + :context + [(interpretation + "設定ファイルを静的に推測するより、リポジトリ全体の候補と session.items の実測値を比較すれば testpaths・明示引数・ignore hook を同じ規則で扱える。") + (interpretation + "既存利用者への導入時の誤爆を避けるため通常 pytest は警告とし、CI 用コマンドだけを strict にする。")] + :decision + [(rule R1 "pytest collection 完了時に、リポジトリ内の実行可能 ADR 候補と実際に収集された item のファイル集合を照合する。") + (rule R2 "既定モードは warn とし、strict では未収集 ADR を列挙して非ゼロ終了する。明示的な off は局所実行用に残す。") + (rule R3 "doeff-adr verify-wiring は strict の collect-only pytest を起動し、CI で一行の配線ゲートとして使えるようにする。") + (rule R4 "defsemgrep は semgrep executable 不在を skip にせず fail-closed のまま維持する。wiring strict とは別の実行時依存検査として扱う。")] + :laws + [(law every-executable-adr-is-collected + :statement "exists(defadr_file) => collected_by_configured_pytest_scope(defadr_file)" + :counterexamples + [(counterexample "docs/adr/defadr_*.hy が存在するが testpaths は tests のみ") + (counterexample "CI が tests だけを明示して docs/adr を走査しない")]) + (law strict-wiring-fails-closed + :statement "uncollected_defadr and wiring_mode(strict) => nonzero_exit_with_paths" + :counterexamples + [(counterexample "未収集 ADR があっても collected test の成功だけで CI が緑になる")])] + :enforcement + [(defsemgrep no-silent-off-wiring-default + :languages ["generic"] + :pattern "default=\"off\"" + :message "doeff-adr wiring must not silently default to off." + :bad ["parser.addini(\"doeff_adr_wiring\", default=\"off\")"] + :good ["parser.addini(\"doeff_adr_wiring\", default=\"warn\")"])] + :plans ["packages/doeff-adr/tests/test_wiring.py" + "packages/doeff-adr/README.md" + ".github/workflows/python-compatibility.yml"]) diff --git a/packages/doeff-adr/README.md b/packages/doeff-adr/README.md index e2c27066..6de698cb 100644 --- a/packages/doeff-adr/README.md +++ b/packages/doeff-adr/README.md @@ -10,6 +10,8 @@ The package provides Hy macros for: - `deftest`: re-exported from `doeff-hy` for ADR-local executable examples. - pytest plugin: collect `defadr_*.hy` / `test_defadr_*.hy` executable ADR files and run generated `test_*` functions. +- wiring self-check: compare every executable ADR in the repository with the + files that the current pytest invocation actually collected. Accepted ADRs must carry at least one executable enforcement. @@ -19,6 +21,37 @@ Run executable ADR files directly: uv run pytest docs/adr/defadr_0018_hypha_app_in_doeff_hy.hy ``` +## Collection wiring gate + +Include every executable ADR directory in the pytest collection roots used by +CI. For the conventional layout: + +```toml +[tool.pytest.ini_options] +testpaths = ["tests", "docs/adr"] +``` + +Then add the one-line fail-closed gate before the test run: + +```bash +uv run doeff-adr verify-wiring +``` + +The command runs pytest collection in `strict` mode and exits nonzero while any +matching executable ADR is absent from the effective collection. Normal pytest +runs default to `warn` so targeted local runs remain usable. Set +`doeff_adr_wiring = "strict"` in pytest configuration when every invocation +should fail closed, or pass `--doeff-adr-wiring=off` only for an intentional +local opt-out. `defsemgrep` enforcement separately fails, rather than skips, +when the `semgrep` executable is unavailable. + +For proboscis-ema and agent-control-plane follow-ups, add their existing +`docs/adr` directory to the pytest roots used by the real CI test command, +install `semgrep` when those ADRs contain `defsemgrep`, and run +`uv run doeff-adr verify-wiring` as a required step. The gate reports every +remaining `defadr_*.hy` path, so migration can be completed without maintaining +a second hand-written ADR file list. + If an ADR uses `deftest`, the consuming project must provide the `doeff_interpreter` pytest fixture in its own `conftest.py` or test shim. `doeff-adr` intentionally does not choose an interpreter, scheduler, handler diff --git a/packages/doeff-adr/pyproject.toml b/packages/doeff-adr/pyproject.toml index 33f56d98..60356bc3 100644 --- a/packages/doeff-adr/pyproject.toml +++ b/packages/doeff-adr/pyproject.toml @@ -11,9 +11,13 @@ requires-python = ">=3.10" dependencies = [ "doeff-hy", "hy>=1.2.0", + "pytest>=8.1.1", "pyyaml>=6.0", ] +[project.scripts] +doeff-adr = "doeff_adr.cli:main" + [tool.hatch.build.targets.wheel] packages = ["src/doeff_adr"] diff --git a/packages/doeff-adr/src/doeff_adr/cli.py b/packages/doeff-adr/src/doeff_adr/cli.py new file mode 100644 index 00000000..e0f899e7 --- /dev/null +++ b/packages/doeff-adr/src/doeff_adr/cli.py @@ -0,0 +1,48 @@ +"""Command-line entry point for doeff-adr repository checks.""" + +import argparse +import subprocess +import sys +from collections.abc import Sequence + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="doeff-adr") + subparsers = parser.add_subparsers(dest="command", required=True) + verify_wiring = subparsers.add_parser( + "verify-wiring", + help="Fail when an executable ADR is outside the effective pytest collection scope.", + ) + verify_wiring.add_argument( + "pytest_args", + nargs=argparse.REMAINDER, + help="Optional pytest paths or collection arguments.", + ) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + arguments = _parser().parse_args(argv) + if arguments.command == "verify-wiring": + command: list[str] = [ + sys.executable, + "-m", + "pytest", + "--collect-only", + "-q", + *arguments.pytest_args, + "--doeff-adr-wiring=strict", + ] + completed: subprocess.CompletedProcess[str] = subprocess.run( + command, + check=False, + capture_output=True, + text=True, + ) + if completed.returncode == 0: + print("doeff-adr wiring verified: every executable ADR was collected.") + return 0 + sys.stdout.write(completed.stdout) + sys.stderr.write(completed.stderr) + return completed.returncode + raise AssertionError(f"unhandled doeff-adr command: {arguments.command}") diff --git a/packages/doeff-adr/src/doeff_adr/pytest_plugin.py b/packages/doeff-adr/src/doeff_adr/pytest_plugin.py index e6dca58b..645d9671 100644 --- a/packages/doeff-adr/src/doeff_adr/pytest_plugin.py +++ b/packages/doeff-adr/src/doeff_adr/pytest_plugin.py @@ -1,12 +1,13 @@ """Pytest plugin for executable ADR Hy files.""" - import fnmatch import importlib import importlib.util +import os import sys +import warnings from pathlib import Path -from typing import Any +from typing import Any, Literal import doeff_hy # noqa: F401 - registers Hy import hooks import pytest @@ -18,6 +19,25 @@ "docs/adr/defadr_*.hy", "docs/adrs/defadr_*.hy", ) +IGNORED_DISCOVERY_DIRECTORIES = frozenset( + { + ".git", + ".hg", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pycache__", + "build", + "dist", + "node_modules", + "venv", + } +) +WiringMode = Literal["off", "warn", "strict"] +WIRING_MODES = frozenset({"off", "warn", "strict"}) def pytest_addoption(parser: pytest.Parser) -> None: @@ -27,6 +47,17 @@ def pytest_addoption(parser: pytest.Parser) -> None: type="linelist", default=[], ) + parser.addini( + "doeff_adr_wiring", + "How to report executable ADR files that pytest did not collect: off, warn, or strict.", + default="warn", + ) + parser.addoption( + "--doeff-adr-wiring", + choices=sorted(WIRING_MODES), + default=None, + help="Override doeff-adr wiring verification mode (off, warn, or strict).", + ) def pytest_collect_file(file_path: Any, parent: pytest.Collector) -> pytest.Collector | None: @@ -38,6 +69,23 @@ def pytest_collect_file(file_path: Any, parent: pytest.Collector) -> pytest.Coll return DoeffAdrHyFile.from_parent(parent, path=path) +def pytest_collection_finish(session: pytest.Session) -> None: + mode = _wiring_mode(session.config) + if mode == "off": + return + root = Path(session.config.rootpath) + patterns = _file_patterns(session.config) + executable_adrs = _discover_executable_adrs(root, patterns) + collected_files = {Path(item.path).resolve() for item in session.items} + uncollected_adrs = sorted(executable_adrs - collected_files) + if not uncollected_adrs: + return + message = _wiring_message(root, uncollected_adrs, mode) + if mode == "strict": + raise pytest.UsageError(message) + warnings.warn(pytest.PytestWarning(message), stacklevel=1) + + class DoeffAdrHyFile(pytest.File): def collect(self) -> Any: module = _import_hy_file(self.path, self.config.rootpath) @@ -58,13 +106,56 @@ def _coerce_path(path: Any) -> Path: def _should_collect_hy_file(path: Path, config: pytest.Config) -> bool: root = Path(config.rootpath) - patterns = [*DEFAULT_FILE_PATTERNS, *config.getini("doeff_adr_hy_files")] + patterns = _file_patterns(config) + return _matches_file_patterns(path, root, patterns) + + +def _file_patterns(config: pytest.Config) -> tuple[str, ...]: + return (*DEFAULT_FILE_PATTERNS, *config.getini("doeff_adr_hy_files")) + + +def _matches_file_patterns(path: Path, root: Path, patterns: tuple[str, ...]) -> bool: rel = _relative_posix(path, root) candidates = {path.name, rel, path.as_posix()} return any( - fnmatch.fnmatch(candidate, pattern) - for pattern in patterns - for candidate in candidates + fnmatch.fnmatch(candidate, pattern) for pattern in patterns for candidate in candidates + ) + + +def _wiring_mode(config: pytest.Config) -> WiringMode: + command_line_mode = config.getoption("doeff_adr_wiring") + configured_mode = command_line_mode or config.getini("doeff_adr_wiring") + if configured_mode == "off": + return "off" + if configured_mode == "warn": + return "warn" + if configured_mode == "strict": + return "strict" + choices = ", ".join(sorted(WIRING_MODES)) + raise pytest.UsageError(f"doeff_adr_wiring must be one of {choices}; got {configured_mode!r}") + + +def _discover_executable_adrs(root: Path, patterns: tuple[str, ...]) -> set[Path]: + executable_adrs: set[Path] = set() + for directory, directory_names, file_names in os.walk(root): + directory_names[:] = sorted( + name for name in directory_names if name not in IGNORED_DISCOVERY_DIRECTORIES + ) + for file_name in sorted(file_names): + path = Path(directory, file_name) + if path.suffix == ".hy" and _matches_file_patterns(path, root, patterns): + executable_adrs.add(path.resolve()) + return executable_adrs + + +def _wiring_message(root: Path, paths: list[Path], mode: WiringMode) -> str: + outcome = "failed" if mode == "strict" else "warning" + rendered_paths = "\n".join(f" - {_relative_posix(path, root)}" for path in paths) + return ( + f"doeff-adr wiring verification {outcome}: executable ADR files exist but were not " + f"collected:\n{rendered_paths}\n" + "Add their directories to pytest testpaths or the CI pytest arguments. " + "Use doeff_adr_wiring=off only for an intentional opt-out." ) diff --git a/packages/doeff-adr/tests/test_defadr_macros.py b/packages/doeff-adr/tests/test_defadr_macros.py index c05a2f79..f025aa4b 100644 --- a/packages/doeff-adr/tests/test_defadr_macros.py +++ b/packages/doeff-adr/tests/test_defadr_macros.py @@ -2,6 +2,7 @@ import sys import textwrap +import doeff_adr.registry import doeff_hy # noqa: F401 - registers Hy import hooks import pytest from doeff_adr.registry import SemgrepSpec, clear_registry, get_adr, get_enforcement @@ -194,6 +195,28 @@ def test_expected_red_defsemgrep_still_validates_rule_fixtures(tmp_hy_dir): mod.test_expected_red_rule_defsemgrep() +def test_defsemgrep_fails_when_semgrep_executable_is_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def missing_executable(_command: str) -> None: + return None + + clear_registry() + doeff_adr.registry.register_semgrep_enforcement( + "missing_semgrep", + pattern="forbidden-token", + bad=["forbidden-token"], + good=["allowed-token"], + ) + monkeypatch.setattr(doeff_adr.registry.shutil, "which", missing_executable) + + try: + with pytest.raises(AssertionError, match="semgrep executable is required"): + doeff_adr.registry.assert_semgrep_enforcement("missing_semgrep") + finally: + clear_registry() + + def test_pytest_plugin_collects_defadr_hy_files(pytester): pytester.makeconftest( """\ diff --git a/packages/doeff-adr/tests/test_wiring.py b/packages/doeff-adr/tests/test_wiring.py index c227629b..c77b5a92 100644 --- a/packages/doeff-adr/tests/test_wiring.py +++ b/packages/doeff-adr/tests/test_wiring.py @@ -99,10 +99,18 @@ def test_verify_wiring_cli_runs_strict_collection( commands: list[Sequence[str]] = [] - def run_command(command: Sequence[str], *, check: bool) -> subprocess.CompletedProcess[str]: + def run_command( + command: Sequence[str], + *, + check: bool, + capture_output: bool, + text: bool, + ) -> subprocess.CompletedProcess[str]: assert not check + assert capture_output + assert text commands.append(command) - return subprocess.CompletedProcess(command, returncode=0) + return subprocess.CompletedProcess(command, returncode=0, stdout="", stderr="") monkeypatch.setattr(doeff_adr.cli.subprocess, "run", run_command) @@ -116,7 +124,7 @@ def run_command(command: Sequence[str], *, check: bool) -> subprocess.CompletedP "pytest", "--collect-only", "-q", - "--doeff-adr-wiring=strict", "docs/adr", + "--doeff-adr-wiring=strict", ] ] diff --git a/pyproject.toml b/pyproject.toml index 8bc733bc..5d3f9b23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" testpaths = [ "tests", + "docs/adr", "packages/doeff-adr/tests", "packages/doeff-time/tests", "packages/doeff-vm/tests", From 9c6286e820ff5d7e615500d205293a581da31365 Mon Sep 17 00:00:00 2001 From: proboscis Date: Tue, 14 Jul 2026 17:46:45 +0900 Subject: [PATCH 3/3] Wire ADR self-check into canonical pytest gate --- .github/workflows/python-compatibility.yml | 6 ---- .../defadr_doeff_adr_001_wiring_selfcheck.hy | 5 +-- ...dr_doeff_domain_001_vocabulary_cohesion.hy | 10 +++--- docs/adr/enforcement-ledger.json | 6 ++-- tests/test_adr_wiring_gate.py | 31 +++++++++++++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 tests/test_adr_wiring_gate.py diff --git a/.github/workflows/python-compatibility.yml b/.github/workflows/python-compatibility.yml index d683f449..1f049310 100644 --- a/.github/workflows/python-compatibility.yml +++ b/.github/workflows/python-compatibility.yml @@ -35,15 +35,9 @@ jobs: - name: Install dependencies run: uv sync --group dev --package doeff --python ${{ matrix.python-version }} - - name: Install Semgrep for executable ADR enforcement - run: uv tool install --python 3.12 semgrep - - name: Verify doeff-vm import run: uv run --package doeff --python ${{ matrix.python-version }} python -c "import doeff_vm; import doeff_vm.doeff_vm" - - name: Verify executable ADR collection wiring - run: uv run --package doeff --python ${{ matrix.python-version }} doeff-adr verify-wiring - - name: Run test suite run: uv run --package doeff --python ${{ matrix.python-version }} pytest -q diff --git a/docs/adr/defadr_doeff_adr_001_wiring_selfcheck.hy b/docs/adr/defadr_doeff_adr_001_wiring_selfcheck.hy index 76279fc6..a46169be 100644 --- a/docs/adr/defadr_doeff_adr_001_wiring_selfcheck.hy +++ b/docs/adr/defadr_doeff_adr_001_wiring_selfcheck.hy @@ -23,7 +23,7 @@ :decision [(rule R1 "pytest collection 完了時に、リポジトリ内の実行可能 ADR 候補と実際に収集された item のファイル集合を照合する。") (rule R2 "既定モードは warn とし、strict では未収集 ADR を列挙して非ゼロ終了する。明示的な off は局所実行用に残す。") - (rule R3 "doeff-adr verify-wiring は strict の collect-only pytest を起動し、CI で一行の配線ゲートとして使えるようにする。") + (rule R3 "doeff-adr verify-wiring は strict の collect-only pytest を起動する。外部リポジトリは CI 一行ゲートとして使える。doeff 自身は pytest 正典(ADR-DOE-ENFORCE-001 R1)に従い tests/test_adr_wiring_gate.py で既定ゲートに常時組み込む。") (rule R4 "defsemgrep は semgrep executable 不在を skip にせず fail-closed のまま維持する。wiring strict とは別の実行時依存検査として扱う。")] :laws [(law every-executable-adr-is-collected @@ -44,4 +44,5 @@ :good ["parser.addini(\"doeff_adr_wiring\", default=\"warn\")"])] :plans ["packages/doeff-adr/tests/test_wiring.py" "packages/doeff-adr/README.md" - ".github/workflows/python-compatibility.yml"]) + "tests/test_adr_wiring_gate.py" + "docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md"]) diff --git a/docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy b/docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy index b8d5aed2..5fc1dd80 100644 --- a/docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy +++ b/docs/adr/defadr_doeff_domain_001_vocabulary_cohesion.hy @@ -42,11 +42,13 @@ (counterexample "新規エージェントが既存 domain 語彙を発見できず、同義の effect 型を別パッケージに追加する — 照会面(SEDA)不在時の既定挙動")])] :enforcement [(deftest test-adr-doe-domain-001-defdomain-exists - ;; RED(2026-07-14): defdomain マクロは未実装。R1 実装で green。 - ;; ソーステキストのピン(マクロは module 属性にならないため)。実装後は挙動ピンに昇格する。 + ;; designed-red を xfail(strict) 相当で表現: 未実装の間は xfail、 + ;; 緑化したら fail して解除を強制。 (import pathlib [Path]) + (import pytest) (setv root (get (. (Path __file__) parents) 2)) (setv macros-src (.read-text (/ root "packages" "doeff-hy" "src" "doeff_hy" "macros.hy"))) - (assert (in "defmacro defdomain" macros-src) - "defdomain マクロが doeff-hy に存在しない — ADR-DOE-DOMAIN-001 R1"))] + (when (in "defmacro defdomain" macros-src) + (pytest.fail "defdomain が実装された — ADR-DOE-DOMAIN-001 の designed-red を解除し、挙動ピンに昇格して記帳すること(E1)")) + (pytest.xfail "defdomain マクロは未実装(E1 待ち)— ADR-DOE-DOMAIN-001 R1"))] :plans ["docs/doeff-2026-07-14-agent-first-investment-architecture-plan.md"]) diff --git a/docs/adr/enforcement-ledger.json b/docs/adr/enforcement-ledger.json index 61d8441a..fca2af4a 100644 --- a/docs/adr/enforcement-ledger.json +++ b/docs/adr/enforcement-ledger.json @@ -1,8 +1,8 @@ { "_comment": "ADR-DOE-ENFORCE-001 R5 anti-drop ratchet の台帳。enforcement 資産の数が黙って減る(または黙って増える)ことを tests/test_enforcement_ledger.py が禁止する。数を変える変更は、この台帳の明示的な更新を同じ変更セットに含めること。", - "defadr_files": 15, + "defadr_files": 16, "semgrep_rules": 229, "adr_deftest_enforcements": 8, - "adr_defsemgrep_enforcements": 19, - "adr_laws": 49 + "adr_defsemgrep_enforcements": 20, + "adr_laws": 51 } diff --git a/tests/test_adr_wiring_gate.py b/tests/test_adr_wiring_gate.py new file mode 100644 index 00000000..2df7178b --- /dev/null +++ b/tests/test_adr_wiring_gate.py @@ -0,0 +1,31 @@ +"""ADR-DOE-ENFORCE-001 R1: ADR wiring is part of the canonical pytest gate.""" + +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def test_all_executable_adrs_are_collected_by_default_pytest_gate() -> None: + result: subprocess.CompletedProcess[str] = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "--collect-only", + "-q", + "--doeff-adr-wiring=strict", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, ( + "doeff-adr wiring gate failed — executable ADR files must be collected by the " + "canonical pytest gate (ADR-DOE-ENFORCE-001 R1).\n" + f"stdout:\n{result.stdout}\n" + f"stderr:\n{result.stderr}" + )