@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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""REP-002 grades the repository-root manifests by itself (PBI 598)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from adl_compliance import runner
|
|
9
|
+
from adl_compliance.core.verdict import Outcome, Verdict
|
|
10
|
+
from adl_compliance.policy.registry import load
|
|
11
|
+
|
|
12
|
+
ROW = load().by_id("REP-002")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _write(root: Path, files: Mapping[str, str]) -> tuple[str, ...]:
|
|
16
|
+
for relative, body in files.items():
|
|
17
|
+
target = root / relative
|
|
18
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
target.write_text(body, encoding="utf-8")
|
|
20
|
+
return tuple(sorted(files))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _grade(root: Path, files: tuple[str, ...], declarations: Mapping[str, str] | None = None) -> Verdict:
|
|
24
|
+
return runner.run(
|
|
25
|
+
ROW, root, files, exhaustive=True, universe=files,
|
|
26
|
+
declarations=dict(declarations or {}), trees={}, declared_trees={},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_a_root_cargo_manifest_without_a_lockfile_fails(tmp_path: Path) -> None:
|
|
31
|
+
files = _write(tmp_path, {"Cargo.toml": '[package]\nname = "x"\nversion = "0.1.0"\n'})
|
|
32
|
+
verdict = _grade(tmp_path, files)
|
|
33
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
34
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LCK-001", "Cargo.lock")]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_a_root_package_json_with_a_bun_lock_passes(tmp_path: Path) -> None:
|
|
38
|
+
files = _write(tmp_path, {"package.json": "{}\n", "bun.lock": "lock\n"})
|
|
39
|
+
verdict = _grade(tmp_path, files)
|
|
40
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_a_root_package_json_without_a_bun_lock_fails(tmp_path: Path) -> None:
|
|
44
|
+
files = _write(tmp_path, {"package.json": "{}\n"})
|
|
45
|
+
verdict = _grade(tmp_path, files)
|
|
46
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LCK-001", "bun.lock")], verdict.render()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_an_empty_root_bun_lock_fails(tmp_path: Path) -> None:
|
|
50
|
+
files = _write(tmp_path, {"package.json": "{}\n", "bun.lock": ""})
|
|
51
|
+
verdict = _grade(tmp_path, files)
|
|
52
|
+
assert [f.path for f in verdict.findings] == ["bun.lock"], verdict.render()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_a_declared_lockfile_elsewhere_is_graded(tmp_path: Path) -> None:
|
|
56
|
+
declarations = {"bun-manifests": "apps/web/package.json", "bun-lockfiles": "apps/web/bun.lock"}
|
|
57
|
+
files = _write(tmp_path, {"apps/web/package.json": "{}\n"})
|
|
58
|
+
verdict = _grade(tmp_path, files, declarations)
|
|
59
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LCK-001", "apps/web/bun.lock")], verdict.render()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_a_declared_lockfile_that_is_present_passes(tmp_path: Path) -> None:
|
|
63
|
+
declarations = {"bun-manifests": "apps/web/package.json", "bun-lockfiles": "apps/web/bun.lock"}
|
|
64
|
+
files = _write(tmp_path, {"apps/web/package.json": "{}\n", "apps/web/bun.lock": "lock\n"})
|
|
65
|
+
verdict = _grade(tmp_path, files, declarations)
|
|
66
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_a_nested_manifest_is_not_graded_unless_declared(tmp_path: Path) -> None:
|
|
70
|
+
"""REWORKED, Tarun 2026-10-08: nothing to grade is a pass, not an error."""
|
|
71
|
+
files = _write(tmp_path, {"apps/web/package.json": "{}\n", "README.md": "x\n"})
|
|
72
|
+
verdict = _grade(tmp_path, files)
|
|
73
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_the_root_pair_is_graded_beside_a_declaration(tmp_path: Path) -> None:
|
|
77
|
+
"""Declaring another lockfile adds to the root pair; it does not replace it."""
|
|
78
|
+
declarations = {"bun-manifests": "apps/web/package.json", "bun-lockfiles": "apps/web/bun.lock"}
|
|
79
|
+
files = _write(tmp_path, {"package.json": "{}\n", "apps/web/package.json": "{}\n", "apps/web/bun.lock": "l\n"})
|
|
80
|
+
verdict = _grade(tmp_path, files, declarations)
|
|
81
|
+
assert [f.path for f in verdict.findings] == ["bun.lock"], verdict.render()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_no_root_manifest_and_no_declaration_is_clean(tmp_path: Path) -> None:
|
|
85
|
+
"""REWORKED, Tarun 2026-10-08: a repository with no manifest has nothing to lock."""
|
|
86
|
+
files = _write(tmp_path, {"README.md": "x\n"})
|
|
87
|
+
verdict = _grade(tmp_path, files)
|
|
88
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_a_lockfile_without_a_manifest_and_no_declaration_is_clean(tmp_path: Path) -> None:
|
|
92
|
+
files = _write(tmp_path, {"bun.lock": "lock\n"})
|
|
93
|
+
verdict = _grade(tmp_path, files)
|
|
94
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_a_declared_manifest_matching_nothing_is_still_could_not_run(tmp_path: Path) -> None:
|
|
98
|
+
declarations = {"bun-manifests": "apps/web/package.json", "bun-lockfiles": "apps/web/bun.lock"}
|
|
99
|
+
files = _write(tmp_path, {"README.md": "x\n"})
|
|
100
|
+
verdict = _grade(tmp_path, files, declarations)
|
|
101
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
@@ -36,7 +36,6 @@ from types import SimpleNamespace
|
|
|
36
36
|
from typing import NamedTuple
|
|
37
37
|
|
|
38
38
|
import pytest
|
|
39
|
-
from fixture_trees import fixture_trees
|
|
40
39
|
from helpers import corpus_files
|
|
41
40
|
|
|
42
41
|
from adl_compliance import runner
|
|
@@ -293,7 +292,7 @@ SHAPES = [
|
|
|
293
292
|
|
|
294
293
|
|
|
295
294
|
def lay(root: Path, files: Mapping[str, str], lock: str | bytes, *, where: str = "backend") -> None:
|
|
296
|
-
"""Write a cargo workspace under `root/where
|
|
295
|
+
"""Write a cargo workspace under `root/where`.
|
|
297
296
|
|
|
298
297
|
A manifest with a `[package]` gets a `src/lib.rs`, so the very same files are also valid
|
|
299
298
|
input to the real `cargo metadata`. `where` is the cargo workspace directory.
|
|
@@ -311,15 +310,16 @@ def lay(root: Path, files: Mapping[str, str], lock: str | bytes, *, where: str =
|
|
|
311
310
|
lockfile.write_bytes(lock)
|
|
312
311
|
else:
|
|
313
312
|
lockfile.write_text(lock, encoding="utf-8")
|
|
314
|
-
(root / "frontend").mkdir(exist_ok=True)
|
|
315
|
-
(root / "frontend" / "bun.lock").write_text("# fixture lock\n", encoding="utf-8")
|
|
316
313
|
|
|
317
314
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
#: Most tests lay their workspace under `backend/`; `grade` declares it unless a test passes its own.
|
|
316
|
+
BACKEND = {"cargo-manifests": "backend/Cargo.toml", "cargo-lockfiles": "backend/Cargo.lock"}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def grade(root: Path, *, declarations: Mapping[str, str] | None = None) -> Verdict:
|
|
321
320
|
"""The real dispatch path, whole tree, exhaustive."""
|
|
322
|
-
|
|
321
|
+
declarations = BACKEND if declarations is None else declarations
|
|
322
|
+
trees: dict[str, str] = {}
|
|
323
323
|
files = corpus_files(root)
|
|
324
324
|
return runner.run(
|
|
325
325
|
ROW,
|
|
@@ -327,7 +327,7 @@ def grade(
|
|
|
327
327
|
files,
|
|
328
328
|
exhaustive=True,
|
|
329
329
|
universe=files,
|
|
330
|
-
declarations=declarations
|
|
330
|
+
declarations=declarations,
|
|
331
331
|
trees=trees,
|
|
332
332
|
declared_trees=trees,
|
|
333
333
|
)
|
|
@@ -1071,12 +1071,24 @@ def test_a_cargo_rejection_is_the_only_finding_for_that_lock(
|
|
|
1071
1071
|
assert [argv[:2] for argv in calls] == [["cargo", "--version"], ["cargo", "metadata"]]
|
|
1072
1072
|
|
|
1073
1073
|
|
|
1074
|
-
def
|
|
1074
|
+
def test_the_root_pair_is_graded_without_any_declaration(
|
|
1075
1075
|
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1076
1076
|
) -> None:
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
assert {f.
|
|
1077
|
+
lay(tmp_path, SINGLE.files, "version = 99\n", where=".")
|
|
1078
|
+
verdict = grade(tmp_path, declarations={})
|
|
1079
|
+
assert {f.path for f in verdict.findings} == {"Cargo.lock"}
|
|
1080
|
+
assert {f.fix for f in verdict.findings} == {
|
|
1081
|
+
"run: (cd . && cargo update) to resync Cargo.lock with Cargo.toml, then commit the result"
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def test_a_workspace_below_the_root_is_not_graded_unless_declared(
|
|
1086
|
+
tmp_path: Path, cargo_accepts: list[list[str]]
|
|
1087
|
+
) -> None:
|
|
1088
|
+
lay(tmp_path, SINGLE.files, "version = 99\n")
|
|
1089
|
+
verdict = grade(tmp_path, declarations={})
|
|
1090
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
1091
|
+
assert cargo_accepts == []
|
|
1080
1092
|
|
|
1081
1093
|
|
|
1082
1094
|
DECLARED = {"cargo-manifests": "nested/*/Cargo.toml", "cargo-lockfiles": "nested/ws/Cargo.lock"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""REP-007 (MLC-001, MLC-002): no multi-line comment in code (Bug 602)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import ClassVar, Literal
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from pydantic import field_validator
|
|
10
|
+
|
|
11
|
+
from ....core.check import Check
|
|
12
|
+
from ....core.context import Context, FileSet
|
|
13
|
+
from ....core.errors import CouldNotRun, SchemaInvalid
|
|
14
|
+
from ....core.finding import Report
|
|
15
|
+
from ....core.models import StrictModel
|
|
16
|
+
from ...lfs.integrity.check import UniqueKeyLoader
|
|
17
|
+
from . import scanner
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Register(StrictModel):
|
|
21
|
+
"""The repository's recorded debt: the files that already carry one."""
|
|
22
|
+
|
|
23
|
+
schema_version: Literal[1]
|
|
24
|
+
files: list[str]
|
|
25
|
+
|
|
26
|
+
@field_validator("files")
|
|
27
|
+
@classmethod
|
|
28
|
+
def _paths_are_normalised_and_unique(cls, value: list[str]) -> list[str]:
|
|
29
|
+
for path in value:
|
|
30
|
+
if "\\" in path or any(part in ("", ".", "..") for part in path.split("/")):
|
|
31
|
+
raise ValueError(f"{path!r} is not a normalised repository-relative file path")
|
|
32
|
+
if len(set(value)) != len(value):
|
|
33
|
+
raise ValueError("a path is listed more than once")
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NoMultilineComments(Check):
|
|
38
|
+
id: ClassVar[str] = "REP-007"
|
|
39
|
+
rule: ClassVar[str] = "MLC-001"
|
|
40
|
+
STALE: ClassVar[str] = "MLC-002"
|
|
41
|
+
UNPARSEABLE: ClassVar[str] = "MLC-003"
|
|
42
|
+
|
|
43
|
+
requires: ClassVar[tuple[str, ...]] = ("ast-grep", "shfmt")
|
|
44
|
+
|
|
45
|
+
FIX: ClassVar[str] = (
|
|
46
|
+
"move the explanation to docs/ and leave at most a one-line pointer"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
inputs: ClassVar[dict[str, str]] = {
|
|
50
|
+
"register": "<compliance>/ratchets/REP-007-multiline-comments.yaml",
|
|
51
|
+
}
|
|
52
|
+
widening_inputs: ClassVar[frozenset[str]] = frozenset({"register"})
|
|
53
|
+
|
|
54
|
+
def structural(self, ctx: Context) -> Iterable[Report]:
|
|
55
|
+
for rel in sorted(self._register(ctx)):
|
|
56
|
+
why = self._why_stale(ctx, rel)
|
|
57
|
+
if why is not None:
|
|
58
|
+
yield self.report(
|
|
59
|
+
rel,
|
|
60
|
+
f"ratchet entry for {rel} is no longer required: {why}",
|
|
61
|
+
f"remove it from the register: the ratchet only shrinks "
|
|
62
|
+
f"(delete {rel} from files in {self._shown(ctx)})",
|
|
63
|
+
rule=self.STALE,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def graded(self, ctx: Context, files: FileSet) -> Iterable[Report]:
|
|
67
|
+
granted = set(self._register(ctx))
|
|
68
|
+
for rel in files:
|
|
69
|
+
if rel in granted or not scanner.is_code_file(rel, lambda rel=rel: self.text(ctx, rel)):
|
|
70
|
+
continue
|
|
71
|
+
try:
|
|
72
|
+
hits = self._hits(ctx, rel)
|
|
73
|
+
except scanner.Unparseable as exc:
|
|
74
|
+
yield self.report(
|
|
75
|
+
rel, str(exc), "fix the file so it parses, or register it in the REP-007 ratchet",
|
|
76
|
+
rule=self.UNPARSEABLE,
|
|
77
|
+
)
|
|
78
|
+
continue
|
|
79
|
+
for line, what in hits:
|
|
80
|
+
yield self.report(
|
|
81
|
+
rel,
|
|
82
|
+
f"multi-line comment ({what})",
|
|
83
|
+
self.FIX,
|
|
84
|
+
line=line,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _hits(self, ctx: Context, rel: str) -> list[tuple[int, str]]:
|
|
88
|
+
body = self.text(ctx, rel)
|
|
89
|
+
return [] if body is None else scanner.find(rel, body)
|
|
90
|
+
|
|
91
|
+
def _why_stale(self, ctx: Context, rel: str) -> str | None:
|
|
92
|
+
if not ctx.path(rel).is_file():
|
|
93
|
+
return "it no longer exists"
|
|
94
|
+
if not scanner.is_code_file(rel, lambda: self.text(ctx, rel)):
|
|
95
|
+
return "it is not a code file this gate grades"
|
|
96
|
+
try:
|
|
97
|
+
hits = self._hits(ctx, rel)
|
|
98
|
+
except scanner.Unparseable:
|
|
99
|
+
return None
|
|
100
|
+
if not hits:
|
|
101
|
+
return "it no longer has a multi-line comment"
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------ the register
|
|
105
|
+
|
|
106
|
+
def _register(self, ctx: Context) -> tuple[str, ...]:
|
|
107
|
+
"""The files granted, or `()` when the repository records none."""
|
|
108
|
+
target = ctx.inputs.get("register")
|
|
109
|
+
if target is None or not target.exists():
|
|
110
|
+
return ()
|
|
111
|
+
shown = self._shown(ctx)
|
|
112
|
+
if not target.is_file():
|
|
113
|
+
raise CouldNotRun(f"REP-007 ratchet register {shown} exists but is not a file")
|
|
114
|
+
body = self.text(ctx, target)
|
|
115
|
+
try:
|
|
116
|
+
loaded = yaml.load(body, Loader=UniqueKeyLoader) # noqa: S506 - a SafeLoader
|
|
117
|
+
return tuple(self._validate(loaded, Register, shown).files)
|
|
118
|
+
except yaml.YAMLError as exc:
|
|
119
|
+
raise CouldNotRun(f"REP-007 ratchet register {shown} is not valid YAML: {exc}") from exc
|
|
120
|
+
except SchemaInvalid as exc:
|
|
121
|
+
raise CouldNotRun(f"REP-007 ratchet register is unusable: {exc.detail}") from exc
|
|
122
|
+
|
|
123
|
+
def _shown(self, ctx: Context) -> str:
|
|
124
|
+
return ctx.relative(ctx.inputs["register"])
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Where multi-line comments are, per language: python by tokenize and ast, shell by shfmt, the rest by ast-grep."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import tokenize
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from ....core.errors import CouldNotRun
|
|
14
|
+
from ....core.scripts import is_code_file, language
|
|
15
|
+
|
|
16
|
+
__all__ = ["Unparseable", "find", "is_code_file"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Unparseable(Exception):
|
|
20
|
+
"""The file cannot be parsed by the tool that reads its language: a finding about that file, not a failed gate."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, relative: str, tool: str, detail: str) -> None:
|
|
23
|
+
super().__init__(f"{relative} cannot be parsed: {tool} {detail}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Syntax:
|
|
27
|
+
"""The scanner's literals, on a class like every gate's."""
|
|
28
|
+
|
|
29
|
+
DIRECTIVE = re.compile(
|
|
30
|
+
r"(?:@ts-expect-error|@ts-ignore|@ts-nocheck"
|
|
31
|
+
r"|eslint-(?:disable|enable)(?:-next-line|-line)?(?:\s+[\w@/.-]+(?:\s*,\s*[\w@/.-]+)*)?"
|
|
32
|
+
r"|noqa(?::\s*[A-Z]+[0-9]+(?:[,\s]+[A-Z]+[0-9]+)*)?"
|
|
33
|
+
r"|type:\s*ignore(?:\[[\w, -]+\])?"
|
|
34
|
+
r"|biome-ignore(?:-all|-start|-end)?(?:\s+[\w/.-]+)*:?"
|
|
35
|
+
r"|shellcheck\s+(?:disable|enable|source|shell)=\S+"
|
|
36
|
+
r"|pyright:\s*(?:ignore|basic|strict|standard|off|report\w+)(?:\[[\w, -]+\])?(?:\s*=\s*\w+)?)"
|
|
37
|
+
r"\s*"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
GRAMMARS = {"rust": "Rust", "slash": "Tsx", "typescript": "TypeScript", "justfile": "Bash"}
|
|
41
|
+
SHFMT_DIALECTS = {"shell": "bash", "posix": "posix", "mksh": "mksh", "zsh": "zsh"}
|
|
42
|
+
BRACES = "{}"
|
|
43
|
+
JSX_LANGUAGES = frozenset({"slash"})
|
|
44
|
+
RUST_RULE = "any:\n - kind: line_comment\n - kind: block_comment"
|
|
45
|
+
COMMENT_RULE = "kind: comment"
|
|
46
|
+
SG_TIMEOUT = 60
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_directive(body: str) -> bool:
|
|
50
|
+
"""A comment that is solely a tool directive; prose after it makes it prose."""
|
|
51
|
+
return Syntax.DIRECTIVE.fullmatch(body.strip()) is not None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find(relative: str, text: str) -> list[tuple[int, str]]:
|
|
55
|
+
"""Every multi-line comment in `text`, ordered by line."""
|
|
56
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
57
|
+
kind = language(relative, text)
|
|
58
|
+
if kind == "python":
|
|
59
|
+
hits = _python(relative, text)
|
|
60
|
+
elif kind is not None:
|
|
61
|
+
hits = _tree(relative, text, kind)
|
|
62
|
+
else:
|
|
63
|
+
hits = []
|
|
64
|
+
return sorted(hits)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _runs(lines: dict[int, bool], what: str) -> list[tuple[int, str]]:
|
|
68
|
+
"""Runs of consecutive lines with two or more prose lines (a directive-only line is transparent), at the first."""
|
|
69
|
+
hits: list[tuple[int, str]] = []
|
|
70
|
+
run: list[int] = []
|
|
71
|
+
for line in [*sorted(lines), -1]:
|
|
72
|
+
if run and line == run[-1] + 1:
|
|
73
|
+
run.append(line)
|
|
74
|
+
continue
|
|
75
|
+
if sum(1 for n in run if not lines[n]) > 1:
|
|
76
|
+
hits.append((run[0], f"{len(run)} consecutive {what}"))
|
|
77
|
+
run = [line]
|
|
78
|
+
return hits
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------- python
|
|
82
|
+
def _python(relative: str, text: str) -> list[tuple[int, str]]:
|
|
83
|
+
try:
|
|
84
|
+
tree = ast.parse(text)
|
|
85
|
+
tokens = list(tokenize.generate_tokens(io.StringIO(text).readline))
|
|
86
|
+
except (SyntaxError, ValueError, tokenize.TokenError) as exc:
|
|
87
|
+
raise Unparseable(relative, "python", str(exc)) from exc
|
|
88
|
+
comment_lines: dict[int, bool] = {}
|
|
89
|
+
for token in tokens:
|
|
90
|
+
if token.type != tokenize.COMMENT:
|
|
91
|
+
continue
|
|
92
|
+
row, col = token.start
|
|
93
|
+
if token.line[:col].strip():
|
|
94
|
+
continue # trailing
|
|
95
|
+
if row == 1 and token.string.startswith("#!"):
|
|
96
|
+
continue
|
|
97
|
+
comment_lines[row] = _is_directive(token.string.lstrip("#"))
|
|
98
|
+
hits = _runs(comment_lines, "line comments")
|
|
99
|
+
single: dict[int, bool] = {}
|
|
100
|
+
for node in ast.walk(tree):
|
|
101
|
+
if not (isinstance(node, ast.Expr) and node.end_lineno is not None and _is_string(node.value)):
|
|
102
|
+
continue
|
|
103
|
+
if node.end_lineno > node.lineno:
|
|
104
|
+
hits.append((node.lineno, f"string statement (docstring) spanning {node.end_lineno - node.lineno + 1} lines"))
|
|
105
|
+
else:
|
|
106
|
+
single[node.lineno] = False
|
|
107
|
+
return hits + _runs(single, "string statements")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _is_string(node: ast.expr) -> bool:
|
|
111
|
+
return isinstance(node, ast.JoinedStr) or (
|
|
112
|
+
isinstance(node, ast.Constant) and isinstance(node.value, (str, bytes))
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------- every other language
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class _Comment:
|
|
119
|
+
"""One comment node: its 1-based first and last line, the text, and its start column."""
|
|
120
|
+
|
|
121
|
+
line: int
|
|
122
|
+
end: int
|
|
123
|
+
text: str
|
|
124
|
+
column: int
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _tree(relative: str, text: str, language: str) -> list[tuple[int, str]]:
|
|
128
|
+
lines = text.split("\n")
|
|
129
|
+
hits: list[tuple[int, str]] = []
|
|
130
|
+
per_line: dict[int, list[_Comment]] = {}
|
|
131
|
+
found = _shell_comments(relative, text, language) if language in Syntax.SHFMT_DIALECTS else _comments(
|
|
132
|
+
relative, text, Syntax.GRAMMARS[language]
|
|
133
|
+
)
|
|
134
|
+
for comment in found:
|
|
135
|
+
if comment.line == 1 and comment.text.startswith("#!"):
|
|
136
|
+
continue
|
|
137
|
+
if comment.end > comment.line:
|
|
138
|
+
hits.append((comment.line, f"block comment spanning {comment.end - comment.line + 1} lines"))
|
|
139
|
+
else:
|
|
140
|
+
per_line.setdefault(comment.line, []).append(comment)
|
|
141
|
+
standalone: dict[int, bool] = {}
|
|
142
|
+
for number, comments in per_line.items():
|
|
143
|
+
rest = lines[number - 1]
|
|
144
|
+
for comment in sorted(comments, key=lambda c: -c.column):
|
|
145
|
+
rest = rest[: comment.column] + rest[comment.column + len(comment.text) :]
|
|
146
|
+
if "".join(rest.split()) != "" and not _jsx_comment_line(lines[number - 1], comments, language):
|
|
147
|
+
continue # trailing
|
|
148
|
+
standalone[number] = all(_is_directive(_body(c.text)) for c in comments)
|
|
149
|
+
return hits + _runs(standalone, "line comments")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _jsx_comment_line(line: str, comments: list[_Comment], language: str) -> bool:
|
|
153
|
+
"""A JSX comment line: `{`, one block comment, `}` and nothing else, in a language that has JSX."""
|
|
154
|
+
if language not in Syntax.JSX_LANGUAGES or len(comments) != 1 or not comments[0].text.startswith("/*"):
|
|
155
|
+
return False
|
|
156
|
+
comment = comments[0]
|
|
157
|
+
before, after = line[: comment.column], line[comment.column + len(comment.text) :]
|
|
158
|
+
return before.strip() == "{" and after.strip() == "}"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _body(text: str) -> str:
|
|
162
|
+
"""A comment without its delimiters."""
|
|
163
|
+
if text.startswith("/*"):
|
|
164
|
+
return text[2:-2].lstrip("*!")
|
|
165
|
+
return text.lstrip("/#!")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _comments(relative: str, text: str, grammar: str) -> list[_Comment]:
|
|
169
|
+
rule = Syntax.RUST_RULE if grammar == "Rust" else Syntax.COMMENT_RULE
|
|
170
|
+
inline = f"id: comments\nseverity: hint\nlanguage: {grammar}\nrule:\n {rule}\n"
|
|
171
|
+
try:
|
|
172
|
+
done = subprocess.run(
|
|
173
|
+
["ast-grep", "scan", "--stdin", "--inline-rules", inline, "--json=stream"],
|
|
174
|
+
input=text, capture_output=True, text=True, check=False, timeout=Syntax.SG_TIMEOUT,
|
|
175
|
+
)
|
|
176
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
177
|
+
raise CouldNotRun(f"{relative} cannot be parsed: ast-grep: {exc}") from exc
|
|
178
|
+
if done.returncode != 0:
|
|
179
|
+
raise Unparseable(relative, "ast-grep", done.stderr.strip() or f"exited {done.returncode}")
|
|
180
|
+
found: list[_Comment] = []
|
|
181
|
+
for record in done.stdout.splitlines():
|
|
182
|
+
if not record.strip():
|
|
183
|
+
continue
|
|
184
|
+
node = json.loads(record)
|
|
185
|
+
body = node["text"].rstrip("\n")
|
|
186
|
+
start = node["range"]["start"]
|
|
187
|
+
found.append(_Comment(start["line"] + 1, start["line"] + 1 + body.count("\n"), body, start["column"]))
|
|
188
|
+
return found
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _shell_comments(relative: str, text: str, language: str) -> list[_Comment]:
|
|
192
|
+
command = ["shfmt", "--tojson", "--language-dialect", Syntax.SHFMT_DIALECTS[language]]
|
|
193
|
+
try:
|
|
194
|
+
done = subprocess.run(
|
|
195
|
+
command, input=text, capture_output=True, text=True, check=False, timeout=Syntax.SG_TIMEOUT
|
|
196
|
+
)
|
|
197
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
198
|
+
raise CouldNotRun(f"{relative} cannot be parsed: shfmt: {exc}") from exc
|
|
199
|
+
if done.returncode != 0:
|
|
200
|
+
raise Unparseable(relative, "shfmt", done.stderr.strip() or f"exited {done.returncode}")
|
|
201
|
+
found: dict[tuple[int, int], _Comment] = {}
|
|
202
|
+
lines = text.split("\n")
|
|
203
|
+
for node in _comment_nodes(json.loads(done.stdout)):
|
|
204
|
+
line, column = node["Pos"]["Line"], node["Pos"]["Col"] - 1
|
|
205
|
+
body = "#" + node["Text"]
|
|
206
|
+
found[(line, column)] = _Comment(line, line, body, len(lines[line - 1].encode()[:column].decode(errors="ignore")))
|
|
207
|
+
return list(found.values())
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _comment_nodes(tree: object) -> list[dict]:
|
|
211
|
+
"""Every comment node in a shfmt syntax tree: the objects carrying a `Hash` and a `Text`."""
|
|
212
|
+
if isinstance(tree, dict):
|
|
213
|
+
own = [tree] if "Hash" in tree and "Text" in tree else []
|
|
214
|
+
return own + [n for value in tree.values() for n in _comment_nodes(value)]
|
|
215
|
+
if isinstance(tree, list):
|
|
216
|
+
return [n for value in tree for n in _comment_nodes(value)]
|
|
217
|
+
return []
|