@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.29 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/adl +2 -2
- package/bin/health-check +2 -2
- package/bin/reconcile +41 -21
- package/bin/setup +2 -1
- package/package.json +1 -1
- package/packages/agent-hooks/README.md +77 -8
- package/packages/agent-hooks/assets/opencode-plugin.template.ts +2 -6
- package/packages/agent-hooks/src/cli/args.ts +2 -2
- package/packages/agent-hooks/src/cli/doctor.ts +2 -1
- package/packages/agent-hooks/src/cli/rules.ts +2 -1
- package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -3
- package/packages/agent-hooks/src/gating/config.ts +11 -0
- package/packages/agent-hooks/src/gating/running-adl.ts +24 -0
- package/packages/agent-hooks/src/hooks/crg/background.ts +7 -4
- package/packages/agent-hooks/src/hooks/log.ts +12 -12
- package/packages/agent-hooks/src/hooks/user-prompt-submit/handler.ts +2 -2
- package/packages/agent-hooks/src/log/writer.ts +170 -24
- package/packages/agent-hooks/src/main.ts +11 -4
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +5 -4
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
- package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
- package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
- package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
- package/packages/azdo/src/azdo-wit.ts +8 -2
- package/packages/azdo/src/mcp-server.ts +1 -1
- package/packages/fleet/manifest.json +2 -1
- package/packages/fleet/src/repo-maintain.ts +5 -2
- package/packages/repo-controls/AGENTS.md +7 -4
- package/packages/repo-controls/README.md +346 -56
- package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
- package/packages/repo-controls/assets/promote.ts +7 -85
- package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
- package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
- package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
- package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
- package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
- package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
- package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
- package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
- package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
- package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
- package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
- package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
- package/packages/repo-controls/assets/version-bump.sh +7 -48
- package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/excluded/web/i18n/locales/zh-CN/strings.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/fresh/fresh.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub1/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub10/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub11/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub2/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub3/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub4/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub5/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub6/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub7/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub8/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub9/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/services/i18n/locales/de/common.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/vendored-crate/Cargo.toml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/c.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/fresh/fresh.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub1/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub10/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub11/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub2/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub3/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub4/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub5/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub6/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub7/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub8/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub9/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/route-target.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/route-target.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
- package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
- package/packages/repo-controls/scripts/hooks/pre-push +38 -20
- package/packages/repo-controls/src/cli.ts +22 -3
- package/packages/repo-controls/src/compliance-config.ts +132 -65
- package/packages/repo-controls/src/config.ts +55 -46
- package/packages/repo-controls/src/constants.ts +9 -0
- package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
- package/packages/repo-controls/src/doctor.ts +20 -30
- package/packages/repo-controls/src/findings-ratchet.ts +14 -0
- package/packages/repo-controls/src/gitignore-block.ts +3 -13
- package/packages/repo-controls/src/hook-log.ts +7 -10
- package/packages/repo-controls/src/hook.ts +127 -83
- package/packages/repo-controls/src/install-compliance.ts +112 -0
- package/packages/repo-controls/src/install.ts +133 -30
|
@@ -22,8 +22,12 @@ push_updates="$(cat)"
|
|
|
22
22
|
# released on EXIT, which serialised THIS HOOK rather than the network push,
|
|
23
23
|
# so the window it claimed to close stayed open the whole time.
|
|
24
24
|
PUSH_TREE=""
|
|
25
|
+
PUSH_ARCHIVE=""
|
|
25
26
|
cleanup_pre_push() {
|
|
26
27
|
local status=$?
|
|
28
|
+
if [[ -n "${PUSH_ARCHIVE}" && -e "${PUSH_ARCHIVE}" ]]; then
|
|
29
|
+
rm -f -- "${PUSH_ARCHIVE}"
|
|
30
|
+
fi
|
|
27
31
|
if [[ -n "${PUSH_TREE}" && -d "${PUSH_TREE}" ]]; then
|
|
28
32
|
rm -rf -- "${PUSH_TREE}"
|
|
29
33
|
fi
|
|
@@ -98,44 +102,58 @@ if [[ "$git_config_count" =~ ^[0-9]+$ ]]; then
|
|
|
98
102
|
fi
|
|
99
103
|
unset GIT_CONFIG_COUNT
|
|
100
104
|
|
|
101
|
-
# Static checks run against HEAD
|
|
102
|
-
# worktree, so same-checkout staged/unstaged foreign breakage cannot block delivery.
|
|
103
|
-
# so same-checkout uncommitted foreign breakage cannot block delivery.
|
|
104
|
-
# 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).
|
|
105
106
|
HEAD_TREE_OID="$(git rev-parse 'HEAD^{tree}')"
|
|
106
|
-
|
|
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"
|
|
107
109
|
if [[ -f "$STATIC_CACHE" ]]; then
|
|
108
110
|
echo "pre-push: reusing static-check pass for tree ${HEAD_TREE_OID}"
|
|
109
111
|
else
|
|
110
112
|
echo 'pre-push: exporting HEAD tree for static checks'
|
|
111
113
|
PUSH_TREE="$(mktemp -d "${TMPDIR:-/tmp}/adl-pre-push-head.XXXXXX")"
|
|
112
|
-
git archive HEAD | tar -x
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
# Not `git archive HEAD | tar -x`: on the macOS CI agent that pipe failed
|
|
115
|
+
# pushes with 141 (SIGPIPE under pipefail) -- a tar that stops reading before
|
|
116
|
+
# git archive finishes writing kills the writer. A file has no reader to lose.
|
|
117
|
+
PUSH_ARCHIVE="${PUSH_TREE}.tar"
|
|
118
|
+
git archive --format=tar -o "$PUSH_ARCHIVE" HEAD
|
|
119
|
+
tar -xf "$PUSH_ARCHIVE" -C "$PUSH_TREE"
|
|
120
|
+
rm -f -- "$PUSH_ARCHIVE"
|
|
121
|
+
# Declared untracked paths the export needs; a missing target fails.
|
|
117
122
|
while IFS= read -r export_link; do
|
|
118
123
|
[[ -n "$export_link" ]] || continue
|
|
119
|
-
[[ -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
|
|
120
128
|
mkdir -p "$(dirname -- "$PUSH_TREE/$export_link")"
|
|
121
129
|
ln -s "$ROOT/$export_link" "$PUSH_TREE/$export_link"
|
|
122
130
|
done <<<"${ADL_REPO_CONTROLS_EXPORT_LINKS:-}"
|
|
123
131
|
|
|
124
132
|
static_check_labels=()
|
|
125
|
-
while IFS
|
|
126
|
-
[[ -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
|
|
127
147
|
echo "pre-push: ${static_check_label} (HEAD)"
|
|
128
148
|
(cd "$PUSH_TREE" && bash -c "$static_check_command" </dev/null)
|
|
129
149
|
static_check_labels+=("$static_check_label")
|
|
130
150
|
done <<<"${ADL_REPO_CONTROLS_STATIC_CHECKS?}"
|
|
131
|
-
# The PreToolUse deny corpus is the only thing asserting that destructive
|
|
132
|
-
# commands stay denied, and nothing else ran it — `git update-ref` sat in
|
|
133
|
-
# its ALLOW list while being exactly what strands a shared checkout. Cheap
|
|
134
|
-
# enough to assert on every push.
|
|
135
151
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
139
157
|
fi
|
|
140
158
|
|
|
141
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}`)
|
|
@@ -79,7 +79,7 @@ export interface RepoControlsConfig {
|
|
|
79
79
|
branches: { protected: string[]; prefixes: string[] }
|
|
80
80
|
preCommit: { staticChecks: StaticCheck[]; stagedCoverage: boolean; docFreshness: boolean; compliance: boolean }
|
|
81
81
|
commitMsg: { staticChecks: StaticCheck[] }
|
|
82
|
-
release: { source: string; target: string; mergeStrategy: MergeStrategy; preBump: string[] }
|
|
82
|
+
release: { optedOut: boolean; source: string; target: string; mergeStrategy: MergeStrategy; preBump: string[] }
|
|
83
83
|
postCommit: { staticChecks: StaticCheck[] }
|
|
84
84
|
prePush: {
|
|
85
85
|
coverageRatchet: boolean
|
|
@@ -101,24 +101,7 @@ export interface RepoControlsConfig {
|
|
|
101
101
|
* `extractTableSpan`/`removeTableSpan` — so it must start with the
|
|
102
102
|
* `[controls]` header line and never contain another line starting with `[`.
|
|
103
103
|
*/
|
|
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
|
-
*/
|
|
104
|
+
/** The `[controls]` attributes `install` scaffolds when absent; rationale in README, "No silent skips". */
|
|
122
105
|
export interface SeededAttribute {
|
|
123
106
|
key: string
|
|
124
107
|
text: string
|
|
@@ -130,23 +113,40 @@ export const SEEDED_ATTRIBUTES: SeededAttribute[] = [
|
|
|
130
113
|
text: `
|
|
131
114
|
# pre-push refuses to push while one of these branches is checked out; prefixes: <prefix>/<kebab>.
|
|
132
115
|
branches = { protected = ["main", "integration"], prefixes = ["feat", "fix", "docs", "chore"] }
|
|
116
|
+
`,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: 'pre_commit',
|
|
120
|
+
text: `
|
|
121
|
+
# Every gate key is declared; a missing key is a config error. Switch a gate off with an explicit false.
|
|
122
|
+
pre_commit = { staged_coverage = true, doc_freshness = true, compliance = true }
|
|
133
123
|
`,
|
|
134
124
|
},
|
|
135
125
|
{
|
|
136
126
|
key: 'pre_push',
|
|
137
127
|
text: `
|
|
138
128
|
# Remote that pre-push and azdo-push push to and read status from.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
129
|
+
pre_push = { remote = "origin", coverage_ratchet = true, coverage_produce = false, findings_ratchet = true }
|
|
130
|
+
`,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: 'release',
|
|
134
|
+
text: `
|
|
135
|
+
# Promotion governance is opted out. To promote through a PR, replace this line with a [controls.release] table declaring source and target.
|
|
136
|
+
release = false
|
|
146
137
|
`,
|
|
147
138
|
},
|
|
148
139
|
]
|
|
149
140
|
|
|
141
|
+
/** The `[controls] integrity` seed for a compliance register folder (README, "Compliance seeding"); appended after `SEEDED_ATTRIBUTES`. */
|
|
142
|
+
export function integritySeed(folder: string): SeededAttribute {
|
|
143
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(folder)) throw new Error(`integrity seed folder has unsafe characters: ${folder}`)
|
|
144
|
+
return {
|
|
145
|
+
key: 'integrity',
|
|
146
|
+
text: `\n# Pre-commit refuses a commit touching the compliance register folder; the owner lands it.\nintegrity = { watched = ["${folder}/**"] }\n`,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
150
|
/**
|
|
151
151
|
* Seeded by `install` as the `[controls]` table of the shared `adl-config.toml`
|
|
152
152
|
* when that table is missing; user-owned afterwards. When the table already exists,
|
|
@@ -261,14 +261,14 @@ export function extractTableSpan(text: string, tableName: string): string | null
|
|
|
261
261
|
*/
|
|
262
262
|
function ownKeysEnd(found: TableRanges): { offset: number; atRangeEnd: boolean } | null {
|
|
263
263
|
if (found.headerLine === -1) return null
|
|
264
|
-
const range = found.ranges.find((candidate) => candidate.startLine
|
|
264
|
+
const range = found.ranges.find((candidate) => candidate.startLine <= found.headerLine && found.headerLine < candidate.endLine)
|
|
265
265
|
if (range === undefined) return null
|
|
266
|
-
let sub =
|
|
266
|
+
let sub = found.headerLine + 1
|
|
267
267
|
while (sub < range.endLine && !/^\[/.test(found.lines[sub] ?? '')) sub++
|
|
268
268
|
if (sub >= range.endLine) return { offset: range.end, atRangeEnd: true }
|
|
269
269
|
let at = sub
|
|
270
|
-
while (at - 1 >
|
|
271
|
-
while (at - 1 >
|
|
270
|
+
while (at - 1 > found.headerLine && (found.lines[at - 1] ?? '').trim().startsWith('#')) at--
|
|
271
|
+
while (at - 1 > found.headerLine && (found.lines[at - 1] ?? '').trim() === '') at--
|
|
272
272
|
return { offset: found.starts[at] ?? found.text.length, atRangeEnd: false }
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -313,12 +313,12 @@ export const RETIRED_ATTRIBUTES = ['version'] as const
|
|
|
313
313
|
export function removeFromTableSpan(text: string, tableName: string, key: string): string {
|
|
314
314
|
const found = findTableRanges(text, tableName)
|
|
315
315
|
const where = ownKeysEnd(found)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const body = text.slice(
|
|
316
|
+
if (where === null) return text
|
|
317
|
+
const headerStart = found.starts[found.headerLine] ?? 0
|
|
318
|
+
const body = text.slice(headerStart, where.offset)
|
|
319
319
|
const assignment = new RegExp(`^${escapeRegExp(key)}\\s*=.*\\n(\\n)?`, 'm')
|
|
320
320
|
if (!assignment.test(body)) return text
|
|
321
|
-
return text.slice(0,
|
|
321
|
+
return text.slice(0, headerStart) + body.replace(assignment, '') + text.slice(where.offset)
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
/** `text` with every range of a table removed (a no-op if there are none); other tables are untouched. */
|
|
@@ -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
|
|
@@ -481,8 +489,7 @@ export function parseConfig(text: string): RepoControlsConfig {
|
|
|
481
489
|
const branches = problems.table(controls, 'branches', ['protected', 'prefixes'])
|
|
482
490
|
// `branches` and `pre_push.remote` are REQUIRED: install always seeds them, and
|
|
483
491
|
// `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.
|
|
492
|
+
// so "no branch policy" cannot be spelled as an empty list.
|
|
486
493
|
const preCommit = problems.optionalTable(controls, 'pre_commit', ['static_checks', 'staged_coverage', 'doc_freshness', 'compliance'])
|
|
487
494
|
const prePush = problems.table(controls, 'pre_push', ['coverage_ratchet', 'coverage_produce', 'findings_ratchet', 'static_checks', 'export_links', 'remote'])
|
|
488
495
|
// Optional, and every field defaults, so a repository that never writes this
|
|
@@ -520,7 +527,10 @@ export function parseConfig(text: string): RepoControlsConfig {
|
|
|
520
527
|
// Which branch is promoted to which. No default pair: defaulting to
|
|
521
528
|
// integration -> main would put one repository's branch names back into
|
|
522
529
|
// this package by the back door.
|
|
523
|
-
const
|
|
530
|
+
const releaseOptedOut = controls.release === false || (isTable(controls.release) && controls.release.enabled === false)
|
|
531
|
+
if (isTable(controls.release) && controls.release.enabled !== undefined && typeof controls.release.enabled !== 'boolean') problems.add('release.enabled must be a boolean')
|
|
532
|
+
if (controls.release === true) problems.add('release must be a table, or false to opt out of promotion governance')
|
|
533
|
+
const release = controls.release === false || controls.release === true ? {} : problems.optionalTable(controls, 'release', ['enabled', 'source', 'target', 'merge_strategy', 'pre_bump'])
|
|
524
534
|
const releaseMergeStrategy = problems.optionalString(release, 'merge_strategy', 'release.merge_strategy', 'squash')
|
|
525
535
|
if (!Object.hasOwn(MERGE_STRATEGIES, releaseMergeStrategy)) {
|
|
526
536
|
problems.add(`release.merge_strategy must be one of ${Object.keys(MERGE_STRATEGIES).join(', ')}`)
|
|
@@ -536,17 +546,17 @@ export function parseConfig(text: string): RepoControlsConfig {
|
|
|
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
|
|
@@ -757,7 +768,5 @@ export function configEnv(repo: string, config: RepoControlsConfig): Record<stri
|
|
|
757
768
|
.map(([name, command]) => `${name}\t${command}`)
|
|
758
769
|
.join('\n'),
|
|
759
770
|
}
|
|
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
771
|
return env
|
|
763
772
|
}
|
|
@@ -69,6 +69,15 @@ export const VERSION_BUMP_REL = '.workflows/version-bump.sh'
|
|
|
69
69
|
/** The standalone CI stage-2 guard: a PR branch must contain integration's tip and be at its version + 1 patch. */
|
|
70
70
|
export const RECONCILE_GUARD_REL = '.workflows/reconcile-guard.sh'
|
|
71
71
|
|
|
72
|
+
/** The folder skeleton `install` bootstraps for the adl standard trees; see README, "Scaffolding". */
|
|
73
|
+
export const SCAFFOLD_ROOT = join(PACKAGE_ROOT, 'assets', 'scaffold')
|
|
74
|
+
export const SCAFFOLD_TREES = ['docs', 'plans', 'workflows'] as const
|
|
75
|
+
/** Repository-root entry files, scaffolded from `assets/scaffold/root/` when absent (never overwritten). */
|
|
76
|
+
export const SCAFFOLD_ROOT_FILES = ['AGENTS.md', 'CLAUDE.md'] as const
|
|
77
|
+
/** The invariants that apply to ALL repositories; install deploys it as `INVARIANTS.md` only when the target has none. */
|
|
78
|
+
export const GLOBAL_INVARIANTS_SOURCE = join(PACKAGE_ROOT, 'assets', 'GLOBAL-INVARIANTS.md')
|
|
79
|
+
export const GLOBAL_INVARIANTS_TARGET = 'INVARIANTS.md'
|
|
80
|
+
|
|
72
81
|
export const MANAGED_ASSETS = [
|
|
73
82
|
{ rel: PROMOTE_REL, source: join(PACKAGE_ROOT, 'assets', 'promote.ts'), mode: 0o755, doctorId: 'promote' },
|
|
74
83
|
{
|