diff --git a/docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md b/docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md index a8cbbdafb..ae702eb37 100644 --- a/docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md +++ b/docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md @@ -83,26 +83,34 @@ checker walks the AST of every changed `.py` file under each rule's `scope_root` and emits `layering-rule-violation` for any `import` or `from … import` of the forbidden top-level package or its submodules. -### 2. 600-line cap on non-test source files +### 2. 500-line cap on non-test source files No non-test, non-generated `.py` file under `implementations/python/packages/` -shall exceed 600 lines, except for paths listed in +shall exceed 500 lines, except for paths listed in `tools/policy/oversized_allowlist.yaml`. +The 500-line value matches SonarCloud's S104 threshold, so a file either +passes both the repo policy and the Sonar quality gate or fails both; there +is no longer a 500–599 band that is repo-compliant but blocks the +`dev → main` integration PR. (The cap was originally 600; see Amendments.) + The allowlist is the inverse of the goal: each entry is technical debt to -be drained. Each child PR of #3 splits its file and removes the entry. -The tracker closes when the allowlist is empty. +be drained. A split PR splits its file and removes the entry. The +modularity tracker closes when the allowlist is empty. -The fixed reference set — the 14 paths over the cap when this ADR landed — -is a *code constant*, `_ADR015_INITIAL_OVERSIZED_FILES` in +The fixed reference set — the paths over the cap when the current cap was +adopted — is a *code constant*, `_ADR015_INITIAL_OVERSIZED_FILES` in `tools/policy/repo_policy.py`, **not** config in `adr_policy.yaml`. Keeping the locked set in PR-editable config would let one PR satisfy "the allowlist only shrinks" by editing the allowlist and the locked set together; pinning -it in the policy module means adding a 15th entry requires a diff to the +it in the policy module means adding an entry requires a diff to the checker code, which PR review scrutinises as policy rather than as data noise. The allowlist must be a subset of that constant — no new entry may be -added; a file over the cap that wasn't one of the initial set must be split, -not allow-listed. Enforcement: +added; a file over the cap that wasn't one of the reference set must be +split, not allow-listed. Lowering the cap re-baselines both the constant and +the allowlist to the files over the new cap at that time (a code-constant +diff, PR-scrutinised the same way); the shrink-only rule then continues from +the new baseline. Enforcement: - `_check_oversized` — emits `oversized-source-file` for an over-cap, non-allowlisted changed file. @@ -170,14 +178,14 @@ defensively. That would be a separate ADR. - The dependency between `aces_sdl` and `aces_processor` is a DAG, enforced by CI. - Each subsequent file split inherits the layering invariant for free. -- The cap creates a forcing function: a file that wants to grow past 600 +- The cap creates a forcing function: a file that wants to grow past 500 lines must justify the split or the allowlist entry. - The allowlist is self-documenting debt; its size measures remaining modularity work. ### Negative -- Fourteen subsequent PRs are needed to drain the allowlist. +- A subsequent PR is needed to drain each allowlist entry. - `docs/api/processor-semantics.rst` is rescoped to planner only; a new `docs/api/sdl-semantics.rst` documents the moved modules. Bookmarks to the processor-semantics page still resolve, to a smaller page. @@ -188,6 +196,12 @@ defensively. That would be a separate ADR. re-introduce a layering bug the current rule doesn't capture. Mitigation: each new package introduction should consider whether it needs its own `layering_rules` entry. -- The 600-line cap is conservative; some cohesive modules may hit it. The +- The 500-line cap is conservative; some cohesive modules may hit it. The allowlist gives an explicit, visible escape hatch. The cap can be tuned later if it proves too aggressive. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-07-12 | #561 | Lowered the non-test source-file cap from 600 to 500 lines to align with SonarCloud's S104 threshold, closing the 500–599 band where a file was repo-compliant but failed the Sonar quality gate at the `dev → main` integration PR (#545). Re-baselined `_ADR015_INITIAL_OVERSIZED_FILES` and `tools/policy/oversized_allowlist.yaml` to the files over the 500-line cap as of this amendment; the shrink-only rule continues from that new baseline. | diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 76cc01cfe..5a7be897a 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -66,7 +66,11 @@ adrs: pin: 940ce80b1f1d94e3efb4a9f56cb9147d09c9a9dc1453a4977c7a5e194238d708 - id: ADR-015 path: docs/decisions/adrs/adr-015-sdl-processor-layering-and-source-file-size-cap.md - pin: 1e6000c3405e84765a993d326a8d97822e7b28a6714463bcd4d1550951f20ec7 + pin: bc5038164e535c4d506962e27cee07e720f5c8a77cb17d018dedb3dba927936f + amendments: + - date: 2026-07-12 + ref: "#561" + summary: "Lowered the non-test source-file cap from 600 to 500 lines to align with SonarCloud S104; re-baselined _ADR015_INITIAL_OVERSIZED_FILES and the oversized allowlist to the files over 500 lines at amendment time." - id: ADR-016 path: docs/decisions/adrs/adr-016-semantic-layer-scope-and-coverage-model.md pin: ff9a71be7f6d427b88ff26317da36e14b6cc5943418cd941df88dca29033449b diff --git a/implementations/python/tests/test_repo_policy_tools.py b/implementations/python/tests/test_repo_policy_tools.py index 6b2d915fb..bed19ddd5 100644 --- a/implementations/python/tests/test_repo_policy_tools.py +++ b/implementations/python/tests/test_repo_policy_tools.py @@ -549,7 +549,7 @@ def test_module_boundaries_reject_runtime_importing_sdl_semantics(tmp_path: Path assert [f.rule_id for f in failures] == ["module-boundary-import"] -# ── ADR-015: 600-line source-file cap ─────────────────────────────────── +# ── ADR-015: 500-line source-file cap ─────────────────────────────────── # A path that is in _ADR015_INITIAL_OVERSIZED_FILES (the code constant in # tools/policy/repo_policy.py), so the allowlist-subset (drain) check passes @@ -629,7 +629,7 @@ def test_allowlist_entry_not_in_locked_set_is_rejected(tmp_path: Path) -> None: def test_allowlist_subset_of_locked_set_passes(tmp_path: Path) -> None: repo_root = setup_policy_repo(tmp_path) - # A strict subset of the 14 initial oversized entries — the drained state. + # A strict subset of the initial oversized entries — the drained state. write_text(repo_root / "tools" / "policy" / "oversized_allowlist.yaml", f"files:\n - {_LOCKED_PATH}\n") write_text(repo_root / _LOCKED_PATH, "x = 1\n" * 700) @@ -752,7 +752,7 @@ def test_stale_allowlist_entry_below_cap_is_rejected(tmp_path: Path) -> None: even though the file's deletion/shrink isn't in the changed set.""" repo_root = setup_policy_repo(tmp_path) write_text(repo_root / "tools" / "policy" / "oversized_allowlist.yaml", f"files:\n - {_LOCKED_PATH}\n") - write_text(repo_root / _LOCKED_PATH, "x = 1\n" * 100) # well under the 600-line cap + write_text(repo_root / _LOCKED_PATH, "x = 1\n" * 100) # well under the 500-line cap failures = evaluate_repo_policy( repo_root, diff --git a/tools/policy/adr_policy.yaml b/tools/policy/adr_policy.yaml index 7a7b48eef..a8a0719f2 100644 --- a/tools/policy/adr_policy.yaml +++ b/tools/policy/adr_policy.yaml @@ -236,16 +236,17 @@ module_boundaries: allowed_top_level_imports: - aces_sdl -# ADR-015: non-test source files under packages/ cap at 600 lines. -# The allowlist (oversized_allowlist.yaml) lists the files that were over -# the cap when ADR-015 landed; each split PR (child of issue #3) removes -# its entry. The allowlist must stay a subset of the *code* constant +# ADR-015: non-test source files under packages/ cap at 500 lines (aligned +# with SonarCloud S104; lowered from 600 per the 2026-07-12 amendment / #561). +# The allowlist (oversized_allowlist.yaml) lists the files that were over the +# cap when the current cap was adopted; each split PR removes its entry. The +# allowlist must stay a subset of the *code* constant # _ADR015_INITIAL_OVERSIZED_FILES in tools/policy/repo_policy.py (the fixed # reference set) — entries can be drained but not added. The locked set is # intentionally NOT mirrored here: keeping it as config the same PR can edit # would make the "shrink only" rule self-satisfiable. oversized_source_files: - line_cap: 600 + line_cap: 500 allowlist_path: tools/policy/oversized_allowlist.yaml scope_roots: - implementations/python/packages diff --git a/tools/policy/oversized_allowlist.yaml b/tools/policy/oversized_allowlist.yaml index 723237d57..531abb5b7 100644 --- a/tools/policy/oversized_allowlist.yaml +++ b/tools/policy/oversized_allowlist.yaml @@ -1,15 +1,34 @@ -# Files over the 600-line cap (tools/policy/adr_policy.yaml :: oversized_source_files). +# Files over the 500-line cap (tools/policy/adr_policy.yaml :: oversized_source_files). # Each entry is a path relative to the repository root and must be one of the # files in _ADR015_INITIAL_OVERSIZED_FILES (tools/policy/repo_policy.py) — the -# fixed reference set. The allowlist may only shrink: each split PR (a child of -# issue #3) removes its file once it's been split below the cap. The modularity -# tracker (#3) closes when this list is empty. Do NOT add new entries — split -# the file instead. (See ADR-015.) +# fixed reference set. The allowlist may only shrink: each split PR removes its +# file once it's been split below the cap. The modularity tracker closes when +# this list is empty. Do NOT add new entries — split the file instead. This set +# was re-baselined to the 500-line cap on 2026-07-12 (#561); see ADR-015 §2 and +# its Amendments section. files: - - implementations/python/packages/aces_processor/models.py - - implementations/python/packages/aces_processor/compiler.py + - implementations/python/packages/aces_backend_libvirt/drivers/libvirt.py + - implementations/python/packages/aces_backend_libvirt/realization.py + - implementations/python/packages/aces_backend_libvirt/techvault_native.py + - implementations/python/packages/aces_backend_stubs/stubs.py + - implementations/python/packages/aces_conformance/conformance.py - implementations/python/packages/aces_contracts/contracts.py + - implementations/python/packages/aces_contracts/workflow.py + - implementations/python/packages/aces_mcp/tools/authoring.py + - implementations/python/packages/aces_mcp/tools/inspection.py + - implementations/python/packages/aces_mcp/tools/reference.py + - implementations/python/packages/aces_operations/_evidence_run_artifact.py + - implementations/python/packages/aces_processor/compiler.py + - implementations/python/packages/aces_processor/models.py - implementations/python/packages/aces_processor/planner.py - - implementations/python/packages/aces_conformance/conformance.py + - implementations/python/packages/aces_runtime/control_plane_api.py + - implementations/python/packages/aces_runtime/control_plane.py + - implementations/python/packages/aces_runtime/workflow_result_contract_checks.py + - implementations/python/packages/aces_sdl/composition.py - implementations/python/packages/aces_sdl/module_registry.py - - implementations/python/packages/aces_mcp/tools/authoring.py + - implementations/python/packages/aces_sdl/orchestration.py + - implementations/python/packages/aces_sdl/participant_behavior.py + - implementations/python/packages/aces_sdl/runtime_mail_service.py + - implementations/python/packages/aces_sdl/runtime_security_monitoring.py + - implementations/python/packages/aces_sdl/semantics/objective_semantics.py + - implementations/python/packages/aces_sdl/semantics/participant_behavior.py diff --git a/tools/policy/repo_policy.py b/tools/policy/repo_policy.py index e143b7acf..0b8c75b8e 100644 --- a/tools/policy/repo_policy.py +++ b/tools/policy/repo_policy.py @@ -114,41 +114,55 @@ def structural_runner(input_document: dict) -> list[PolicyFailure]: # ────────────────────────────────────────────────────────────────────────── -# ADR-015: SDL→processor layering rule + 600-line source-file cap. +# ADR-015: SDL→processor layering rule + 500-line source-file cap. # # These two gates catch unintentional regressions in normal contributions: # a developer who accidentally writes `import aces_processor` in `aces_sdl/`, -# or one who pushes a >600-line file, or a split PR that forgets to drain +# or one who pushes a >500-line file, or a split PR that forgets to drain # its allowlist entry. The policy and its YAML config are PR-mutable; PR # review (not this code) defends against deliberate weakening. See ADR-015. # ────────────────────────────────────────────────────────────────────────── _POLICY_CONFIG_PATH = "tools/policy/adr_policy.yaml" -# The set of source files that were over the 600-line cap when ADR-015 -# landed. The size-cap allowlist (tools/policy/oversized_allowlist.yaml) may -# only ever be a SUBSET of this set: entries are removed as child PRs of #3 -# split their file, and no new entry may be added. This is a code constant — -# not config in adr_policy.yaml — so that "the allowlist only shrinks" is -# enforced against a fixed reference rather than against an input the same PR -# can edit. Adding a 15th oversized file therefore requires a diff to this -# module, which PR review scrutinises as policy, not as data noise. +# The set of source files that were over the source-file cap when the current +# cap was adopted. The cap was originally 600 lines (ADR-015 landing); it was +# lowered to 500 on 2026-07-12 (#561, ADR-015 Amendments), and this reference +# set was re-baselined to the files over 500 lines at that time. The size-cap +# allowlist (tools/policy/oversized_allowlist.yaml) may only ever be a SUBSET +# of this set: entries are removed as split PRs shrink their file, and no new +# entry may be added. This is a code constant — not config in adr_policy.yaml — +# so that "the allowlist only shrinks" is enforced against a fixed reference +# rather than against an input the same PR can edit. Adding an oversized file +# therefore requires a diff to this module, which PR review scrutinises as +# policy, not as data noise. _ADR015_INITIAL_OVERSIZED_FILES: frozenset[str] = frozenset( { - "implementations/python/packages/aces_processor/models.py", - "implementations/python/packages/aces_processor/manager.py", - "implementations/python/packages/aces_processor/compiler.py", - "implementations/python/packages/aces_sdl/validator.py", - "implementations/python/packages/aces_contracts/contracts.py", - "implementations/python/packages/aces_processor/planner.py", - "implementations/python/packages/aces_processor/control_plane.py", - "implementations/python/packages/aces_conformance/conformance.py", + "implementations/python/packages/aces_backend_libvirt/drivers/libvirt.py", + "implementations/python/packages/aces_backend_libvirt/realization.py", + "implementations/python/packages/aces_backend_libvirt/techvault_native.py", "implementations/python/packages/aces_backend_stubs/stubs.py", - "implementations/python/packages/aces_sdl/module_registry.py", - "implementations/python/packages/aces_processor/control_plane_api.py", + "implementations/python/packages/aces_conformance/conformance.py", + "implementations/python/packages/aces_contracts/contracts.py", + "implementations/python/packages/aces_contracts/workflow.py", "implementations/python/packages/aces_mcp/tools/authoring.py", "implementations/python/packages/aces_mcp/tools/inspection.py", + "implementations/python/packages/aces_mcp/tools/reference.py", + "implementations/python/packages/aces_operations/_evidence_run_artifact.py", + "implementations/python/packages/aces_processor/compiler.py", + "implementations/python/packages/aces_processor/models.py", + "implementations/python/packages/aces_processor/planner.py", + "implementations/python/packages/aces_runtime/control_plane.py", + "implementations/python/packages/aces_runtime/control_plane_api.py", + "implementations/python/packages/aces_runtime/workflow_result_contract_checks.py", + "implementations/python/packages/aces_sdl/composition.py", + "implementations/python/packages/aces_sdl/module_registry.py", "implementations/python/packages/aces_sdl/orchestration.py", + "implementations/python/packages/aces_sdl/participant_behavior.py", + "implementations/python/packages/aces_sdl/runtime_mail_service.py", + "implementations/python/packages/aces_sdl/runtime_security_monitoring.py", + "implementations/python/packages/aces_sdl/semantics/objective_semantics.py", + "implementations/python/packages/aces_sdl/semantics/participant_behavior.py", } ) @@ -505,7 +519,7 @@ def _check_drain(allowlist: frozenset[str], allowlist_path: str) -> list[PolicyF "oversized-allowlist-locked", ( f"'{path}' is in {allowlist_path} but is not one of the files that were over the " - "600-line cap when ADR-015 landed (_ADR015_INITIAL_OVERSIZED_FILES in " + "source-file cap when it was adopted (_ADR015_INITIAL_OVERSIZED_FILES in " "tools/policy/repo_policy.py); the allowlist may only shrink — split the file instead of adding it" ), allowlist_path,