@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.30 → 0.1.33
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/adl-git-push-policy.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-hooks/src/rules/shell/protected-carriers.ts +19 -5
- package/packages/agent-settings/assets/GLOBAL-AGENTS.md +1 -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/lib/commit-types.ts +8 -0
- package/packages/azdo/src/lib/validate-pr-metadata.ts +4 -1
- package/packages/fleet/manifest.json +2 -1
- package/packages/fleet/src/repo-maintain.ts +5 -2
- package/packages/repo-controls/AGENTS.md +8 -5
- package/packages/repo-controls/README.md +348 -58
- 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/commit-msg +1 -1
- 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 +62 -52
- 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
- package/packages/repo-controls/src/integrity.ts +5 -18
|
@@ -102,12 +102,10 @@ if [[ "$git_config_count" =~ ^[0-9]+$ ]]; then
|
|
|
102
102
|
fi
|
|
103
103
|
unset GIT_CONFIG_COUNT
|
|
104
104
|
|
|
105
|
-
# Static checks run against HEAD
|
|
106
|
-
# worktree, so same-checkout staged/unstaged foreign breakage cannot block delivery.
|
|
107
|
-
# so same-checkout uncommitted foreign breakage cannot block delivery.
|
|
108
|
-
# Reuse a prior pass for the same immutable tree OID under .coverage-cache/.
|
|
105
|
+
# Static checks run against HEAD, keyed by tree + declared checks + export links (see README, No silent skips).
|
|
109
106
|
HEAD_TREE_OID="$(git rev-parse 'HEAD^{tree}')"
|
|
110
|
-
|
|
107
|
+
STATIC_CACHE_KEY="$(printf '%s\n%s\n%s\n' "$HEAD_TREE_OID" "${ADL_REPO_CONTROLS_STATIC_CHECKS?}" "${ADL_REPO_CONTROLS_EXPORT_LINKS:-}" | git hash-object --stdin)"
|
|
108
|
+
STATIC_CACHE="$ROOT/.coverage-cache/pre-push-static/${HEAD_TREE_OID}-${STATIC_CACHE_KEY}.ok"
|
|
111
109
|
if [[ -f "$STATIC_CACHE" ]]; then
|
|
112
110
|
echo "pre-push: reusing static-check pass for tree ${HEAD_TREE_OID}"
|
|
113
111
|
else
|
|
@@ -120,32 +118,42 @@ else
|
|
|
120
118
|
git archive --format=tar -o "$PUSH_ARCHIVE" HEAD
|
|
121
119
|
tar -xf "$PUSH_ARCHIVE" -C "$PUSH_TREE"
|
|
122
120
|
rm -f -- "$PUSH_ARCHIVE"
|
|
123
|
-
#
|
|
124
|
-
# `ln -s "$ROOT/node_modules"` -- a package that calls itself language
|
|
125
|
-
# agnostic assuming JavaScript. A Rust repo links `target/`, a Python one its
|
|
126
|
-
# virtualenv, and a repo with no ecosystem links nothing.
|
|
121
|
+
# Declared untracked paths the export needs; a missing target fails.
|
|
127
122
|
while IFS= read -r export_link; do
|
|
128
123
|
[[ -n "$export_link" ]] || continue
|
|
129
|
-
[[ -e "$ROOT/$export_link" ]]
|
|
124
|
+
if [[ ! -e "$ROOT/$export_link" ]]; then
|
|
125
|
+
echo "pre-push: FAIL — export_links entry '${export_link}' does not exist in the checkout." >&2
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
130
128
|
mkdir -p "$(dirname -- "$PUSH_TREE/$export_link")"
|
|
131
129
|
ln -s "$ROOT/$export_link" "$PUSH_TREE/$export_link"
|
|
132
130
|
done <<<"${ADL_REPO_CONTROLS_EXPORT_LINKS:-}"
|
|
133
131
|
|
|
134
132
|
static_check_labels=()
|
|
135
|
-
while IFS
|
|
136
|
-
[[ -n "$
|
|
133
|
+
while IFS= read -r static_check_line; do
|
|
134
|
+
[[ -n "$static_check_line" ]] || continue
|
|
135
|
+
# Split on the first tab by hand; `read` with a tab IFS collapses a leading tab.
|
|
136
|
+
static_check_label="${static_check_line%%$'\t'*}"
|
|
137
|
+
static_check_command=""
|
|
138
|
+
[[ "$static_check_line" == *$'\t'* ]] && static_check_command="${static_check_line#*$'\t'}"
|
|
139
|
+
if [[ -z "$static_check_label" ]]; then
|
|
140
|
+
echo 'pre-push: FAIL — static check has an empty label.' >&2
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
143
|
+
if [[ -z "$static_check_command" ]]; then
|
|
144
|
+
echo "pre-push: FAIL — static check '${static_check_label}' has an empty command." >&2
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
137
147
|
echo "pre-push: ${static_check_label} (HEAD)"
|
|
138
148
|
(cd "$PUSH_TREE" && bash -c "$static_check_command" </dev/null)
|
|
139
149
|
static_check_labels+=("$static_check_label")
|
|
140
150
|
done <<<"${ADL_REPO_CONTROLS_STATIC_CHECKS?}"
|
|
141
|
-
# The PreToolUse deny corpus is the only thing asserting that destructive
|
|
142
|
-
# commands stay denied, and nothing else ran it — `git update-ref` sat in
|
|
143
|
-
# its ALLOW list while being exactly what strands a shared checkout. Cheap
|
|
144
|
-
# enough to assert on every push.
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
if ((${#static_check_labels[@]} > 0)); then
|
|
153
|
+
mkdir -p "$ROOT/.coverage-cache/pre-push-static"
|
|
154
|
+
printf '%s\n%s\n' "${static_check_labels[*]}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$STATIC_CACHE"
|
|
155
|
+
echo "pre-push: recorded static-check pass for tree ${HEAD_TREE_OID}"
|
|
156
|
+
fi
|
|
149
157
|
fi
|
|
150
158
|
|
|
151
159
|
# The outgoing-commit coverage ratchet moved to `coverageRatchetStep` in
|
|
@@ -7,6 +7,7 @@ import { logHookEvent } from './hook-log.ts'
|
|
|
7
7
|
import { isHookName, isToolName, runHook, runTool, TOOLS } from './hook.ts'
|
|
8
8
|
import { resolveRepo } from './runtime.ts'
|
|
9
9
|
import { type Action, install, uninstall } from './install.ts'
|
|
10
|
+
import { gradeInstalledRepo, UvMissingError } from './install-compliance.ts'
|
|
10
11
|
import type { Env } from './runtime.ts'
|
|
11
12
|
|
|
12
13
|
export interface Io {
|
|
@@ -17,7 +18,8 @@ export interface Io {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const USAGE = `usage:
|
|
20
|
-
${TOOL} install [--repo <path>] [--dry-run]
|
|
21
|
+
${TOOL} install [--repo <path>] [--dry-run] [--json] [--no-grade] [--rewire]
|
|
22
|
+
${TOOL} grade [--repo <path>] (prints the compliance verdict as JSON; exit 0 whatever the verdict)
|
|
21
23
|
${TOOL} uninstall [--repo <path>] [--dry-run]
|
|
22
24
|
${TOOL} doctor [--repo <path>] [--json]
|
|
23
25
|
${TOOL} hook <${HOOK_NAMES.join('|')}> [git hook args...]
|
|
@@ -82,8 +84,14 @@ async function dispatch(argv: string[], io: Io): Promise<number> {
|
|
|
82
84
|
io.out(USAGE)
|
|
83
85
|
return 0
|
|
84
86
|
case 'install': {
|
|
85
|
-
const parsed = parse(command, args, ['--dry-run'])
|
|
86
|
-
const
|
|
87
|
+
const parsed = parse(command, args, ['--dry-run', '--json', '--no-grade', '--rewire'])
|
|
88
|
+
const rewire = parsed.flags.has('--rewire')
|
|
89
|
+
const grade = parsed.flags.has('--no-grade') || rewire ? {} : { grade: { env: io.env } }
|
|
90
|
+
const result = install(parsed.repo, { dryRun: parsed.flags.has('--dry-run'), rewire, ...grade })
|
|
91
|
+
if (parsed.flags.has('--json')) {
|
|
92
|
+
io.out(JSON.stringify(result))
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
87
95
|
printActions(io, result.actions, result.dryRun)
|
|
88
96
|
// Independent review, 2026-09-29, finding 8: a genuine no-op run (and
|
|
89
97
|
// any --dry-run preview) never claims to have "installed" anything —
|
|
@@ -91,6 +99,17 @@ async function dispatch(argv: string[], io: Io): Promise<number> {
|
|
|
91
99
|
if (!result.dryRun && result.actions.length > 0) {
|
|
92
100
|
io.out(`${TOOL} ${VERSION} installed into ${result.repo}`)
|
|
93
101
|
}
|
|
102
|
+
for (const line of result.compliance?.guidance ?? []) io.out(line)
|
|
103
|
+
return 0
|
|
104
|
+
}
|
|
105
|
+
case 'grade': {
|
|
106
|
+
const parsed = parse(command, args, [])
|
|
107
|
+
try {
|
|
108
|
+
io.out(JSON.stringify(gradeInstalledRepo(resolveRepo(parsed.repo), io.env)))
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (!(error instanceof UvMissingError)) throw error
|
|
111
|
+
io.out(JSON.stringify({ ok: false, skipped: 'uv-missing', enabled: [], failing: [], guidance: [error.message] }))
|
|
112
|
+
}
|
|
94
113
|
return 0
|
|
95
114
|
}
|
|
96
115
|
case 'uninstall': {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { isTable, Problems, type Table } from './config.ts'
|
|
4
4
|
import { CONFIG_REL, INSTALL_HINT } from './constants.ts'
|
|
@@ -41,6 +41,8 @@ export interface ComplianceConfig {
|
|
|
41
41
|
* non-empty `roots` into one `--declare markdown-roots=a,b` flag.
|
|
42
42
|
*/
|
|
43
43
|
markdown?: MarkdownDeclaration
|
|
44
|
+
/** LAY-008's extra asset directories from `[controls.compliance.placement]`; omitted when undeclared. */
|
|
45
|
+
placement?: PlacementDeclaration
|
|
44
46
|
/**
|
|
45
47
|
* The manifest and lockfile paths REP-002 scans and grades, from
|
|
46
48
|
* `[controls.compliance.lockfiles]`. OMITTED (not `undefined`-valued) when the
|
|
@@ -138,61 +140,69 @@ interface LimitsDeclaration {
|
|
|
138
140
|
*
|
|
139
141
|
* Nested under `controls`, not a root-level `[compliance]`: see `OWNED_TABLES`.
|
|
140
142
|
*/
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
143
|
+
const TREES_SEED: SeededAttribute = {
|
|
144
|
+
key: 'trees',
|
|
145
|
+
text: `
|
|
146
|
+
# docs, plans, workflows and compliance are the four adl standard trees. Edit them
|
|
147
|
+
# if this repository's layout differs; "" declares "this repository has no such
|
|
148
|
+
# tree". compliance is the register folder: it appears when the first ratchet
|
|
149
|
+
# or exemption register is written, not at install. Pre-commit protects it.
|
|
150
|
+
[controls.compliance.trees]
|
|
151
|
+
docs = "docs"
|
|
152
|
+
plans = "plans"
|
|
153
|
+
workflows = "workflows"
|
|
154
|
+
compliance = "compliance"
|
|
153
155
|
`,
|
|
154
|
-
|
|
155
|
-
]
|
|
156
|
+
}
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
// Every gate in the engine registry, one domain.name address each (strict-gates-registry.test.ts keeps them equal); see README, "Compliance seeding".
|
|
159
|
+
export const STRICT_ENABLED_GATES = [
|
|
160
|
+
'docs.adr-lifecycle',
|
|
161
|
+
'docs.english-only',
|
|
162
|
+
'docs.index-updated',
|
|
163
|
+
'docs.markdown-ast',
|
|
164
|
+
'docs.okf-profile',
|
|
165
|
+
'docs.semantics',
|
|
166
|
+
'docs.indexes',
|
|
167
|
+
'docs.ledger',
|
|
168
|
+
'docs.closed-set',
|
|
169
|
+
'docs.filename',
|
|
170
|
+
'docs.directory-size',
|
|
171
|
+
'layout.agents-layout',
|
|
172
|
+
'layout.ci-paths',
|
|
173
|
+
'layout.directory-ratchet',
|
|
174
|
+
'layout.markdown-trees',
|
|
175
|
+
'layout.placement',
|
|
176
|
+
'layout.plans-layout',
|
|
177
|
+
'layout.references',
|
|
178
|
+
'layout.root-closed-set',
|
|
179
|
+
'layout.root-markdown-allowlist',
|
|
180
|
+
'layout.scripts-size-depth',
|
|
181
|
+
'layout.open-work-cap',
|
|
182
|
+
'lfs.integrity',
|
|
183
|
+
'repository.license-declared',
|
|
184
|
+
'repository.lockfile-integrity',
|
|
185
|
+
'repository.no-legacy-search',
|
|
186
|
+
'repository.ignored-paths',
|
|
187
|
+
'repository.no-vcs-in-scripts',
|
|
188
|
+
'repository.no-multiline-comments',
|
|
189
|
+
] as const
|
|
164
190
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*/
|
|
175
|
-
export function seededComplianceAttributesFor(repo: string): SeededAttribute[] {
|
|
176
|
-
const plansPath = join(repo, 'plans')
|
|
177
|
-
const hasPlans = existsSync(plansPath) && lstatSync(plansPath).isDirectory()
|
|
178
|
-
if (!hasPlans) return SEEDED_COMPLIANCE_ATTRIBUTES
|
|
179
|
-
return [
|
|
180
|
-
{
|
|
181
|
-
key: 'enabled',
|
|
182
|
-
text: `${ENABLED_SEED_COMMENT}
|
|
183
|
-
# This repository has a \`plans/\` tree, so \`docs.ledger\` is pre-armed: a
|
|
184
|
-
# ledger contract with nowhere to enforce it is worse than none (gate-integrity
|
|
185
|
-
# finding, 2026-09-17).
|
|
186
|
-
enabled = ["docs.ledger"]
|
|
187
|
-
`,
|
|
188
|
-
},
|
|
189
|
-
]
|
|
190
|
-
}
|
|
191
|
+
const ENABLED_SEED_TEXT = `# Strict by default (Bug 570): every gate the engine defines is armed from the first commit.
|
|
192
|
+
# To switch a gate off, delete its line; that is this repository's decision, and adl-config.toml
|
|
193
|
+
# is a protected file landed by the owner. A gate id (RST-001), a domain.name address
|
|
194
|
+
# (layout.references) or a whole domain (layout) is accepted; an entry matching no gate is a
|
|
195
|
+
# hard error, never a silent no-op.
|
|
196
|
+
enabled = [
|
|
197
|
+
${STRICT_ENABLED_GATES.map((gate) => ` "${gate}",`).join('\n')}
|
|
198
|
+
]
|
|
199
|
+
`
|
|
191
200
|
|
|
192
|
-
/** `
|
|
193
|
-
export
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
/** Exported: `install` seeds each attribute the repository's `[controls.compliance]` lacks. */
|
|
202
|
+
export const SEEDED_COMPLIANCE_ATTRIBUTES: SeededAttribute[] = [{ key: 'enabled', text: ENABLED_SEED_TEXT }, TREES_SEED]
|
|
203
|
+
|
|
204
|
+
/** Seeded by `install` when `[controls.compliance]` is missing; see README, "Compliance seeding". */
|
|
205
|
+
export const DEFAULT_COMPLIANCE_CONFIG_TEXT = `[controls.compliance]\n${SEEDED_COMPLIANCE_ATTRIBUTES.map((a) => a.text).join('')}`
|
|
196
206
|
|
|
197
207
|
const ENABLED_RE = /^\S+$/
|
|
198
208
|
|
|
@@ -216,13 +226,11 @@ export function parseComplianceConfig(text: string): ComplianceConfig {
|
|
|
216
226
|
const problems = new Problems()
|
|
217
227
|
problems.unknownKeys(
|
|
218
228
|
compliance,
|
|
219
|
-
['enabled', 'registry', 'trees', 'inputs', 'license', 'root', 'limits', 'markdown', 'lockfiles', 'directory-ratchet'],
|
|
229
|
+
['enabled', 'registry', 'trees', 'inputs', 'license', 'root', 'limits', 'markdown', 'placement', 'lockfiles', 'directory-ratchet'],
|
|
220
230
|
'',
|
|
221
231
|
)
|
|
222
232
|
|
|
223
|
-
// Absent is the documented default, never an error
|
|
224
|
-
// and `enabled`, and `registry`/`trees` both have working defaults. Present but the
|
|
225
|
-
// wrong shape is still a hard error.
|
|
233
|
+
// Absent is the documented default, never an error; present but the wrong shape is a hard error (README, "Compliance seeding").
|
|
226
234
|
const enabled = compliance.enabled === undefined ? [] : parseEnabled(compliance.enabled, problems)
|
|
227
235
|
const registry = compliance.registry === undefined ? 'default' : parseRegistry(compliance.registry, problems)
|
|
228
236
|
const trees = compliance.trees === undefined ? {} : parseTrees(compliance.trees, problems, 'trees', true)
|
|
@@ -245,6 +253,7 @@ export function parseComplianceConfig(text: string): ComplianceConfig {
|
|
|
245
253
|
if (compliance.root !== undefined) config.root = parseRoot(compliance.root, problems)
|
|
246
254
|
if (compliance.limits !== undefined) config.limits = parseLimits(compliance.limits, problems)
|
|
247
255
|
if (compliance.markdown !== undefined) config.markdown = parseMarkdown(compliance.markdown, problems)
|
|
256
|
+
if (compliance.placement !== undefined) config.placement = parsePlacement(compliance.placement, problems)
|
|
248
257
|
if (compliance.lockfiles !== undefined) {
|
|
249
258
|
config.lockfiles = parseLockfiles(compliance.lockfiles, problems)
|
|
250
259
|
rejectSupersededInputs(config.lockfiles, inputs, problems)
|
|
@@ -318,15 +327,13 @@ function parseTrees(
|
|
|
318
327
|
return trees
|
|
319
328
|
}
|
|
320
329
|
|
|
321
|
-
/**
|
|
322
|
-
* A root entry is a NAME directly under the repository root, and the whole list
|
|
323
|
-
* travels to the engine as ONE comma-separated `--declare root-files=a,b` value.
|
|
324
|
-
* So a name may not contain `,` (it would split into two names), `=` (it collides
|
|
325
|
-
* with `--declare name=value`'s own separator) or `/` and `..` (a root name is not
|
|
326
|
-
* a path: either would escape the root or reach below it). Empty and
|
|
327
|
-
* whitespace-only names are refused because the engine rejects an empty item.
|
|
328
|
-
*/
|
|
330
|
+
/** Root entry rules (dirs are bare names, files may be relative paths); see README, "Root declarations". */
|
|
329
331
|
const ROOT_NAME_FORBIDDEN = /[,/=]|\.\./
|
|
332
|
+
const ROOT_FILE_FORBIDDEN = /[,=*?[\]{}\\]/
|
|
333
|
+
|
|
334
|
+
function isRootFilePath(item: string): boolean {
|
|
335
|
+
return !ROOT_FILE_FORBIDDEN.test(item) && item.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..')
|
|
336
|
+
}
|
|
330
337
|
|
|
331
338
|
function parseRoot(value: unknown, problems: Problems): RootDeclaration {
|
|
332
339
|
const root: RootDeclaration = { files: [], dirs: [] }
|
|
@@ -346,7 +353,9 @@ function parseRoot(value: unknown, problems: Problems): RootDeclaration {
|
|
|
346
353
|
const label = `root.${key}[${index}]`
|
|
347
354
|
if (typeof item !== 'string') problems.add(`${label} must be a string`)
|
|
348
355
|
else if (item.trim() === '') problems.add(`${label} must not be empty`)
|
|
349
|
-
else if (
|
|
356
|
+
else if (key === 'files' && !isRootFilePath(item)) {
|
|
357
|
+
problems.add(`${label} must be a repository-relative path without a leading '/', a '.', '..' or empty segment, a glob character, ',' or '='`)
|
|
358
|
+
} else if (key === 'dirs' && ROOT_NAME_FORBIDDEN.test(item)) problems.add(`${label} must not contain ',', '/', '=' or '..'`)
|
|
350
359
|
else root[key].push(item)
|
|
351
360
|
})
|
|
352
361
|
}
|
|
@@ -394,6 +403,46 @@ function parseLimits(value: unknown, problems: Problems): LimitsDeclaration {
|
|
|
394
403
|
*/
|
|
395
404
|
const MARKDOWN_ROOT_FORBIDDEN = /[,=]/
|
|
396
405
|
|
|
406
|
+
interface PlacementDeclaration {
|
|
407
|
+
asset_dirs: string[]
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** `[controls.compliance.placement].asset_dirs`: repository-relative directories, validated like markdown roots (no leading '/', no '.', '..' or empty segment, no ',' or '='). */
|
|
411
|
+
function parsePlacement(value: unknown, problems: Problems): PlacementDeclaration {
|
|
412
|
+
const placement: PlacementDeclaration = { asset_dirs: [] }
|
|
413
|
+
if (!isTable(value)) {
|
|
414
|
+
problems.add('placement must be a table')
|
|
415
|
+
return placement
|
|
416
|
+
}
|
|
417
|
+
problems.unknownKeys(value, ['asset_dirs'], 'placement.')
|
|
418
|
+
const list = value.asset_dirs
|
|
419
|
+
if (list === undefined) return placement
|
|
420
|
+
if (!Array.isArray(list)) {
|
|
421
|
+
problems.add('placement.asset_dirs must be an array of strings')
|
|
422
|
+
return placement
|
|
423
|
+
}
|
|
424
|
+
list.forEach((item: unknown, index) => {
|
|
425
|
+
const label = `placement.asset_dirs[${index}]`
|
|
426
|
+
if (typeof item !== 'string') {
|
|
427
|
+
problems.add(`${label} must be a string`)
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
const trimmed = item.trim()
|
|
431
|
+
if (trimmed === '') {
|
|
432
|
+
problems.add(`${label} must not be empty`)
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
const dir = trimmed.replace(/\/+$/, '')
|
|
436
|
+
const bad = trimmed.startsWith('/') || dir.split('/').some((part) => part === '' || part === '.' || part === '..')
|
|
437
|
+
if (MARKDOWN_ROOT_FORBIDDEN.test(dir) || bad) {
|
|
438
|
+
problems.add(`${label} must be a repository-relative directory without a leading '/', a '.', '..' or empty segment, ',' or '='`)
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
placement.asset_dirs.push(dir)
|
|
442
|
+
})
|
|
443
|
+
return placement
|
|
444
|
+
}
|
|
445
|
+
|
|
397
446
|
function parseMarkdown(value: unknown, problems: Problems): MarkdownDeclaration {
|
|
398
447
|
const markdown: MarkdownDeclaration = { roots: [] }
|
|
399
448
|
if (!isTable(value)) {
|
|
@@ -601,6 +650,24 @@ function rejectSupersededInputs(
|
|
|
601
650
|
}
|
|
602
651
|
}
|
|
603
652
|
|
|
653
|
+
/** The repo-relative folder `[controls.compliance.trees].compliance` declares; null when there is no such table, key or the value is `""`. A non-string value throws. */
|
|
654
|
+
export function declaredComplianceTree(configText: string): string | null {
|
|
655
|
+
let raw: unknown
|
|
656
|
+
try {
|
|
657
|
+
raw = Bun.TOML.parse(configText)
|
|
658
|
+
} catch (error) {
|
|
659
|
+
throw new Error(`${CONFIG_REL} is not valid TOML: ${(error as Error).message}`)
|
|
660
|
+
}
|
|
661
|
+
const controls = isTable(raw) && isTable(raw.controls) ? raw.controls : undefined
|
|
662
|
+
const compliance = controls !== undefined && isTable(controls.compliance) ? controls.compliance : undefined
|
|
663
|
+
const trees = compliance !== undefined && isTable(compliance.trees) ? compliance.trees : undefined
|
|
664
|
+
const declared = trees?.compliance
|
|
665
|
+
if (declared === undefined) return null
|
|
666
|
+
if (typeof declared !== 'string') throw new Error(`${CONFIG_REL} [controls.compliance.trees] compliance must be a string`)
|
|
667
|
+
const folder = declared.replace(/^(\.\/)+/, '').replace(/\/+$/, '')
|
|
668
|
+
return folder === '' ? null : folder
|
|
669
|
+
}
|
|
670
|
+
|
|
604
671
|
export function loadComplianceConfig(repo: string): ComplianceConfig {
|
|
605
672
|
const path = join(repo, CONFIG_REL)
|
|
606
673
|
if (!existsSync(path)) throw new Error(`${CONFIG_REL} is missing — ${INSTALL_HINT}`)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { BRANCH_PREFIXES, COMMIT_TYPES } from '../../azdo/src/lib/commit-types.ts'
|
|
3
4
|
import { CONFIG_REL, INSTALL_HINT } from './constants.ts'
|
|
4
5
|
import { MERGE_STRATEGIES, type MergeStrategy } from './promote.ts'
|
|
5
6
|
|
|
@@ -76,10 +77,10 @@ export interface FindingsConfig {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
export interface RepoControlsConfig {
|
|
79
|
-
branches: { protected: string[]
|
|
80
|
+
branches: { protected: string[] }
|
|
80
81
|
preCommit: { staticChecks: StaticCheck[]; stagedCoverage: boolean; docFreshness: boolean; compliance: boolean }
|
|
81
82
|
commitMsg: { staticChecks: StaticCheck[] }
|
|
82
|
-
release: { source: string; target: string; mergeStrategy: MergeStrategy; preBump: string[] }
|
|
83
|
+
release: { optedOut: boolean; source: string; target: string; mergeStrategy: MergeStrategy; preBump: string[] }
|
|
83
84
|
postCommit: { staticChecks: StaticCheck[] }
|
|
84
85
|
prePush: {
|
|
85
86
|
coverageRatchet: boolean
|
|
@@ -101,24 +102,7 @@ export interface RepoControlsConfig {
|
|
|
101
102
|
* `extractTableSpan`/`removeTableSpan` — so it must start with the
|
|
102
103
|
* `[controls]` header line and never contain another line starting with `[`.
|
|
103
104
|
*/
|
|
104
|
-
/**
|
|
105
|
-
* The `[controls]` attributes `install` SCAFFOLDS, each independently.
|
|
106
|
-
*
|
|
107
|
-
* Install wires the hooks; it does not arm a gate. Only the three attributes below
|
|
108
|
-
* are written, and only when absent — an attribute already in the file is never
|
|
109
|
-
* rewritten, reordered or reformatted, whatever its value. Everything else
|
|
110
|
-
* (`pre_commit.*`, `pre_push.coverage_ratchet`, `pre_push.findings_ratchet`,
|
|
111
|
-
* `pre_push.static_checks`) is omitted and defaults to off, so a first install
|
|
112
|
-
* changes what no `git push` or `git commit` does until somebody opts in.
|
|
113
|
-
*
|
|
114
|
-
* `branches` and `pre_push.remote` are seeded rather than omitted because they are
|
|
115
|
-
* not gates — they are the vocabulary the gates are phrased in — and because
|
|
116
|
-
* `pre-push` reads their env vars with `${VAR:?}`, which aborts on an empty value.
|
|
117
|
-
* "No branch policy" therefore cannot be spelled as an empty list.
|
|
118
|
-
*
|
|
119
|
-
* Granularity is the top-level key inside `[controls]`: an inline table is one
|
|
120
|
-
* attribute, because editing inside one means rewriting bytes the user owns.
|
|
121
|
-
*/
|
|
105
|
+
/** The `[controls]` attributes `install` scaffolds when absent; rationale in README, "No silent skips". */
|
|
122
106
|
export interface SeededAttribute {
|
|
123
107
|
key: string
|
|
124
108
|
text: string
|
|
@@ -128,25 +112,42 @@ export const SEEDED_ATTRIBUTES: SeededAttribute[] = [
|
|
|
128
112
|
{
|
|
129
113
|
key: 'branches',
|
|
130
114
|
text: `
|
|
131
|
-
# pre-push refuses to push while one of these branches is checked out; prefixes
|
|
132
|
-
branches = { protected = ["main", "integration"]
|
|
115
|
+
# pre-push refuses to push while one of these branches is checked out; branch prefixes are fixed to the commit types.
|
|
116
|
+
branches = { protected = ["main", "integration"] }
|
|
117
|
+
`,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: 'pre_commit',
|
|
121
|
+
text: `
|
|
122
|
+
# Every gate key is declared; a missing key is a config error. Switch a gate off with an explicit false.
|
|
123
|
+
pre_commit = { staged_coverage = true, doc_freshness = true, compliance = true }
|
|
133
124
|
`,
|
|
134
125
|
},
|
|
135
126
|
{
|
|
136
127
|
key: 'pre_push',
|
|
137
128
|
text: `
|
|
138
129
|
# Remote that pre-push and azdo-push push to and read status from.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
130
|
+
pre_push = { remote = "origin", coverage_ratchet = true, coverage_produce = false, findings_ratchet = true }
|
|
131
|
+
`,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
key: 'release',
|
|
135
|
+
text: `
|
|
136
|
+
# Promotion governance is opted out. To promote through a PR, replace this line with a [controls.release] table declaring source and target.
|
|
137
|
+
release = false
|
|
146
138
|
`,
|
|
147
139
|
},
|
|
148
140
|
]
|
|
149
141
|
|
|
142
|
+
/** The `[controls] integrity` seed for a compliance register folder (README, "Compliance seeding"); appended after `SEEDED_ATTRIBUTES`. */
|
|
143
|
+
export function integritySeed(folder: string): SeededAttribute {
|
|
144
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(folder)) throw new Error(`integrity seed folder has unsafe characters: ${folder}`)
|
|
145
|
+
return {
|
|
146
|
+
key: 'integrity',
|
|
147
|
+
text: `\n# Pre-commit refuses a commit touching the compliance register folder; the owner lands it.\nintegrity = { watched = ["${folder}/**"] }\n`,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
150
151
|
/**
|
|
151
152
|
* Seeded by `install` as the `[controls]` table of the shared `adl-config.toml`
|
|
152
153
|
* when that table is missing; user-owned afterwards. When the table already exists,
|
|
@@ -261,14 +262,14 @@ export function extractTableSpan(text: string, tableName: string): string | null
|
|
|
261
262
|
*/
|
|
262
263
|
function ownKeysEnd(found: TableRanges): { offset: number; atRangeEnd: boolean } | null {
|
|
263
264
|
if (found.headerLine === -1) return null
|
|
264
|
-
const range = found.ranges.find((candidate) => candidate.startLine
|
|
265
|
+
const range = found.ranges.find((candidate) => candidate.startLine <= found.headerLine && found.headerLine < candidate.endLine)
|
|
265
266
|
if (range === undefined) return null
|
|
266
|
-
let sub =
|
|
267
|
+
let sub = found.headerLine + 1
|
|
267
268
|
while (sub < range.endLine && !/^\[/.test(found.lines[sub] ?? '')) sub++
|
|
268
269
|
if (sub >= range.endLine) return { offset: range.end, atRangeEnd: true }
|
|
269
270
|
let at = sub
|
|
270
|
-
while (at - 1 >
|
|
271
|
-
while (at - 1 >
|
|
271
|
+
while (at - 1 > found.headerLine && (found.lines[at - 1] ?? '').trim().startsWith('#')) at--
|
|
272
|
+
while (at - 1 > found.headerLine && (found.lines[at - 1] ?? '').trim() === '') at--
|
|
272
273
|
return { offset: found.starts[at] ?? found.text.length, atRangeEnd: false }
|
|
273
274
|
}
|
|
274
275
|
|
|
@@ -313,12 +314,12 @@ export const RETIRED_ATTRIBUTES = ['version'] as const
|
|
|
313
314
|
export function removeFromTableSpan(text: string, tableName: string, key: string): string {
|
|
314
315
|
const found = findTableRanges(text, tableName)
|
|
315
316
|
const where = ownKeysEnd(found)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const body = text.slice(
|
|
317
|
+
if (where === null) return text
|
|
318
|
+
const headerStart = found.starts[found.headerLine] ?? 0
|
|
319
|
+
const body = text.slice(headerStart, where.offset)
|
|
319
320
|
const assignment = new RegExp(`^${escapeRegExp(key)}\\s*=.*\\n(\\n)?`, 'm')
|
|
320
321
|
if (!assignment.test(body)) return text
|
|
321
|
-
return text.slice(0,
|
|
322
|
+
return text.slice(0, headerStart) + body.replace(assignment, '') + text.slice(where.offset)
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
/** `text` with every range of a table removed (a no-op if there are none); other tables are untouched. */
|
|
@@ -340,7 +341,6 @@ export function extractControlsSpan(text: string): string | null {
|
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
const BRANCH_RE = /^[a-z0-9][a-z0-9/-]*$/
|
|
343
|
-
const PREFIX_RE = /^[a-z0-9][a-z0-9-]*$/
|
|
344
344
|
const LABEL_RE = /^[a-z0-9][a-z0-9-]*$/
|
|
345
345
|
const REMOTE_RE = /^\S+$/
|
|
346
346
|
|
|
@@ -392,7 +392,7 @@ export class Problems {
|
|
|
392
392
|
}
|
|
393
393
|
/**
|
|
394
394
|
* A table this package SEEDS but does not require. Absent means "every control in
|
|
395
|
-
* it is off", never an error — installing
|
|
395
|
+
* it is off", never an error — installing arms no push or commit gate, so a
|
|
396
396
|
* repository that never opted into a control simply has no key for it. Present but
|
|
397
397
|
* the wrong type is still a hard error, and unknown keys inside it still are too.
|
|
398
398
|
*/
|
|
@@ -400,6 +400,14 @@ export class Problems {
|
|
|
400
400
|
if (parent[key] === undefined) return {}
|
|
401
401
|
return this.table(parent, key, known)
|
|
402
402
|
}
|
|
403
|
+
/** A gate key has no default: absent is a config error naming the key. */
|
|
404
|
+
requiredBool(table: Table, key: string, name: string): boolean {
|
|
405
|
+
if (table[key] === undefined) {
|
|
406
|
+
this.add(`${name} is required — declare ${name} = true or ${name} = false (a missing gate key is never defaulted)`)
|
|
407
|
+
return false
|
|
408
|
+
}
|
|
409
|
+
return this.bool(table, key, name)
|
|
410
|
+
}
|
|
403
411
|
/** Absent is the documented default; present-but-not-a-boolean is still an error. */
|
|
404
412
|
optionalBool(table: Table, key: string, name: string, fallback: boolean): boolean {
|
|
405
413
|
if (table[key] === undefined) return fallback
|
|
@@ -479,10 +487,10 @@ export function parseConfig(text: string): RepoControlsConfig {
|
|
|
479
487
|
problems.unknownKeys(controls, ['branches', 'pre_commit', 'pre_push', 'commit_msg', 'post_commit', 'compliance', 'coverage', 'findings', 'integrity', 'release'], '')
|
|
480
488
|
|
|
481
489
|
const branches = problems.table(controls, 'branches', ['protected', 'prefixes'])
|
|
490
|
+
if (branches.prefixes !== undefined) problems.add('branches.prefixes is no longer configurable — delete it; branch prefixes are fixed to the commit types')
|
|
482
491
|
// `branches` and `pre_push.remote` are REQUIRED: install always seeds them, and
|
|
483
492
|
// `pre-push` reads their env vars with `${VAR:?}`, which aborts on an empty value —
|
|
484
|
-
// so "no branch policy" cannot be spelled as an empty list.
|
|
485
|
-
// optional and defaults to off.
|
|
493
|
+
// so "no branch policy" cannot be spelled as an empty list.
|
|
486
494
|
const preCommit = problems.optionalTable(controls, 'pre_commit', ['static_checks', 'staged_coverage', 'doc_freshness', 'compliance'])
|
|
487
495
|
const prePush = problems.table(controls, 'pre_push', ['coverage_ratchet', 'coverage_produce', 'findings_ratchet', 'static_checks', 'export_links', 'remote'])
|
|
488
496
|
// Optional, and every field defaults, so a repository that never writes this
|
|
@@ -520,7 +528,10 @@ export function parseConfig(text: string): RepoControlsConfig {
|
|
|
520
528
|
// Which branch is promoted to which. No default pair: defaulting to
|
|
521
529
|
// integration -> main would put one repository's branch names back into
|
|
522
530
|
// this package by the back door.
|
|
523
|
-
const
|
|
531
|
+
const releaseOptedOut = controls.release === false || (isTable(controls.release) && controls.release.enabled === false)
|
|
532
|
+
if (isTable(controls.release) && controls.release.enabled !== undefined && typeof controls.release.enabled !== 'boolean') problems.add('release.enabled must be a boolean')
|
|
533
|
+
if (controls.release === true) problems.add('release must be a table, or false to opt out of promotion governance')
|
|
534
|
+
const release = controls.release === false || controls.release === true ? {} : problems.optionalTable(controls, 'release', ['enabled', 'source', 'target', 'merge_strategy', 'pre_bump'])
|
|
524
535
|
const releaseMergeStrategy = problems.optionalString(release, 'merge_strategy', 'release.merge_strategy', 'squash')
|
|
525
536
|
if (!Object.hasOwn(MERGE_STRATEGIES, releaseMergeStrategy)) {
|
|
526
537
|
problems.add(`release.merge_strategy must be one of ${Object.keys(MERGE_STRATEGIES).join(', ')}`)
|
|
@@ -529,24 +540,23 @@ export function parseConfig(text: string): RepoControlsConfig {
|
|
|
529
540
|
const config: RepoControlsConfig = {
|
|
530
541
|
branches: {
|
|
531
542
|
protected: problems.names(branches, 'protected', 'branches.protected', BRANCH_RE),
|
|
532
|
-
prefixes: problems.names(branches, 'prefixes', 'branches.prefixes', PREFIX_RE),
|
|
533
543
|
},
|
|
534
544
|
preCommit: {
|
|
535
545
|
// The repository declares WHAT runs at commit and WHICH hook it runs in.
|
|
536
546
|
// Same shape as `pre_push.static_checks`; this package never interprets
|
|
537
547
|
// the command and names no tool.
|
|
538
548
|
staticChecks: preCommit.static_checks === undefined ? [] : parseStaticChecks(preCommit.static_checks, problems, 'pre_commit.static_checks'),
|
|
539
|
-
stagedCoverage: problems.
|
|
540
|
-
docFreshness: problems.
|
|
541
|
-
compliance: problems.
|
|
549
|
+
stagedCoverage: problems.requiredBool(preCommit, 'staged_coverage', 'pre_commit.staged_coverage'),
|
|
550
|
+
docFreshness: problems.requiredBool(preCommit, 'doc_freshness', 'pre_commit.doc_freshness'),
|
|
551
|
+
compliance: problems.requiredBool(preCommit, 'compliance', 'pre_commit.compliance'),
|
|
542
552
|
},
|
|
543
553
|
prePush: {
|
|
544
|
-
coverageRatchet: problems.
|
|
554
|
+
coverageRatchet: problems.requiredBool(prePush, 'coverage_ratchet', 'pre_push.coverage_ratchet'),
|
|
545
555
|
// New gates opt in: the coverage measurement moved to pre-push, so a
|
|
546
556
|
// repository that never writes this key keeps measuring (or not) exactly
|
|
547
557
|
// where it did before.
|
|
548
|
-
coverageProduce: problems.
|
|
549
|
-
findingsRatchet: problems.
|
|
558
|
+
coverageProduce: problems.requiredBool(prePush, 'coverage_produce', 'pre_push.coverage_produce'),
|
|
559
|
+
findingsRatchet: problems.requiredBool(prePush, 'findings_ratchet', 'pre_push.findings_ratchet'),
|
|
550
560
|
staticChecks: prePush.static_checks === undefined ? [] : parseStaticChecks(prePush.static_checks, problems, 'pre_push.static_checks'),
|
|
551
561
|
// Untracked paths the HEAD export needs, e.g. a dependency directory.
|
|
552
562
|
// The hook used to hardcode `node_modules`, which assumed JavaScript in a
|
|
@@ -571,6 +581,7 @@ export function parseConfig(text: string): RepoControlsConfig {
|
|
|
571
581
|
staticChecks: commitMsg.static_checks === undefined ? [] : parseStaticChecks(commitMsg.static_checks, problems, 'commit_msg.static_checks'),
|
|
572
582
|
},
|
|
573
583
|
release: {
|
|
584
|
+
optedOut: releaseOptedOut,
|
|
574
585
|
source: problems.optionalString(release, 'source', 'release.source', ''),
|
|
575
586
|
target: problems.optionalString(release, 'target', 'release.target', ''),
|
|
576
587
|
// The MCP server's PR tool admits only Squash, and that enum is the
|
|
@@ -736,7 +747,8 @@ export function configEnv(repo: string, config: RepoControlsConfig): Record<stri
|
|
|
736
747
|
const env: Record<string, string> = {
|
|
737
748
|
ADL_REPO_CONTROLS_ROOT: repo,
|
|
738
749
|
ADL_REPO_CONTROLS_PROTECTED_BRANCHES: config.branches.protected.join('|'),
|
|
739
|
-
ADL_REPO_CONTROLS_BRANCH_PREFIXES:
|
|
750
|
+
ADL_REPO_CONTROLS_BRANCH_PREFIXES: BRANCH_PREFIXES.join('|'),
|
|
751
|
+
ADL_REPO_CONTROLS_COMMIT_TYPES: COMMIT_TYPES.join('|'),
|
|
740
752
|
ADL_REPO_CONTROLS_COVERAGE_RATCHET: config.prePush.coverageRatchet ? '1' : '0',
|
|
741
753
|
ADL_REPO_CONTROLS_FINDINGS_RATCHET: config.prePush.findingsRatchet ? '1' : '0',
|
|
742
754
|
ADL_REPO_CONTROLS_EXPORT_LINKS: config.prePush.exportLinks.join('\n'),
|
|
@@ -757,7 +769,5 @@ export function configEnv(repo: string, config: RepoControlsConfig): Record<stri
|
|
|
757
769
|
.map(([name, command]) => `${name}\t${command}`)
|
|
758
770
|
.join('\n'),
|
|
759
771
|
}
|
|
760
|
-
// enact-m5's pre-commit honours ADL_SKIP_STAGED_COVERAGE=1; disabled config sets it.
|
|
761
|
-
if (!config.preCommit.stagedCoverage) env.ADL_SKIP_STAGED_COVERAGE = '1'
|
|
762
772
|
return env
|
|
763
773
|
}
|