@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
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
LAY-006 a Markdown file outside the declared docs trees and not on the
|
|
4
4
|
exclusion list.
|
|
5
5
|
|
|
6
|
-
The allowed roots are RESOLVED, not listed: `<docs>`, `<plans>`, `<compliance
|
|
7
|
-
|
|
6
|
+
The allowed roots are RESOLVED, not listed: `<docs>`, `<plans>`, `<compliance>`
|
|
7
|
+
and `<workflows>` come from
|
|
8
8
|
`[controls.compliance.trees]`, so a repository that spells a tree differently is
|
|
9
9
|
graded against its own spelling. The only literals left are `*/LEDGER.md` (a
|
|
10
10
|
register beside the tree it records) and the exclusion list below.
|
|
@@ -15,14 +15,6 @@ directory whose Markdown is site or application content rather than
|
|
|
15
15
|
documentation. A declared root only ever WIDENS the allowlist, so declaring
|
|
16
16
|
nothing is the stricter grade.
|
|
17
17
|
|
|
18
|
-
This docstring used to read "docs/, .plans/, .specs/, or .changeset/" and three
|
|
19
|
-
of those four were wrong. `.specs` was deleted 2026-09-16 as a path that matched
|
|
20
|
-
nothing in this estate and never could; `.plans` became the resolvable `<plans>`
|
|
21
|
-
when 7b8c057 renamed the tree; `.changeset/*.md` went the same way as `.specs`,
|
|
22
|
-
for the same reason -- zero files across all four repositories. A rule's own
|
|
23
|
-
description is the first thing anyone reads, and this one described the
|
|
24
|
-
pre-migration world.
|
|
25
|
-
|
|
26
18
|
LAY-006 a Markdown file outside the allowed docs trees and not on the
|
|
27
19
|
exclusion list.
|
|
28
20
|
|
|
@@ -54,11 +46,11 @@ from ....policy.root_shape import (
|
|
|
54
46
|
GOVERNANCE,
|
|
55
47
|
GUIDES,
|
|
56
48
|
REGISTERS,
|
|
57
|
-
ROOT_FILES_DECLARATION,
|
|
58
49
|
declared_root_names,
|
|
59
50
|
)
|
|
60
51
|
from ....core.check import Check
|
|
61
52
|
from ....core.context import Context, FileSet
|
|
53
|
+
from ....core.declarations import declared_root_files
|
|
62
54
|
from ....core.errors import CouldNotRun
|
|
63
55
|
from ....core.finding import Report
|
|
64
56
|
|
|
@@ -83,16 +75,11 @@ class MarkdownTrees(Check):
|
|
|
83
75
|
# Agent DEFINITIONS, same reasoning and same shape as the skill
|
|
84
76
|
# definitions two lines up: `name`/`description`/`model`/`tools`
|
|
85
77
|
# frontmatter, loaded by the runtime, not read as prose. Caught live
|
|
86
|
-
# against
|
|
78
|
+
# against the extensions repository: `extensions/plugin-dev/agents/{agent-
|
|
87
79
|
# creator,plugin-validator,skill-reviewer}.md` were reported as
|
|
88
80
|
# documentation outside the allowed docs trees. COVERAGE COST, stated
|
|
89
|
-
# not buried:
|
|
90
|
-
#
|
|
91
|
-
# enact-m5 pair, `docs/architecture/agents/{mcp-servers,
|
|
92
|
-
# orchestration}.md`, are prose docs already reachable via `docs/*.md`
|
|
93
|
-
# in ALLOWED below, so this exclusion changes nothing about whether
|
|
94
|
-
# they are reported, only widens what would also excuse them if they
|
|
95
|
-
# ever moved outside `docs/`).
|
|
81
|
+
# not buried: `agents/*.md` files stop being graded by this check; prose
|
|
82
|
+
# docs among them are still reachable via `docs/*.md` in ALLOWED below.
|
|
96
83
|
"agents/*", "*/agents/*",
|
|
97
84
|
# Root governance and guides come from policy/root_shape.py, the single list
|
|
98
85
|
# 63cd856 created when three gates were found carrying three drifting copies
|
|
@@ -104,14 +91,9 @@ class MarkdownTrees(Check):
|
|
|
104
91
|
# `_excluded`.
|
|
105
92
|
*sorted(GOVERNANCE | GUIDES | REGISTERS),
|
|
106
93
|
"AGENTS.md", "CLAUDE.md", "CHANGELOG.md", "RELEASE.md",
|
|
107
|
-
"INVARIANTS.md", "README.md",
|
|
108
|
-
"INDEPENDENT-VERIFICATION.md",
|
|
94
|
+
"INVARIANTS.md", "README.md",
|
|
109
95
|
"*/AGENTS.md", "*/CLAUDE.md", "*/SKILLS.md", "*/CHANGELOG.md",
|
|
110
96
|
"*/README.md",
|
|
111
|
-
# A tree's CLAUDE-INVARIANTS.md is the same class of control artifact
|
|
112
|
-
# as its AGENTS.md and CLAUDE.md above; the root one is allowed two
|
|
113
|
-
# patterns up and nowhere else.
|
|
114
|
-
"*/CLAUDE-INVARIANTS.md",
|
|
115
97
|
)
|
|
116
98
|
|
|
117
99
|
# FIXED 2026-09-16 (K-42 sweep, LOUD form, approved): these five patterns
|
|
@@ -124,39 +106,19 @@ class MarkdownTrees(Check):
|
|
|
124
106
|
# literal below its resolved root: named subject matter, not a resolvable
|
|
125
107
|
# path.
|
|
126
108
|
EXCLUDED_TEMPLATES: ClassVar[tuple[tuple[str, str], ...]] = (
|
|
127
|
-
("frontend_root", ".claude/*"),
|
|
128
109
|
("workflows_root", "changesets/*"),
|
|
129
|
-
# Two patterns, not three — see check.sh's comment: the nested form
|
|
130
|
-
# needs its own line because the literal `tests/` anchors it, and
|
|
131
|
-
# `tests/fixture*/*` already covers the zero-directory case.
|
|
132
|
-
("rust_root", "crates/*/tests/fixture*/*"),
|
|
133
|
-
("rust_root", "crates/*/tests/*/fixture*/*"),
|
|
134
|
-
("rust_root", "crates/enact-session/src/backend/codex_init_prompt.md"),
|
|
135
|
-
("frontend_root", "examples/*"),
|
|
136
|
-
("workflows_root", "COVERAGE.md"),
|
|
137
110
|
)
|
|
138
111
|
|
|
139
112
|
# Allowlisted Markdown roots.
|
|
140
113
|
# `plans/`, not `.plans/`: 7b8c057 renamed the tree and `config.toml` now reads
|
|
141
114
|
# `plans = "plans"`. This list did not follow, so every file in the tree the
|
|
142
115
|
# registry points at was reported as being outside it -- 18 findings against
|
|
143
|
-
#
|
|
116
|
+
# a managed repository alone. The dotted spelling stays alongside it because a repository
|
|
144
117
|
# that has not migrated yet is not thereby wrong.
|
|
145
118
|
# FIXED 2026-09-16 (K-42 sweep, LOUD form, approved): `docs/*.md`/
|
|
146
|
-
# `docs/*.mdx
|
|
147
|
-
#
|
|
148
|
-
# directly rather than resolved. `.plans/*.md` (legacy dotted spelling)
|
|
149
|
-
# and bare `specs/*.md` (no documented migration away from `.specs`)
|
|
150
|
-
# stay literal alternate-spelling allowances, exactly as before -- this
|
|
151
|
-
# fix only moves the entry that stood for "the packaged default",
|
|
152
|
-
# leaving the other spelling in each pair untouched.
|
|
119
|
+
# `docs/*.mdx` and `plans/*.md` were also hardcoded literals -- the packaged
|
|
120
|
+
# default for each tree, spelled out directly rather than resolved.
|
|
153
121
|
ALLOWED: ClassVar[tuple[str, ...]] = (
|
|
154
|
-
# `specs/*.md` deleted 2026-09-16. It was copy-paste from the
|
|
155
|
-
# `plans/`/`.plans/` pair, which stood for a REAL rename. No repository
|
|
156
|
-
# in this estate has `specs/` or `.specs/`, and nothing records an
|
|
157
|
-
# undotted spelling as a rename target -- it matched nothing and never
|
|
158
|
-
# could. The resolvable `<specs>` spelling is handled with the other
|
|
159
|
-
# declared trees above.
|
|
160
122
|
# A LEDGER beside the tree it records. `root_shape.REGISTERS` already
|
|
161
123
|
# permits a register at the repository root "because that is where
|
|
162
124
|
# someone arriving at the repository looks" -- a per-tree ledger is the
|
|
@@ -185,20 +147,11 @@ class MarkdownTrees(Check):
|
|
|
185
147
|
"registers": "<compliance>",
|
|
186
148
|
"docs_root": "<docs>",
|
|
187
149
|
"plans_root": "<plans>",
|
|
188
|
-
"rust_root": "<rust>",
|
|
189
|
-
"frontend_root": "<frontend>",
|
|
190
150
|
"workflows_root": "<workflows>",
|
|
191
151
|
}
|
|
192
152
|
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
# not exist, and a false declaration is undetectable. Absent -> stricter.
|
|
196
|
-
#
|
|
197
|
-
# This input is reachable only by a caller that builds a Context by hand (the
|
|
198
|
-
# unit tests do): `cli.py` routes global inputs to batch rows alone, so no
|
|
199
|
-
# `adl-config.toml` key ever reached `specs_root`. A repository's own extra
|
|
200
|
-
# Markdown roots arrive through the `markdown-roots` declaration below.
|
|
201
|
-
widening_inputs: ClassVar[frozenset[str]] = frozenset({"specs_root"})
|
|
153
|
+
# `registers` only ever ADDS allowlist entries (PBI 599); no ratchet means the fixed list alone.
|
|
154
|
+
widening_inputs: ClassVar[frozenset[str]] = frozenset({"registers"})
|
|
202
155
|
|
|
203
156
|
# `[controls.compliance.markdown].roots` -- directories a repository declares
|
|
204
157
|
# as homes for Markdown it authors outside every tree above, e.g. an Astro
|
|
@@ -251,9 +204,6 @@ class MarkdownTrees(Check):
|
|
|
251
204
|
plans_root = ctx.inputs.get("plans_root")
|
|
252
205
|
if plans_root is not None:
|
|
253
206
|
allowed = (*allowed, f"{ctx.relative(plans_root)}/*.md")
|
|
254
|
-
specs_root = ctx.inputs.get("specs_root")
|
|
255
|
-
if specs_root is not None:
|
|
256
|
-
allowed = (*allowed, f"{ctx.relative(specs_root)}/*.md")
|
|
257
207
|
# A declared root admits both suffixes this gate grades, like `docs_root`
|
|
258
208
|
# -- admitting `.md` and reporting `.mdx` beside it would look like half
|
|
259
209
|
# the declaration was ignored. Escaped: `_matches_any` is `fnmatchcase`, so
|
|
@@ -270,16 +220,12 @@ class MarkdownTrees(Check):
|
|
|
270
220
|
root = ctx.inputs.get(input_name)
|
|
271
221
|
if root is not None:
|
|
272
222
|
excluded = (*excluded, f"{ctx.relative(root)}/{suffix}")
|
|
273
|
-
# Root Markdown
|
|
274
|
-
# files` (the `root-files` declaration). Matched EXACTLY: `_matches_any`
|
|
275
|
-
# is `fnmatchcase`, so a declared name carrying `*`, `?` or `[` must be
|
|
276
|
-
# escaped or it would exclude more than it names. A root name has no `/`,
|
|
277
|
-
# so a nested file of the same name is not the declared one.
|
|
223
|
+
# Root Markdown declared under `[controls.compliance.root] files`, matched exactly (no globs).
|
|
278
224
|
declared = sorted(
|
|
279
|
-
name for name in
|
|
225
|
+
name for name in declared_root_files(ctx.declarations)
|
|
280
226
|
if "/" not in name and name.endswith(self.MARKDOWN_SUFFIXES)
|
|
281
227
|
)
|
|
282
|
-
return (*excluded, *
|
|
228
|
+
return (*excluded, *declared)
|
|
283
229
|
|
|
284
230
|
def graded(self, ctx: Context, files: FileSet) -> Iterable[Report]:
|
|
285
231
|
allowed = self._allowed(ctx)
|
|
@@ -295,13 +241,7 @@ class MarkdownTrees(Check):
|
|
|
295
241
|
yield self.report(
|
|
296
242
|
relative,
|
|
297
243
|
"documentation file outside the allowed docs trees",
|
|
298
|
-
# The remedy names the trees THIS repository declared
|
|
299
|
-
# the same way the rule was graded. It used to read
|
|
300
|
-
# "docs/, plans/, .specs/, or .changeset/" -- two retired trees
|
|
301
|
-
# and a hardcoded literal -- so a reader following it moved the
|
|
302
|
-
# file somewhere no repository has, and the gate failed again.
|
|
303
|
-
# A fix line that cannot be followed is worse than none: it
|
|
304
|
-
# spends the reader's trust and returns nothing.
|
|
244
|
+
# The remedy names the trees THIS repository declared.
|
|
305
245
|
f"move {relative} under one of this repository's declared doc "
|
|
306
246
|
f"trees ({', '.join(declared) if declared else 'none declared'}), "
|
|
307
247
|
"or add it to the exclusion list in this gate if it is a "
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
Two corpora, committed byte-for-byte from the shell tree's fixtures:
|
|
4
4
|
`markdown-trees-stray/` puts one Markdown page outside every allowed tree
|
|
5
5
|
beside one that IS allowed, so the gate must report exactly the first;
|
|
6
|
-
`markdown-trees-clean/` holds
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
`markdown-trees-clean/` holds two files, each allowed for a DIFFERENT reason —
|
|
7
|
+
an allowed tree and a control artifact (`backend/AGENTS.md`) — so that a gate
|
|
8
|
+
reporting every `.md` it sees would fail it. (The crate test fixture it used to
|
|
9
|
+
hold was a consumer-layout exemption, deleted by PBI 598.)
|
|
10
10
|
|
|
11
11
|
This check is not yet wired into `checks/__init__.py` (a later step wires this
|
|
12
12
|
port's whole wave into the registry in one shot, to avoid concurrent edits to
|
|
@@ -40,7 +40,6 @@ CLEAN = FIXTURES / "markdown-trees-clean"
|
|
|
40
40
|
|
|
41
41
|
STRAY_FILES = ("backend/notes.md", "docs/page.md")
|
|
42
42
|
CLEAN_FILES = (
|
|
43
|
-
"backend/crates/enact-example/tests/fixtures/sample.md",
|
|
44
43
|
"backend/AGENTS.md",
|
|
45
44
|
"docs/page.md",
|
|
46
45
|
)
|
|
@@ -54,9 +53,6 @@ def graded(root: pathlib.Path, given: tuple[str, ...]):
|
|
|
54
53
|
inputs={
|
|
55
54
|
"docs_root": root / "docs",
|
|
56
55
|
"plans_root": root / "plans",
|
|
57
|
-
"specs_root": root / ".specs",
|
|
58
|
-
"rust_root": root / "backend",
|
|
59
|
-
"frontend_root": root / "frontend",
|
|
60
56
|
"workflows_root": root / ".workflows",
|
|
61
57
|
},
|
|
62
58
|
)
|
|
@@ -91,13 +87,26 @@ def test_a_clean_corpus_of_allowed_and_excluded_paths_reports_nothing() -> None:
|
|
|
91
87
|
|
|
92
88
|
|
|
93
89
|
def test_each_exclusion_family_is_independently_load_bearing() -> None:
|
|
94
|
-
"""
|
|
95
|
-
against a gate that reports every .md it is handed, and either exclusion
|
|
96
|
-
family (control artifact, crate test fixture) could rot unnoticed."""
|
|
90
|
+
"""A corpus of only allowed-tree files must not pass a gate that reports every .md."""
|
|
97
91
|
assert graded(CLEAN, ("backend/AGENTS.md",)).findings == ()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_a_workflows_coverage_note_is_no_longer_excluded() -> None:
|
|
95
|
+
"""Deliberate: the exemption named one consumer's file; the tree is graded like any other."""
|
|
96
|
+
verdict = graded(CLEAN, (".workflows/COVERAGE.md",))
|
|
97
|
+
assert [f.path for f in verdict.findings] == [".workflows/COVERAGE.md"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_former_rust_and_frontend_exclusions_are_gone() -> None:
|
|
101
|
+
"""PBI 598: the crate-fixture, frontend and session-prompt exclusions are deleted with their trees."""
|
|
102
|
+
former = (
|
|
103
|
+
"backend/crates/x/tests/fixtures/sample.md",
|
|
104
|
+
"backend/crates/x/tests/a/fixture-b/sample.md",
|
|
105
|
+
"frontend/.claude/notes.md",
|
|
106
|
+
"frontend/examples/demo.md",
|
|
107
|
+
)
|
|
108
|
+
verdict = graded(CLEAN, former)
|
|
109
|
+
assert sorted(f.path for f in verdict.findings) == sorted(former), verdict.render()
|
|
101
110
|
|
|
102
111
|
|
|
103
112
|
# ------------------------------------------- intersection, half one: the set
|
|
@@ -131,7 +140,7 @@ def test_a_register_in_the_declared_compliance_tree_is_allowed(tmp_path) -> None
|
|
|
131
140
|
"""A markdown register belongs beside the data it registers.
|
|
132
141
|
|
|
133
142
|
`ALLOWED` is a fixed tuple, so before this the only homes for a `.md` file
|
|
134
|
-
were `docs
|
|
143
|
+
were `docs/` and `plans/` — and a repository
|
|
135
144
|
declaring `compliance = "compliance"` (the tree holding its own ratchets)
|
|
136
145
|
could not put a register there without tripping this gate. The tree is the
|
|
137
146
|
REPOSITORY's declaration; hardcoding `compliance/*.md` here would have been
|
|
@@ -186,8 +195,8 @@ def test_a_redeclared_estate_still_allows_and_excludes_the_resolved_entries(
|
|
|
186
195
|
) -> None:
|
|
187
196
|
"""The LOUD false-finding this check shipped with.
|
|
188
197
|
|
|
189
|
-
`docs/*.md`/`docs/*.mdx
|
|
190
|
-
|
|
198
|
+
`docs/*.md`/`docs/*.mdx` and `plans/*.md` in ALLOWED, and the
|
|
199
|
+
`.workflows/...` entries in EXCLUDED, were
|
|
191
200
|
all hardcoded literals. A repository whose corresponding root is not the
|
|
192
201
|
packaged default had a real, legitimately-admitted or -excluded file fail
|
|
193
202
|
every one of those patterns and get reported "outside the allowed docs
|
|
@@ -199,21 +208,13 @@ def test_a_redeclared_estate_still_allows_and_excludes_the_resolved_entries(
|
|
|
199
208
|
given = (
|
|
200
209
|
"documentation/page.md",
|
|
201
210
|
"roadmap/plan.md",
|
|
202
|
-
"specifications/note.md",
|
|
203
|
-
"webapp/.claude/notes.md",
|
|
204
211
|
"automation/changesets/note.md",
|
|
205
|
-
"rustcode/crates/enact-example/tests/fixture-a/sample.md",
|
|
206
|
-
"webapp/examples/demo.md",
|
|
207
|
-
"automation/COVERAGE.md",
|
|
208
212
|
)
|
|
209
213
|
ctx = Context(
|
|
210
214
|
root=tmp_path, row=ROW,
|
|
211
215
|
inputs={
|
|
212
216
|
"docs_root": tmp_path / "documentation",
|
|
213
217
|
"plans_root": tmp_path / "roadmap",
|
|
214
|
-
"specs_root": tmp_path / "specifications",
|
|
215
|
-
"rust_root": tmp_path / "rustcode",
|
|
216
|
-
"frontend_root": tmp_path / "webapp",
|
|
217
218
|
"workflows_root": tmp_path / "automation",
|
|
218
219
|
},
|
|
219
220
|
)
|
|
@@ -269,13 +270,11 @@ def test_a_declared_root_name_is_excluded_at_the_root_only(tmp_path) -> None:
|
|
|
269
270
|
assert [f.path for f in verdict.findings] == ["backend/STATUS.md"], verdict.render()
|
|
270
271
|
|
|
271
272
|
|
|
272
|
-
def
|
|
273
|
-
"""`
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
verdict = graded_declaring(tmp_path, {"root-files": "*.md"}, ("anything.md",))
|
|
278
|
-
assert [f.path for f in verdict.findings] == ["anything.md"], verdict.render()
|
|
273
|
+
def test_a_declared_name_carrying_glob_characters_is_could_not_run(tmp_path) -> None:
|
|
274
|
+
"""A glob in `root-files` is refused for every gate that reads it, not matched literally."""
|
|
275
|
+
for bad in ("a[b].md", "*.md"):
|
|
276
|
+
with pytest.raises(CouldNotRun, match="root-files"):
|
|
277
|
+
graded_declaring(tmp_path, {"root-files": bad}, ("a[b].md", "ab.md"))
|
|
279
278
|
|
|
280
279
|
|
|
281
280
|
def test_a_root_dirs_declaration_does_not_exclude_a_root_markdown_file(tmp_path) -> None:
|
|
@@ -412,3 +411,7 @@ def test_a_malformed_declared_markdown_root_is_could_not_run_through_the_runner(
|
|
|
412
411
|
)
|
|
413
412
|
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
414
413
|
assert DECLARED_ROOTS in verdict.message
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_widening_inputs_name_only_declared_inputs() -> None:
|
|
417
|
+
assert set(MarkdownTrees.widening_inputs) <= set(MarkdownTrees.inputs)
|
|
@@ -1,43 +1,26 @@
|
|
|
1
|
-
"""LAY-008 — reject
|
|
1
|
+
"""LAY-008 — reject stray artifacts: throwaway files and undeclared images.
|
|
2
2
|
|
|
3
|
-
Ported from `compliance_new/checks/layout/placement/check.sh`, which
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
nothing to contribute.
|
|
3
|
+
Ported from `compliance_new/checks/layout/placement/check.sh`, which graded the
|
|
4
|
+
given set only: no walk, no git call, no self-location. The row is entirely
|
|
5
|
+
`graded()` — the rule is answered by the path SHAPE of a member of the given
|
|
6
|
+
set, so `structural()` has nothing to contribute.
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
One rule, evaluated per path (Tarun, 2026-10-08, PBI 599/601). The shell script's
|
|
9
|
+
other rules are deleted: the root allowlist duplicated LAY-012 (`root-closed-set`),
|
|
10
|
+
and "`apps/` is retired" was one monorepo's history.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
2. apps/ retired any path under the retired `apps/` tree -- unless the
|
|
17
|
-
repository declares `apps` among its root directories
|
|
18
|
-
(`root-dirs`), which says `apps/` is live there.
|
|
19
|
-
3. stray artifacts a throwaway extension (`.log`/`.tmp`/`.bak`/`.swp`,
|
|
20
|
-
checked STRICTLY, everywhere) or a screenshot/image
|
|
21
|
-
basename outside the enumerated asset directories.
|
|
22
|
-
4. md sprawl a `.md` file inside `backend/` or `agent/`'s `src/`
|
|
23
|
-
tree, except the two exempted files.
|
|
12
|
+
stray artifacts a throwaway extension (`.log`/`.tmp`/`.bak`/`.swp`, checked
|
|
13
|
+
STRICTLY, everywhere, asset directories included) or a
|
|
14
|
+
screenshot/image basename outside the asset directories the
|
|
15
|
+
REPOSITORY DECLARES.
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
documents for `FRM-009`/`FRM-010` (both hardcoded a rule literal that had
|
|
30
|
-
drifted from their row). It is reproduced here rather than silently
|
|
31
|
-
corrected, because the port's job is the shell tree's ACTUAL behaviour —
|
|
32
|
-
`test.sh` asserts findings prefixed `^LAY-007 ` — not the behaviour it should
|
|
33
|
-
have had; the mismatch is called out in the porting notes instead.
|
|
17
|
+
The declaration is `asset-dirs` (`--declare asset-dirs=a/b,c`, from
|
|
18
|
+
`[controls.compliance.placement] asset_dirs`), a comma-separated list of
|
|
19
|
+
repository-relative directories. An image anywhere under one is admitted; the
|
|
20
|
+
registry names no consumer layout, so with no declaration every image is reported.
|
|
34
21
|
|
|
35
|
-
The
|
|
36
|
-
|
|
37
|
-
throwaway/screenshot messages read `"-artifact:"` with their lead word gone
|
|
38
|
-
entirely). Message TEXT is not the rule the row grades, so this port writes
|
|
39
|
-
clean prose rather than reproducing the corruption; the logic — which
|
|
40
|
-
extensions, which prefixes, which exemptions — is carried over faithfully.
|
|
22
|
+
**The rule id.** The shell `check.sh` hardcoded `RULE_ID=LAY-007`, a DIFFERENT
|
|
23
|
+
row's id; this port reports under this row's own `LAY-008`.
|
|
41
24
|
"""
|
|
42
25
|
|
|
43
26
|
from __future__ import annotations
|
|
@@ -48,174 +31,48 @@ from typing import ClassVar
|
|
|
48
31
|
|
|
49
32
|
from ....core.check import Check
|
|
50
33
|
from ....core.context import Context, FileSet
|
|
34
|
+
from ....core.errors import CouldNotRun
|
|
51
35
|
from ....core.finding import Report
|
|
52
|
-
from ....policy.root_shape import
|
|
53
|
-
ALLOWED_ROOT_FILES,
|
|
54
|
-
ROOT_DIRS_DECLARATION,
|
|
55
|
-
allowed_root_files,
|
|
56
|
-
declared_root_names,
|
|
57
|
-
)
|
|
36
|
+
from ....policy.root_shape import declared_root_names
|
|
58
37
|
|
|
59
38
|
|
|
60
39
|
class Placement(Check):
|
|
61
40
|
id: ClassVar[str] = "LAY-008"
|
|
62
|
-
# Matches check.sh's hardcoded RULE_ID exactly (see module docstring: this
|
|
63
|
-
# is the shell tree's real, if mismatched, behaviour — not a typo here).
|
|
64
|
-
# Was "LAY-007" — a copy-paste that printed every finding of this gate under
|
|
65
|
-
# the neighbouring gate's id, so a reader chasing a placement violation was
|
|
66
|
-
# sent to the pipeline-templates gate instead.
|
|
67
41
|
rule: ClassVar[str] = "LAY-008"
|
|
68
42
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
# `.attributes` and `.ignore`, which LAY-012 had long since corrected to
|
|
72
|
-
# `.gitattributes` and `.gitignore` — so the same file was permitted by one
|
|
73
|
-
# gate and reported by the other.
|
|
74
|
-
#
|
|
75
|
-
# This is the CENTRAL default only. What is actually graded is the effective
|
|
76
|
-
# set, `allowed_root_files(ctx.declarations)`: this plus the names the
|
|
77
|
-
# repository declares under `[controls.compliance.root] files`.
|
|
78
|
-
ROOT_ALLOWLIST: ClassVar[frozenset[str]] = ALLOWED_ROOT_FILES
|
|
43
|
+
# `[controls.compliance.placement] asset_dirs`, sent as `--declare asset-dirs=a,b`.
|
|
44
|
+
ASSET_DIRS_DECLARATION: ClassVar[str] = "asset-dirs"
|
|
79
45
|
|
|
80
46
|
THROWAWAY_SUFFIXES: ClassVar[tuple[str, ...]] = (
|
|
81
47
|
".log", ".LOG", ".tmp", ".TMP", ".bak", ".BAK", ".swp", ".SWP",
|
|
82
48
|
)
|
|
83
49
|
|
|
84
|
-
inputs: ClassVar[dict[str, str]] = {
|
|
85
|
-
"docs_root": "<docs>", "rust_root": "<rust>",
|
|
86
|
-
"frontend_root": "<frontend>",
|
|
87
|
-
}
|
|
88
|
-
"""FIXED 2026-09-16 (K-42 sweep, approved): every `docs/...`/`backend/...`/
|
|
89
|
-
`.specs/...`/`frontend/...` entry below (ASSET_DIR_PREFIXES,
|
|
90
|
-
ASSET_EXACT_FILES, MD_SRC_EXEMPT, MD_SRC_TREES's `backend/`) was a
|
|
91
|
-
hardcoded literal. `_asset_exempt` is a LOUD shape -- a repository whose
|
|
92
|
-
root is not the packaged default had a real, legitimately-placed asset
|
|
93
|
-
fail the exemption and get reported "looks like a screenshot", a visible
|
|
94
|
-
wrong finding. `MD_SRC_TREES`/`MD_SRC_EXEMPT` are the opposite, SILENT
|
|
95
|
-
shape: a redeclared rust root meant `_md_sprawl`'s own `startswith` never
|
|
96
|
-
matched, so real `.md` sprawl under that tree went unreported entirely.
|
|
97
|
-
`agent/` (MD_SRC_TREES, MD_SRC_EXEMPT) stays literal -- there is no
|
|
98
|
-
`<agent>` tree in `policy/config.toml`. Every other suffix stays literal
|
|
99
|
-
below its resolved root: named subject matter, not a resolvable path."""
|
|
100
|
-
|
|
101
|
-
# Enumerated product asset directories — a screenshot/image basename is
|
|
102
|
-
# legitimate ONLY under one of these (a directory prefix) or as this one
|
|
103
|
-
# exact file. Checked ahead of the screenshot/image basename test, exactly
|
|
104
|
-
# as check.sh's nested `case "$f"` does before falling through to
|
|
105
|
-
# `case "$base"`.
|
|
106
|
-
ASSET_DIR_TEMPLATES: ClassVar[tuple[tuple[str, str], ...]] = (
|
|
107
|
-
("docs_root", "assets/research/mobile-reference/"),
|
|
108
|
-
("rust_root", "crates/enact-app/assets/builtin-assistants/avatars/"),
|
|
109
|
-
("rust_root", "crates/enact-assets/assets/logos/"),
|
|
110
|
-
("frontend_root", "mobile/assets/images/"),
|
|
111
|
-
("frontend_root", "packages/desktop/src/renderer/assets/logos/"),
|
|
112
|
-
("frontend_root", "packages/desktop/src/renderer/assets/themes/"),
|
|
113
|
-
("frontend_root", "public/pwa/"),
|
|
114
|
-
("frontend_root", "resources/"),
|
|
115
|
-
)
|
|
116
|
-
ASSET_EXACT_FILE_TEMPLATES: ClassVar[tuple[tuple[str, str], ...]] = (
|
|
117
|
-
("frontend_root", "tests/e2e/helpers/screenshots.ts"),
|
|
118
|
-
)
|
|
119
|
-
|
|
120
50
|
SCREENSHOT_PATTERN: ClassVar[re.Pattern[str]] = re.compile("screenshot", re.IGNORECASE)
|
|
121
51
|
IMAGE_SUFFIXES: ClassVar[tuple[str, ...]] = (
|
|
122
52
|
".png", ".PNG", ".jpg", ".JPG", ".jpeg", ".JPEG",
|
|
123
53
|
)
|
|
124
54
|
|
|
125
|
-
MD_SRC_EXEMPT_LITERAL: ClassVar[tuple[str, ...]] = (
|
|
126
|
-
"agent/crates/enact-providers/src/snapshots/README.md",
|
|
127
|
-
)
|
|
128
|
-
MD_SRC_EXEMPT_TEMPLATES: ClassVar[tuple[tuple[str, str], ...]] = (
|
|
129
|
-
("rust_root", "crates/enact-session/src/backend/codex_init_prompt.md"),
|
|
130
|
-
)
|
|
131
|
-
MD_SRC_TREES_LITERAL: ClassVar[tuple[str, ...]] = ("agent/",)
|
|
132
|
-
|
|
133
|
-
def _asset_dir_prefixes(self, ctx: Context) -> tuple[str, ...]:
|
|
134
|
-
return tuple(
|
|
135
|
-
f"{ctx.relative(ctx.inputs[input_name])}/{suffix}"
|
|
136
|
-
for input_name, suffix in self.ASSET_DIR_TEMPLATES
|
|
137
|
-
if input_name in ctx.inputs # a widening input: absent -> no entry
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
def _asset_exact_files(self, ctx: Context) -> tuple[str, ...]:
|
|
141
|
-
return tuple(
|
|
142
|
-
f"{ctx.relative(ctx.inputs[input_name])}/{suffix}"
|
|
143
|
-
for input_name, suffix in self.ASSET_EXACT_FILE_TEMPLATES
|
|
144
|
-
if input_name in ctx.inputs
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
def _md_src_exempt(self, ctx: Context) -> tuple[str, ...]:
|
|
148
|
-
return self.MD_SRC_EXEMPT_LITERAL + tuple(
|
|
149
|
-
f"{ctx.relative(ctx.inputs[input_name])}/{suffix}"
|
|
150
|
-
for input_name, suffix in self.MD_SRC_EXEMPT_TEMPLATES
|
|
151
|
-
if input_name in ctx.inputs
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
def _md_src_trees(self, ctx: Context) -> tuple[str, ...]:
|
|
155
|
-
# Decision for a tree declared absent (`rust = ""`, which drops
|
|
156
|
-
# `rust_root` from the resolved inputs): the rust root only contributes
|
|
157
|
-
# an extra PREFIX to the trees whose `src/` Markdown is reported, so a
|
|
158
|
-
# repository with no Rust tree has nothing under it to report and
|
|
159
|
-
# contributes nothing. The literal `agent/` tree still governs. This read
|
|
160
|
-
# `ctx.inputs['rust_root']` unguarded -- a `KeyError` traceback (exit 1),
|
|
161
|
-
# not a verdict, on any `.md` path.
|
|
162
|
-
rust_root = ctx.inputs.get("rust_root")
|
|
163
|
-
if rust_root is None:
|
|
164
|
-
return self.MD_SRC_TREES_LITERAL
|
|
165
|
-
return self.MD_SRC_TREES_LITERAL + (f"{ctx.relative(rust_root)}/",)
|
|
166
|
-
|
|
167
55
|
def graded(self, ctx: Context, files: FileSet) -> Iterable[Report]:
|
|
168
|
-
|
|
169
|
-
# string ordering matches it for the ASCII repository-relative paths
|
|
170
|
-
# this row ever sees.
|
|
56
|
+
asset_dirs = self._asset_dirs(ctx)
|
|
171
57
|
for relative in sorted(files):
|
|
172
|
-
yield from self.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
f"move '{relative}' into an owning subdirectory, or declare it under "
|
|
191
|
-
"[controls.compliance.root] files in this repository's adl-config.toml "
|
|
192
|
-
"if it genuinely belongs at the root",
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
# -- Rule 2 (numbered 4 in check.sh): apps/ is retired ------------------
|
|
196
|
-
|
|
197
|
-
def _apps_retired(self, ctx: Context, relative: str) -> Iterable[Report]:
|
|
198
|
-
# A repository that declares `apps` as a root directory has said `apps/`
|
|
199
|
-
# is live there; "apps is retired" would contradict its own declaration.
|
|
200
|
-
if "apps" in declared_root_names(ctx.declarations, ROOT_DIRS_DECLARATION):
|
|
201
|
-
return
|
|
202
|
-
if relative.startswith("apps/"):
|
|
203
|
-
yield self.report(
|
|
204
|
-
relative,
|
|
205
|
-
f"apps-retired: '{relative}' is staged under retired apps/; "
|
|
206
|
-
"use the owning current tree",
|
|
207
|
-
f"move '{relative}' into frontend/ (or the current owning tree) "
|
|
208
|
-
"and remove it from apps/",
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
# -- Rule 3 (numbered 2 in check.sh): stray test/ artifacts -------------
|
|
212
|
-
|
|
213
|
-
def _stray_artifact(self, ctx: Context, relative: str) -> Iterable[Report]:
|
|
214
|
-
# check.sh's `case "$f" in /*) ;; *) ... esac` skips this whole rule
|
|
215
|
-
# for a path beginning with a literal `/`. The given set is always
|
|
216
|
-
# repository-relative (never absolute), so this arm is inert in
|
|
217
|
-
# practice — carried over rather than dropped, since it is what the
|
|
218
|
-
# shell source actually branches on.
|
|
58
|
+
yield from self._stray_artifact(relative, asset_dirs)
|
|
59
|
+
|
|
60
|
+
def _asset_dirs(self, ctx: Context) -> tuple[str, ...]:
|
|
61
|
+
"""The directories the repository declared as homes for images; malformed ones are could-not-run."""
|
|
62
|
+
dirs: list[str] = []
|
|
63
|
+
for name in sorted(declared_root_names(ctx.declarations, self.ASSET_DIRS_DECLARATION)):
|
|
64
|
+
directory = name.rstrip("/")
|
|
65
|
+
if name.startswith("/") or any(part in ("", ".", "..") for part in directory.split("/")):
|
|
66
|
+
raise CouldNotRun(
|
|
67
|
+
f"{self.id}: `{self.ASSET_DIRS_DECLARATION}` declares {name!r}, which is not "
|
|
68
|
+
"a repository-relative directory; fix [controls.compliance.placement] "
|
|
69
|
+
"asset_dirs in adl-config.toml"
|
|
70
|
+
)
|
|
71
|
+
dirs.append(directory)
|
|
72
|
+
return tuple(dirs)
|
|
73
|
+
|
|
74
|
+
def _stray_artifact(self, relative: str, asset_dirs: tuple[str, ...]) -> Iterable[Report]:
|
|
75
|
+
# check.sh skipped a leading `/`; inert here (paths are repo-relative), kept from the shell source.
|
|
219
76
|
if relative.startswith("/"):
|
|
220
77
|
return
|
|
221
78
|
|
|
@@ -231,38 +88,14 @@ class Placement(Check):
|
|
|
231
88
|
)
|
|
232
89
|
return
|
|
233
90
|
|
|
234
|
-
if
|
|
91
|
+
if any(relative.startswith(f"{directory}/") for directory in asset_dirs):
|
|
235
92
|
return
|
|
236
93
|
|
|
237
94
|
if self.SCREENSHOT_PATTERN.search(base) or base.endswith(self.IMAGE_SUFFIXES):
|
|
238
95
|
yield self.report(
|
|
239
96
|
relative,
|
|
240
|
-
f"'{relative}' looks like a screenshot
|
|
241
|
-
".gitignore
|
|
242
|
-
f"move '{relative}' under a gitignored path or
|
|
243
|
-
"
|
|
97
|
+
f"'{relative}' looks like a screenshot or image outside the declared "
|
|
98
|
+
"asset directories — belongs in .gitignore or an asset directory",
|
|
99
|
+
f"move '{relative}' under a gitignored path, or declare its directory under "
|
|
100
|
+
"[controls.compliance.placement] asset_dirs in this repository's adl-config.toml",
|
|
244
101
|
)
|
|
245
|
-
|
|
246
|
-
def _asset_exempt(self, ctx: Context, relative: str) -> bool:
|
|
247
|
-
if relative in self._asset_exact_files(ctx):
|
|
248
|
-
return True
|
|
249
|
-
return any(relative.startswith(prefix) for prefix in self._asset_dir_prefixes(ctx))
|
|
250
|
-
|
|
251
|
-
# -- Rule 4 (numbered 3 in check.sh): .md sprawl into backend//agent/ src/ --
|
|
252
|
-
|
|
253
|
-
def _md_sprawl(self, ctx: Context, relative: str) -> Iterable[Report]:
|
|
254
|
-
if not relative.endswith(".md"):
|
|
255
|
-
return
|
|
256
|
-
if relative in self._md_src_exempt(ctx):
|
|
257
|
-
return
|
|
258
|
-
if not relative.startswith(self._md_src_trees(ctx)):
|
|
259
|
-
return
|
|
260
|
-
if "/src/" not in relative:
|
|
261
|
-
return
|
|
262
|
-
yield self.report(
|
|
263
|
-
relative,
|
|
264
|
-
f"md-sprawl: '{relative}' is a markdown file inside a src/ tree "
|
|
265
|
-
"— narrative docs belong in docs/ or plans/",
|
|
266
|
-
f"move '{relative}' into docs/ or plans/, or add it to check.py's "
|
|
267
|
-
"src/ Markdown exemption list if it must stay",
|
|
268
|
-
)
|