@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/adl +2 -2
- package/bin/health-check +2 -2
- package/bin/reconcile +41 -21
- package/bin/setup +2 -1
- package/package.json +1 -1
- package/packages/agent-hooks/README.md +77 -8
- package/packages/agent-hooks/assets/opencode-plugin.template.ts +2 -6
- package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -3
- package/packages/agent-hooks/src/hooks/crg/background.ts +7 -4
- package/packages/agent-hooks/src/hooks/log.ts +12 -12
- package/packages/agent-hooks/src/log/writer.ts +170 -24
- package/packages/agent-hooks/src/main.ts +1 -1
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
- package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
- package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
- package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
- package/packages/fleet/manifest.json +2 -1
- package/packages/fleet/src/repo-maintain.ts +5 -2
- package/packages/repo-controls/AGENTS.md +7 -4
- package/packages/repo-controls/README.md +346 -56
- package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
- package/packages/repo-controls/assets/promote.ts +7 -85
- package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
- package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
- package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
- package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
- package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
- package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
- package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
- package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
- package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
- package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
- package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
- package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
- package/packages/repo-controls/assets/version-bump.sh +7 -48
- package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/excluded/web/i18n/locales/zh-CN/strings.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/fresh/fresh.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub1/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub10/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub11/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub2/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub3/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub4/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub5/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub6/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub7/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub8/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub9/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/services/i18n/locales/de/common.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/vendored-crate/Cargo.toml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/c.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/fresh/fresh.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub1/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub10/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub11/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub2/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub3/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub4/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub5/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub6/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub7/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub8/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub9/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/route-target.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/route-target.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
- package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
- package/packages/repo-controls/scripts/hooks/pre-push +27 -19
- package/packages/repo-controls/src/cli.ts +22 -3
- package/packages/repo-controls/src/compliance-config.ts +132 -65
- package/packages/repo-controls/src/config.ts +55 -46
- package/packages/repo-controls/src/constants.ts +9 -0
- package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
- package/packages/repo-controls/src/doctor.ts +20 -30
- package/packages/repo-controls/src/findings-ratchet.ts +14 -0
- package/packages/repo-controls/src/gitignore-block.ts +3 -13
- package/packages/repo-controls/src/hook-log.ts +7 -10
- package/packages/repo-controls/src/hook.ts +127 -83
- package/packages/repo-controls/src/install-compliance.ts +112 -0
- package/packages/repo-controls/src/install.ts +133 -30
|
@@ -31,13 +31,12 @@ from typing import ClassVar
|
|
|
31
31
|
|
|
32
32
|
from ....core.check import Check
|
|
33
33
|
from ....core.context import Context, FileSet
|
|
34
|
+
from ....core.declarations import declared_root_files
|
|
34
35
|
from ....core.finding import Report
|
|
35
36
|
from ....policy.root_shape import (
|
|
36
37
|
GOVERNANCE,
|
|
37
38
|
GUIDES,
|
|
38
39
|
REGISTERS,
|
|
39
|
-
ROOT_FILES_DECLARATION,
|
|
40
|
-
declared_root_names,
|
|
41
40
|
)
|
|
42
41
|
|
|
43
42
|
|
|
@@ -49,12 +48,10 @@ class RootMarkdownAllowlist(Check):
|
|
|
49
48
|
"workflows_root": "<workflows>",
|
|
50
49
|
"plans_root": "<plans>",
|
|
51
50
|
"docs_root": "<docs>",
|
|
52
|
-
"rust_root": "<rust>",
|
|
53
|
-
"frontend_root": "<frontend>",
|
|
54
51
|
}
|
|
55
52
|
"""FIXED 2026-09-16 (K-42 sweep, LOUD form, approved): every
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
`.workflows/...`/`plans/...`/`docs/...` entry below was a hardcoded
|
|
54
|
+
literal, tested with `==`/
|
|
58
55
|
`startswith` inside `_allowed_markdown`. A repository whose corresponding
|
|
59
56
|
root is not the packaged default had a real, legitimately-allowed
|
|
60
57
|
Markdown file fail every one of those tests, and the gate reported it as
|
|
@@ -63,19 +60,8 @@ class RootMarkdownAllowlist(Check):
|
|
|
63
60
|
stays literal: named documents and code-asset trees, not resolvable
|
|
64
61
|
paths."""
|
|
65
62
|
|
|
66
|
-
# A tree that EXISTS needs a guide
|
|
67
|
-
|
|
68
|
-
# to create a guide for a directory it had deliberately never made — and
|
|
69
|
-
# `.plans/` was named with a dot this layout no longer uses, so the guide
|
|
70
|
-
# that does exist at `plans/AGENTS.md` went unseen while a phantom one was
|
|
71
|
-
# demanded. The rule is about trees being documented, not about these exact
|
|
72
|
-
# paths being present.
|
|
73
|
-
#
|
|
74
|
-
# `.workflows/` is deliberately not listed (owner decision, 2026-10-01): it is
|
|
75
|
-
# a machine-managed tree, written wholesale by `adl-repo-controls install` and
|
|
76
|
-
# never hand-authored, so it does not need a guide of its own. A guide that
|
|
77
|
-
# does exist there is still admitted by `_allowed_markdown`.
|
|
78
|
-
GUIDED_TREES: ClassVar[tuple[str, ...]] = ("plans", "specs", "docs")
|
|
63
|
+
# A tree that EXISTS needs a guide; `.workflows/` is deliberately not listed (machine-managed).
|
|
64
|
+
GUIDED_INPUTS: ClassVar[tuple[str, ...]] = ("plans_root", "docs_root")
|
|
79
65
|
|
|
80
66
|
# Trees whose Markdown this repository does not author: vendored provider
|
|
81
67
|
# documentation, and archives kept for reference. Renaming a README inside
|
|
@@ -83,7 +69,6 @@ class RootMarkdownAllowlist(Check):
|
|
|
83
69
|
UNOWNED_ROOTS: ClassVar[tuple[str, ...]] = (
|
|
84
70
|
".agents-archive/",
|
|
85
71
|
".claude-plugin/",
|
|
86
|
-
".donors/",
|
|
87
72
|
"node_modules/",
|
|
88
73
|
"scripts-archive/",
|
|
89
74
|
"workflows-archived/",
|
|
@@ -112,53 +97,8 @@ class RootMarkdownAllowlist(Check):
|
|
|
112
97
|
".opencode/",
|
|
113
98
|
)
|
|
114
99
|
|
|
115
|
-
# Ported from repo-hygiene/check-markdown-placement.py during the
|
|
116
|
-
# repo-hygiene -> .workflows dissolution (2026-08-04): explicit non-guide
|
|
117
|
-
# Markdown assets retained outside the published knowledge trees.
|
|
118
|
-
# Donor absorb triage: every commit of the 2026-08-12 donor fast-forward
|
|
119
|
-
# with an ABSORB / NOT USEFUL verdict. Tracked deliberately so the
|
|
120
|
-
# verdicts stay reviewable. Genuinely no resolvable tree prefix.
|
|
121
|
-
GOVERNANCE_FILES: ClassVar[frozenset[str]] = frozenset({".donors/MANIFEST.md"})
|
|
122
|
-
RUST_GOVERNANCE_SUFFIXES: ClassVar[frozenset[str]] = frozenset(
|
|
123
|
-
{"CHANGELOG.md", "crates/enact-session/src/backend/codex_init_prompt.md"}
|
|
124
|
-
)
|
|
125
|
-
FRONTEND_GOVERNANCE_SUFFIXES: ClassVar[frozenset[str]] = frozenset({"CHANGELOG.md"})
|
|
126
|
-
# repo-hygiene/CHANGELOG.md and repo-hygiene/LORE_COMMIT_IDS.md were
|
|
127
|
-
# deleted outright during the repo-hygiene -> .workflows dissolution — no
|
|
128
|
-
# successor entry.
|
|
129
|
-
WORKFLOWS_GOVERNANCE_SUFFIXES: ClassVar[frozenset[str]] = frozenset({"COVERAGE.md"})
|
|
130
|
-
|
|
131
|
-
# Genuinely no resolvable tree prefix -- "compliance" here is not
|
|
132
|
-
# `<compliance>` (that resolves to `compliance_py`); this is a distinct,
|
|
133
|
-
# literal historical path, unexamined by this fix.
|
|
134
|
-
RETAINED_MARKDOWN_FILES: ClassVar[frozenset[str]] = frozenset({"compliance/fixtures.md"})
|
|
135
|
-
# Three entries for now-parked trees stood here. Both trees are gone from
|
|
136
|
-
# the checkout and neither path is tracked, so the entries allowlisted
|
|
137
|
-
# nothing while keeping this file in violation of meta.parked-references.
|
|
138
|
-
WORKFLOWS_RETAINED_SUFFIXES: ClassVar[frozenset[str]] = frozenset(
|
|
139
|
-
{"config/exceptions/KNOWN-ISSUES.md"}
|
|
140
|
-
)
|
|
141
100
|
CODE_ASSET_ROOTS: ClassVar[tuple[str, ...]] = (
|
|
142
101
|
".claude/skills/",
|
|
143
|
-
"agent/docs/",
|
|
144
|
-
)
|
|
145
|
-
RUST_CODE_ASSET_SUFFIXES: ClassVar[tuple[str, ...]] = (
|
|
146
|
-
"crates/enact-app/assets/builtin-assistants/rules/",
|
|
147
|
-
"crates/enact-app/assets/builtin-skills/",
|
|
148
|
-
"crates/enact-extension/tests/fixtures/builtin-skills-fixture/",
|
|
149
|
-
)
|
|
150
|
-
FRONTEND_CODE_ASSET_SUFFIXES: ClassVar[tuple[str, ...]] = (
|
|
151
|
-
".claude/",
|
|
152
|
-
".gemini/",
|
|
153
|
-
"examples/",
|
|
154
|
-
)
|
|
155
|
-
FRONTEND_LOCAL_DOC_SUFFIXES: ClassVar[tuple[str, ...]] = (
|
|
156
|
-
"packages/",
|
|
157
|
-
"scripts/",
|
|
158
|
-
"tests/e2e/",
|
|
159
|
-
)
|
|
160
|
-
FRONTEND_LOCAL_DOC_NAMES: ClassVar[frozenset[str]] = frozenset(
|
|
161
|
-
{"README.en.md", "MIGRATION.md"}
|
|
162
102
|
)
|
|
163
103
|
|
|
164
104
|
def structural(self, ctx: Context) -> Iterable[Report]:
|
|
@@ -167,7 +107,11 @@ class RootMarkdownAllowlist(Check):
|
|
|
167
107
|
# file in the set" but "is the tree missing a required guide", which
|
|
168
108
|
# no file set can answer — the set never contains a path that was
|
|
169
109
|
# never created.
|
|
170
|
-
for
|
|
110
|
+
for name in self.GUIDED_INPUTS:
|
|
111
|
+
declared = ctx.inputs.get(name)
|
|
112
|
+
if declared is None:
|
|
113
|
+
continue
|
|
114
|
+
tree = ctx.relative(declared)
|
|
171
115
|
if not ctx.path(tree).is_dir():
|
|
172
116
|
continue
|
|
173
117
|
guide = f"{tree}/AGENTS.md"
|
|
@@ -187,6 +131,7 @@ class RootMarkdownAllowlist(Check):
|
|
|
187
131
|
# A path in the set that is not a file on disk is a DELETION, not a
|
|
188
132
|
# finding: a removed Markdown file cannot violate an allowlist. Same
|
|
189
133
|
# filter check.sh's `is_file()` applied to `ADL_CHANGED_FILES`.
|
|
134
|
+
declared_root_files(ctx.declarations)
|
|
190
135
|
for path in sorted(files.existing(ctx.root)):
|
|
191
136
|
# check.sh: `path.endswith("/README.md") or (path != "README.md"
|
|
192
137
|
# and path.endswith("README.md"))` — the first clause already
|
|
@@ -209,9 +154,9 @@ class RootMarkdownAllowlist(Check):
|
|
|
209
154
|
yield self.report(
|
|
210
155
|
path,
|
|
211
156
|
"tracked Markdown is outside the root allowlist",
|
|
212
|
-
f"move {path} under an allowed tree (docs/, plans/, .
|
|
213
|
-
"
|
|
214
|
-
"
|
|
157
|
+
f"move {path} under an allowed tree (docs/, plans/, etc.), or, "
|
|
158
|
+
"if it is a Markdown file this repository genuinely carries, "
|
|
159
|
+
"declare it under "
|
|
215
160
|
"[controls.compliance.root] files in this repository's "
|
|
216
161
|
"adl-config.toml",
|
|
217
162
|
)
|
|
@@ -219,9 +164,6 @@ class RootMarkdownAllowlist(Check):
|
|
|
219
164
|
def _allowed_markdown(self, ctx: Context, path: str) -> bool:
|
|
220
165
|
if path == "README.md":
|
|
221
166
|
return True
|
|
222
|
-
# ADR-095 admits this one root-relative verification contract by
|
|
223
|
-
# literal name.
|
|
224
|
-
#
|
|
225
167
|
# `policy/root_shape.py` is THE list of what may sit at EVERY repository's
|
|
226
168
|
# root — GUIDES (AGENTS.md/CLAUDE.md/INVARIANTS.md/…), GOVERNANCE
|
|
227
169
|
# (CONTRIBUTING.md/SECURITY.md/…) and REGISTERS (GAPS-AND-FINDINGS.md,
|
|
@@ -239,9 +181,7 @@ class RootMarkdownAllowlist(Check):
|
|
|
239
181
|
# (tests/core/test_root_shape.py passes `ctx=None`).
|
|
240
182
|
if path in (GUIDES | GOVERNANCE | REGISTERS):
|
|
241
183
|
return True
|
|
242
|
-
if
|
|
243
|
-
ctx.declarations, ROOT_FILES_DECLARATION
|
|
244
|
-
):
|
|
184
|
+
if path in declared_root_files(ctx.declarations):
|
|
245
185
|
return True
|
|
246
186
|
# Ported from check-markdown-placement.py: any AGENTS.md/CLAUDE.md
|
|
247
187
|
# guide file is allowed regardless of folder — the AGENTS.md/CLAUDE.md
|
|
@@ -259,19 +199,6 @@ class RootMarkdownAllowlist(Check):
|
|
|
259
199
|
plans_root = ctx.relative(ctx.inputs["plans_root"])
|
|
260
200
|
if path.startswith(f"{plans_root}/") and path.endswith(".md"):
|
|
261
201
|
return True
|
|
262
|
-
# Widening: a repository with no specs tree simply allows nothing on its
|
|
263
|
-
# account. Shrinking an allowlist can only add findings, never hide one.
|
|
264
|
-
specs_input = ctx.inputs.get("specs_root")
|
|
265
|
-
if specs_input is not None:
|
|
266
|
-
specs_root = ctx.relative(specs_input)
|
|
267
|
-
if path == f"{specs_root}/AGENTS.md" or (
|
|
268
|
-
path.startswith(f"{specs_root}/") and path.endswith(".md")
|
|
269
|
-
):
|
|
270
|
-
return True
|
|
271
|
-
if path.startswith(".changeset/") and path.endswith(".md"):
|
|
272
|
-
return True
|
|
273
|
-
if path == ".donors/MANIFEST.md":
|
|
274
|
-
return True
|
|
275
202
|
docs_root = ctx.relative(ctx.inputs["docs_root"])
|
|
276
203
|
if path.startswith(f"{docs_root}/") and path.endswith(".md"):
|
|
277
204
|
return True
|
|
@@ -279,53 +206,6 @@ class RootMarkdownAllowlist(Check):
|
|
|
279
206
|
# shell (I-41) — its Markdown is a separate concern from docs/.
|
|
280
207
|
if path.startswith(".docs/") and path.endswith(".md"):
|
|
281
208
|
return True
|
|
282
|
-
# Ported enact-integration-specific exceptions (see class attributes
|
|
283
|
-
# above).
|
|
284
|
-
if path in self.GOVERNANCE_FILES or path in self.RETAINED_MARKDOWN_FILES:
|
|
285
|
-
return True
|
|
286
|
-
# Decision for a tree declared absent (`rust = ""` / `frontend = ""`, which
|
|
287
|
-
# drops `rust_root` / `frontend_root` from the resolved inputs): both only
|
|
288
|
-
# ever ADD allowlist entries -- the governance suffixes and code-asset
|
|
289
|
-
# roots beneath the tree -- so a repository with no such tree has nothing
|
|
290
|
-
# there to admit and they contribute nothing. Strictly stricter, never a
|
|
291
|
-
# silent pass. These were read unguarded (`ctx.inputs["rust_root"]`): a
|
|
292
|
-
# stray root Markdown file, which every allowance above declines, reached
|
|
293
|
-
# them and raised `KeyError` -- a traceback with exit 1, not a verdict.
|
|
294
|
-
rust_input = ctx.inputs.get("rust_root")
|
|
295
|
-
rust_root = None if rust_input is None else ctx.relative(rust_input)
|
|
296
|
-
frontend_input = ctx.inputs.get("frontend_root")
|
|
297
|
-
frontend_root = None if frontend_input is None else ctx.relative(frontend_input)
|
|
298
|
-
if rust_root is not None and path in {
|
|
299
|
-
f"{rust_root}/{suffix}" for suffix in self.RUST_GOVERNANCE_SUFFIXES
|
|
300
|
-
}:
|
|
301
|
-
return True
|
|
302
|
-
if frontend_root is not None and path in {
|
|
303
|
-
f"{frontend_root}/{suffix}" for suffix in self.FRONTEND_GOVERNANCE_SUFFIXES
|
|
304
|
-
}:
|
|
305
|
-
return True
|
|
306
|
-
if path in {f"{workflows_root}/{suffix}" for suffix in self.WORKFLOWS_GOVERNANCE_SUFFIXES}:
|
|
307
|
-
return True
|
|
308
|
-
if path in {f"{workflows_root}/{suffix}" for suffix in self.WORKFLOWS_RETAINED_SUFFIXES}:
|
|
309
|
-
return True
|
|
310
209
|
if any(path.startswith(code_root) for code_root in self.CODE_ASSET_ROOTS):
|
|
311
210
|
return True
|
|
312
|
-
if rust_root is not None and any(
|
|
313
|
-
path.startswith(f"{rust_root}/{suffix}") for suffix in self.RUST_CODE_ASSET_SUFFIXES
|
|
314
|
-
):
|
|
315
|
-
return True
|
|
316
|
-
if frontend_root is not None and any(
|
|
317
|
-
path.startswith(f"{frontend_root}/{suffix}")
|
|
318
|
-
for suffix in self.FRONTEND_CODE_ASSET_SUFFIXES
|
|
319
|
-
):
|
|
320
|
-
return True
|
|
321
|
-
name = path.rsplit("/", 1)[-1]
|
|
322
|
-
if (
|
|
323
|
-
frontend_root is not None
|
|
324
|
-
and name in self.FRONTEND_LOCAL_DOC_NAMES
|
|
325
|
-
and any(
|
|
326
|
-
path.startswith(f"{frontend_root}/{suffix}")
|
|
327
|
-
for suffix in self.FRONTEND_LOCAL_DOC_SUFFIXES
|
|
328
|
-
)
|
|
329
|
-
):
|
|
330
|
-
return True
|
|
331
211
|
return False
|
|
@@ -7,7 +7,7 @@ Markdown file — the two violators live inside the same conforming tree, so a
|
|
|
7
7
|
corpus flips between clean and broken by changing only which paths are in the
|
|
8
8
|
given set, exactly as test.sh's own comment describes. `root-markdown-missing-
|
|
9
9
|
guide/` holds only the four files the structural rule reads, with
|
|
10
|
-
|
|
10
|
+
`docs/AGENTS.md` absent.
|
|
11
11
|
|
|
12
12
|
This check is not yet wired into `checks/__init__.py` (a later step wires this
|
|
13
13
|
port's whole wave into the registry in one shot, to avoid concurrent edits to
|
|
@@ -29,7 +29,7 @@ Registry-row note, not fixed here (this task does not touch
|
|
|
29
29
|
`'**/README.md'`, with no `'**/*.md'` companion of the kind LAY-006 carries for
|
|
30
30
|
the identical reason (see that row's own comment). Read through `runner.run`,
|
|
31
31
|
every nested non-README path in the clean corpus (`.plans/AGENTS.md`,
|
|
32
|
-
`docs/AGENTS.md`,
|
|
32
|
+
`docs/AGENTS.md`, `docs/architecture/note.md`, ...) would be narrowed away
|
|
33
33
|
before this check ever ran — which is exactly why every test below calls the
|
|
34
34
|
check directly with the full given set, rather than through the row's scope.
|
|
35
35
|
"""
|
|
@@ -38,9 +38,12 @@ from __future__ import annotations
|
|
|
38
38
|
|
|
39
39
|
import pathlib
|
|
40
40
|
|
|
41
|
+
import pytest
|
|
42
|
+
|
|
41
43
|
from adl_compliance.checks.layout.root_markdown_allowlist import RootMarkdownAllowlist
|
|
42
44
|
from adl_compliance.core import scope
|
|
43
45
|
from adl_compliance.core.context import Context, FileSet
|
|
46
|
+
from adl_compliance.core.errors import CouldNotRun
|
|
44
47
|
from adl_compliance.core.verdict import Outcome, decide
|
|
45
48
|
from adl_compliance.policy.registry import load
|
|
46
49
|
|
|
@@ -59,14 +62,11 @@ CLEAN_GIVEN = (
|
|
|
59
62
|
"CLAUDE.md",
|
|
60
63
|
"CHANGELOG.md",
|
|
61
64
|
"RELEASE.md",
|
|
62
|
-
"INDEPENDENT-VERIFICATION.md",
|
|
63
65
|
"README.md",
|
|
64
66
|
".workflows/AGENTS.md",
|
|
65
67
|
".plans/AGENTS.md",
|
|
66
|
-
".specs/AGENTS.md",
|
|
67
68
|
"docs/AGENTS.md",
|
|
68
69
|
"docs/architecture/note.md",
|
|
69
|
-
".changeset/some-change.md",
|
|
70
70
|
)
|
|
71
71
|
# Every file the clean corpus actually holds on disk, violators included —
|
|
72
72
|
# the shape a tree-scoped phase (or test.sh's `corpus_set` with no explicit
|
|
@@ -91,10 +91,7 @@ def graded(root: pathlib.Path, given: tuple[str, ...]):
|
|
|
91
91
|
inputs={
|
|
92
92
|
"workflows_root": root / ".workflows",
|
|
93
93
|
"plans_root": root / "plans",
|
|
94
|
-
"specs_root": root / ".specs",
|
|
95
94
|
"docs_root": root / "docs",
|
|
96
|
-
"rust_root": root / "backend",
|
|
97
|
-
"frontend_root": root / "frontend",
|
|
98
95
|
},
|
|
99
96
|
)
|
|
100
97
|
findings = tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=given)))
|
|
@@ -110,10 +107,7 @@ def structural(root: pathlib.Path):
|
|
|
110
107
|
inputs={
|
|
111
108
|
"workflows_root": root / ".workflows",
|
|
112
109
|
"plans_root": root / "plans",
|
|
113
|
-
"specs_root": root / ".specs",
|
|
114
110
|
"docs_root": root / "docs",
|
|
115
|
-
"rust_root": root / "backend",
|
|
116
|
-
"frontend_root": root / "frontend",
|
|
117
111
|
},
|
|
118
112
|
)
|
|
119
113
|
findings = tuple(RootMarkdownAllowlist().structural(ctx))
|
|
@@ -130,10 +124,7 @@ def full(root: pathlib.Path, given: tuple[str, ...]):
|
|
|
130
124
|
inputs={
|
|
131
125
|
"workflows_root": root / ".workflows",
|
|
132
126
|
"plans_root": root / "plans",
|
|
133
|
-
"specs_root": root / ".specs",
|
|
134
127
|
"docs_root": root / "docs",
|
|
135
|
-
"rust_root": root / "backend",
|
|
136
|
-
"frontend_root": root / "frontend",
|
|
137
128
|
},
|
|
138
129
|
)
|
|
139
130
|
findings = tuple(RootMarkdownAllowlist().run(ctx, FileSet(paths=given)))
|
|
@@ -239,7 +230,6 @@ def test_a_tree_that_does_not_exist_is_not_asked_for_a_guide() -> None:
|
|
|
239
230
|
did."""
|
|
240
231
|
reported = {f.path for f in structural(MISSING_GUIDE).findings}
|
|
241
232
|
assert "specs/AGENTS.md" not in reported
|
|
242
|
-
assert ".specs/AGENTS.md" not in reported
|
|
243
233
|
|
|
244
234
|
|
|
245
235
|
def test_a_workflows_tree_without_a_guide_is_not_a_finding(tmp_path: pathlib.Path) -> None:
|
|
@@ -292,9 +282,9 @@ def test_a_plugin_root_readme_is_a_published_description(tmp_path: pathlib.Path)
|
|
|
292
282
|
given = (
|
|
293
283
|
"AGENTS.md",
|
|
294
284
|
"README.md",
|
|
295
|
-
".claude-plugin/
|
|
296
|
-
".codex-plugin/
|
|
297
|
-
".cursor-plugin/
|
|
285
|
+
".claude-plugin/my-repo/README.md",
|
|
286
|
+
".codex-plugin/my-repo/README.md",
|
|
287
|
+
".cursor-plugin/my-repo/README.md",
|
|
298
288
|
)
|
|
299
289
|
verdict = graded(tmp_path, given)
|
|
300
290
|
assert not [
|
|
@@ -309,7 +299,7 @@ def test_a_redeclared_estate_still_allows_the_resolved_tree_exceptions(
|
|
|
309
299
|
"""The LOUD false-finding this check shipped with.
|
|
310
300
|
|
|
311
301
|
Every `backend/...`/`frontend/...`/`.workflows/...`/`plans/...`/
|
|
312
|
-
|
|
302
|
+
`docs/...` entry in `_allowed_markdown` was a hardcoded
|
|
313
303
|
literal. A repository whose roots are not the packaged defaults had a
|
|
314
304
|
real, legitimately-allowed Markdown file (a crate's CHANGELOG, a
|
|
315
305
|
workflows coverage note, a docs page) fail every one of those tests and
|
|
@@ -327,19 +317,13 @@ def test_a_redeclared_estate_still_allows_the_resolved_tree_exceptions(
|
|
|
327
317
|
"documentation/AGENTS.md",
|
|
328
318
|
"documentation/architecture/note.md",
|
|
329
319
|
"automation/AGENTS.md",
|
|
330
|
-
"automation/COVERAGE.md",
|
|
331
|
-
"rustcode/CHANGELOG.md",
|
|
332
|
-
"webapp/CHANGELOG.md",
|
|
333
320
|
)
|
|
334
321
|
ctx = Context(
|
|
335
322
|
root=tmp_path, row=ROW,
|
|
336
323
|
inputs={
|
|
337
324
|
"workflows_root": tmp_path / "automation",
|
|
338
325
|
"plans_root": tmp_path / "roadmap",
|
|
339
|
-
"specs_root": tmp_path / "specifications",
|
|
340
326
|
"docs_root": tmp_path / "documentation",
|
|
341
|
-
"rust_root": tmp_path / "rustcode",
|
|
342
|
-
"frontend_root": tmp_path / "webapp",
|
|
343
327
|
},
|
|
344
328
|
)
|
|
345
329
|
findings = tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=given)))
|
|
@@ -350,6 +334,19 @@ def test_a_redeclared_estate_still_allows_the_resolved_tree_exceptions(
|
|
|
350
334
|
)
|
|
351
335
|
|
|
352
336
|
|
|
337
|
+
def test_no_legacy_estate_file_is_admitted_by_name(tmp_path: pathlib.Path) -> None:
|
|
338
|
+
given = (
|
|
339
|
+
"compliance/fixtures.md",
|
|
340
|
+
".workflows/config/exceptions/KNOWN-ISSUES.md",
|
|
341
|
+
".workflows/COVERAGE.md",
|
|
342
|
+
)
|
|
343
|
+
for rel in given:
|
|
344
|
+
(tmp_path / rel).parent.mkdir(parents=True, exist_ok=True)
|
|
345
|
+
(tmp_path / rel).write_text("x\n", encoding="utf-8")
|
|
346
|
+
verdict = graded(tmp_path, given)
|
|
347
|
+
assert sorted(f.path for f in verdict.findings) == sorted(given), verdict.render()
|
|
348
|
+
|
|
349
|
+
|
|
353
350
|
# ---------------------------------------------- per-repository root declarations
|
|
354
351
|
def graded_declaring(root: pathlib.Path, declarations: dict[str, str], given: tuple[str, ...]):
|
|
355
352
|
"""`graded`, plus what the repository declared about its own root
|
|
@@ -361,10 +358,7 @@ def graded_declaring(root: pathlib.Path, declarations: dict[str, str], given: tu
|
|
|
361
358
|
inputs={
|
|
362
359
|
"workflows_root": root / ".workflows",
|
|
363
360
|
"plans_root": root / "plans",
|
|
364
|
-
"specs_root": root / ".specs",
|
|
365
361
|
"docs_root": root / "docs",
|
|
366
|
-
"rust_root": root / "backend",
|
|
367
|
-
"frontend_root": root / "frontend",
|
|
368
362
|
},
|
|
369
363
|
)
|
|
370
364
|
findings = tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=given)))
|
|
@@ -448,18 +442,12 @@ def test_the_central_names_are_allowed_without_reading_the_context() -> None:
|
|
|
448
442
|
assert RootMarkdownAllowlist()._allowed_markdown(None, "GAPS-AND-FINDINGS.md") # type: ignore[arg-type]
|
|
449
443
|
|
|
450
444
|
|
|
451
|
-
#
|
|
452
|
-
def
|
|
445
|
+
# ------------------------------------------- deleted rust and frontend trees
|
|
446
|
+
def test_the_former_rust_and_frontend_entries_admit_nothing(
|
|
453
447
|
tmp_path: pathlib.Path,
|
|
454
448
|
) -> None:
|
|
455
|
-
"""
|
|
456
|
-
|
|
457
|
-
suffixes and code-asset roots beneath them), so absent they contribute
|
|
458
|
-
nothing and the file is reported -- strictly stricter, never a silent pass.
|
|
459
|
-
A stray root-level Markdown file reached `ctx.inputs["rust_root"]` unguarded
|
|
460
|
-
and raised `KeyError`, a traceback with exit 1, measured against enact-combined
|
|
461
|
-
with `--tree rust= --tree frontend=`."""
|
|
462
|
-
_touch(tmp_path, "NOTES.md", "backend/crates/enact-app/assets/builtin-skills/s.md")
|
|
449
|
+
"""PBI 598: the rust and frontend suffix admissions are deleted with their trees."""
|
|
450
|
+
_touch(tmp_path, "NOTES.md", "backend/crates/app/assets/builtin-skills/s.md")
|
|
463
451
|
ctx = Context(
|
|
464
452
|
root=tmp_path, row=ROW,
|
|
465
453
|
inputs={
|
|
@@ -468,23 +456,83 @@ def test_rust_and_frontend_declared_absent_contribute_nothing_to_the_allowlist(
|
|
|
468
456
|
"docs_root": tmp_path / "docs",
|
|
469
457
|
},
|
|
470
458
|
)
|
|
471
|
-
given = ("NOTES.md", "backend/crates/
|
|
459
|
+
given = ("NOTES.md", "backend/crates/app/assets/builtin-skills/s.md")
|
|
472
460
|
findings = tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=given)))
|
|
473
461
|
assert sorted(f.path for f in findings) == sorted(given), findings
|
|
474
462
|
assert all(f.message == "tracked Markdown is outside the root allowlist" for f in findings)
|
|
475
463
|
|
|
476
464
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
465
|
+
# ---------------------------------------- PBI 601: INDEPENDENT-VERIFICATION.md
|
|
466
|
+
def test_independent_verification_is_a_declaration_not_a_central_name(
|
|
467
|
+
tmp_path: pathlib.Path,
|
|
468
|
+
) -> None:
|
|
469
|
+
"""PBI 601: INDEPENDENT-VERIFICATION.md left the central list; reported undeclared, silent once declared."""
|
|
470
|
+
_touch(tmp_path, "INDEPENDENT-VERIFICATION.md")
|
|
471
|
+
undeclared = graded_declaring(tmp_path, {}, ("INDEPENDENT-VERIFICATION.md",))
|
|
472
|
+
assert [f.path for f in undeclared.findings] == ["INDEPENDENT-VERIFICATION.md"]
|
|
473
|
+
declared = graded_declaring(
|
|
474
|
+
tmp_path, {"root-files": "INDEPENDENT-VERIFICATION.md"}, ("INDEPENDENT-VERIFICATION.md",)
|
|
475
|
+
)
|
|
476
|
+
assert declared.findings == ()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def test_an_arbitrary_tree_markdown_file_is_outside_the_allowlist(tmp_path: pathlib.Path) -> None:
|
|
480
|
+
_touch(tmp_path, "changes/some-change.md")
|
|
481
|
+
verdict = graded_declaring(tmp_path, {}, ("changes/some-change.md",))
|
|
482
|
+
assert [f.path for f in verdict.findings] == ["changes/some-change.md"], verdict.render()
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def test_the_remedy_names_only_trees_the_gate_admits(tmp_path: pathlib.Path) -> None:
|
|
486
|
+
_touch(tmp_path, "STRAY.md")
|
|
487
|
+
[finding] = graded_declaring(tmp_path, {}, ("STRAY.md",)).findings
|
|
488
|
+
assert "docs/, plans/" in finding.fix
|
|
489
|
+
assert "etc.)" in finding.fix
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _guides(tmp_path: pathlib.Path, plans: str, docs: str):
|
|
480
493
|
ctx = Context(
|
|
481
494
|
root=tmp_path, row=ROW,
|
|
482
|
-
inputs={
|
|
483
|
-
"workflows_root": tmp_path / "workflows",
|
|
484
|
-
"plans_root": tmp_path / "plans",
|
|
485
|
-
"docs_root": tmp_path / "docs",
|
|
486
|
-
"rust_root": tmp_path / "rust",
|
|
487
|
-
"frontend_root": tmp_path / "web",
|
|
488
|
-
},
|
|
495
|
+
inputs={"workflows_root": tmp_path / ".workflows", "plans_root": tmp_path / plans, "docs_root": tmp_path / docs},
|
|
489
496
|
)
|
|
490
|
-
|
|
497
|
+
return [f.path for f in RootMarkdownAllowlist().structural(ctx)]
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def test_the_guided_trees_are_the_declared_ones(tmp_path: pathlib.Path) -> None:
|
|
501
|
+
(tmp_path / "roadmap").mkdir()
|
|
502
|
+
(tmp_path / "documentation").mkdir()
|
|
503
|
+
(tmp_path / "plans").mkdir()
|
|
504
|
+
assert _guides(tmp_path, "roadmap", "documentation") == ["roadmap/AGENTS.md", "documentation/AGENTS.md"]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_an_undeclared_default_tree_is_not_asked_for_a_guide(tmp_path: pathlib.Path) -> None:
|
|
508
|
+
(tmp_path / "plans").mkdir()
|
|
509
|
+
(tmp_path / "docs").mkdir()
|
|
510
|
+
assert _guides(tmp_path, "roadmap", "documentation") == []
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
DONORS = ".donors/MANIFEST.md"
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def test_a_declared_nested_root_file_is_allowed(tmp_path: pathlib.Path) -> None:
|
|
517
|
+
_touch(tmp_path, DONORS)
|
|
518
|
+
verdict = graded_declaring(tmp_path, {"root-files": DONORS}, (DONORS,))
|
|
519
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def test_an_undeclared_nested_root_file_is_a_finding(tmp_path: pathlib.Path) -> None:
|
|
523
|
+
_touch(tmp_path, DONORS)
|
|
524
|
+
verdict = graded_declaring(tmp_path, {}, (DONORS,))
|
|
525
|
+
assert [f.path for f in verdict.findings] == [DONORS], verdict.render()
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def test_a_declaration_admits_only_the_exact_nested_path(tmp_path: pathlib.Path) -> None:
|
|
529
|
+
_touch(tmp_path, "other/MANIFEST.md")
|
|
530
|
+
verdict = graded_declaring(tmp_path, {"root-files": DONORS}, ("other/MANIFEST.md",))
|
|
531
|
+
assert [f.path for f in verdict.findings] == ["other/MANIFEST.md"], verdict.render()
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@pytest.mark.parametrize("bad", ["../x.md", "/x.md", "a/../x.md", "a//x.md", "docs/*.md", "a/./x.md"])
|
|
535
|
+
def test_a_malformed_root_file_declaration_is_could_not_run(tmp_path: pathlib.Path, bad: str) -> None:
|
|
536
|
+
_touch(tmp_path, "STATUS.md")
|
|
537
|
+
with pytest.raises(CouldNotRun, match="root-files"):
|
|
538
|
+
graded_declaring(tmp_path, {"root-files": bad}, ("STATUS.md",))
|
|
@@ -66,7 +66,7 @@ class ScriptsSizeDepth(Check):
|
|
|
66
66
|
|
|
67
67
|
# `<workflows>` indirection, not the literal `.workflows/scripts` this held
|
|
68
68
|
# until 2026-09-15. That literal is a consumer path inside the package
|
|
69
|
-
# (K-42), and it was not a near-miss:
|
|
69
|
+
# (K-42), and it was not a near-miss: a managed repository keeps CI in `workflows/`
|
|
70
70
|
# while `.workflows/` is repo-controls' own install artefact, so the gate
|
|
71
71
|
# reported `missing .workflows/scripts` and then called all 44 real scripts
|
|
72
72
|
# "outside scripts root" — 45 findings, none of them true. The remedy it
|
|
@@ -131,7 +131,7 @@ def test_the_rows_scope_selects_nothing_from_an_empty_given_set() -> None:
|
|
|
131
131
|
def test_the_scripts_root_follows_the_declared_tree(tmp_path: Path) -> None:
|
|
132
132
|
"""The regression guard for a literal `.workflows/scripts` in the package.
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
a managed repository keeps CI in `workflows/` while `.workflows/` is repo-controls' own
|
|
135
135
|
install artefact, so the hardcoded constant made this gate report
|
|
136
136
|
`missing .workflows/scripts` and then call all 44 real scripts "outside
|
|
137
137
|
scripts root" — 45 findings, not one of them true. Its printed remedy was
|
|
@@ -44,7 +44,7 @@ length: the WORKTREE file is not a safe source, because a correctly-tracked LFS
|
|
|
44
44
|
file is a pointer in Git and is smudged back to full content on disk, so a check
|
|
45
45
|
reading the worktree flags the very files that are doing the right thing.
|
|
46
46
|
check.sh called that risk "latent, not hypothetical". It was measured on
|
|
47
|
-
2026-10-01 against
|
|
47
|
+
2026-10-01 against the combined repository: `live-probes/local/chat-local/output/
|
|
48
48
|
evidence/probe-result-video.webm` has `git check-attr filter` = lfs, a committed
|
|
49
49
|
blob of 132 bytes (`git cat-file -s :path`) and a 1,170,147-byte worktree file,
|
|
50
50
|
and the worktree-reading version of this check reported it as a violation. No
|
|
@@ -356,7 +356,7 @@ def test_a_pointer_header_over_the_threshold_under_a_non_matching_rule_is_report
|
|
|
356
356
|
# graded where. A path with NO committed fact is graded by its worktree file, as
|
|
357
357
|
# before -- the existing cases above all run that way, with no facts at all.
|
|
358
358
|
|
|
359
|
-
SMUDGED = 1_170_147 # the measured size of a real smudged LFS file on
|
|
359
|
+
SMUDGED = 1_170_147 # the measured size of a real smudged LFS file on the combined repository
|
|
360
360
|
POINTER_SIZE = 132 # `git cat-file -s` of the pointer committed for it
|
|
361
361
|
|
|
362
362
|
|
|
@@ -808,20 +808,24 @@ def test_a_compliance_tree_declared_absent_drops_the_register(tmp_path: Path) ->
|
|
|
808
808
|
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
|
|
809
809
|
|
|
810
810
|
|
|
811
|
-
def
|
|
811
|
+
def test_a_compliance_tree_missing_from_the_map_drops_the_register_and_grades_bare(
|
|
812
812
|
tmp_path: Path,
|
|
813
813
|
) -> None:
|
|
814
|
-
#
|
|
815
|
-
# it reaches the widening branch, so a tree ABSENT from the map (neither a path
|
|
816
|
-
# nor "") is could-not-run even for a widening input -- reproduced through the
|
|
817
|
-
# real CLI on enact-combined with no `--tree compliance=...`. The message names
|
|
818
|
-
# exactly what to declare, and both repositories in this estate declare it. The
|
|
819
|
-
# fix is in `runner.resolve_inputs`, not in this check; this test flips with it.
|
|
814
|
+
# PBI 599/601: an absent register means no grants, never could-not-run; the excused file is reported.
|
|
820
815
|
root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
|
|
821
816
|
trees = {name: path for name, path in fixture_trees().items() if name != "compliance"}
|
|
822
817
|
verdict = runner.run(ROW, root, corpus_files(root), trees=trees, declared_trees=trees)
|
|
823
|
-
assert verdict.outcome is Outcome.
|
|
824
|
-
assert
|
|
818
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
819
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def test_a_declared_compliance_tree_with_no_folder_grants_nothing(tmp_path: Path) -> None:
|
|
823
|
+
root = build(tmp_path, {"photo.png": BIG})
|
|
824
|
+
trees = {**fixture_trees(), "compliance": "compliance"}
|
|
825
|
+
assert not (root / "compliance").exists()
|
|
826
|
+
verdict = runner.run(ROW, root, corpus_files(root), trees=trees, declared_trees=trees)
|
|
827
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
828
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
|
|
825
829
|
|
|
826
830
|
|
|
827
831
|
# --------------------------------------------------------- the register's shape
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Shared scanner (`base.py`) and fixture corpus for REP-004 and REP-006.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import out of a check is right for all of them or wrong for all of them.
|
|
3
|
+
Holds no `check.py`: each gate has its own folder, `legacy_search/` and
|
|
4
|
+
`vcs_in_scripts/`, because the layout is one `check.py` per registry row.
|
|
6
5
|
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from .check import BannedCommands
|
|
11
|
-
|
|
12
|
-
__all__ = ["BannedCommands"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The scanner behind REP-004 and REP-006; see `docs/operations/adl-compliance-invariants.md`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from typing import ClassVar
|
|
8
|
+
|
|
9
|
+
from ....core.check import Check
|
|
10
|
+
from ....core.context import Context, FileSet
|
|
11
|
+
from ....core.finding import Report
|
|
12
|
+
from ....core.scripts import is_shell_script
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BannedCommand(Check):
|
|
16
|
+
"""The shared scanner. Not registered; a gate subclasses it."""
|
|
17
|
+
|
|
18
|
+
WORD: ClassVar[str]
|
|
19
|
+
"""The forbidden command, assembled by the subclass and never spelled whole."""
|
|
20
|
+
|
|
21
|
+
MESSAGE: ClassVar[str]
|
|
22
|
+
FIX: ClassVar[str]
|
|
23
|
+
|
|
24
|
+
BOUNDARY: ClassVar[str] = r"(?:^|[;&|()`\s'\"])"
|
|
25
|
+
"""A command START, backtick included."""
|
|
26
|
+
|
|
27
|
+
TRAILING: ClassVar[str] = r"(?:\s|$)"
|
|
28
|
+
|
|
29
|
+
COMMENT: ClassVar[re.Pattern[str]] = re.compile(r"^\s*#")
|
|
30
|
+
"""A whole-line comment only; a trailing comment stays flagged."""
|
|
31
|
+
|
|
32
|
+
def graded(self, ctx: Context, files: FileSet) -> Iterable[Report]:
|
|
33
|
+
targets = sorted(p for p in files if is_shell_script(p, lambda p=p: self.text(ctx, p)))
|
|
34
|
+
pattern = re.compile(f"{self.BOUNDARY}{re.escape(self.WORD)}{self.TRAILING}")
|
|
35
|
+
for relative in FileSet(paths=tuple(targets)).existing(ctx.root):
|
|
36
|
+
for line, found in self.scan(ctx, relative, pattern):
|
|
37
|
+
# `found.string` is the line: `scan` matches line by line.
|
|
38
|
+
if self.COMMENT.match(found.string):
|
|
39
|
+
continue
|
|
40
|
+
yield self.report(
|
|
41
|
+
relative, f"{self.MESSAGE} ({found.string.strip()})", self.FIX, line=line,
|
|
42
|
+
)
|