@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.12 → 0.1.28
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/git +16 -17
- package/bin/git-ado +25 -24
- package/bin/git-defaults +173 -0
- package/bin/health-check +8 -1
- package/bin/kimi-hooks +1 -1
- package/bin/lib/adl-package.sh +257 -0
- package/bin/lib/fleet-host.sh +8 -5
- package/bin/reconcile +482 -232
- package/bin/reconcile-host +30 -20
- package/bin/setup +20 -17
- package/bin/shell-path-policy +15 -5
- package/bin/sync-and-reconcile +62 -64
- package/bin/validate-manifest +33 -9
- package/package.json +2 -10
- package/packages/agent-hooks/README.md +14 -9
- package/packages/agent-hooks/assets/hooks.json +0 -20
- package/packages/agent-hooks/src/cli/args.ts +1 -1
- package/packages/agent-hooks/src/cli/doctor.ts +6 -58
- package/packages/agent-hooks/src/deploy/deploy.ts +12 -41
- package/packages/agent-hooks/src/deploy/doctor.ts +8 -11
- package/packages/agent-hooks/src/deploy/index.ts +14 -14
- package/packages/agent-hooks/src/deploy/json-merge.ts +7 -8
- package/packages/agent-hooks/src/deploy/legacy-lock.ts +2 -2
- package/packages/agent-hooks/src/deploy/list.ts +1 -1
- package/packages/agent-hooks/src/deploy/lock.ts +31 -12
- package/packages/agent-hooks/src/deploy/profile.ts +3 -4
- package/packages/agent-hooks/src/deploy/source.ts +1 -1
- package/packages/agent-hooks/src/gating/config.ts +12 -24
- package/packages/agent-hooks/src/gating/root.ts +3 -3
- package/packages/agent-hooks/src/hooks/briefing/handler.ts +2 -2
- package/packages/agent-hooks/src/hooks/briefing/index.ts +27 -287
- package/packages/agent-hooks/src/hooks/contract.ts +17 -17
- package/packages/agent-hooks/src/hooks/crg/background.ts +27 -0
- package/packages/agent-hooks/src/hooks/crg/index.ts +7 -6
- package/packages/agent-hooks/src/hooks/log.ts +8 -10
- package/packages/agent-hooks/src/hooks/user-prompt-submit/match.ts +1 -1
- package/packages/agent-hooks/src/hooks/user-prompt-submit/tokenizer.ts +1 -1
- package/packages/agent-hooks/src/hooks/user-prompt-submit/vector.ts +6 -5
- package/packages/agent-hooks/src/hosts/registry.ts +1 -1
- package/packages/agent-hooks/src/log/adl-dir.ts +44 -0
- package/packages/agent-hooks/src/log/writer.ts +16 -2
- package/packages/agent-hooks/src/main.ts +7 -41
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +71 -82
- package/packages/agent-hooks/src/rules/engine.ts +13 -17
- package/packages/agent-hooks/src/rules/impl/adl-build-run-denied.ts +3 -3
- package/packages/agent-hooks/src/rules/impl/adl-git-credential-bypass.ts +12 -14
- package/packages/agent-hooks/src/rules/impl/adl-git-identity.ts +28 -28
- package/packages/agent-hooks/src/rules/impl/adl-git-push-policy.ts +8 -9
- package/packages/agent-hooks/src/rules/impl/adl-human-azdo-bypass.ts +56 -59
- package/packages/agent-hooks/src/rules/impl/adl-identity.ts +16 -23
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +19 -16
- package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +2 -2
- package/packages/agent-hooks/src/rules/impl/adl-pr-review-role.ts +9 -12
- package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +5 -7
- package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +411 -0
- package/packages/agent-hooks/src/rules/impl/adl-token-minting-role.ts +4 -5
- package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +6 -7
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +77 -70
- package/packages/agent-hooks/src/rules/impl/deny-allowlist.ts +131 -0
- package/packages/agent-hooks/src/rules/impl/git-helpers.ts +21 -23
- package/packages/agent-hooks/src/rules/impl/git-worktree.ts +14 -28
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +3 -3
- package/packages/agent-hooks/src/rules/impl/messages.ts +1 -1
- package/packages/agent-hooks/src/rules/impl/pr-completion.ts +1 -1
- package/packages/agent-hooks/src/rules/impl/tool-azure-devops.ts +35 -46
- package/packages/agent-hooks/src/rules/impl/tool-find.ts +34 -7
- package/packages/agent-hooks/src/rules/impl/tool-grep.ts +33 -12
- package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +52 -9
- package/packages/agent-hooks/src/rules/ports.ts +5 -7
- package/packages/agent-hooks/src/rules/registry.ts +4 -6
- package/packages/agent-hooks/src/rules/shell/git-identity.ts +86 -101
- package/packages/agent-hooks/src/rules/shell/git-internals.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/invocation.ts +24 -118
- package/packages/agent-hooks/src/rules/shell/piped-script.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/prefixes.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/protected-carrier-writes.ts +11 -10
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +26 -26
- package/packages/agent-hooks/src/rules/shell/substitution.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/xargs.ts +2 -2
- package/packages/agent-hooks/src/rules/spec.ts +2 -2
- package/packages/agent-settings/README.md +287 -0
- package/packages/agent-settings/assets/GLOBAL-AGENTS.md +14 -16
- package/packages/agent-settings/assets/agents/architect.toml +2 -0
- package/packages/agent-settings/assets/agents/code-reviewer.toml +2 -0
- package/packages/agent-settings/assets/agents/executor.toml +2 -0
- package/packages/agent-settings/assets/agents/explore.toml +2 -0
- package/packages/agent-settings/assets/agents/planner.toml +2 -0
- package/packages/agent-settings/assets/mcp.json +0 -4
- package/packages/agent-settings/deploy-profile.json +139 -0
- package/packages/agent-settings/deploy-profile.schema.json +147 -0
- package/packages/agent-settings/managed/README.md +29 -30
- package/packages/agent-settings/managed/codex-requirements.toml +9 -9
- package/packages/agent-settings/package.json +20 -0
- package/packages/agent-settings/src/deploy/agent-render.ts +2 -2
- package/packages/agent-settings/src/deploy/agent-toml.ts +1 -1
- package/packages/agent-settings/src/deploy/agents-deploy.ts +17 -3
- package/packages/agent-settings/src/deploy/doctor.ts +3 -83
- package/packages/agent-settings/src/deploy/index.ts +6 -33
- package/packages/agent-settings/src/deploy/legacy-lock.ts +6 -6
- package/packages/agent-settings/src/deploy/list.ts +5 -19
- package/packages/agent-settings/src/deploy/lock.ts +8 -18
- package/packages/agent-settings/src/deploy/mcp-deploy.ts +10 -5
- package/packages/agent-settings/src/deploy/mcp-merge.ts +22 -3
- package/packages/agent-settings/src/deploy/mcp-source.ts +1 -1
- package/packages/agent-settings/src/deploy/profile.ts +11 -35
- package/packages/agent-settings/src/deploy/retire-lean-ctx.ts +184 -0
- package/packages/agent-settings/src/deploy/settings.ts +17 -22
- package/packages/agent-settings/src/global-surface-sweep.ts +0 -2
- package/packages/agent-settings/src/kimi-hooks.ts +339 -192
- package/packages/agent-settings/src/opencode-hooks.ts +1 -1
- package/packages/agent-settings/tsconfig.json +23 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +120 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +276 -0
- package/packages/agent-skills/assets/debug-issue/SKILL.md +8 -7
- package/packages/agent-skills/assets/explore-codebase/SKILL.md +13 -13
- package/packages/agent-skills/assets/refactor-safely/SKILL.md +12 -11
- package/packages/agent-skills/assets/review-changes/SKILL.md +7 -7
- package/packages/agent-skills/assets/testing/references/e2e.md +37 -0
- package/packages/agent-skills/assets/testing/references/integration.md +37 -0
- package/packages/agent-skills/assets/testing/references/unit.md +32 -0
- package/packages/agent-skills/dist/deploy.d.ts +2 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +27 -1
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/dist/doctor.d.ts +0 -2
- package/packages/agent-skills/dist/doctor.d.ts.map +1 -1
- package/packages/agent-skills/dist/doctor.js +1 -1
- package/packages/agent-skills/dist/doctor.js.map +1 -1
- package/packages/agent-skills/dist/fs-utils.d.ts +0 -1
- package/packages/agent-skills/dist/fs-utils.d.ts.map +1 -1
- package/packages/agent-skills/dist/fs-utils.js +1 -1
- package/packages/agent-skills/dist/fs-utils.js.map +1 -1
- package/packages/agent-skills/src/deploy.ts +29 -1
- package/packages/agent-skills/src/doctor.ts +1 -1
- package/packages/agent-skills/src/fs-utils.ts +1 -1
- package/packages/azdo/README.md +10 -9
- package/packages/azdo/package.json +1 -3
- package/packages/azdo/scripts/probe-ado-rules.ts +311 -0
- package/packages/azdo/src/azdo-pl.ts +2 -54
- package/packages/azdo/src/azdo-pr-review.ts +4 -4
- package/packages/azdo/src/azdo-repo-doctor.ts +101 -52
- package/packages/azdo/src/azdo-wit.ts +44 -2
- package/packages/azdo/src/lib/agent-identity.ts +12 -14
- package/packages/azdo/src/lib/agent-token.ts +2 -2
- package/packages/azdo/src/lib/client.ts +0 -5
- package/packages/azdo/src/lib/graph-identity.ts +4 -4
- package/packages/azdo/src/workflow-ladder.csv +241 -0
- package/packages/azdo/src/workflow-rules-matrix.csv +85 -0
- package/packages/azdo/src/workflow-rules.ts +194 -0
- package/packages/fleet/manifest.json +35 -11
- package/packages/fleet/manifest.schema.json +62 -10
- package/packages/fleet/src/default-agent-file.ts +3 -3
- package/packages/fleet/src/install-adl-package.ts +73 -15
- package/packages/fleet/src/install-embedding-model.ts +13 -13
- package/packages/fleet/src/managed-settings.ts +7 -7
- package/packages/fleet/src/repo-maintain.ts +91 -55
- package/packages/fleet/src/scratch-reap.ts +5 -5
- package/packages/fleet/src/vscode-settings.ts +5 -5
- package/packages/repo-controls/README.md +141 -43
- package/packages/repo-controls/assets/promote.ts +4 -4
- package/packages/repo-controls/assets/reconcile-guard.sh +10 -46
- package/packages/repo-controls/assets/version-bump.sh +31 -84
- package/packages/repo-controls/bunfig.toml +6 -7
- package/packages/repo-controls/package/adl-compliance/AGENTS.md +5 -6
- package/packages/repo-controls/package/adl-compliance/conftest.py +3 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +4 -16
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +9 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/notes.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/check.py +266 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.drafts/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.drafts/d.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/.hidden.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/data.csv +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/diagram.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/mock.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs/guide/sub/nested.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/docs.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p1.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p2.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p3.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p4.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-assets/plans/deep/p5.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/architecture/build.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/architecture/overview.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/guide/install.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/docs/guide/start.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-clean/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-001-tenancy.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-002-carrier.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/adr-003-cli.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/archive/adr-000-old.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/decisions/archive/adr-000-older.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-decisions/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-01.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-02.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-03.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-04.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-05.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-06.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-07.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-08.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-09.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-10.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-11.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-12.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-13.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-14.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-15.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/docs/guide/page-16.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-default-cap/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-exempt/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-grew/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/d.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/docs/guide/e.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-no-section/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/docs/short/only.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-over-cap/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/held/d.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/shrunk/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/docs/shrunk/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-stale/grants.yaml +6 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/a.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/b.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/docs/guide/c.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/fixtures/docs-size-unusable/grants.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/directory_size/test_check.py +345 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +18 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +35 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/check.py +91 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/CLAUDE.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/-leading-hyphen.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/double--hyphen.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/getting started.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/nested/page-here.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/trailing-hyphen-.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/user_profile.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/assets/UI-CONTRACT.png +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/assets/diagram.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/index.md +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/notes.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/reference.html +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/2026-10-experiment.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +6 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs.md +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +194 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +14 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +5 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-shipped-register-legacy/docs/compliance/index.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-shipped-register-type/docs/compliance/index.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +58 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/check.py +64 -17
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +60 -9
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-ledger-clean/docs/notes.md +17 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-ledger-no-lines/docs/notes.md +13 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs/architecture/demo.md +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs-okf-profiles.yaml +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/no-type.md +9 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-experiment.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-research.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs-okf-profiles.yaml +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs/architecture/bad-status.md +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs-okf-profiles.yaml +0 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/experiment-ok.md +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-last-update.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-sources.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +14 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/architecture/created-date.md +21 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/architecture/research-shaped.md +19 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research-neighbours/docs/operations/runbook.md +26 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +228 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +11 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/__init__.py +5 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +160 -44
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +0 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/frontend/packages/desktop/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/frontend/packages/desktop/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/grants.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +0 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/a.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/b.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/frontend/packages/desktop/src/c.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +0 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/frontend/packages/desktop/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/grants.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/register.py +127 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +265 -29
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +75 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +194 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +56 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +258 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +40 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +108 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +9 -4
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/items/az351-restore-entrypoint.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +2 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +23 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +76 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +58 -17
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +161 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +35 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +108 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +281 -61
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/absolute-path.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/backslash-path.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/baseline-is-a-list.yaml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/boolean-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/dot-segment.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/duplicate-path.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/empty-file.yaml +0 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/empty-segment.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/float-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/negative-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/no-over-limit-baseline.yaml +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/no-schema-version.yaml +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/non-string-path.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/not-yaml.yaml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/null-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/parent-segment.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/string-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/top-level-is-a-list.yaml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/trailing-slash.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/unknown-key.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/wrong-schema-version.yaml +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/malformed/zero-size.yaml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/valid.yaml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +758 -45
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/check.py +4 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/test_check.py +2 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +490 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +368 -55
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +4 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +523 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +1179 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +122 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +33 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +0 -11
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/context.py +16 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -2
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +12 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +133 -147
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +133 -147
- 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 +52 -10
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +23 -4
- package/packages/repo-controls/package.json +2 -9
- package/packages/repo-controls/scripts/hooks/post-merge +13 -0
- package/packages/repo-controls/scripts/hooks/pre-push +3 -3
- package/packages/repo-controls/src/azdo-push.ts +64 -36
- package/packages/repo-controls/src/azure-devops-governance.ts +7 -9
- package/packages/repo-controls/src/azure-devops-remote.ts +1 -1
- package/packages/repo-controls/src/cli.ts +29 -8
- package/packages/repo-controls/src/compliance-config.ts +410 -2
- package/packages/repo-controls/src/config.ts +133 -60
- package/packages/repo-controls/src/constants.ts +13 -27
- package/packages/repo-controls/src/coverage-ratchet.ts +4 -4
- package/packages/repo-controls/src/doctor.ts +46 -19
- package/packages/repo-controls/src/gitignore-block.ts +5 -29
- package/packages/repo-controls/src/hook-log.ts +1 -2
- package/packages/repo-controls/src/hook.ts +201 -45
- package/packages/repo-controls/src/install.ts +14 -8
- package/packages/repo-controls/src/integrity.ts +3 -3
- package/packages/repo-controls/src/lfs.ts +111 -0
- package/packages/repo-controls/src/promote.ts +8 -8
- package/packages/repo-controls/src/runtime.ts +9 -1
- package/packages/repo-controls/src/workflows-drift.ts +4 -5
- package/packages/repo-controls/src/worktree-guard.ts +3 -3
|
@@ -3,19 +3,32 @@
|
|
|
3
3
|
test.sh builds one fixture inline (`mktemp -d`, real `git init`/`add`/`commit`
|
|
4
4
|
calls, `head -c 1048577 /dev/zero`, `git lfs install --local --force`) and
|
|
5
5
|
walks it through a sequence of states, asserting the gate's verdict at each
|
|
6
|
-
one.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
one. This port builds one corpus per state in `tmp_path`, at test time.
|
|
7
|
+
|
|
8
|
+
WHY NOTHING IS COMMITTED. The previous port committed one directory per state.
|
|
9
|
+
Those directories held `.gitattributes` lines of `*.bin filter=lfs ...`, and a
|
|
10
|
+
committed `.gitattributes` also governs the OUTER repository: git-lfs converted
|
|
11
|
+
the raw 1,048,577-byte `attribute-without-conversion/large file.bin` into a
|
|
12
|
+
132-byte pointer at commit time, byte-identical to `clean/`. On a fresh clone
|
|
13
|
+
exactly one of the `clean` / `attribute-without-conversion` tests then failed,
|
|
14
|
+
depending on whether the checkout smudged LFS. A corpus generated under
|
|
15
|
+
`tmp_path` lies outside the repository, so no outer filter can touch it.
|
|
16
|
+
|
|
17
|
+
Every case asserts its PRECONDITION (size, first line, which files exist)
|
|
18
|
+
before it is graded, and that the runner graded at least one file, so a corpus
|
|
19
|
+
that is not what its name says fails loudly instead of passing for the wrong
|
|
20
|
+
reason. Dispatch is the REAL registry -> `CHECKS` -> `runner.run` path: nothing
|
|
21
|
+
here patches `runner.lookup`.
|
|
22
|
+
|
|
23
|
+
check.sh reads `:path`/`HEAD:path` (index or `HEAD`, by `ADL_GATE_PHASE`).
|
|
24
|
+
This port reads the committed size the CLI supplies in `Context.committed_sizes`
|
|
25
|
+
where there is one, and otherwise the WORKTREE file -- see `check.py`'s module
|
|
26
|
+
docstring (K-41: no check calls git, the CLI measures; K-43: no phase). The
|
|
27
|
+
corpora below carry no committed sizes, so they all take the worktree path, and
|
|
28
|
+
the section at the bottom covers the committed one. Concretely, that collapses
|
|
29
|
+
two of test.sh's cases into one corpus each here:
|
|
30
|
+
|
|
31
|
+
test.sh case -> corpus
|
|
19
32
|
---------------------------------------------------------------------
|
|
20
33
|
pre-commit: raw blob staged, no .gitattributes missing-filter
|
|
21
34
|
pre-commit: .gitattributes matches, blob still raw attribute-without-conversion
|
|
@@ -24,43 +37,154 @@ that collapses two of test.sh's cases into one fixture each here:
|
|
|
24
37
|
qua_deploy: payload.dat, large, unmatched extension unmatched-extension
|
|
25
38
|
pre-commit: small.txt, silent clean (txt)
|
|
26
39
|
|
|
27
|
-
`clean
|
|
40
|
+
`clean` alone therefore stands in for THREE of test.sh's assertions (the
|
|
28
41
|
converted-pointer pass at both phases, plus the untouched small file), which
|
|
29
42
|
is exact rather than a simplification: with no phase in this port, "passes at
|
|
30
43
|
pre-commit" and "passes at qua_deploy" are the same claim about the same
|
|
31
44
|
file.
|
|
45
|
+
|
|
46
|
+
A real 132-byte pointer is at most the threshold, so `check.py` skips it on
|
|
47
|
+
size alone and `clean` never reaches the pointer comparison. That comparison is
|
|
48
|
+
reached only by a file OVER the threshold whose first line is the pointer
|
|
49
|
+
header; the `pointer header over the threshold` cases below are the ones that
|
|
50
|
+
exercise it, and they differ from `attribute-without-conversion` only in that
|
|
51
|
+
first line.
|
|
32
52
|
"""
|
|
33
53
|
|
|
34
54
|
from __future__ import annotations
|
|
35
55
|
|
|
36
|
-
|
|
56
|
+
import hashlib
|
|
57
|
+
import re
|
|
58
|
+
from collections.abc import Mapping, Sequence
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
|
|
61
|
+
import pytest
|
|
62
|
+
from fixture_trees import fixture_trees
|
|
63
|
+
from helpers import corpus_files
|
|
37
64
|
|
|
38
65
|
from adl_compliance import runner
|
|
66
|
+
from adl_compliance.checks import lookup
|
|
39
67
|
from adl_compliance.checks.lfs.integrity import Integrity
|
|
40
|
-
from adl_compliance.core
|
|
68
|
+
from adl_compliance.core import scope
|
|
69
|
+
from adl_compliance.core.verdict import Outcome, Verdict
|
|
41
70
|
from adl_compliance.policy.registry import load
|
|
42
71
|
|
|
43
|
-
import pytest
|
|
44
|
-
|
|
45
|
-
FIXTURES = corpus("lfs", "integrity")
|
|
46
72
|
ROW = load().by_id("LFS-001")
|
|
47
73
|
|
|
74
|
+
AT_THRESHOLD = 1_048_576
|
|
75
|
+
OVER_THRESHOLD = 1_048_577
|
|
76
|
+
RAW_OVER = b"\0" * OVER_THRESHOLD
|
|
77
|
+
RAW_AT = b"\0" * AT_THRESHOLD
|
|
78
|
+
|
|
79
|
+
LFS_ATTRIBUTES = "*.bin filter=lfs diff=lfs merge=lfs -text\n"
|
|
80
|
+
POINTER_HEADER = "version https://git-lfs.github.com/spec/v1"
|
|
81
|
+
# What a real pointer says about the 1,048,577-byte blob it stands in for.
|
|
82
|
+
POINTER_TEXT = (
|
|
83
|
+
f"{POINTER_HEADER}\n"
|
|
84
|
+
f"oid sha256:{hashlib.sha256(RAW_OVER).hexdigest()}\n"
|
|
85
|
+
f"size {OVER_THRESHOLD}\n"
|
|
86
|
+
)
|
|
87
|
+
POINTER = POINTER_TEXT.encode()
|
|
88
|
+
# The same header followed by zero bytes out to OVER_THRESHOLD: a file the size
|
|
89
|
+
# check does not skip whose first line IS the pointer header.
|
|
90
|
+
POINTER_HEADER_OVER = POINTER + b"\0" * (OVER_THRESHOLD - len(POINTER))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------ building a corpus
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build(tmp_path: Path, files: Mapping[str, bytes], gitattributes: str | None = None) -> Path:
|
|
97
|
+
root = tmp_path / "corpus"
|
|
98
|
+
root.mkdir()
|
|
99
|
+
if gitattributes is not None:
|
|
100
|
+
(root / ".gitattributes").write_text(gitattributes)
|
|
101
|
+
for rel, content in files.items():
|
|
102
|
+
(root / rel).write_bytes(content)
|
|
103
|
+
return root
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def first_line(root: Path, rel: str) -> str:
|
|
107
|
+
with (root / rel).open("rb") as handle:
|
|
108
|
+
prefix = handle.read(4096)
|
|
109
|
+
return prefix.split(b"\n", 1)[0].decode("utf-8", errors="replace").rstrip("\r")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def expect_raw_blob(root: Path, rel: str, size: int) -> None:
|
|
113
|
+
"""Precondition: `rel` is exactly `size` raw bytes, not a pointer."""
|
|
114
|
+
assert (root / rel).stat().st_size == size, f"{rel}: expected {size} bytes"
|
|
115
|
+
assert first_line(root, rel) != POINTER_HEADER, f"{rel} must not start with the pointer header"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def expect_real_pointer(root: Path, rel: str) -> None:
|
|
119
|
+
"""Precondition: `rel` is a genuine 3-line LFS pointer, well under the threshold."""
|
|
120
|
+
text = (root / rel).read_text()
|
|
121
|
+
lines = text.splitlines()
|
|
122
|
+
assert len(lines) == 3, f"{rel}: a pointer has exactly three lines, got {lines!r}"
|
|
123
|
+
assert lines[0] == POINTER_HEADER
|
|
124
|
+
assert re.fullmatch(r"oid sha256:[0-9a-f]{64}", lines[1]), lines[1]
|
|
125
|
+
assert re.fullmatch(r"size \d+", lines[2]), lines[2]
|
|
126
|
+
assert (root / rel).stat().st_size == len(POINTER) == 132
|
|
127
|
+
assert (root / rel).stat().st_size <= Integrity.THRESHOLD
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def expect_pointer_header_over_threshold(root: Path, rel: str) -> None:
|
|
131
|
+
"""Precondition: `rel` is over the threshold AND its first line is the pointer header."""
|
|
132
|
+
assert (root / rel).stat().st_size == OVER_THRESHOLD, f"{rel}: expected {OVER_THRESHOLD} bytes"
|
|
133
|
+
assert first_line(root, rel) == POINTER_HEADER
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def expect_attributes(root: Path, present: bool) -> None:
|
|
137
|
+
assert (root / ".gitattributes").is_file() is present
|
|
138
|
+
|
|
48
139
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# class — everything else (scope intersection, structural/graded split,
|
|
57
|
-
# verdict decision) runs for real.
|
|
58
|
-
monkeypatch.setattr(runner, "lookup", lambda gate_id: Integrity)
|
|
140
|
+
def grade(root: Path) -> Verdict:
|
|
141
|
+
files = corpus_files(root)
|
|
142
|
+
verdict = runner.run(ROW, root, files)
|
|
143
|
+
assert verdict.outcome is not Outcome.COULD_NOT_RUN, verdict.render()
|
|
144
|
+
assert verdict.units > 0, f"the runner graded no files: {verdict.render()}"
|
|
145
|
+
assert verdict.units == len(files), f"graded {verdict.units} of {len(files)}: {files}"
|
|
146
|
+
return verdict
|
|
59
147
|
|
|
60
148
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
149
|
+
# --------------------------------------------------------------- the corpora
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def missing_filter(tmp_path: Path) -> Path:
|
|
153
|
+
root = build(tmp_path, {"large file.bin": RAW_OVER})
|
|
154
|
+
expect_attributes(root, present=False)
|
|
155
|
+
expect_raw_blob(root, "large file.bin", OVER_THRESHOLD)
|
|
156
|
+
return root
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def attribute_without_conversion(tmp_path: Path) -> Path:
|
|
160
|
+
root = build(tmp_path, {"large file.bin": RAW_OVER}, gitattributes=LFS_ATTRIBUTES)
|
|
161
|
+
expect_attributes(root, present=True)
|
|
162
|
+
assert "filter=lfs" in (root / ".gitattributes").read_text()
|
|
163
|
+
expect_raw_blob(root, "large file.bin", OVER_THRESHOLD)
|
|
164
|
+
return root
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def unmatched_extension(tmp_path: Path) -> Path:
|
|
168
|
+
root = build(tmp_path, {"payload.dat": RAW_OVER}, gitattributes=LFS_ATTRIBUTES)
|
|
169
|
+
expect_attributes(root, present=True)
|
|
170
|
+
assert not (root / "payload.dat").name.endswith(".bin")
|
|
171
|
+
expect_raw_blob(root, "payload.dat", OVER_THRESHOLD)
|
|
172
|
+
return root
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def clean(tmp_path: Path) -> Path:
|
|
176
|
+
root = build(
|
|
177
|
+
tmp_path,
|
|
178
|
+
{"large file.bin": POINTER, "small.txt": b"small\n"},
|
|
179
|
+
gitattributes=LFS_ATTRIBUTES,
|
|
180
|
+
)
|
|
181
|
+
expect_attributes(root, present=True)
|
|
182
|
+
expect_real_pointer(root, "large file.bin")
|
|
183
|
+
assert (root / "small.txt").stat().st_size <= Integrity.THRESHOLD
|
|
184
|
+
return root
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------------ the tests
|
|
64
188
|
|
|
65
189
|
|
|
66
190
|
def test_the_row_identifies_this_check() -> None:
|
|
@@ -68,10 +192,25 @@ def test_the_row_identifies_this_check() -> None:
|
|
|
68
192
|
assert ROW.name == "integrity"
|
|
69
193
|
|
|
70
194
|
|
|
71
|
-
def
|
|
195
|
+
def test_the_real_registry_dispatch_reaches_this_check() -> None:
|
|
196
|
+
# No monkeypatch anywhere in this module: if LFS-001 fell out of
|
|
197
|
+
# `checks/__init__.py`'s CHECKS, every graded case below would come back
|
|
198
|
+
# could-not-run, and this names the cause.
|
|
199
|
+
assert lookup("LFS-001") is Integrity
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_the_corpus_sizes_sit_either_side_of_the_checks_threshold() -> None:
|
|
203
|
+
# If `THRESHOLD` moves, the boundary cases below stop testing the boundary;
|
|
204
|
+
# this makes that loud rather than silent.
|
|
205
|
+
assert AT_THRESHOLD == Integrity.THRESHOLD
|
|
206
|
+
assert OVER_THRESHOLD == Integrity.THRESHOLD + 1
|
|
207
|
+
assert POINTER_HEADER == Integrity.POINTER
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_a_large_staged_blob_outside_the_lfs_filter_is_rejected(tmp_path: Path) -> None:
|
|
72
211
|
# test.sh: `run_gate pre-commit -- "large file.bin"` on a raw 1048577-byte
|
|
73
212
|
# blob with no `.gitattributes` at all must fail.
|
|
74
|
-
verdict =
|
|
213
|
+
verdict = grade(missing_filter(tmp_path))
|
|
75
214
|
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
76
215
|
assert len(verdict.findings) == 1
|
|
77
216
|
finding = verdict.findings[0]
|
|
@@ -80,19 +219,19 @@ def test_a_large_staged_blob_outside_the_lfs_filter_is_rejected() -> None:
|
|
|
80
219
|
assert finding.line is None
|
|
81
220
|
|
|
82
221
|
|
|
83
|
-
def test_the_filename_with_spaces_is_not_truncated() -> None:
|
|
222
|
+
def test_the_filename_with_spaces_is_not_truncated(tmp_path: Path) -> None:
|
|
84
223
|
# test.sh: "A space in the name on purpose: a diagnostic that truncates at
|
|
85
224
|
# the space names a file nobody can find." check.sh's own shell diagnostics
|
|
86
225
|
# ran the risk of stopping at the first word; `Finding.path` and `render()`
|
|
87
226
|
# are plain Python strings with no such hazard, asserted directly rather
|
|
88
227
|
# than by absence.
|
|
89
|
-
verdict =
|
|
228
|
+
verdict = grade(missing_filter(tmp_path))
|
|
90
229
|
assert verdict.findings[0].path == "large file.bin"
|
|
91
230
|
assert "LFS-002 large file.bin: " in verdict.render()
|
|
92
231
|
|
|
93
232
|
|
|
94
|
-
def test_the_finding_names_the_size_and_an_actionable_fix() -> None:
|
|
95
|
-
verdict =
|
|
233
|
+
def test_the_finding_names_the_size_and_an_actionable_fix(tmp_path: Path) -> None:
|
|
234
|
+
verdict = grade(missing_filter(tmp_path))
|
|
96
235
|
finding = verdict.findings[0]
|
|
97
236
|
assert finding.message == (
|
|
98
237
|
"large file.bin is a 1048577-byte blob entering Git without the LFS filter"
|
|
@@ -103,31 +242,605 @@ def test_the_finding_names_the_size_and_an_actionable_fix() -> None:
|
|
|
103
242
|
)
|
|
104
243
|
|
|
105
244
|
|
|
106
|
-
def test_a_matching_gitattributes_rule_does_not_excuse_a_raw_blob() -> None:
|
|
245
|
+
def test_a_matching_gitattributes_rule_does_not_excuse_a_raw_blob(tmp_path: Path) -> None:
|
|
107
246
|
# test.sh: "the .attributes rule is added AFTER the blob was staged...
|
|
108
247
|
# This is the case that makes the `||` in the check load-bearing." A
|
|
109
248
|
# `.gitattributes` match alone must not pass a file that was never
|
|
110
249
|
# actually converted to an LFS pointer.
|
|
111
|
-
verdict =
|
|
250
|
+
verdict = grade(attribute_without_conversion(tmp_path))
|
|
112
251
|
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
113
252
|
assert verdict.rules == ["LFS-002"]
|
|
114
253
|
assert verdict.findings[0].path == "large file.bin"
|
|
115
254
|
|
|
116
255
|
|
|
117
|
-
def test_a_correctly_tracked_lfs_pointer_and_a_small_file_both_stay_silent(
|
|
256
|
+
def test_a_correctly_tracked_lfs_pointer_and_a_small_file_both_stay_silent(
|
|
257
|
+
tmp_path: Path,
|
|
258
|
+
) -> None:
|
|
118
259
|
# test.sh: the renormalized pointer passes at both pre-commit and
|
|
119
260
|
# qua_deploy, and `small.txt` is silent throughout — three assertions,
|
|
120
|
-
# one
|
|
121
|
-
verdict =
|
|
261
|
+
# one corpus, per this test module's own docstring.
|
|
262
|
+
verdict = grade(clean(tmp_path))
|
|
122
263
|
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
123
264
|
assert verdict.findings == ()
|
|
124
265
|
|
|
125
266
|
|
|
126
|
-
def test_a_large_ordinary_blob_of_an_unmatched_extension_is_reported() -> None:
|
|
267
|
+
def test_a_large_ordinary_blob_of_an_unmatched_extension_is_reported(tmp_path: Path) -> None:
|
|
127
268
|
# test.sh: "qua_deploy sees a large ordinary blob in HEAD" — .gitattributes
|
|
128
269
|
# matches `*.bin` only, so `payload.dat` is committed as an ordinary
|
|
129
270
|
# object and must still be reported.
|
|
130
|
-
verdict =
|
|
271
|
+
verdict = grade(unmatched_extension(tmp_path))
|
|
272
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
273
|
+
assert verdict.rules == ["LFS-002"]
|
|
274
|
+
assert verdict.findings[0].path == "payload.dat"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ------------------------------------------------------ the size boundary
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_a_blob_of_exactly_the_threshold_is_clean(tmp_path: Path) -> None:
|
|
281
|
+
root = build(tmp_path, {"at.bin": RAW_AT})
|
|
282
|
+
expect_attributes(root, present=False)
|
|
283
|
+
expect_raw_blob(root, "at.bin", AT_THRESHOLD)
|
|
284
|
+
verdict = grade(root)
|
|
285
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_one_byte_over_the_threshold_is_reported_and_exactly_the_threshold_is_not(
|
|
289
|
+
tmp_path: Path,
|
|
290
|
+
) -> None:
|
|
291
|
+
# Both in ONE corpus, both graded: the only difference the gate can be
|
|
292
|
+
# reacting to is the single byte.
|
|
293
|
+
root = build(tmp_path, {"at.bin": RAW_AT, "over.bin": RAW_OVER})
|
|
294
|
+
expect_attributes(root, present=False)
|
|
295
|
+
expect_raw_blob(root, "at.bin", AT_THRESHOLD)
|
|
296
|
+
expect_raw_blob(root, "over.bin", OVER_THRESHOLD)
|
|
297
|
+
verdict = grade(root)
|
|
298
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
299
|
+
assert [finding.path for finding in verdict.findings] == ["over.bin"]
|
|
300
|
+
assert verdict.findings[0].message == (
|
|
301
|
+
"over.bin is a 1048577-byte blob entering Git without the LFS filter"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ------------------------------------------ the pointer branch (over threshold)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_a_pointer_header_over_the_threshold_under_a_matching_rule_is_clean(
|
|
309
|
+
tmp_path: Path,
|
|
310
|
+
) -> None:
|
|
311
|
+
# The only way to reach `check.py`'s pointer comparison: over the size
|
|
312
|
+
# threshold, tracked by a `filter=lfs` rule, and first line == the pointer
|
|
313
|
+
# header. The corpus is `attribute_without_conversion` with one difference,
|
|
314
|
+
# the first line -- and that one line flips the verdict.
|
|
315
|
+
root = build(tmp_path, {"large file.bin": POINTER_HEADER_OVER}, gitattributes=LFS_ATTRIBUTES)
|
|
316
|
+
expect_attributes(root, present=True)
|
|
317
|
+
expect_pointer_header_over_threshold(root, "large file.bin")
|
|
318
|
+
verdict = grade(root)
|
|
319
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
320
|
+
assert verdict.findings == ()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_a_pointer_header_over_the_threshold_with_no_tracking_rule_is_reported(
|
|
324
|
+
tmp_path: Path,
|
|
325
|
+
) -> None:
|
|
326
|
+
# check.py: "a pointer-shaped first line alone does not excuse a path with
|
|
327
|
+
# no rule tracking it" -- both conditions are required, not either one.
|
|
328
|
+
root = build(tmp_path, {"large file.bin": POINTER_HEADER_OVER})
|
|
329
|
+
expect_attributes(root, present=False)
|
|
330
|
+
expect_pointer_header_over_threshold(root, "large file.bin")
|
|
331
|
+
verdict = grade(root)
|
|
332
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
333
|
+
assert verdict.rules == ["LFS-002"]
|
|
334
|
+
assert verdict.findings[0].path == "large file.bin"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def test_a_pointer_header_over_the_threshold_under_a_non_matching_rule_is_reported(
|
|
338
|
+
tmp_path: Path,
|
|
339
|
+
) -> None:
|
|
340
|
+
root = build(tmp_path, {"payload.dat": POINTER_HEADER_OVER}, gitattributes=LFS_ATTRIBUTES)
|
|
341
|
+
expect_attributes(root, present=True)
|
|
342
|
+
expect_pointer_header_over_threshold(root, "payload.dat")
|
|
343
|
+
verdict = grade(root)
|
|
131
344
|
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
132
345
|
assert verdict.rules == ["LFS-002"]
|
|
133
346
|
assert verdict.findings[0].path == "payload.dat"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ------------------------------------- the committed blob, supplied by the CLI
|
|
350
|
+
#
|
|
351
|
+
# `cli.py` is the only module that calls git, so it hands the engine one fact per
|
|
352
|
+
# tracked path: the size of the blob Git STORES for it (`Context.committed_sizes`).
|
|
353
|
+
# A correctly tracked LFS file is a ~132-byte pointer in Git and the full file in
|
|
354
|
+
# the worktree (the filter smudges it on checkout), so the worktree size is the
|
|
355
|
+
# wrong thing to grade whenever a committed size exists. These cases pin what is
|
|
356
|
+
# graded where. A path with NO committed fact is graded by its worktree file, as
|
|
357
|
+
# before -- the existing cases above all run that way, with no facts at all.
|
|
358
|
+
|
|
359
|
+
SMUDGED = 1_170_147 # the measured size of a real smudged LFS file on enact-combined
|
|
360
|
+
POINTER_SIZE = 132 # `git cat-file -s` of the pointer committed for it
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def grade_with_facts(root: Path, committed_sizes: Mapping[str, int]) -> Verdict:
|
|
364
|
+
files = corpus_files(root)
|
|
365
|
+
verdict = runner.run(ROW, root, files, committed_sizes=committed_sizes)
|
|
366
|
+
assert verdict.outcome is not Outcome.COULD_NOT_RUN, verdict.render()
|
|
367
|
+
assert verdict.units == len(files), f"graded {verdict.units} of {len(files)}: {files}"
|
|
368
|
+
return verdict
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_a_smudged_lfs_file_whose_committed_blob_is_a_pointer_is_clean(tmp_path: Path) -> None:
|
|
372
|
+
# The measured false positive: Git stores 132 bytes, the worktree holds the
|
|
373
|
+
# smudged 1,170,147-byte file. Nothing under the repository can fix it -- the
|
|
374
|
+
# path IS correctly tracked -- so the gate must grade what Git stores.
|
|
375
|
+
root = build(tmp_path, {"video.webm": b"\0" * SMUDGED}, gitattributes=LFS_ATTRIBUTES)
|
|
376
|
+
assert (root / "video.webm").stat().st_size == SMUDGED > Integrity.THRESHOLD
|
|
377
|
+
verdict = grade_with_facts(root, {"video.webm": POINTER_SIZE})
|
|
378
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
379
|
+
assert verdict.findings == ()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def test_the_same_smudged_file_is_reported_when_no_committed_size_is_known(
|
|
383
|
+
tmp_path: Path,
|
|
384
|
+
) -> None:
|
|
385
|
+
# The pair of the case above: the ONLY difference is the committed fact. A
|
|
386
|
+
# missing fact must never silently pass a big file.
|
|
387
|
+
root = build(tmp_path, {"video.webm": b"\0" * SMUDGED}, gitattributes=LFS_ATTRIBUTES)
|
|
388
|
+
verdict = grade_with_facts(root, {})
|
|
389
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
390
|
+
assert [finding.path for finding in verdict.findings] == ["video.webm"]
|
|
391
|
+
assert verdict.findings[0].message == (
|
|
392
|
+
f"video.webm is a {SMUDGED}-byte blob entering Git without the LFS filter"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_a_large_committed_blob_is_reported_with_the_committed_size(tmp_path: Path) -> None:
|
|
397
|
+
root = build(tmp_path, {"photo.png": b"\0" * 2_097_152})
|
|
398
|
+
verdict = grade_with_facts(root, {"photo.png": 2_097_152})
|
|
399
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
400
|
+
finding = verdict.findings[0]
|
|
401
|
+
assert finding.rule == "LFS-002"
|
|
402
|
+
assert finding.path == "photo.png"
|
|
403
|
+
assert finding.message == "photo.png is a 2097152-byte blob entering Git without the LFS filter"
|
|
404
|
+
assert finding.fix == (
|
|
405
|
+
"add a .gitattributes rule matching photo.png to filter=lfs, run "
|
|
406
|
+
"'git lfs track' for that pattern, then re-add photo.png"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_the_committed_size_decides_not_the_worktree_when_the_two_disagree(
|
|
411
|
+
tmp_path: Path,
|
|
412
|
+
) -> None:
|
|
413
|
+
# Git stores 2,000,000 bytes; the worktree copy has since been cut down. What
|
|
414
|
+
# Git stores is what enters the repository, so it is still reported -- and the
|
|
415
|
+
# message names the committed size, not the 11 bytes on disk.
|
|
416
|
+
root = build(tmp_path, {"photo.png": b"shrunk\n"})
|
|
417
|
+
verdict = grade_with_facts(root, {"photo.png": 2_000_000})
|
|
418
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
419
|
+
assert verdict.findings[0].message == (
|
|
420
|
+
"photo.png is a 2000000-byte blob entering Git without the LFS filter"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_a_committed_blob_exactly_at_the_threshold_is_clean_and_one_over_is_not(
|
|
425
|
+
tmp_path: Path,
|
|
426
|
+
) -> None:
|
|
427
|
+
# Worktree sizes are deliberately the OPPOSITE of the verdicts, so the only
|
|
428
|
+
# thing that can produce them is the committed size.
|
|
429
|
+
root = build(tmp_path, {"at.bin": RAW_OVER, "over.bin": b"tiny\n"})
|
|
430
|
+
verdict = grade_with_facts(root, {"at.bin": AT_THRESHOLD, "over.bin": OVER_THRESHOLD})
|
|
431
|
+
assert [finding.path for finding in verdict.findings] == ["over.bin"]
|
|
432
|
+
assert verdict.findings[0].message == (
|
|
433
|
+
"over.bin is a 1048577-byte blob entering Git without the LFS filter"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def test_a_path_with_no_committed_size_is_graded_by_its_worktree_beside_one_that_has_one(
|
|
438
|
+
tmp_path: Path,
|
|
439
|
+
) -> None:
|
|
440
|
+
# `smudged.webm` has a fact (a pointer) and is clean; `new.png` is untracked
|
|
441
|
+
# (no fact) and big, so it is graded exactly as before. One fact existing for
|
|
442
|
+
# another path changes nothing about it.
|
|
443
|
+
root = build(
|
|
444
|
+
tmp_path,
|
|
445
|
+
{"smudged.webm": b"\0" * SMUDGED, "new.png": RAW_OVER},
|
|
446
|
+
gitattributes=LFS_ATTRIBUTES,
|
|
447
|
+
)
|
|
448
|
+
verdict = grade_with_facts(root, {"smudged.webm": POINTER_SIZE})
|
|
449
|
+
assert [finding.path for finding in verdict.findings] == ["new.png"]
|
|
450
|
+
assert verdict.findings[0].message == (
|
|
451
|
+
"new.png is a 1048577-byte blob entering Git without the LFS filter"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_a_path_with_a_committed_size_is_graded_even_when_its_worktree_file_is_gone(
|
|
456
|
+
tmp_path: Path,
|
|
457
|
+
) -> None:
|
|
458
|
+
# In the index but deleted on disk (an unstaged deletion): the blob is still
|
|
459
|
+
# stored, so it is still a large blob outside the LFS filter. Without a fact,
|
|
460
|
+
# an absent worktree file is skipped, as before.
|
|
461
|
+
root = build(tmp_path, {"keep.txt": b"x\n"})
|
|
462
|
+
files = ("keep.txt", "gone.png")
|
|
463
|
+
stored = runner.run(ROW, root, files, committed_sizes={"gone.png": 2_000_000})
|
|
464
|
+
assert [finding.path for finding in stored.findings] == ["gone.png"]
|
|
465
|
+
unknown = runner.run(ROW, root, files, committed_sizes={})
|
|
466
|
+
assert unknown.outcome is Outcome.CLEAN, unknown.render()
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ------------------------------------------------- the shrink-only ratchet register
|
|
470
|
+
#
|
|
471
|
+
# The repository's own register of KNOWN large blobs, `<compliance>/ratchets/
|
|
472
|
+
# LFS-001-large-blobs.yaml`: `over_limit_baseline` maps a repository-relative path
|
|
473
|
+
# to the size in bytes it had when its debt was recorded. It only ever ADDS an
|
|
474
|
+
# allowance (a widening input), so no register means nothing is ratcheted.
|
|
475
|
+
#
|
|
476
|
+
# LFS-002 over the threshold with no entry (as before)
|
|
477
|
+
# LFS-003 over its own recorded entry -- names both sizes
|
|
478
|
+
# LFS-004 an entry that is no longer required, so the register only shrinks
|
|
479
|
+
#
|
|
480
|
+
# `Context` carries no "exhaustive" fact and `core/` is not this gate's to change,
|
|
481
|
+
# so LFS-004 is not gated on the run being whole-tree. It never infers staleness
|
|
482
|
+
# from a path being ABSENT FROM THE SET; it probes each entry's own path exactly
|
|
483
|
+
# the way the set's paths are graded. See check.py's docstring.
|
|
484
|
+
|
|
485
|
+
REGISTER_NAME = "LFS-001-large-blobs.yaml"
|
|
486
|
+
REGISTER_PATH = f"{fixture_trees()['compliance']}/ratchets/{REGISTER_NAME}"
|
|
487
|
+
RECORDED = 2_000_000
|
|
488
|
+
BIG = b"\0" * RECORDED
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def register_body(entries: Mapping[str, int] | None = None, *, version: int = 1) -> str:
|
|
492
|
+
lines = [f"schema_version: {version}"]
|
|
493
|
+
if not entries:
|
|
494
|
+
lines.append("over_limit_baseline: {}")
|
|
495
|
+
else:
|
|
496
|
+
lines.append("over_limit_baseline:")
|
|
497
|
+
lines.extend(f' "{path}": {size}' for path, size in entries.items())
|
|
498
|
+
return "\n".join(lines) + "\n"
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def write_register(root: Path, body: str) -> None:
|
|
502
|
+
target = root / REGISTER_PATH
|
|
503
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
504
|
+
target.write_text(body)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def ratcheted(
|
|
508
|
+
tmp_path: Path,
|
|
509
|
+
files: Mapping[str, bytes],
|
|
510
|
+
entries: Mapping[str, int],
|
|
511
|
+
gitattributes: str | None = None,
|
|
512
|
+
) -> Path:
|
|
513
|
+
root = build(tmp_path, {}, gitattributes=gitattributes)
|
|
514
|
+
for rel, content in files.items():
|
|
515
|
+
(root / rel).write_bytes(content)
|
|
516
|
+
write_register(root, register_body(entries))
|
|
517
|
+
return root
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def grade_register(
|
|
521
|
+
root: Path,
|
|
522
|
+
*,
|
|
523
|
+
files: Sequence[str] | None = None,
|
|
524
|
+
committed_sizes: Mapping[str, int] | None = None,
|
|
525
|
+
exhaustive: bool = False,
|
|
526
|
+
) -> Verdict:
|
|
527
|
+
given = tuple(files) if files is not None else corpus_files(root)
|
|
528
|
+
return runner.run(ROW, root, given, exhaustive=exhaustive, committed_sizes=committed_sizes)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_a_ratcheted_blob_at_its_recorded_size_is_clean(tmp_path: Path) -> None:
|
|
532
|
+
root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
|
|
533
|
+
assert (root / "photo.png").stat().st_size == RECORDED > Integrity.THRESHOLD
|
|
534
|
+
verdict = grade_register(root)
|
|
535
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
536
|
+
assert verdict.findings == ()
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def test_a_ratcheted_blob_that_shrank_but_is_still_over_the_limit_is_clean(tmp_path: Path) -> None:
|
|
540
|
+
smaller = b"\0" * (RECORDED - 1000)
|
|
541
|
+
root = ratcheted(tmp_path, {"photo.png": smaller}, {"photo.png": RECORDED})
|
|
542
|
+
assert Integrity.THRESHOLD < (root / "photo.png").stat().st_size < RECORDED
|
|
543
|
+
verdict = grade_register(root)
|
|
544
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def test_a_ratcheted_blob_that_grew_is_reported_naming_both_sizes(tmp_path: Path) -> None:
|
|
548
|
+
root = ratcheted(tmp_path, {"photo.png": b"\0" * (RECORDED + 1)}, {"photo.png": RECORDED})
|
|
549
|
+
verdict = grade_register(root)
|
|
550
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
551
|
+
assert verdict.rules == ["LFS-003"]
|
|
552
|
+
finding = verdict.findings[0]
|
|
553
|
+
assert finding.path == "photo.png"
|
|
554
|
+
assert str(RECORDED + 1) in finding.message and str(RECORDED) in finding.message
|
|
555
|
+
assert finding.message == (
|
|
556
|
+
f"photo.png is a {RECORDED + 1}-byte blob entering Git without the LFS filter, "
|
|
557
|
+
f"over its ratchet entry of {RECORDED} bytes"
|
|
558
|
+
)
|
|
559
|
+
# The register only shrinks, so the repair never offers to raise the entry.
|
|
560
|
+
assert finding.fix == (
|
|
561
|
+
f"shrink photo.png to at most {RECORDED} bytes, or move it to LFS (add a "
|
|
562
|
+
".gitattributes rule matching photo.png to filter=lfs, run 'git lfs track' for "
|
|
563
|
+
"that pattern, then re-add photo.png); the register only shrinks, so do not raise "
|
|
564
|
+
"its entry"
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def test_a_ratcheted_tracked_blob_is_graded_by_its_committed_size_not_the_worktree(
|
|
569
|
+
tmp_path: Path,
|
|
570
|
+
) -> None:
|
|
571
|
+
# Worktree is 7 bytes, Git stores RECORDED: at the entry -> clean. One more
|
|
572
|
+
# stored byte -> LFS-003 naming the STORED size, not the 7 bytes on disk.
|
|
573
|
+
root = ratcheted(tmp_path, {"photo.png": b"shrunk\n"}, {"photo.png": RECORDED})
|
|
574
|
+
at = grade_register(root, committed_sizes={"photo.png": RECORDED})
|
|
575
|
+
assert at.outcome is Outcome.CLEAN, at.render()
|
|
576
|
+
over = grade_register(root, committed_sizes={"photo.png": RECORDED + 1})
|
|
577
|
+
assert over.rules == ["LFS-003"], over.render()
|
|
578
|
+
assert str(RECORDED + 1) in over.findings[0].message
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def test_an_unratcheted_blob_is_still_reported_beside_a_ratcheted_one(tmp_path: Path) -> None:
|
|
582
|
+
root = ratcheted(tmp_path, {"known.png": BIG, "new.png": BIG}, {"known.png": RECORDED})
|
|
583
|
+
verdict = grade_register(root)
|
|
584
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
585
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "new.png")]
|
|
586
|
+
assert verdict.findings[0].message == (
|
|
587
|
+
f"new.png is a {RECORDED}-byte blob entering Git without the LFS filter"
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_an_entry_does_not_excuse_a_different_path(tmp_path: Path) -> None:
|
|
592
|
+
root = ratcheted(tmp_path, {"other.png": BIG}, {"known.png": RECORDED})
|
|
593
|
+
verdict = grade_register(root)
|
|
594
|
+
assert ("LFS-002", "other.png") in [(f.rule, f.path) for f in verdict.findings]
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ------------------------------------------------------ stale entries (LFS-004)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def test_an_entry_for_a_file_now_at_the_threshold_is_stale(tmp_path: Path) -> None:
|
|
601
|
+
root = ratcheted(tmp_path, {"photo.png": RAW_AT}, {"photo.png": RECORDED})
|
|
602
|
+
verdict = grade_register(root)
|
|
603
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
604
|
+
assert verdict.rules == ["LFS-004"]
|
|
605
|
+
finding = verdict.findings[0]
|
|
606
|
+
assert finding.path == "photo.png"
|
|
607
|
+
assert finding.message == (
|
|
608
|
+
"ratchet entry for photo.png is no longer required: it is 1048576 bytes, "
|
|
609
|
+
"at or under the 1048576-byte threshold"
|
|
610
|
+
)
|
|
611
|
+
assert finding.fix == f"delete photo.png from over_limit_baseline in {REGISTER_PATH}"
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def test_an_entry_for_a_file_that_is_gone_is_stale(tmp_path: Path) -> None:
|
|
615
|
+
root = ratcheted(tmp_path, {"keep.txt": b"x\n"}, {"gone.png": RECORDED})
|
|
616
|
+
verdict = grade_register(root)
|
|
617
|
+
assert verdict.rules == ["LFS-004"], verdict.render()
|
|
618
|
+
assert verdict.findings[0].path == "gone.png"
|
|
619
|
+
assert verdict.findings[0].message == (
|
|
620
|
+
"ratchet entry for gone.png is no longer required: it is not a file in the tree"
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def test_an_entry_for_a_file_git_now_stores_as_a_pointer_is_stale(tmp_path: Path) -> None:
|
|
625
|
+
# Moved to LFS: Git stores the ~132-byte pointer while the worktree holds the
|
|
626
|
+
# smudged full file, so the worktree size would wrongly keep the entry alive.
|
|
627
|
+
root = ratcheted(
|
|
628
|
+
tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED}, gitattributes="*.png filter=lfs\n"
|
|
629
|
+
)
|
|
630
|
+
verdict = grade_register(root, committed_sizes={"photo.png": POINTER_SIZE})
|
|
631
|
+
assert verdict.rules == ["LFS-004"], verdict.render()
|
|
632
|
+
assert verdict.findings[0].message == (
|
|
633
|
+
"ratchet entry for photo.png is no longer required: Git stores 132 bytes for it, "
|
|
634
|
+
"at or under the 1048576-byte threshold"
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def test_an_entry_for_a_pointer_shaped_file_under_a_matching_rule_is_stale(
|
|
639
|
+
tmp_path: Path,
|
|
640
|
+
) -> None:
|
|
641
|
+
root = ratcheted(
|
|
642
|
+
tmp_path,
|
|
643
|
+
{"large file.bin": POINTER_HEADER_OVER},
|
|
644
|
+
{"large file.bin": OVER_THRESHOLD},
|
|
645
|
+
gitattributes=LFS_ATTRIBUTES,
|
|
646
|
+
)
|
|
647
|
+
expect_pointer_header_over_threshold(root, "large file.bin")
|
|
648
|
+
verdict = grade_register(root)
|
|
649
|
+
assert verdict.rules == ["LFS-004"], verdict.render()
|
|
650
|
+
assert verdict.findings[0].message == (
|
|
651
|
+
"ratchet entry for large file.bin is no longer required: "
|
|
652
|
+
"it is an LFS pointer under a matching .gitattributes rule"
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def test_a_ratcheted_blob_and_its_stale_neighbour_are_told_apart(tmp_path: Path) -> None:
|
|
657
|
+
root = ratcheted(
|
|
658
|
+
tmp_path,
|
|
659
|
+
{"live.png": BIG, "small.png": b"tiny\n"},
|
|
660
|
+
{"live.png": RECORDED, "small.png": RECORDED, "gone.png": RECORDED},
|
|
661
|
+
)
|
|
662
|
+
verdict = grade_register(root)
|
|
663
|
+
assert sorted((f.rule, f.path) for f in verdict.findings) == [
|
|
664
|
+
("LFS-004", "gone.png"),
|
|
665
|
+
("LFS-004", "small.png"),
|
|
666
|
+
]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# ---------------- staleness is probed per entry, never inferred from the given set
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def test_an_entry_whose_file_the_set_does_not_name_but_is_still_big_is_not_stale(
|
|
673
|
+
tmp_path: Path,
|
|
674
|
+
) -> None:
|
|
675
|
+
# The false positive a narrower set must not create: `live.png` is simply not
|
|
676
|
+
# in this partial set, and a gate that read "unseen" as "stale" would report it.
|
|
677
|
+
root = ratcheted(tmp_path, {"live.png": BIG, "other.txt": b"x\n"}, {"live.png": RECORDED})
|
|
678
|
+
for exhaustive in (False, True):
|
|
679
|
+
verdict = grade_register(root, files=("other.txt",), exhaustive=exhaustive)
|
|
680
|
+
assert verdict.outcome is Outcome.CLEAN, verdict.render()
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def test_a_stale_entry_is_found_by_probing_its_path_whether_or_not_the_set_names_it(
|
|
684
|
+
tmp_path: Path,
|
|
685
|
+
) -> None:
|
|
686
|
+
# The pair of the case above: `small.png` is no longer required whether the
|
|
687
|
+
# set lists it or not, and `exhaustive` changes neither answer. `Context`
|
|
688
|
+
# does not carry that fact, so this pins the behaviour that stands in for it.
|
|
689
|
+
root = ratcheted(
|
|
690
|
+
tmp_path, {"small.png": b"tiny\n", "other.txt": b"x\n"}, {"small.png": RECORDED}
|
|
691
|
+
)
|
|
692
|
+
for given in (("other.txt",), ("other.txt", "small.png")):
|
|
693
|
+
for exhaustive in (False, True):
|
|
694
|
+
verdict = grade_register(root, files=given, exhaustive=exhaustive)
|
|
695
|
+
assert verdict.rules == ["LFS-004"], (given, exhaustive, verdict.render())
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
# --------------------------------------------- a register that is not usable
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
702
|
+
MALFORMED = sorted((FIXTURES / "malformed").glob("*.yaml"))
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def test_the_malformed_fixtures_are_all_there() -> None:
|
|
706
|
+
# An empty glob would parametrize to zero cases and pass having tested nothing.
|
|
707
|
+
assert len(MALFORMED) == 22, [path.name for path in MALFORMED]
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
@pytest.mark.parametrize("fixture", MALFORMED, ids=[path.stem for path in MALFORMED])
|
|
711
|
+
def test_a_register_that_exists_but_is_malformed_is_could_not_run(
|
|
712
|
+
tmp_path: Path, fixture: Path
|
|
713
|
+
) -> None:
|
|
714
|
+
# Loud and exit 2, never ignored: ignoring it would grade every ratcheted
|
|
715
|
+
# blob as a fresh violation, or worse, silently drop an entry's allowance.
|
|
716
|
+
root = build(tmp_path, {"photo.png": BIG})
|
|
717
|
+
write_register(root, fixture.read_text(encoding="utf-8"))
|
|
718
|
+
verdict = grade_register(root)
|
|
719
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
720
|
+
assert REGISTER_PATH in verdict.message
|
|
721
|
+
assert "ratchet register" in verdict.message
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def test_the_committed_valid_register_ratchets_exactly_what_it_names(tmp_path: Path) -> None:
|
|
725
|
+
# `fixtures/valid.yaml` is a real register: a plain path and a nested path with
|
|
726
|
+
# a space in it. Files at their recorded sizes are clean; the one that grew is
|
|
727
|
+
# not, and the entry for the path nobody created is stale.
|
|
728
|
+
root = build(tmp_path, {"photo.png": b"\0" * 2_000_000})
|
|
729
|
+
(root / "assets").mkdir()
|
|
730
|
+
(root / "assets" / "large file.bin").write_bytes(b"\0" * 1_500_001)
|
|
731
|
+
write_register(root, (FIXTURES / "valid.yaml").read_text(encoding="utf-8"))
|
|
732
|
+
verdict = grade_register(root)
|
|
733
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-003", "assets/large file.bin")]
|
|
734
|
+
(root / "assets" / "large file.bin").write_bytes(b"\0" * 1_500_000)
|
|
735
|
+
clean = grade_register(root)
|
|
736
|
+
assert clean.outcome is Outcome.CLEAN, clean.render()
|
|
737
|
+
(root / "photo.png").unlink()
|
|
738
|
+
gone = grade_register(root)
|
|
739
|
+
assert [(f.rule, f.path) for f in gone.findings] == [("LFS-004", "photo.png")]
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def test_a_register_path_that_is_a_directory_is_could_not_run(tmp_path: Path) -> None:
|
|
743
|
+
root = build(tmp_path, {"photo.png": BIG})
|
|
744
|
+
(root / REGISTER_PATH).mkdir(parents=True)
|
|
745
|
+
verdict = grade_register(root)
|
|
746
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
747
|
+
assert "ratchet register" in verdict.message
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def test_a_duplicate_path_is_refused_even_when_the_last_value_would_have_passed(
|
|
751
|
+
tmp_path: Path,
|
|
752
|
+
) -> None:
|
|
753
|
+
# PyYAML keeps the LAST of a repeated key. "Lowering" an entry by adding a
|
|
754
|
+
# second line while leaving the old, higher one in place would then be ignored
|
|
755
|
+
# without a word, so a repeated path is a malformed register.
|
|
756
|
+
root = build(tmp_path, {"photo.png": BIG})
|
|
757
|
+
write_register(
|
|
758
|
+
root,
|
|
759
|
+
f'schema_version: 1\nover_limit_baseline:\n "photo.png": {RECORDED - 1}\n'
|
|
760
|
+
f' "photo.png": {RECORDED}\n',
|
|
761
|
+
)
|
|
762
|
+
verdict = grade_register(root)
|
|
763
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
764
|
+
assert "photo.png" in verdict.message
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
# ------------------------------------------------ no register: grading as before
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def test_an_absent_register_grades_exactly_as_before(tmp_path: Path) -> None:
|
|
771
|
+
root = build(tmp_path, {"photo.png": BIG})
|
|
772
|
+
assert not (root / REGISTER_PATH).exists()
|
|
773
|
+
verdict = grade_register(root)
|
|
774
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
775
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
|
|
776
|
+
assert verdict.findings[0].message == (
|
|
777
|
+
f"photo.png is a {RECORDED}-byte blob entering Git without the LFS filter"
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def test_an_empty_baseline_grades_exactly_as_before(tmp_path: Path) -> None:
|
|
782
|
+
root = ratcheted(tmp_path, {"photo.png": BIG}, {})
|
|
783
|
+
verdict = grade_register(root)
|
|
784
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def test_a_register_whose_tree_the_repository_did_not_declare_is_dropped_not_fatal(
|
|
788
|
+
tmp_path: Path,
|
|
789
|
+
) -> None:
|
|
790
|
+
# `resolve_inputs`' widening branch (runner.py): the effective map knows the
|
|
791
|
+
# tree but the repository's OWN declarations do not. The register is never
|
|
792
|
+
# read, a widening input grades STRICTER when absent, and the file its entry
|
|
793
|
+
# would have excused is reported as before.
|
|
794
|
+
root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
|
|
795
|
+
verdict = runner.run(ROW, root, corpus_files(root), declared_trees={})
|
|
796
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
797
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def test_a_compliance_tree_declared_absent_drops_the_register(tmp_path: Path) -> None:
|
|
801
|
+
# `compliance = ""` is the documented way to say "this repository has no such
|
|
802
|
+
# tree" (`policy.config.absent`): the input is skipped, so a repository with no
|
|
803
|
+
# compliance tree keeps grading LFS-001 exactly as before.
|
|
804
|
+
root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
|
|
805
|
+
trees = {**fixture_trees(), "compliance": ""}
|
|
806
|
+
verdict = runner.run(ROW, root, corpus_files(root), trees=trees, declared_trees=trees)
|
|
807
|
+
assert verdict.outcome is Outcome.FINDINGS, verdict.render()
|
|
808
|
+
assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def test_a_compliance_tree_missing_from_the_map_is_could_not_run_naming_what_to_declare(
|
|
812
|
+
tmp_path: Path,
|
|
813
|
+
) -> None:
|
|
814
|
+
# PINS A GAP, not a goal. `resolve_inputs` expands every declared input before
|
|
815
|
+
# it reaches the widening branch, so a tree ABSENT from the map (neither a path
|
|
816
|
+
# nor "") is could-not-run even for a widening input -- reproduced through the
|
|
817
|
+
# real CLI on enact-combined with no `--tree compliance=...`. The message names
|
|
818
|
+
# exactly what to declare, and both repositories in this estate declare it. The
|
|
819
|
+
# fix is in `runner.resolve_inputs`, not in this check; this test flips with it.
|
|
820
|
+
root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
|
|
821
|
+
trees = {name: path for name, path in fixture_trees().items() if name != "compliance"}
|
|
822
|
+
verdict = runner.run(ROW, root, corpus_files(root), trees=trees, declared_trees=trees)
|
|
823
|
+
assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
|
|
824
|
+
assert "[controls.compliance.trees].compliance" in verdict.message
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
# --------------------------------------------------------- the register's shape
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def test_the_register_is_a_widening_consumer_input() -> None:
|
|
831
|
+
assert Integrity.inputs == {"register": f"<compliance>/ratchets/{REGISTER_NAME}"}
|
|
832
|
+
assert "register" in Integrity.widening_inputs
|
|
833
|
+
assert "register" not in Integrity.resources
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def test_editing_the_register_selects_the_gate() -> None:
|
|
837
|
+
# No `<compliance>` scope entry is needed (and one would make the row
|
|
838
|
+
# could-not-run in a repository that declares no such tree): `**` already
|
|
839
|
+
# selects the register, so a commit that edits it is graded.
|
|
840
|
+
assert scope.intersect(ROW.engine.scope, [REGISTER_PATH]) == (REGISTER_PATH,)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def test_the_new_finding_ids_are_distinct_and_collide_with_no_row() -> None:
|
|
844
|
+
ids = {Integrity.rule, Integrity.GREW, Integrity.STALE}
|
|
845
|
+
assert ids == {"LFS-002", "LFS-003", "LFS-004"}
|
|
846
|
+
assert not ids & {row.id for row in load().rows}
|