@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/adl +2 -2
- package/bin/health-check +2 -2
- package/bin/reconcile +41 -21
- package/bin/setup +2 -1
- package/package.json +1 -1
- package/packages/agent-hooks/README.md +77 -8
- package/packages/agent-hooks/assets/opencode-plugin.template.ts +2 -6
- package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -3
- package/packages/agent-hooks/src/hooks/crg/background.ts +7 -4
- package/packages/agent-hooks/src/hooks/log.ts +12 -12
- package/packages/agent-hooks/src/log/writer.ts +170 -24
- package/packages/agent-hooks/src/main.ts +1 -1
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
- package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
- package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
- package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
- package/packages/fleet/manifest.json +2 -1
- package/packages/fleet/src/repo-maintain.ts +5 -2
- package/packages/repo-controls/AGENTS.md +7 -4
- package/packages/repo-controls/README.md +346 -56
- package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
- package/packages/repo-controls/assets/promote.ts +7 -85
- package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
- package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
- package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
- package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
- package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
- package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
- package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
- package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
- package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
- package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
- package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
- package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
- package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
- package/packages/repo-controls/assets/version-bump.sh +7 -48
- package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/excluded/web/i18n/locales/zh-CN/strings.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/fresh/fresh.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub1/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub10/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub11/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub2/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub3/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub4/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub5/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub6/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub7/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub8/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub9/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/services/i18n/locales/de/common.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/vendored-crate/Cargo.toml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/c.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/fresh/fresh.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub1/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub10/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub11/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub2/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub3/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub4/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub5/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub6/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub7/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub8/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub9/only.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/web/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/route-target.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/route-target.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
- package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
- package/packages/repo-controls/scripts/hooks/pre-push +27 -19
- package/packages/repo-controls/src/cli.ts +22 -3
- package/packages/repo-controls/src/compliance-config.ts +132 -65
- package/packages/repo-controls/src/config.ts +55 -46
- package/packages/repo-controls/src/constants.ts +9 -0
- package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
- package/packages/repo-controls/src/doctor.ts +20 -30
- package/packages/repo-controls/src/findings-ratchet.ts +14 -0
- package/packages/repo-controls/src/gitignore-block.ts +3 -13
- package/packages/repo-controls/src/hook-log.ts +7 -10
- package/packages/repo-controls/src/hook.ts +127 -83
- package/packages/repo-controls/src/install-compliance.ts +112 -0
- package/packages/repo-controls/src/install.ts +133 -30
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""REP-007 (`repository.no-multiline-comments`), Bug 602."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from fixture_trees import fixture_trees
|
|
10
|
+
|
|
11
|
+
from adl_compliance import runner
|
|
12
|
+
from adl_compliance.core.verdict import Outcome, Verdict
|
|
13
|
+
from adl_compliance.policy.registry import load
|
|
14
|
+
|
|
15
|
+
ROW = load().by_id("REP-007")
|
|
16
|
+
REGISTER_PATH = f"{fixture_trees()['compliance']}/ratchets/REP-007-multiline-comments.yaml"
|
|
17
|
+
FIX = "move the explanation to docs/ and leave at most a one-line pointer"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def tree(tmp_path: Path, files: Mapping[str, str], register: str | None = None) -> Path:
|
|
21
|
+
root = tmp_path / "repo"
|
|
22
|
+
root.mkdir(parents=True)
|
|
23
|
+
for rel, body in files.items():
|
|
24
|
+
target = root / rel
|
|
25
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
target.write_text(body, encoding="utf-8")
|
|
27
|
+
if register is not None:
|
|
28
|
+
target = root / REGISTER_PATH
|
|
29
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
target.write_text(register, encoding="utf-8")
|
|
31
|
+
return root
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def register_of(*paths: str) -> str:
|
|
35
|
+
return "schema_version: 1\nfiles:\n" + "".join(f" - {p}\n" for p in paths) if paths else (
|
|
36
|
+
"schema_version: 1\nfiles: []\n"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def grade(root: Path, *, files: tuple[str, ...] | None = None) -> Verdict:
|
|
41
|
+
given = files if files is not None else tuple(
|
|
42
|
+
sorted(
|
|
43
|
+
str(p.relative_to(root)) for p in root.rglob("*")
|
|
44
|
+
if p.is_file() and str(p.relative_to(root)) != REGISTER_PATH
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return runner.run(ROW, root, given, exhaustive=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
BANNED = {
|
|
51
|
+
"two-line-comments.ts": "const a = 1;\n// one\n// two\nconst b = 2;\n",
|
|
52
|
+
"two-line-comments.tsx": "// one\n// two\nexport const x = 1;\n",
|
|
53
|
+
"two-line-comments.js": "// one\n// two\n",
|
|
54
|
+
"two-line-comments.mjs": "// one\n// two\n",
|
|
55
|
+
"two-line-comments.cjs": "// one\n// two\n",
|
|
56
|
+
"doc-block.ts": "/**\n * Explains.\n */\nexport const x = 1;\n",
|
|
57
|
+
"plain-block.ts": "/* one\n two */\nexport const x = 1;\n",
|
|
58
|
+
"plain-block.rs": "/* one\n two */\nfn main() {}\n",
|
|
59
|
+
"doc-lines.rs": "/// one\n/// two\nfn main() {}\n",
|
|
60
|
+
"hash-pair.py": "x = 1\n# one\n# two\n",
|
|
61
|
+
"docstring.py": 'def f():\n """One.\n\n Two.\n """\n',
|
|
62
|
+
"module-docstring.py": '"""One.\nTwo."""\n',
|
|
63
|
+
"hash-pair.sh": "#!/bin/sh\n# one\n# two\necho hi\n",
|
|
64
|
+
"hash-pair.bash": "# one\n# two\n",
|
|
65
|
+
"hash-pair.zsh": "# one\n# two\n",
|
|
66
|
+
"justfile": "# one\n# two\nbuild:\n echo hi\n",
|
|
67
|
+
"nested/justfile": "build:\n echo hi\n# one\n# two\n",
|
|
68
|
+
"Justfile": "# one\n# two\nbuild:\n echo hi\n",
|
|
69
|
+
"two-line-comments.jsx": "// one\n// two\n",
|
|
70
|
+
"two-line-comments.mts": "// one\n// two\n",
|
|
71
|
+
"two-line-comments.cts": "// one\n// two\n",
|
|
72
|
+
"bin/py-tool": "#!/usr/bin/env python3\n# one\n# two\nx = 1\n",
|
|
73
|
+
"bin/py-direct": "#!/usr/bin/python\nx = 1\n# one\n# two\n",
|
|
74
|
+
"bin/sh-tool": "#!/bin/sh\n# one\n# two\n",
|
|
75
|
+
"bin/bash-env": "#!/usr/bin/env bash\n# one\n# two\n",
|
|
76
|
+
"bin/env-s": "#!/usr/bin/env -S bash -e\n# one\n# two\n",
|
|
77
|
+
"bin/node-tool": "#!/usr/bin/env node\n// one\n// two\n",
|
|
78
|
+
"heredoc-then-pair.sh": "cat <<'EOF'\n# not\n# comment\nEOF\n# real\n# pair\n",
|
|
79
|
+
"heredoc-dash-then-pair.sh": "cat <<-EOF\n\t# x\n\t# y\n\tEOF\n# real\n# pair\n",
|
|
80
|
+
"heredoc-in-string.sh": 'echo "use <<END here"\n# real\n# pair\n',
|
|
81
|
+
"heredoc-in-single-quotes.sh": "echo 'use <<END here'\n# real\n# pair\n",
|
|
82
|
+
"heredoc-in-comment.sh": "echo hi # <<EOF\n# real\n# pair\n",
|
|
83
|
+
"heredoc-arithmetic.sh": "x=$((1<<FOO))\n# real\n# pair\n",
|
|
84
|
+
"here-string.sh": "cat <<<WORD\n# real\n# pair\n",
|
|
85
|
+
"two-heredocs.sh": "cat <<A <<B\n# x\nA\n# y\nB\n# real\n# pair\n",
|
|
86
|
+
"shellcheck-pair.sh": "# explanation one\n# shellcheck is why\n",
|
|
87
|
+
"noqa-pair.py": "# a long explanation\n# see noqa policy\nx = 1\n",
|
|
88
|
+
"type-ignore-pair.py": "# first\n# the type: ignore story\nx = 1\n",
|
|
89
|
+
"pyright-pair.py": "# first\n# about pyright: modes\nx = 1\n",
|
|
90
|
+
"directive-pair.ts": "// first\n// why eslint-disable exists\nconst a = 1;\n",
|
|
91
|
+
"two-one-line-blocks.ts": "/* one */\n/* two */\nconst x = 1;\n",
|
|
92
|
+
"doc-then-line.ts": "/** one */\n// two\nconst x = 1;\n",
|
|
93
|
+
"line-then-block.ts": "// one\n/* two */\nconst x = 1;\n",
|
|
94
|
+
"cr-only.ts": "// a\r// b\rconst a = 1;\r",
|
|
95
|
+
"cr-only.py": "# a\r# b\rx = 1\r",
|
|
96
|
+
"bin/mksh-tool": "#!/bin/mksh\n# one\n# two\n",
|
|
97
|
+
"bin/ksh93-env": "#!/usr/bin/env ksh93\n# one\n# two\n",
|
|
98
|
+
"bin/env-glued-S": "#!/usr/bin/env -Sbash\n# one\n# two\n",
|
|
99
|
+
"bin/env-spaced-S": "#!/usr/bin/env -S bash\n# one\n# two\n",
|
|
100
|
+
"bin/env-split-string": "#!/usr/bin/env --split-string=bash -e\n# one\n# two\n",
|
|
101
|
+
"jsx-comments.tsx": "export const a = (\n <div>\n {/* a */}\n {/* b */}\n </div>\n);\n",
|
|
102
|
+
"jsx-comments.jsx": "export const a = (\n <div>\n {/* a */}\n {/* b */}\n </div>\n);\n",
|
|
103
|
+
"jsx-comments.js": "export const a = (\n <div>\n { /* a */ }\n {/* b */}\n </div>\n);\n",
|
|
104
|
+
"fstring-statement.py": 'f"""a\n{1}\nb"""\n',
|
|
105
|
+
"ansi-c-quote.sh": "echo $'it\\'s'\n# real one\n# real two\n",
|
|
106
|
+
"backslash-heredoc.sh": "cat <<\\EOF\ndon't\nEOF\n# real one\n# real two\n",
|
|
107
|
+
"heredoc-dash-delimiter.sh": "cat <<END-X\nbody\nEND-X\n# real one\n# real two\n",
|
|
108
|
+
"heredoc-dot-delimiter.sh": "cat <<E.O\nbody\nE.O\n# real one\n# real two\n",
|
|
109
|
+
"heredoc-partly-quoted.sh": 'cat <<E"OF"\nbody\nEOF\n# real one\n# real two\n',
|
|
110
|
+
"multiline-arith.sh": "((\n x = 1 << FOO\n))\n# real one\n# real two\n",
|
|
111
|
+
"old-arith.sh": "y=$[1<<FOO]\n# real one\n# real two\n",
|
|
112
|
+
"nested-quotes-in-subst.sh": 'x="$(echo "it\'s")"\n# real one\n# real two\n',
|
|
113
|
+
"nested-quotes-in-backticks.sh": 'x="`echo "it\'s"`"\n# real one\n# real two\n',
|
|
114
|
+
"nested-quotes-in-param.sh": 'x="${y:-"it\'s"}"\n# real one\n# real two\n',
|
|
115
|
+
"backtick-apostrophe.sh": "x=`echo it\\'s`\n# real one\n# real two\n",
|
|
116
|
+
"param-strip-quote.sh": "y=${x#'a'}\n# real one\n# real two\n",
|
|
117
|
+
"two-heredocs-then-pair.sh": "cat <<A <<'B'\n# a\nA\n# b\nB\n# real one\n# real two\n",
|
|
118
|
+
"heredoc-in-subst.sh": "x=$(cat <<EOF\n# a\n# b\nEOF\n)\n# real one\n# real two\n",
|
|
119
|
+
"directive-between-prose.sh": "# prose a\n# shellcheck disable=SC2034\n# prose b\n",
|
|
120
|
+
"directive-between-prose.py": "# prose a\n# noqa\n# prose b\nx = 1\n",
|
|
121
|
+
"directive-between-prose.ts": "// prose a\n// @ts-ignore\n// prose b\n",
|
|
122
|
+
"directive-then-prose.py": "# noqa: E501 this is a long explanation\n# noqa: E501 that continues here\nx = 1\n",
|
|
123
|
+
"type-ignore-prose.py": "# type: ignore because the library is\n# type: ignore untyped and we cannot fix\nx = 1\n",
|
|
124
|
+
"eslint-reason-prose.ts": "// eslint-disable-next-line no-x -- long reason\n// eslint-disable-next-line no-y -- more reason\nx;\n",
|
|
125
|
+
"two-blocks-per-line.ts": "/* a */ /* b */\n/* c */ /* d */\n",
|
|
126
|
+
"bytes-statement.py": "b'''a\nb'''\n",
|
|
127
|
+
"consecutive-string-statements.py": "'a'\n'b'\n'c'\n",
|
|
128
|
+
"bin/env-u-bash": "#!/usr/bin/env -u FOO bash\n# one\n# two\n",
|
|
129
|
+
"bin/uv-script": "#!/usr/bin/env -S uv run --script\n# one\n# two\nx = 1\n",
|
|
130
|
+
"bin/tsx-tool": "#!/usr/bin/env tsx\n// one\n// two\n",
|
|
131
|
+
"x.just": "# one\n# two\nbuild:\n echo hi\n",
|
|
132
|
+
".justfile": "# one\n# two\nbuild:\n echo hi\n",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.parametrize(("name", "body"), sorted(BANNED.items()))
|
|
137
|
+
def test_each_banned_shape_is_reported_at_file_and_line(tmp_path: Path, name: str, body: str) -> None:
|
|
138
|
+
root = tree(tmp_path, {name: body})
|
|
139
|
+
verdict = grade(root)
|
|
140
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
141
|
+
assert {f.rule for f in verdict.findings} == {"MLC-001"}
|
|
142
|
+
finding = verdict.findings[0]
|
|
143
|
+
assert finding.path == name
|
|
144
|
+
assert finding.line is not None and finding.line >= 1
|
|
145
|
+
assert f"{name}:{finding.line}:" in finding.render()
|
|
146
|
+
assert FIX in finding.fix
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_the_reported_line_is_where_the_comment_starts(tmp_path: Path) -> None:
|
|
150
|
+
root = tree(tmp_path, {"a.ts": "const a = 1;\n\n// one\n// two\n"})
|
|
151
|
+
assert [f.line for f in grade(root).findings] == [3]
|
|
152
|
+
root = tree(tmp_path / "b", {"a.ts": "const a = 1;\n/* one\n two */\n"})
|
|
153
|
+
assert [f.line for f in grade(root).findings] == [2]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
ALLOWED = {
|
|
157
|
+
"single.ts": "// one\nconst a = 1;\n// two\nconst b = 2;\n",
|
|
158
|
+
"single.py": "# one\nx = 1\n# two\ny = 2\n",
|
|
159
|
+
"one-line-docblock.ts": "/** One. */\nexport const x = 1;\n",
|
|
160
|
+
"one-line-block.ts": "/* one */\nexport const x = 1;\n",
|
|
161
|
+
"one-line-docstring.py": '"""One."""\n',
|
|
162
|
+
"shebang-then-one.sh": "#!/usr/bin/env bash\n# one\necho hi\n",
|
|
163
|
+
"shebang-only.py": "#!/usr/bin/env python3\nx = 1\n",
|
|
164
|
+
"ts-expect-error.ts": "// @ts-expect-error legacy\nconst a: number = 'x';\n// @ts-ignore\nconst b = 1;\n",
|
|
165
|
+
"eslint.ts": "// eslint-disable-next-line no-console\nconsole.log(1);\n",
|
|
166
|
+
"biome.ts": "// biome-ignore lint/suspicious/noExplicitAny: legacy\nconst a: any = 1;\n",
|
|
167
|
+
"noqa.py": "import os # noqa: F401\nimport sys # type: ignore\n",
|
|
168
|
+
"directive-pair-apart.py": "# type: ignore\nx = 1\n# noqa\ny = 2\n",
|
|
169
|
+
"trailing.ts": "const a = 1; // one\nconst b = 2; // two\n",
|
|
170
|
+
"trailing.sh": "echo a # one\necho b # two\n",
|
|
171
|
+
"string-lines.ts": "const s = `\n// not a comment\n// not a comment\n`;\n",
|
|
172
|
+
"string-url.ts": "const u = 'http://example.com'; const v = \"//x\";\n",
|
|
173
|
+
"string-block.ts": "const s = '/* not\\n a comment */';\n",
|
|
174
|
+
"regex.ts": "const r = /\\/\\/+/g;\nconst q = /[\"']/;\n",
|
|
175
|
+
"string-hash.py": 'x = """\n# not a comment\n# not a comment\n"""\n',
|
|
176
|
+
"heredoc.sh": "cat <<'EOF'\n# not a comment\n# not a comment\nEOF\n",
|
|
177
|
+
"rust-char.rs": "fn f() -> char { '\"' }\nfn g<'a>(x: &'a str) {}\n",
|
|
178
|
+
"rust-raw.rs": 'const S: &str = r#"\n// not\n// comments\n"#;\n',
|
|
179
|
+
"notes.md": "# one\n# two\n\n/* a\n b */\n",
|
|
180
|
+
"data.yaml": "# one\n# two\n",
|
|
181
|
+
"LICENSE": "# one\n# two\n",
|
|
182
|
+
"bin/plain-text": "not a script\n# one\n# two\n",
|
|
183
|
+
"bin/other-interpreter": "#!/usr/bin/env ruby\n# one\n# two\n",
|
|
184
|
+
"bin/one-comment": "#!/bin/sh\n# one\necho hi\n",
|
|
185
|
+
"multiline-string.sh": 'echo "\n# not\n# comment\n"\n',
|
|
186
|
+
"multiline-single-quote.sh": "echo '\n# not\n# comment\n'\n",
|
|
187
|
+
"arith-then-code.sh": "x=$((1<<FOO))\necho hi\n",
|
|
188
|
+
"directive-only-run.ts": "// eslint-disable-next-line a\n// @ts-expect-error b\nconst a = 1;\n",
|
|
189
|
+
"block-directive-pair.ts": "/* eslint-disable */\n/* eslint-enable */\nconst a = 1;\n",
|
|
190
|
+
"separated-blocks.ts": "/* one */\nconst a = 1;\n/* two */\nconst b = 2;\n",
|
|
191
|
+
"brace-then-line-comment.js": "{} // a\n// b\n",
|
|
192
|
+
"brace-then-line-comment.tsx": "{} // a\n// b\n",
|
|
193
|
+
"brace-block-in-ts.ts": "{/* a */}\n{/* b */}\n",
|
|
194
|
+
"brace-two-blocks.js": "{/* a */ /* b */}\n{/* c */ /* d */}\n",
|
|
195
|
+
"brace-wrong-order.js": "}/* a */{\n}/* b */{\n",
|
|
196
|
+
"jsx-single-comment.tsx": "export const a = (\n <div>\n {/* a */}\n <b />\n </div>\n);\n",
|
|
197
|
+
"jsx-brace-code.tsx": "const a = {\n b: 1, /* a */\n};\nconst c = {};\n",
|
|
198
|
+
"trailing-blocks.ts": "const a = 1; /* one */\nconst b = 2; /* two */\n",
|
|
199
|
+
"backslash-heredoc-hashes.sh": "cat <<\\EOF\n# x\n# y\nEOF\necho\n",
|
|
200
|
+
"heredoc-indented-terminator.sh": "cat <<EOF\n EOF\n# x\n# y\nEOF\necho\n",
|
|
201
|
+
"two-heredocs-hashes.sh": "cat <<A <<'B'\n# a\n# a2\nA\n# b\n# b2\nB\necho\n",
|
|
202
|
+
"heredoc-tab-hashes.sh": "cat <<-EOF\n\t# a\n\t# b\n\tEOF\necho\n",
|
|
203
|
+
"directive-then-one-prose.py": "# noqa\n# prose\nx = 1\n",
|
|
204
|
+
"directive-with-code-list.py": "# noqa: E501, F401\n# noqa: S101\nx = 1\n",
|
|
205
|
+
"single-string-statements-apart.py": "'a'\nx = 1\n'b'\n",
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@pytest.mark.parametrize(("name", "body"), sorted(ALLOWED.items()))
|
|
210
|
+
def test_each_allowed_shape_is_silent(tmp_path: Path, name: str, body: str) -> None:
|
|
211
|
+
root = tree(tmp_path, {name: body})
|
|
212
|
+
verdict = grade(root)
|
|
213
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_the_banned_and_allowed_corpora_are_not_empty() -> None:
|
|
217
|
+
assert len(BANNED) >= 15 and len(ALLOWED) >= 20
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_fixture_files_are_excluded_by_the_row_scope(tmp_path: Path) -> None:
|
|
221
|
+
root = tree(tmp_path, {"checks/x/fixtures/bad.ts": "// one\n// two\n", "ok.ts": "// one\n"})
|
|
222
|
+
verdict = grade(root)
|
|
223
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ------------------------------------------------------------------ the register
|
|
227
|
+
def test_an_absent_register_means_no_grants(tmp_path: Path) -> None:
|
|
228
|
+
root = tree(tmp_path, {"a.ts": "// one\n// two\n"})
|
|
229
|
+
assert not (root / REGISTER_PATH).exists()
|
|
230
|
+
verdict = grade(root)
|
|
231
|
+
assert verdict.outcome is Outcome.FINDINGS
|
|
232
|
+
assert [f.path for f in verdict.findings] == ["a.ts"]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_a_file_on_the_register_is_exempt(tmp_path: Path) -> None:
|
|
236
|
+
root = tree(
|
|
237
|
+
tmp_path, {"a.ts": "// one\n// two\n", "b.py": "x = 1\n"}, register_of("a.ts")
|
|
238
|
+
)
|
|
239
|
+
verdict = grade(root)
|
|
240
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_every_file_off_the_register_must_still_be_clean(tmp_path: Path) -> None:
|
|
244
|
+
root = tree(
|
|
245
|
+
tmp_path,
|
|
246
|
+
{"a.ts": "// one\n// two\n", "new.ts": "// one\n// two\n"},
|
|
247
|
+
register_of("a.ts"),
|
|
248
|
+
)
|
|
249
|
+
verdict = grade(root)
|
|
250
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("MLC-001", "new.ts")]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_a_register_entry_with_no_multiline_comment_left_is_stale(tmp_path: Path) -> None:
|
|
254
|
+
root = tree(tmp_path, {"a.ts": "// one\nconst a = 1;\n"}, register_of("a.ts"))
|
|
255
|
+
verdict = grade(root)
|
|
256
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
257
|
+
[finding] = verdict.findings
|
|
258
|
+
assert (finding.rule, finding.path) == ("MLC-002", "a.ts")
|
|
259
|
+
assert "remove it from the register: the ratchet only shrinks" in finding.fix
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_a_register_entry_for_a_missing_file_is_stale(tmp_path: Path) -> None:
|
|
263
|
+
root = tree(tmp_path, {"a.ts": "const a = 1;\n"}, register_of("gone.ts"))
|
|
264
|
+
verdict = grade(root)
|
|
265
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("MLC-002", "gone.ts")]
|
|
266
|
+
assert "no longer exists" in verdict.findings[0].message
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_staleness_does_not_depend_on_the_set_naming_the_file(tmp_path: Path) -> None:
|
|
270
|
+
root = tree(tmp_path, {"a.ts": "// one\n// two\n", "b.ts": "x\n"}, register_of("a.ts"))
|
|
271
|
+
assert grade(root, files=("b.ts",)).outcome is Outcome.CLEAN
|
|
272
|
+
root = tree(tmp_path / "s", {"a.ts": "const a = 1;\n", "b.ts": "x\n"}, register_of("a.ts"))
|
|
273
|
+
verdict = grade(root, files=("b.ts",))
|
|
274
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("MLC-002", "a.ts")]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@pytest.mark.parametrize(
|
|
278
|
+
"body",
|
|
279
|
+
[
|
|
280
|
+
"files: [a.ts]\n",
|
|
281
|
+
"schema_version: 2\nfiles: [a.ts]\n",
|
|
282
|
+
"schema_version: 1\n",
|
|
283
|
+
"schema_version: 1\nfiles: [a.ts, a.ts]\n",
|
|
284
|
+
"schema_version: 1\nfiles: ['../a.ts']\n",
|
|
285
|
+
"schema_version: 1\nfiles: [a.ts]\nextra: 1\n",
|
|
286
|
+
"schema_version: 1\nfiles: {a.ts: 3}\n",
|
|
287
|
+
"not: [valid\n",
|
|
288
|
+
],
|
|
289
|
+
)
|
|
290
|
+
def test_a_register_that_exists_but_is_unusable_is_could_not_run(tmp_path: Path, body: str) -> None:
|
|
291
|
+
root = tree(tmp_path, {"a.ts": "// one\n// two\n"}, body)
|
|
292
|
+
verdict = grade(root)
|
|
293
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
294
|
+
assert "ratchet register" in verdict.message
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_the_register_row_scope_selects_the_register_itself() -> None:
|
|
298
|
+
from adl_compliance.core import scope
|
|
299
|
+
|
|
300
|
+
assert scope.in_scope(ROW.engine.scope, REGISTER_PATH)
|
|
301
|
+
assert not scope.in_scope(ROW.engine.scope, "x/fixtures/a.ts")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_the_row_is_addressable_by_domain_and_name() -> None:
|
|
305
|
+
assert load().resolve("repository.no-multiline-comments").id == "REP-007"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_the_committed_valid_register_exempts_exactly_what_it_names(tmp_path: Path) -> None:
|
|
309
|
+
body = (Path(__file__).parent / "fixtures" / "valid-register.yaml").read_text(encoding="utf-8")
|
|
310
|
+
both = "// one\n// two\n"
|
|
311
|
+
root = tree(
|
|
312
|
+
tmp_path,
|
|
313
|
+
{"packages/app/src/legacy.ts": both, "scripts/old.sh": "# one\n# two\n", "packages/app/src/new.ts": both},
|
|
314
|
+
body,
|
|
315
|
+
)
|
|
316
|
+
verdict = grade(root)
|
|
317
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("MLC-001", "packages/app/src/new.ts")]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ------------------------------------------------- parse failures and missing tools
|
|
321
|
+
UNPARSEABLE = {
|
|
322
|
+
"broken.sh": "if then fi (\n",
|
|
323
|
+
"broken.py": "def f(:\n",
|
|
324
|
+
}
|
|
325
|
+
GOOD_PAIR = ("pair.ts", "// one\n// two\n")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@pytest.mark.parametrize(("name", "body"), sorted(UNPARSEABLE.items()))
|
|
329
|
+
def test_an_unparseable_file_is_a_per_file_finding_and_the_rest_is_still_graded(
|
|
330
|
+
tmp_path: Path, name: str, body: str
|
|
331
|
+
) -> None:
|
|
332
|
+
root = tree(tmp_path, {name: body, GOOD_PAIR[0]: GOOD_PAIR[1]})
|
|
333
|
+
verdict = grade(root)
|
|
334
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
335
|
+
by_rule = {(f.rule, f.path) for f in verdict.findings}
|
|
336
|
+
assert by_rule == {("MLC-003", name), ("MLC-001", GOOD_PAIR[0])}
|
|
337
|
+
parse = next(f for f in verdict.findings if f.rule == "MLC-003")
|
|
338
|
+
assert "cannot be parsed" in parse.message
|
|
339
|
+
assert ("shfmt" if name.endswith(".sh") else "python") in parse.message
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@pytest.mark.parametrize(("name", "body"), sorted(UNPARSEABLE.items()))
|
|
343
|
+
def test_a_registered_unparseable_file_is_not_reported_or_stale(tmp_path: Path, name: str, body: str) -> None:
|
|
344
|
+
root = tree(tmp_path, {name: body}, register_of(name))
|
|
345
|
+
verdict = grade(root)
|
|
346
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@pytest.mark.parametrize(
|
|
350
|
+
("name", "body"),
|
|
351
|
+
[
|
|
352
|
+
("dash.sh", "#!/bin/sh\nx=1\n# one\n# two\n"),
|
|
353
|
+
("posix-bad.sh", "#!/bin/sh\nx=(a b)\n"),
|
|
354
|
+
("bin/ksh-tool", "#!/bin/ksh\n# one\n# two\n"),
|
|
355
|
+
("zsh-tool.zsh", "# one\n# two\nprint -r x\n"),
|
|
356
|
+
],
|
|
357
|
+
)
|
|
358
|
+
def test_a_shebang_selects_the_shfmt_dialect(tmp_path: Path, name: str, body: str) -> None:
|
|
359
|
+
root = tree(tmp_path, {name: body})
|
|
360
|
+
verdict = grade(root)
|
|
361
|
+
expected = "MLC-003" if name == "posix-bad.sh" else "MLC-001"
|
|
362
|
+
assert {f.rule for f in verdict.findings} == {expected}, verdict.render()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_a_missing_shfmt_is_could_not_run_for_the_whole_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
366
|
+
root = tree(tmp_path, {"a.sh": "# one\n# two\n", "b.ts": "// x\n"})
|
|
367
|
+
monkeypatch.setattr(runner.shutil, "which", lambda tool: None if tool == "shfmt" else "/usr/bin/" + tool)
|
|
368
|
+
verdict = grade(root)
|
|
369
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
370
|
+
assert "shfmt" in verdict.message
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_a_missing_ast_grep_is_could_not_run_for_the_whole_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
374
|
+
root = tree(tmp_path, {"b.ts": "// x\n// y\n"})
|
|
375
|
+
monkeypatch.setattr(runner.shutil, "which", lambda tool: None if tool == "ast-grep" else "/usr/bin/" + tool)
|
|
376
|
+
verdict = grade(root)
|
|
377
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
378
|
+
assert "ast-grep" in verdict.message
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@pytest.mark.parametrize("name", ["a.sh", "b.ts"])
|
|
382
|
+
def test_a_tool_that_cannot_be_launched_is_could_not_run_not_a_finding(
|
|
383
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, name: str
|
|
384
|
+
) -> None:
|
|
385
|
+
root = tree(tmp_path, {name: "# one\n"})
|
|
386
|
+
empty = tmp_path / "empty-bin"
|
|
387
|
+
empty.mkdir()
|
|
388
|
+
monkeypatch.setattr(runner.shutil, "which", lambda tool: "/usr/bin/" + tool)
|
|
389
|
+
monkeypatch.setenv("PATH", str(empty))
|
|
390
|
+
verdict = grade(root)
|
|
391
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""REP-006 (GIT-001) on the shared `banned_commands/base.py` scanner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from ..banned_commands.base import BannedCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VcsInScripts(BannedCommand):
|
|
11
|
+
id: ClassVar[str] = "REP-006"
|
|
12
|
+
rule: ClassVar[str] = "GIT-001"
|
|
13
|
+
WORD: ClassVar[str] = "gi" + "t"
|
|
14
|
+
MESSAGE: ClassVar[str] = "version-control invocation is forbidden in shell scripts and justfiles"
|
|
15
|
+
FIX: ClassVar[str] = ("read the set the runner hands you, or drop the call if it only built "
|
|
16
|
+
"a repository the assertion never needed")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""REP-006 (GIT-001) reports its own rule and no other."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
from adl_compliance import runner
|
|
8
|
+
from adl_compliance.core.verdict import Outcome
|
|
9
|
+
from adl_compliance.policy.registry import load
|
|
10
|
+
|
|
11
|
+
CORPUS = (
|
|
12
|
+
pathlib.Path(__file__).parent.parent / "banned_commands" / "fixtures" / "no-banned-commands"
|
|
13
|
+
)
|
|
14
|
+
ROW = load().by_id("REP-006")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _whole() -> tuple[str, ...]:
|
|
18
|
+
return tuple(
|
|
19
|
+
sorted(p.relative_to(CORPUS).as_posix() for p in CORPUS.rglob("*") if p.is_file())
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_reports_only_its_own_rule() -> None:
|
|
24
|
+
verdict = runner.run(ROW, CORPUS, _whole())
|
|
25
|
+
assert verdict.outcome is Outcome.FINDINGS
|
|
26
|
+
assert {f.rule for f in verdict.findings} == {"GIT-001"}
|
|
27
|
+
assert len(verdict.findings) == 2
|
|
28
|
+
assert "loose-git.sh" in {pathlib.PurePosixPath(f.path).name for f in verdict.findings}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_is_addressable_by_its_registry_name() -> None:
|
|
32
|
+
assert load().resolve("repository.no-vcs-in-scripts").id == "REP-006"
|
|
@@ -33,6 +33,7 @@ import yaml
|
|
|
33
33
|
from pydantic import ValidationError
|
|
34
34
|
|
|
35
35
|
from . import runner
|
|
36
|
+
from .checks import lookup
|
|
36
37
|
from .core.constants import Constants
|
|
37
38
|
from .core.errors import CouldNotRun
|
|
38
39
|
from .core.receipt import from_verdict
|
|
@@ -298,6 +299,13 @@ def write_logs(directory: Path, verdicts: Sequence[Verdict]) -> tuple[Path, Path
|
|
|
298
299
|
return all_path, failures_path
|
|
299
300
|
|
|
300
301
|
|
|
302
|
+
def _empty_scope_is_clean(gate: str) -> bool:
|
|
303
|
+
try:
|
|
304
|
+
return lookup(gate).empty_scope_is_clean
|
|
305
|
+
except KeyError:
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
|
|
301
309
|
def write_receipts(directory: Path, verdicts: Sequence[Verdict], *, exhaustive: bool) -> Path:
|
|
302
310
|
"""C1: one `core.receipt.Receipt` per row, JSONL, alongside the two logs.
|
|
303
311
|
|
|
@@ -310,7 +318,10 @@ def write_receipts(directory: Path, verdicts: Sequence[Verdict], *, exhaustive:
|
|
|
310
318
|
directory.mkdir(parents=True, exist_ok=True)
|
|
311
319
|
receipts_path = directory / Constants.RECEIPTS_LOG
|
|
312
320
|
lines = [
|
|
313
|
-
from_verdict(
|
|
321
|
+
from_verdict(
|
|
322
|
+
verdict, exhaustive=exhaustive, nothing_to_grade_is_clean=_empty_scope_is_clean(verdict.row.id)
|
|
323
|
+
).model_dump_json()
|
|
324
|
+
for verdict in verdicts
|
|
314
325
|
]
|
|
315
326
|
receipts_path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
|
316
327
|
return receipts_path
|
|
@@ -126,9 +126,8 @@ class AstGrepCheck(Check):
|
|
|
126
126
|
)
|
|
127
127
|
|
|
128
128
|
def _is_owned_suppression(self, ctx: Context, match: AstGrepMatch) -> bool:
|
|
129
|
-
"""Owner ruling
|
|
130
|
-
|
|
131
|
-
for every `engine.rule` row, not one gate's patch: a match is skipped
|
|
129
|
+
"""Owner ruling: "if it's legitimate, own it" / "no exception". One
|
|
130
|
+
mechanism for every `engine.rule` row, not one gate's patch: a match is skipped
|
|
132
131
|
only when the row DECLARES an equivalent biome rule (`engine.
|
|
133
132
|
suppression`) and the match is immediately preceded — allowing a
|
|
134
133
|
multi-line comment continuation — by a `// biome-ignore <rule>:
|
|
@@ -125,6 +125,9 @@ class Check(Constants, ABC):
|
|
|
125
125
|
`adl.exception-catalog/v1` is a hard could-not-run.
|
|
126
126
|
"""
|
|
127
127
|
|
|
128
|
+
empty_scope_is_clean: ClassVar[bool] = False
|
|
129
|
+
"""A scope selecting nothing is clean, not could-not-run (for gates whose subject exists only in some repositories)."""
|
|
130
|
+
|
|
128
131
|
whole_scope: ClassVar[bool] = False
|
|
129
132
|
"""A triggered check grades its complete declared scope.
|
|
130
133
|
|
|
@@ -47,7 +47,7 @@ class Constants:
|
|
|
47
47
|
# Two directories, similar names, different owners, and conflating them is a
|
|
48
48
|
# real bug rather than a tidiness question: the K-42 sweep converted
|
|
49
49
|
# `.workflows/AGENTS.md` into `<workflows>/AGENTS.md`, which repoints a path
|
|
50
|
-
# this tool fixes everywhere at a tree the repository declares.
|
|
50
|
+
# this tool fixes everywhere at a tree the repository declares. a managed repository has
|
|
51
51
|
# BOTH and an AGENTS.md in each, so that conversion silently swapped which
|
|
52
52
|
# file two gates were reading.
|
|
53
53
|
#
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Declarations a repository sends the engine, validated once for every gate that reads them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from ..policy.root_shape import ROOT_FILES_DECLARATION, declared_root_names
|
|
8
|
+
from .errors import CouldNotRun
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def declared_root_files(declarations: Mapping[str, str]) -> frozenset[str]:
|
|
12
|
+
"""The repository-relative files declared under `root-files`; an absolute, dotted or glob entry is could-not-run."""
|
|
13
|
+
names = declared_root_names(declarations, ROOT_FILES_DECLARATION)
|
|
14
|
+
for name in sorted(names):
|
|
15
|
+
if name.startswith("/") or any(part in ("", ".", "..") for part in name.split("/")) or any(
|
|
16
|
+
char in name for char in "*?["
|
|
17
|
+
):
|
|
18
|
+
raise CouldNotRun(
|
|
19
|
+
f"`{ROOT_FILES_DECLARATION}` declares {name!r}, which is not a repository-relative "
|
|
20
|
+
"file path (it is absolute, has a `.`, `..` or empty segment, or is a glob); fix "
|
|
21
|
+
"[controls.compliance.root].files in adl-config.toml -- a malformed name could "
|
|
22
|
+
"only widen the allowlist, so the gate does not grade against it"
|
|
23
|
+
)
|
|
24
|
+
return names
|
|
@@ -37,7 +37,7 @@ class Receipt(StrictModel):
|
|
|
37
37
|
message: str
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def from_verdict(verdict: Verdict, *, exhaustive: bool) -> Receipt:
|
|
40
|
+
def from_verdict(verdict: Verdict, *, exhaustive: bool, nothing_to_grade_is_clean: bool = False) -> Receipt:
|
|
41
41
|
"""Build one gate's receipt.
|
|
42
42
|
|
|
43
43
|
`exhaustive` is the same fact `runner.applicable_files` used to decide
|
|
@@ -48,7 +48,7 @@ def from_verdict(verdict: Verdict, *, exhaustive: bool) -> Receipt:
|
|
|
48
48
|
`runner.run` cannot produce one (it raises `CouldNotRun` first). This
|
|
49
49
|
function checks that second claim rather than assuming it.
|
|
50
50
|
"""
|
|
51
|
-
if exhaustive and verdict.units == 0 and verdict.outcome is Outcome.CLEAN:
|
|
51
|
+
if exhaustive and verdict.units == 0 and verdict.outcome is Outcome.CLEAN and not nothing_to_grade_is_clean:
|
|
52
52
|
raise ValueError(
|
|
53
53
|
f"{verdict.row.id}: refusing to record a PASS receipt for a gate "
|
|
54
54
|
"whose exhaustive scope resolved to zero units — a dead scope is "
|