@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.29 → 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/cli/args.ts +2 -2
- package/packages/agent-hooks/src/cli/doctor.ts +2 -1
- package/packages/agent-hooks/src/cli/rules.ts +2 -1
- package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -3
- package/packages/agent-hooks/src/gating/config.ts +11 -0
- package/packages/agent-hooks/src/gating/running-adl.ts +24 -0
- 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/hooks/user-prompt-submit/handler.ts +2 -2
- package/packages/agent-hooks/src/log/writer.ts +170 -24
- package/packages/agent-hooks/src/main.ts +11 -4
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +5 -4
- 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/azdo/src/azdo-wit.ts +8 -2
- package/packages/azdo/src/mcp-server.ts +1 -1
- 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 +38 -20
- 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
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""LAY-008 — ported from `compliance_new/checks/layout/placement/test.sh
|
|
1
|
+
"""LAY-008 — ported from `compliance_new/checks/layout/placement/test.sh`; rebuilt to its
|
|
2
|
+
throwaway-and-image rule only (Tarun 2026-10-08: PBI 599/601).
|
|
2
3
|
|
|
3
|
-
Two corpora, same names
|
|
4
|
-
`placement-
|
|
5
|
-
and
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
Two corpora, same names as the shell fixtures: `placement-allowed` (paths that
|
|
5
|
+
must stay silent) and `placement-stray` (one path per rule that must fire: a
|
|
6
|
+
screenshot and a file under retired `apps/`). PBI 598 (Tarun, 2026-10-08)
|
|
7
|
+
deleted the enumerated product asset directories and the `src/` Markdown sprawl
|
|
8
|
+
rule, so the allowed corpus no longer holds product-asset paths.
|
|
8
9
|
|
|
9
10
|
The shell `test.sh` drives `check.sh` DIRECTLY with the whole corpus as
|
|
10
11
|
`ADL_CHANGED_FILES` — it does not go through `gate-runner.sh`'s row-scope
|
|
@@ -61,17 +62,7 @@ def _whole_corpus(root: pathlib.Path) -> tuple[str, ...]:
|
|
|
61
62
|
def graded_directly(root: pathlib.Path, paths: tuple[str, ...]) -> Verdict:
|
|
62
63
|
"""Bypasses row-scope narrowing entirely — the check's OWN filtering
|
|
63
64
|
only. Mirrors the shell test's direct `check.sh` invocation."""
|
|
64
|
-
|
|
65
|
-
# defaults, which every fixture below already uses.
|
|
66
|
-
ctx = Context(
|
|
67
|
-
root=root, row=ROW,
|
|
68
|
-
inputs={
|
|
69
|
-
"docs_root": root / "docs",
|
|
70
|
-
"rust_root": root / "backend",
|
|
71
|
-
"frontend_root": root / "frontend",
|
|
72
|
-
"specs_root": root / ".specs",
|
|
73
|
-
},
|
|
74
|
-
)
|
|
65
|
+
ctx = Context(root=root, row=ROW)
|
|
75
66
|
findings = tuple(Placement().graded(ctx, FileSet(paths=paths)))
|
|
76
67
|
return decide(ROW, findings, 0, ())
|
|
77
68
|
|
|
@@ -89,82 +80,72 @@ def run_via_row_scope(
|
|
|
89
80
|
f"engine.scope matches no file on disk ({' | '.join(ROW.engine.scope)}); "
|
|
90
81
|
"a scope selecting nothing is could-not-run, never a clean pass",
|
|
91
82
|
)
|
|
92
|
-
|
|
93
|
-
# defaults, which every fixture below already uses.
|
|
94
|
-
ctx = Context(
|
|
95
|
-
root=root, row=ROW,
|
|
96
|
-
inputs={
|
|
97
|
-
"docs_root": root / "docs",
|
|
98
|
-
"rust_root": root / "backend",
|
|
99
|
-
"frontend_root": root / "frontend",
|
|
100
|
-
"specs_root": root / ".specs",
|
|
101
|
-
},
|
|
102
|
-
)
|
|
83
|
+
ctx = Context(root=root, row=ROW)
|
|
103
84
|
findings = tuple(Placement().run(ctx, narrowed))
|
|
104
85
|
return decide(ROW, findings, 0, ())
|
|
105
86
|
|
|
106
87
|
|
|
107
|
-
def
|
|
108
|
-
"""Every allowed path is actually graded, not narrowed out of the set
|
|
109
|
-
all 11 enumerated paths, exactly as the shell test asserts, or a
|
|
110
|
-
corpus drift would go unnoticed."""
|
|
88
|
+
def test_the_allowed_corpus_is_all_silent() -> None:
|
|
89
|
+
"""Every allowed path is actually graded, not narrowed out of the set."""
|
|
111
90
|
given = _whole_corpus(ALLOWED)
|
|
112
|
-
assert len(given) ==
|
|
91
|
+
assert len(given) == 1, given
|
|
113
92
|
verdict = graded_directly(ALLOWED, given)
|
|
114
93
|
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
115
94
|
assert verdict.findings == ()
|
|
116
95
|
|
|
117
96
|
|
|
97
|
+
def test_an_image_in_a_former_product_asset_directory_is_now_reported() -> None:
|
|
98
|
+
"""PBI 598: no hardcoded asset directory; a formerly exempt path is a stray image."""
|
|
99
|
+
former = (
|
|
100
|
+
"frontend/public/pwa/icon-192.png",
|
|
101
|
+
"backend/crates/assets/logos/tools/coding/tool.png",
|
|
102
|
+
"docs/assets/research/mobile-reference/shot.png",
|
|
103
|
+
)
|
|
104
|
+
verdict = graded_directly(ALLOWED, former)
|
|
105
|
+
assert sorted(f.path for f in verdict.findings) == sorted(former), verdict.render()
|
|
106
|
+
|
|
107
|
+
|
|
118
108
|
def test_out_of_place_paths_are_reported() -> None:
|
|
109
|
+
"""PBI 599/601: apps/ is no longer retired, so only the screenshot is reported."""
|
|
119
110
|
given = _whole_corpus(STRAY)
|
|
120
|
-
assert len(given) ==
|
|
111
|
+
assert len(given) == 2, given
|
|
121
112
|
verdict = graded_directly(STRAY, given)
|
|
122
113
|
assert verdict.outcome is Outcome.FINDINGS
|
|
123
|
-
assert
|
|
114
|
+
assert [f.path for f in verdict.findings] == ["frontend/unapproved/screenshot.png"]
|
|
124
115
|
# LAY-008, not LAY-007: the check used to report under its neighbour's id,
|
|
125
116
|
# and this assertion pinned the bug in place.
|
|
126
117
|
assert all(f.rule == "LAY-008" for f in verdict.findings)
|
|
127
118
|
|
|
128
119
|
|
|
129
|
-
def
|
|
120
|
+
def test_an_undeclared_screenshot_is_reported() -> None:
|
|
130
121
|
given = _whole_corpus(STRAY)
|
|
131
122
|
verdict = graded_directly(STRAY, given)
|
|
132
123
|
paths = {f.path for f in verdict.findings}
|
|
133
124
|
assert "frontend/unapproved/screenshot.png" in paths
|
|
134
125
|
|
|
135
126
|
|
|
136
|
-
def test_a_markdown_file_inside_a_crate_src_tree_is_reported() -> None:
|
|
137
|
-
given = _whole_corpus(STRAY)
|
|
138
|
-
verdict = graded_directly(STRAY, given)
|
|
139
|
-
paths = {f.path for f in verdict.findings}
|
|
140
|
-
assert "backend/crates/enact-session/src/backend/README.md" in paths
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def test_a_file_under_retired_apps_is_reported() -> None:
|
|
144
|
-
given = _whole_corpus(STRAY)
|
|
145
|
-
verdict = graded_directly(STRAY, given)
|
|
146
|
-
paths = {f.path for f in verdict.findings}
|
|
147
|
-
assert "apps/legacy/file.ts" in paths
|
|
148
|
-
|
|
149
|
-
|
|
150
127
|
def test_a_throwaway_extension_is_reported_strictly_everywhere() -> None:
|
|
151
|
-
"""Rule 3
|
|
152
|
-
directory — check.sh's own comment: checked "strictly everywhere",
|
|
153
|
-
ahead of the asset-directory exemption."""
|
|
128
|
+
"""Rule 3 (throwaway extensions) fires everywhere, per check.sh."""
|
|
154
129
|
verdict = graded_directly(
|
|
155
|
-
ALLOWED, ("
|
|
130
|
+
ALLOWED, ("assets/build.log",),
|
|
156
131
|
)
|
|
157
132
|
assert verdict.outcome is Outcome.FINDINGS
|
|
158
133
|
assert len(verdict.findings) == 1
|
|
159
|
-
assert verdict.findings[0].path == "
|
|
134
|
+
assert verdict.findings[0].path == "assets/build.log"
|
|
160
135
|
assert "throwaway artifact" in verdict.findings[0].message
|
|
161
136
|
|
|
162
137
|
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
assert verdict.
|
|
167
|
-
assert
|
|
138
|
+
def test_rule_1_the_root_allowlist_is_gone() -> None:
|
|
139
|
+
"""PBI 599/601: the root allowlist moved to LAY-012, so a root file off every list is silent here."""
|
|
140
|
+
verdict = graded_directly(ALLOWED, ("random.txt", "STATUS.md"))
|
|
141
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
142
|
+
assert not hasattr(Placement, "ROOT_ALLOWLIST")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_rule_2_apps_is_not_retired() -> None:
|
|
146
|
+
"""PBI 599/601: a file under `apps/` is graded like any other path."""
|
|
147
|
+
verdict = graded_directly(ALLOWED, ("apps/legacy/file.ts", "apps/cli/src/main.ts"))
|
|
148
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
168
149
|
|
|
169
150
|
|
|
170
151
|
def test_a_root_level_allowlisted_file_is_silent() -> None:
|
|
@@ -180,159 +161,60 @@ def test_no_set_is_could_not_run() -> None:
|
|
|
180
161
|
assert verdict.outcome is Outcome.COULD_NOT_RUN
|
|
181
162
|
|
|
182
163
|
|
|
183
|
-
|
|
184
|
-
"""The LOUD false-finding ASSET_DIR_TEMPLATES shipped with.
|
|
185
|
-
|
|
186
|
-
Every entry was a hardcoded literal, tested with `startswith`/`==` in
|
|
187
|
-
`_asset_exempt`. A repository whose frontend root is not the packaged
|
|
188
|
-
default had a real, legitimately-placed themed asset fail that
|
|
189
|
-
exemption and get reported "looks like a screenshot" -- a visible,
|
|
190
|
-
wrong finding about a file the exemption exists to admit."""
|
|
191
|
-
ctx = Context(
|
|
192
|
-
root=tmp_path, row=ROW,
|
|
193
|
-
inputs={
|
|
194
|
-
"docs_root": tmp_path / "docs",
|
|
195
|
-
"rust_root": tmp_path / "backend",
|
|
196
|
-
"frontend_root": tmp_path / "webapp",
|
|
197
|
-
"specs_root": tmp_path / ".specs",
|
|
198
|
-
},
|
|
199
|
-
)
|
|
200
|
-
path = "webapp/packages/desktop/src/renderer/assets/themes/screenshot-dark.png"
|
|
201
|
-
findings = tuple(Placement().graded(ctx, FileSet(paths=(path,))))
|
|
202
|
-
assert findings == (), (
|
|
203
|
-
f"a real themed asset under a redeclared frontend root was wrongly "
|
|
204
|
-
f"reported as a stray screenshot: {findings}"
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def test_a_redeclared_rust_root_still_catches_md_sprawl(tmp_path: pathlib.Path) -> None:
|
|
209
|
-
"""The SILENT false pass MD_SRC_TREES/MD_SRC_EXEMPT shipped with.
|
|
210
|
-
|
|
211
|
-
`MD_SRC_TREES`'s `backend/` entry was a hardcoded literal, tested with
|
|
212
|
-
`startswith` in `_md_sprawl`. A repository whose rust root is not the
|
|
213
|
-
packaged default had real narrative-doc sprawl inside its `src/` tree
|
|
214
|
-
fail that `startswith` test, and the gate never examined it at all: no
|
|
215
|
-
error, no finding, indistinguishable from a repository with no sprawl."""
|
|
216
|
-
ctx = Context(
|
|
217
|
-
root=tmp_path, row=ROW,
|
|
218
|
-
inputs={
|
|
219
|
-
"docs_root": tmp_path / "docs",
|
|
220
|
-
"rust_root": tmp_path / "rustcode",
|
|
221
|
-
"frontend_root": tmp_path / "frontend",
|
|
222
|
-
"specs_root": tmp_path / ".specs",
|
|
223
|
-
},
|
|
224
|
-
)
|
|
225
|
-
path = "rustcode/crates/enact-example/src/notes.md"
|
|
226
|
-
findings = tuple(Placement().graded(ctx, FileSet(paths=(path,))))
|
|
227
|
-
assert any("md-sprawl" in f.message for f in findings), (
|
|
228
|
-
f"real markdown sprawl under a redeclared rust root went "
|
|
229
|
-
f"unreported: {findings}"
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
# ---------------------------------------------- per-repository root declarations
|
|
164
|
+
# ----------------------------------------------- the repository's declared asset dirs
|
|
234
165
|
def graded_declaring(
|
|
235
166
|
declarations: dict[str, str], paths: tuple[str, ...], root: pathlib.Path = ALLOWED
|
|
236
167
|
) -> Verdict:
|
|
237
|
-
"""`graded_directly
|
|
238
|
-
|
|
239
|
-
declarations)."""
|
|
240
|
-
ctx = Context(
|
|
241
|
-
root=root, row=ROW, declarations=declarations,
|
|
242
|
-
inputs={
|
|
243
|
-
"docs_root": root / "docs",
|
|
244
|
-
"rust_root": root / "backend",
|
|
245
|
-
"frontend_root": root / "frontend",
|
|
246
|
-
"specs_root": root / ".specs",
|
|
247
|
-
},
|
|
248
|
-
)
|
|
168
|
+
"""`graded_directly` plus the repository declarations (`asset_dirs` arrives as `asset-dirs`)."""
|
|
169
|
+
ctx = Context(root=root, row=ROW, declarations=declarations)
|
|
249
170
|
findings = tuple(Placement().graded(ctx, FileSet(paths=paths)))
|
|
250
171
|
return decide(ROW, findings, 0, ())
|
|
251
172
|
|
|
252
173
|
|
|
253
|
-
def
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
assert [f.path for f in verdict.findings] == ["STATUS.md"]
|
|
259
|
-
assert "root allowlist" in verdict.findings[0].message
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def test_a_declared_root_file_is_accepted() -> None:
|
|
263
|
-
verdict = graded_declaring({"root-files": "STATUS.md,DRIFT.md"}, ("STATUS.md", "DRIFT.md"))
|
|
174
|
+
def test_a_declared_asset_dir_admits_an_image_at_any_depth() -> None:
|
|
175
|
+
verdict = graded_declaring(
|
|
176
|
+
{"asset-dirs": "web/public,docs/assets"},
|
|
177
|
+
("web/public/logo.png", "web/public/deep/er/icon.JPG", "docs/assets/shot/screenshot-1.png"),
|
|
178
|
+
)
|
|
264
179
|
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
265
180
|
|
|
266
181
|
|
|
267
|
-
def
|
|
268
|
-
"""
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
182
|
+
def test_an_undeclared_image_is_reported_beside_a_declaration() -> None:
|
|
183
|
+
"""A gate that stops grading images once any declaration is present must fail this."""
|
|
184
|
+
verdict = graded_declaring(
|
|
185
|
+
{"asset-dirs": "web/public"}, ("web/public/logo.png", "web/other/logo.png", "logo.png")
|
|
186
|
+
)
|
|
187
|
+
assert sorted(f.path for f in verdict.findings) == ["logo.png", "web/other/logo.png"]
|
|
272
188
|
|
|
273
189
|
|
|
274
|
-
def
|
|
275
|
-
verdict = graded_declaring({"
|
|
276
|
-
assert [f.path for f in verdict.findings] == ["
|
|
190
|
+
def test_an_asset_dir_is_a_directory_not_a_name_prefix() -> None:
|
|
191
|
+
verdict = graded_declaring({"asset-dirs": "web/public"}, ("web/public-extra/logo.png",))
|
|
192
|
+
assert [f.path for f in verdict.findings] == ["web/public-extra/logo.png"]
|
|
277
193
|
|
|
278
194
|
|
|
279
|
-
def
|
|
280
|
-
"""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
assert "[controls.compliance.root]" in finding.fix, finding.fix
|
|
284
|
-
assert "adl-config.toml" in finding.fix, finding.fix
|
|
285
|
-
assert "root_shape.py" not in finding.fix, finding.fix
|
|
195
|
+
def test_a_declared_asset_dir_does_not_admit_a_throwaway_file() -> None:
|
|
196
|
+
verdict = graded_declaring({"asset-dirs": "web/public"}, ("web/public/build.log",))
|
|
197
|
+
assert [f.path for f in verdict.findings] == ["web/public/build.log"]
|
|
198
|
+
assert "throwaway" in verdict.findings[0].message
|
|
286
199
|
|
|
287
200
|
|
|
288
|
-
def
|
|
289
|
-
"""
|
|
290
|
-
|
|
291
|
-
for declarations in ({}, {"root-dirs": "packaging"}, {"root-files": "apps"}):
|
|
292
|
-
verdict = graded_declaring(declarations, ("apps/legacy/file.ts",))
|
|
293
|
-
assert [f.path for f in verdict.findings] == ["apps/legacy/file.ts"], declarations
|
|
294
|
-
assert "apps-retired" in verdict.findings[0].message
|
|
201
|
+
def test_a_trailing_slash_in_a_declared_asset_dir_is_tolerated() -> None:
|
|
202
|
+
verdict = graded_declaring({"asset-dirs": "web/public/"}, ("web/public/logo.png",))
|
|
203
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
295
204
|
|
|
296
205
|
|
|
297
|
-
def
|
|
298
|
-
|
|
299
|
-
for declarations in ({"root-dirs": "apps"}, {"root-dirs": "packaging, apps"}):
|
|
300
|
-
verdict = graded_declaring(declarations, ("apps/legacy/file.ts", "apps/cli/src/main.ts"))
|
|
301
|
-
assert verdict.outcome is Outcome.CLEAN, (declarations, verdict.render())
|
|
206
|
+
def test_a_malformed_asset_dir_is_could_not_run() -> None:
|
|
207
|
+
import pytest
|
|
302
208
|
|
|
209
|
+
from adl_compliance.core.errors import CouldNotRun
|
|
303
210
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
permanently. Neither is declared by hand under `[controls.compliance.root]`."""
|
|
308
|
-
verdict = graded_declaring({}, ("adl-config.toml", "enact-config.toml"))
|
|
309
|
-
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
211
|
+
for bad in ("/abs", "a/../b", "./x"):
|
|
212
|
+
with pytest.raises(CouldNotRun, match="asset-dirs"):
|
|
213
|
+
graded_declaring({"asset-dirs": bad}, ("a/logo.png",))
|
|
310
214
|
|
|
311
215
|
|
|
312
|
-
def
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
assert
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
# ------------------------------------------------- a tree declared absent
|
|
320
|
-
def test_a_rust_tree_declared_absent_contributes_nothing_to_md_sprawl(
|
|
321
|
-
tmp_path: pathlib.Path,
|
|
322
|
-
) -> None:
|
|
323
|
-
"""`rust = ""` (the repository states it has no Rust tree) drops `rust_root`
|
|
324
|
-
from the resolved inputs. In `_md_src_trees` that input only ever ADDS a
|
|
325
|
-
prefix to the trees whose `src/` Markdown is reported, so absent it
|
|
326
|
-
contributes nothing and the literal `agent/` tree still governs. This was a
|
|
327
|
-
`KeyError: 'rust_root'` traceback (exit 1, not could-not-run) on any `.md`
|
|
328
|
-
path, measured against enact-combined with `--tree rust= --tree frontend=`.
|
|
329
|
-
The rule is not switched off: `agent/**/src/*.md` is still reported."""
|
|
330
|
-
ctx = Context(root=tmp_path, row=ROW, inputs={"docs_root": tmp_path / "docs"})
|
|
331
|
-
findings = tuple(
|
|
332
|
-
Placement().graded(
|
|
333
|
-
ctx,
|
|
334
|
-
FileSet(paths=("notes/overview.md", "backend/crates/x/src/n.md", "agent/crates/x/src/n.md")),
|
|
335
|
-
)
|
|
336
|
-
)
|
|
337
|
-
assert [f.path for f in findings] == ["agent/crates/x/src/n.md"], findings
|
|
338
|
-
assert "md-sprawl" in findings[0].message
|
|
216
|
+
def test_the_row_scope_is_every_file_and_excludes_fixtures() -> None:
|
|
217
|
+
assert scope.in_scope(ROW.engine.scope, "logo.png")
|
|
218
|
+
assert scope.in_scope(ROW.engine.scope, "deep/er/notes.log")
|
|
219
|
+
assert not scope.in_scope(ROW.engine.scope, "checks/x/fixtures/logo.png")
|
|
220
|
+
assert ROW.title == "Reject throwaway files and undeclared image placement"
|
|
@@ -84,12 +84,13 @@ class PlansLayout(Check):
|
|
|
84
84
|
# Three gates held an opinion on this file and they did not agree: LAY-006
|
|
85
85
|
# allowed `*/LEDGER.md`, DOC-009 required `LEDGER.yaml` and reported the
|
|
86
86
|
# Markdown form as "legacy", and this list allowed only the YAML. A
|
|
87
|
-
# repository could not satisfy all three at once --
|
|
87
|
+
# repository could not satisfy all three at once -- a managed repository's conversion
|
|
88
88
|
# failed here the moment it succeeded elsewhere.
|
|
89
89
|
ROOT_ALLOWED_FILES: ClassVar[tuple[str, ...]] = (
|
|
90
90
|
"AGENTS.md", "CLAUDE.md", "LEDGER.md", "GAPS-AND-FINDINGS.md",
|
|
91
91
|
)
|
|
92
92
|
IGNORED: ClassVar[str] = ".DS_Store"
|
|
93
|
+
KEEP: ClassVar[str] = ".gitkeep"
|
|
93
94
|
|
|
94
95
|
# az<work item id>-<kebab-slug>
|
|
95
96
|
FEATURE_DIR: ClassVar[re.Pattern[str]] = re.compile(r"^az([0-9]+)-[a-z0-9]+(-[a-z0-9]+)*$")
|
|
@@ -245,11 +246,26 @@ class PlansLayout(Check):
|
|
|
245
246
|
|
|
246
247
|
def _feature_dirs(self, ctx: Context, given: set[str]) -> Iterator[Finding]:
|
|
247
248
|
features = f"{self._plans(ctx)}/{self.FEATURES}"
|
|
248
|
-
|
|
249
|
+
entries = sorted(self._under(features, given))
|
|
250
|
+
# `.gitkeep` is allowed in features/ only until the first Feature folder exists.
|
|
251
|
+
has_feature = any(
|
|
252
|
+
self.FEATURE_DIR.match(entry.rsplit("/", 1)[-1]) and ctx.path(entry).is_dir()
|
|
253
|
+
for entry in entries
|
|
254
|
+
)
|
|
255
|
+
for entry in entries:
|
|
249
256
|
name = entry.rsplit("/", 1)[-1]
|
|
250
257
|
if name == self.IGNORED:
|
|
251
258
|
continue
|
|
252
259
|
|
|
260
|
+
if name == self.KEEP and not ctx.path(entry).is_dir():
|
|
261
|
+
if has_feature:
|
|
262
|
+
yield self.report(
|
|
263
|
+
entry,
|
|
264
|
+
"features/ no longer needs .gitkeep: a Feature folder exists",
|
|
265
|
+
f"delete {entry} now that the first Feature exists",
|
|
266
|
+
)
|
|
267
|
+
continue
|
|
268
|
+
|
|
253
269
|
if not ctx.path(entry).is_dir():
|
|
254
270
|
yield self.report(
|
|
255
271
|
entry,
|
|
@@ -165,6 +165,55 @@ def test_a_loose_file_in_features_is_reported(tmp_path: pathlib.Path) -> None:
|
|
|
165
165
|
assert any("one directory per Feature" in f.message for f in verdict.findings)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
# LAY-009: an empty features/ may hold `.gitkeep`; the first Feature folder retires it.
|
|
169
|
+
|
|
170
|
+
def _plans_files(*extra: str) -> dict[str, str]:
|
|
171
|
+
return {"plans/AGENTS.md": "x", **{path: "" for path in extra}}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_a_gitkeep_alone_in_an_empty_features_directory_is_silent(
|
|
175
|
+
tmp_path: pathlib.Path,
|
|
176
|
+
) -> None:
|
|
177
|
+
verdict = graded_directly(build(tmp_path, _plans_files("plans/features/.gitkeep")))
|
|
178
|
+
assert not any(".gitkeep" in f.path for f in verdict.findings), verdict.render()
|
|
179
|
+
assert not any("one directory per Feature" in f.message for f in verdict.findings)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_a_gitkeep_beside_a_feature_is_reported_and_names_the_deletion(
|
|
183
|
+
tmp_path: pathlib.Path,
|
|
184
|
+
) -> None:
|
|
185
|
+
files = {
|
|
186
|
+
"plans/features/az1-x/spec.md": FEATURE.format(work_item=1),
|
|
187
|
+
"plans/features/.gitkeep": "",
|
|
188
|
+
}
|
|
189
|
+
verdict = graded_directly(build(tmp_path, {"plans/AGENTS.md": "x", **files}))
|
|
190
|
+
kept = [f for f in verdict.findings if f.path == "plans/features/.gitkeep"]
|
|
191
|
+
assert len(kept) == 1, verdict.render()
|
|
192
|
+
assert "delete plans/features/.gitkeep" in kept[0].fix
|
|
193
|
+
assert "first Feature" in kept[0].fix
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_any_other_loose_file_is_still_reported_beside_a_gitkeep(
|
|
197
|
+
tmp_path: pathlib.Path,
|
|
198
|
+
) -> None:
|
|
199
|
+
verdict = graded_directly(
|
|
200
|
+
build(tmp_path, _plans_files("plans/features/.gitkeep", "plans/features/stray.md"))
|
|
201
|
+
)
|
|
202
|
+
paths = {f.path for f in verdict.findings}
|
|
203
|
+
assert "plans/features/stray.md" in paths
|
|
204
|
+
assert "plans/features/.gitkeep" not in paths
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_a_gitkeep_is_only_the_features_directorys_exemption(tmp_path: pathlib.Path) -> None:
|
|
208
|
+
"""Strictness: the allowance is for `features/` alone, not a Feature folder."""
|
|
209
|
+
files = {
|
|
210
|
+
"plans/features/az1-x/spec.md": FEATURE.format(work_item=1),
|
|
211
|
+
"plans/features/az1-x/.gitkeep": "",
|
|
212
|
+
}
|
|
213
|
+
verdict = graded_directly(build(tmp_path, {"plans/AGENTS.md": "x", **files}))
|
|
214
|
+
assert any(f.path == "plans/features/az1-x/.gitkeep" for f in verdict.findings)
|
|
215
|
+
|
|
216
|
+
|
|
168
217
|
def test_a_feature_directory_not_named_az_id_is_reported(tmp_path: pathlib.Path) -> None:
|
|
169
218
|
verdict = graded_directly(
|
|
170
219
|
build(tmp_path, {"plans/features/coverage/spec.md": FEATURE.format(work_item=1)})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Links"
|
|
3
|
+
description: "Three links on this page do not resolve"
|
|
4
|
+
document_type: architecture
|
|
5
|
+
status: canonical
|
|
6
|
+
last_update: "2026-07-27T00:00:00Z"
|
|
7
|
+
reason: "fixture"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
[Sibling](./route-target.md)
|
|
11
|
+
|
|
12
|
+
[Published route](/architecture/guides/route-target/)
|
|
13
|
+
|
|
14
|
+
[Completed plan](../../../.plans/c_done/26-07-27-history.md)
|
|
15
|
+
|
|
16
|
+
[Missing](./does-not-exist.md)
|
|
17
|
+
|
|
18
|
+
[Wrong case](../../readme.md)
|
|
19
|
+
|
|
20
|
+
Retired path: `docs/strategy/old-page.md`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Links"
|
|
3
|
+
description: "Every link on this page resolves"
|
|
4
|
+
document_type: architecture
|
|
5
|
+
status: canonical
|
|
6
|
+
last_update: "2026-07-27T00:00:00Z"
|
|
7
|
+
reason: "fixture"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
[Sibling](./route-target.md)
|
|
11
|
+
|
|
12
|
+
[Published route](/architecture/guides/route-target/)
|
|
13
|
+
|
|
14
|
+
[Exact case](../../README.md)
|
|
15
|
+
|
|
16
|
+
[Completed plan](../../../.plans/c_done/26-07-27-history.md)
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
[Inside a fence](./does-not-exist.md)
|
|
20
|
+
```
|
|
@@ -72,7 +72,7 @@ def test_broken_links_are_reported_exactly() -> None:
|
|
|
72
72
|
|
|
73
73
|
rendered = verdict.render()
|
|
74
74
|
assert (
|
|
75
|
-
"docs/architecture/
|
|
75
|
+
"docs/architecture/guides/links.md:16: missing local reference "
|
|
76
76
|
"./does-not-exist.md" in rendered
|
|
77
77
|
), rendered
|
|
78
78
|
|
|
@@ -81,7 +81,7 @@ def test_broken_links_are_reported_exactly() -> None:
|
|
|
81
81
|
# a check that tested existence alone would call this link resolved. It is
|
|
82
82
|
# wrong by case and by nothing else.
|
|
83
83
|
assert (
|
|
84
|
-
"docs/architecture/
|
|
84
|
+
"docs/architecture/guides/links.md:18: missing local reference "
|
|
85
85
|
"../../readme.md" in rendered
|
|
86
86
|
), rendered
|
|
87
87
|
|
|
@@ -104,7 +104,7 @@ def test_a_page_outside_the_given_set_is_not_graded() -> None:
|
|
|
104
104
|
to grade."""
|
|
105
105
|
verdict = graded(
|
|
106
106
|
CORPORA / "link-targets-broken",
|
|
107
|
-
given=("docs/architecture/
|
|
107
|
+
given=("docs/architecture/guides/route-target.md",),
|
|
108
108
|
)
|
|
109
109
|
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
110
110
|
|
|
@@ -118,7 +118,7 @@ def test_a_completed_plan_resolves_as_a_target_though_excluded_as_a_source() ->
|
|
|
118
118
|
missing one."""
|
|
119
119
|
verdict = graded(
|
|
120
120
|
CORPORA / "link-targets-clean",
|
|
121
|
-
given=("docs/architecture/
|
|
121
|
+
given=("docs/architecture/guides/links.md", "docs/architecture/guides/route-target.md"),
|
|
122
122
|
)
|
|
123
123
|
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
124
124
|
|
|
@@ -58,13 +58,13 @@ from typing import ClassVar
|
|
|
58
58
|
|
|
59
59
|
from ....core.check import Check
|
|
60
60
|
from ....core.context import Context, FileSet
|
|
61
|
+
from ....core.declarations import declared_root_files
|
|
61
62
|
from ....core.finding import Report
|
|
62
63
|
from ....policy.root_shape import (
|
|
63
64
|
ALLOWED_ROOT_DIRS,
|
|
64
65
|
ALLOWED_ROOT_FILES,
|
|
65
66
|
REQUIRED_ROOT_FILES,
|
|
66
67
|
allowed_root_dirs,
|
|
67
|
-
allowed_root_files,
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
|
|
@@ -100,7 +100,7 @@ class RootClosedSet(Check):
|
|
|
100
100
|
|
|
101
101
|
# PERMITTED at the root, same as the files above: present is fine, absent
|
|
102
102
|
# is fine. Several entries here name trees no longer in use anywhere
|
|
103
|
-
# (`.code-review-graph_old`, `.plans
|
|
103
|
+
# (`.code-review-graph_old`, `.plans`) and they cost nothing while
|
|
104
104
|
# they only permit; under the old manifest reading, each was a finding.
|
|
105
105
|
#
|
|
106
106
|
# The CENTRAL default. The effective set is
|
|
@@ -135,7 +135,7 @@ class RootClosedSet(Check):
|
|
|
135
135
|
root_files.add(path)
|
|
136
136
|
|
|
137
137
|
allowed_dirs = allowed_root_dirs(ctx.declarations)
|
|
138
|
-
allowed_files =
|
|
138
|
+
allowed_files = ALLOWED_ROOT_FILES | declared_root_files(ctx.declarations)
|
|
139
139
|
for name in sorted(dirs):
|
|
140
140
|
if name not in allowed_dirs:
|
|
141
141
|
yield self.report(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
root-layout corpus: content under a declared top-level directory.
|
|
@@ -41,8 +41,15 @@ def clean_set() -> tuple[str, ...]:
|
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
# PBI 599/601: the corpus declares its dirs (`root-dirs`) and files (`root-files`) as a real repository does.
|
|
45
|
+
CORPUS_DIRS = {
|
|
46
|
+
"root-dirs": "backend,dev,docker,frontend,infra,.docs,.notes,.donors",
|
|
47
|
+
"root-files": "CLAUDE-INVARIANTS.md,INDEPENDENT-VERIFICATION.md",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
44
51
|
def verdict_for(given: tuple[str, ...]) -> Verdict:
|
|
45
|
-
ctx = Context(root=CORPUS, row=ROW)
|
|
52
|
+
ctx = Context(root=CORPUS, row=ROW, declarations=CORPUS_DIRS)
|
|
46
53
|
reported = tuple(RootClosedSet().run(ctx, FileSet(paths=given)))
|
|
47
54
|
findings = tuple(r for r in reported if isinstance(r, Finding))
|
|
48
55
|
advisories = tuple(r for r in reported if isinstance(r, Advisory))
|