Skip to content

feat(policy): require_pinned_constraint to ban unbounded dep ranges#1494

Merged
danielmeppiel merged 6 commits into
mainfrom
feat/policy-require-pinned
May 27, 2026
Merged

feat(policy): require_pinned_constraint to ban unbounded dep ranges#1494
danielmeppiel merged 6 commits into
mainfrom
feat/policy-require-pinned

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

feat(policy): require_pinned_constraint to ban unbounded dep ranges

Closes #1491.

TL;DR

Adds a new boolean field policy.dependencies.require_pinned_constraint to apm.policy.yml. When true, dependency policy checks flag any direct APM dependency whose constraint is unbounded (missing ref, *, bare branch, bare >=X.Y) and route the violation through the existing policy.enforcement (warn | block | off).

Default is false. No behaviour change for existing users until they opt in.

Why (governance problem this closes)

Lockfiles deliver per-install reproducibility, but the apm.yml manifest itself can express intent that defeats lockfile discipline on the next apm update:

dependencies:
  apm:
    - acme/skills              # no ref - tracks default branch
    - other/lib@>=1.0          # no upper bound - any 99.x is valid
    - third/lib#*              # any version

Enterprise audit flows want to declare "we only ship deps that pin to a bounded constraint." Today no built-in policy field expresses that intent; the only workaround is custom CI scripting per repo. This PR closes that gap.

What changed

File LOC Purpose
src/apm_cli/policy/schema.py +9 New require_pinned_constraint: bool = False on DependencyPolicy
src/apm_cli/policy/parser.py +9 YAML parse + boolean validation
src/apm_cli/policy/inheritance.py +6 Strict-wins merge (parent OR child)
src/apm_cli/policy/_constraint_pinning.py (NEW) 199 UnboundedReason enum + classify_unbounded_reason() / is_pinned_constraint() / humanize_reason()
src/apm_cli/policy/policy_checks.py +63 _check_pinned_constraints() + wire-up in run_dependency_policy_checks
src/apm_cli/policy/_help_text.py +12 REQUIRE_PINNED_CONSTRAINT_HELP constant
tests/unit/policy/test_pinned_constraint.py (NEW) 240 Table-driven classifier tests
tests/unit/policy/test_schema_pinned_constraint.py (NEW) 95 Schema + parser + inheritance
tests/unit/policy/test_integration_pinned_constraint.py (NEW) 200 Runner + four-call-site parametrization
tests/integration/test_policy_pinned_constraint_e2e.py (NEW) 130 End-to-end apm install with block + warn modes
docs/src/content/docs/reference/policy-schema.md +40 Field row, examples, diagnostic shape, merge rule
packages/apm-guide/.apm/skills/apm-usage/governance.md +3 Policy YAML scaffold + merge rule + violation row

Classification rules (single source of truth)

Implemented in _constraint_pinning.py. The classifier operates on the declared constraint string only -- no I/O, no subprocess, deterministic, O(1) per dep.

Order (first match wins):

  1. dep.is_local -> pinned (no version surface)
  2. dep.source == "registry" -> range-shape check (registry resolver also pins via resolved_hash in the lockfile)
  3. dep.reference is None | "" -> NO_REF
  4. 40-char hex SHA -> pinned (deterministic)
  5. Literal v\d+\.\d+\.\d+... tag -> pinned
  6. Standalone * / x / X -> WILDCARD
  7. is_semver_range(spec) matches -> range-shape (OPEN_UPPER / GREATER_THAN_ONLY / pinned)
  8. Else -> BARE_BRANCH

Range-shape (_classify_range) treats a range as bounded iff at least one component pins the upper edge (<, <=, ^, ~, partial X.Y.x, or a bare exact version).

Why a built-in field, not custom_checks (#519)

Per the issue body and the design pack convergence note. Documenting the rationale here so reviewers don't have to dig:

  1. Enterprise-default governance concern -- this belongs alongside allow / deny / require as a first-class policy surface, not buried in a per-project custom_checks block.
  2. Deterministic constraint-string analysis -- pure string classification, no subprocess needed. custom_checks is for cases that genuinely need to shell out.
  3. O(1) per dep, no process spawn -- the runner adds microseconds even on a 100-dep manifest. custom_checks would impose subprocess overhead for the same answer.
  4. Both can coexist -- once feat(policy): add custom_checks for arbitrary subprocess validation in apm-policy.yml #519 lands, an org with stricter rules (e.g. "ban SHA pins on internal repos") can layer a custom_checks validator on top of the built-in pinning gate. They are not mutually exclusive.

Where the check runs (four call sites)

Wired once at the seam (run_dependency_policy_checks in policy_checks.py), so it fires at every dependency-policy entry point introduced by #1471:

  • install/phases/policy_gate.py (install-time gate, primary surface)
  • install/phases/policy_target_check.py (post-targets pass; check runs but is filtered out -- the gate already surfaced any violation; no double-emission)
  • policy/policy_checks.py::run_policy_checks (apm audit --ci wrapper)
  • policy/install_preflight.py::run_policy_preflight

Test test_policy_pinned_check_runs_at_all_four_call_sites parametrizes over the four call patterns and confirms the dependency-pinned-constraint check name appears in every result.

Soft dependency on #1488 (git-source semver routing)

The classifier is shape-based, so it already handles acme/lib#^1.2.0 regardless of source. Today that ref reaches the policy seam unchanged; the classifier sees ^1.2.0, recognises it as a bounded semver range, and reports the dep as pinned.

test_classify_git_semver_dep_returns_pinned_none is marked xfail(strict=False, reason="awaits #1488...") and contains a sentinel raise AssertionError that fires the moment a source="git-semver" discriminator is wired in the resolver -- signalling that the xfail can be removed and the test promoted to a hard regression trap.

test_classify_marketplace_dep_with_caret_returns_pinned_none pins the same shape-based invariant for the marketplace source (will route uniformly once PR #1422 lands).

SHA pins are pinned (rationale for the security panel)

SHAs are deterministic. The policy is about constraint expressiveness ("does this dep declare a bounded version surface?"), not source provenance ("did the author audit this commit before pinning?"). A separate policy field (or a custom check via #519) can express the stricter provenance question without overloading this knob.

Trade-offs

  • Direct deps only. The check runs on the resolved dep set, but the classification reads dep.reference which the consumer authored. Transitive deps are pinned by their parent's manifest; the consumer has no way to rewrite that constraint. Adding a transitive variant is a follow-up; the design pack flags it as require_pinned_constraint_transitive: bool = False if demand surfaces.
  • Boolean today, not tri-state. Per the design pack convergence note: ship boolean now; add the sibling field later if demand for a direct | transitive split materialises. Holding this line unless a NEW technical reason surfaces.
  • No allowlist of approved unbounded patterns (e.g. allow main on internal repos). If demand surfaces, follow up with require_pinned_constraint: {except: [...]}.

Validation evidence

Tests (post-change)

$ uv run --extra dev pytest tests/unit/policy/test_pinned_constraint.py \
    tests/unit/policy/test_schema_pinned_constraint.py \
    tests/unit/policy/test_integration_pinned_constraint.py \
    tests/integration/test_policy_pinned_constraint_e2e.py -q
65 passed, 1 xfailed

Full unit suite: 15595 passed, 1 skipped, 21 xfailed.
Full policy integration suite: 59 passed.

Lint (canonical contract, all seven gates)

$ uv run --extra dev ruff check src/ tests/
All checks passed!
$ uv run --extra dev ruff format --check src/ tests/
1105 files already formatted
$ uv run --extra dev python -m pylint --disable=all --enable=R0801 \
    --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/
Your code has been rated at 10.00/10
$ bash scripts/lint-auth-signals.sh
[+] auth-signal lint clean

YAML I/O guard, file-length cap (policy_checks.py is 1124 lines, under the 2450 cap), and relative_to grep guards: not touched by this PR (no new file I/O, no new path manipulation).

How to test

# apm.policy.yml
enforcement: block
dependencies:
  require_pinned_constraint: true
# apm.yml
name: demo
version: 0.1.0
dependencies:
  apm:
    - acme/skills              # FAIL: no ref
    - other/lib@>=1.0          # FAIL: unbounded upper
    - good/lib#^1.2.0          # OK
$ apm install
[x] Policy violation: 2 dependency(ies) use unbounded constraints
    (hint: pin to a semver range, literal tag, or SHA)
    - acme/skills: no ref; resolves to default branch
    - other/lib: unbounded upper; pair with '<X.Y' or use a caret range

Switch enforcement: warn to verify install proceeds with the same diagnostic at [!] severity.

Out of scope (intentional)

Adds a new boolean field 'policy.dependencies.require_pinned_constraint'
to apm.policy.yml. When set to 'true', dependency policy checks flag any
direct APM dependency whose constraint is unbounded:

- NO_REF           : ref missing / empty (tracks default branch)
- BARE_BRANCH      : ref is a branch name ('main', 'develop', ...)
- WILDCARD         : ref is '*', 'x', or 'X'
- OPEN_UPPER       : range opens upward without paired upper (e.g. '>=1.0')
- GREATER_THAN_ONLY: range is a bare '>X.Y.Z'

Pinned (== bounded) constraints: exact versions, '^'/'~'/bounded ranges,
'X.Y.x' partial wildcards, literal 'vX.Y.Z' tags, 40-char SHAs, and
local-path deps (no version surface).

Default is false; opt-in via apm.policy.yml. Strict-wins inheritance
(once a parent policy enables it, child cannot relax). Routed through
the existing 'policy.enforcement' (warn | block | off). Runs from the
shared run_dependency_policy_checks seam so it fires at all four call
sites (policy_gate, policy_target_check, run_policy_checks,
run_policy_preflight).

The classification module operates on the declared constraint string
only -- deterministic, no I/O, no subprocess.

Closes #1491.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 26, 2026 23:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in governance rule to apm.policy.yml that enforces “bounded/pinned” dependency constraints and surfaces violations through the existing policy.enforcement routing. This extends the policy system’s dependency checks with a deterministic, string-only classifier and associated test + documentation coverage.

Changes:

  • Adds policy.dependencies.require_pinned_constraint: bool to the policy schema, parser, and inheritance merge logic.
  • Introduces _constraint_pinning.py to classify “unbounded” constraints (missing ref, wildcard, bare branch, open-upper ranges) and provide actionable diagnostics.
  • Wires a new dependency-policy check (dependency-pinned-constraint) into run_dependency_policy_checks, with new unit + integration + E2E tests and docs updates.
Show a summary per file
File Description
src/apm_cli/policy/schema.py Adds require_pinned_constraint to DependencyPolicy.
src/apm_cli/policy/parser.py Validates/parses dependencies.require_pinned_constraint from YAML.
src/apm_cli/policy/inheritance.py Merges the new field with strict-wins semantics (logical OR).
src/apm_cli/policy/_constraint_pinning.py New classifier + reason enum + humanized hints for unbounded constraints.
src/apm_cli/policy/policy_checks.py Adds _check_pinned_constraints() and hooks it into dependency policy checks.
src/apm_cli/policy/_help_text.py Adds a help-text constant for the new policy field.
tests/unit/policy/test_schema_pinned_constraint.py Verifies schema defaults, parsing, and inheritance behavior.
tests/unit/policy/test_policy_checks.py Updates expected total check count to include the new check.
tests/unit/policy/test_pinned_constraint.py Table-driven tests for classifier correctness + ASCII-only hints.
tests/unit/policy/test_integration_pinned_constraint.py Ensures the new check runs through key policy-check seams.
tests/integration/test_policy_pinned_constraint_e2e.py E2E apm install coverage for block vs warn behavior.
docs/src/content/docs/reference/policy-schema.md Documents the field, examples, diagnostics, and merge rules.
packages/apm-guide/.apm/skills/apm-usage/governance.md Updates governance scaffolding + merge/violation tables.

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 2

Comment thread src/apm_cli/policy/policy_checks.py
Comment thread docs/src/content/docs/reference/policy-schema.md Outdated
danielmeppiel and others added 2 commits May 27, 2026 01:15
…aint

Add require_pinned_constraint to sibling enterprise/consumer docs so
the new field is consistent across all surfaces that enumerate
dependencies.* policy fields:

- enterprise/policy-reference.md: YAML scaffold, reference section,
  check-name row, inheritance merge row, line-552 Dependencies bullet,
  violation-class diagnostic row
- enterprise/governance-guide.md: scope-matrix row, merge-diagram
  caption (require_pinned_constraint=OR)
- enterprise/apm-policy-getting-started.md: YAML scaffold (keeps
  lockstep with governance.md scaffold)
- consumer/governance-on-the-consumer-ramp.md: one-liner so consumers
  are not surprised by an install block

Plus three small code-side fixes from the panel:
- _constraint_pinning.py: precompile _PARTIAL_WILDCARD_RE at module
  scope (was recompiled per-component in _classify_range)
- _help_text.py: drop stale '--why' / 'apm policy explain' forward
  reference (avoids documentation debt before those surfaces exist)
- reference/policy-schema.md: clarify scope - the check classifies
  every dep declared in apm.yml (transitives pass because their
  parents pinned them), not just 'direct' deps

Lint clean (ruff, ruff format, pylint R0801, auth-signals).
65 new policy tests + 1 xfailed (the #1488 sentinel) still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship-with-followups

Adds require_pinned_constraint policy field -- one YAML line bans unbounded dep ranges org-wide, unlocking enterprise audit approval for governed installs.

cc @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

Panel converged unanimously: six active specialists, zero blocking findings, zero dissent. The heaviest signal -- doc-writer identifying drift across four sibling enterprise pages -- was folded in this round alongside the devx-ux stale-comment fix and the python-architect regex-precompile nit. Test-coverage-expert confirms all critical-promise surfaces pass: block/warn/off modes, strict-wins inheritance, four call sites, audit wrapper, and e2e CliRunner integration-with-fixtures. Evidence is load-bearing and unchallenged.

Strategically this is Theme 3 of the parallel governance push -- shape-based constraint classification is something only a cross-vendor tool can credibly own. The enum taxonomy (NO_REF, BARE_BRANCH, WILDCARD, OPEN_UPPER, GREATER_THAN_ONLY) is tight, pure-functional, and extensible without breaking existing policy files. Remaining follow-ups (MCP extension, tag-mutability doc-comment) are separable post-merge work that do not weaken the current guarantee.

Aligned with: governed-by-policy (one YAML field triggers org-wide constraint enforcement with strict-wins inheritance), secure-by-default (unbounded ranges are now classifiable and blockable at install time; opt-in today, designed for default-on in a future major), oss-community-driven (closes #1491; the #1488 xfail sentinel signals transparent backlog management to contributors).

Growth signal. Launch story is enterprise-shaped and ready: "APM 0.15: ban unbounded dep ranges org-wide -- one YAML line." The diagnostic output matches the established CheckResult voice that enterprise audit teams already parse. CHANGELOG entry should lead with the governance unlock, not the implementation detail. This is a moat-extension beat for Theme 3 positioning.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 2 Architecturally clean: well-placed module, enum + pure-function pattern, lazy imports, correct strict-wins, thorough coverage.
CLI Logging Expert 0 0 1 Diagnostic shape matches established CheckResult voice; ASCII enforced; hints actionable.
DevX UX Expert 0 0 2 No new CLI surface; behavior gated by opt-in YAML; hints are concrete and copy-pasteable.
Supply-Chain Security 0 3 0 Classification is sound; scope-clarity and MCP-extension worth following up post-merge.
OSS Growth Hacker 0 0 2 Theme 3 moat-extension beat; ship-and-amplify in the next release note.
Doc Writer 0 4 2 Sibling-page drift identified and folded in this round; corpus consistent.
Test Coverage Expert 0 0 0 All critical surfaces covered at integration-with-fixtures tier; e2e CliRunner block/warn both green.

B = blocking-severity findings, R = recommended, N = nits. Counts are signal strength, not gates. The maintainer ships.

Top 3 follow-ups

  1. [Supply-Chain Security] Extend _check_pinned_constraints to MCP dependency declarations -- MCP deps bypass the new classifier today; filing a tracked issue prevents a silent coverage gap as MCP adoption grows.
  2. [Supply-Chain Security] Add doc-comment clarifying that literal-tag classification relies on lockfile SHA for integrity against mutable refs -- without it, a future contributor may assume tag-pinning alone is sufficient; the lockfile is the actual integrity boundary.
  3. [OSS Growth Hacker] Update README inline governance list and add a cross-link from policy-reference.md to the new constraint check -- separable launch amplification work; deferred from this PR to keep the diff focused but should land before the 0.15 release note.

Architecture

classDiagram
    direction LR
    class UnboundedReason {
        <<Enum>>
        +NO_REF str
        +BARE_BRANCH str
        +WILDCARD str
        +OPEN_UPPER str
        +GREATER_THAN_ONLY str
    }
    class DependencyPolicy {
        <<ValueObject>>
        +allow tuple
        +deny tuple
        +require tuple
        +require_pinned_constraint bool
        +max_depth int
    }
    class DependencyReference {
        <<ValueObject>>
        +reference str
        +source str
        +is_local bool
    }
    class CheckResult {
        <<ValueObject>>
        +name str
        +passed bool
        +message str
        +details list
    }
    class classify_unbounded_reason {
        <<Pure>>
    }
    class humanize_reason {
        <<Pure>>
    }
    class _check_pinned_constraints {
        <<Pure>>
    }
    class run_dependency_policy_checks {
        <<IOBoundary>>
    }
    _check_pinned_constraints ..> classify_unbounded_reason : per dep
    _check_pinned_constraints ..> humanize_reason : format hint
    _check_pinned_constraints ..> DependencyPolicy : reads field
    _check_pinned_constraints ..> CheckResult : returns
    classify_unbounded_reason ..> DependencyReference : inspects
    classify_unbounded_reason ..> UnboundedReason : returns
    run_dependency_policy_checks *-- _check_pinned_constraints
Loading
flowchart TD
    A["apm install / apm audit"] --> B["run_dependency_policy_checks"];
    B --> C{"require_pinned_constraint?"};
    C -- false --> D["passed (disabled)"];
    C -- true --> E["per dep: classify_unbounded_reason"];
    E --> F{"is_local?"};
    F -- yes --> P["pinned"];
    F -- no --> G{"source == registry?"};
    G -- yes --> H["semver range -> _classify_range"];
    G -- no --> I{"empty ref?"};
    I -- yes --> J["NO_REF"];
    I -- no --> K{"40-char SHA?"};
    K -- yes --> P;
    K -- no --> L{"literal tag v1.2.3?"};
    L -- yes --> P;
    L -- no --> M{"bare *, x, X?"};
    M -- yes --> N["WILDCARD"];
    M -- no --> O{"is_semver_range?"};
    O -- yes --> H;
    O -- no --> Q["BARE_BRANCH"];
    H --> R{"shape"};
    R -- bounded --> P;
    R -- "&gt;=X" --> S["OPEN_UPPER"];
    R -- "&gt;X" --> T["GREATER_THAN_ONLY"];
    R -- wildcard --> N;
    E --> U["humanize_reason -> violation"];
    U --> V["CheckResult aggregated -> CIAuditResult"];
Loading
Full per-persona findings

python-architect (active, 2 nits)

  • nit (src/apm_cli/policy/_constraint_pinning.py:133): Lazy import of is_semver_range inside classify_unbounded_reason runs per-dep in a loop. Module cache makes the cost negligible; moving the import to the caller (_check_pinned_constraints) outside the loop would make the one-time cost explicit. Not folded -- current placement keeps lazy-import discipline at the boundary that needs it.
  • nit (src/apm_cli/policy/_constraint_pinning.py:99): re.match(r"^\d+\.\d+\.[xX*]$", p) recompiled per iteration. FOLDED IN as _PARTIAL_WILDCARD_RE module constant.

cli-logging-expert (active, 1 nit)

  • nit (src/apm_cli/policy/policy_checks.py): "dependency(ies)" parenthetical plural reads like machine output. NOT folded -- the exact same pattern is used by the sibling allow/deny/max_depth checks in the same file; touching only the new one would create inconsistency, and refactoring all four is out of scope.

devx-ux-expert (active, 2 nits)

  • nit (src/apm_cli/policy/_constraint_pinning.py): <X.Y in the hint reads as a shell redirect when copy-pasted into a terminal. Kept -- the hint is quoted ('<X.Y') in the rendered diagnostic.
  • nit (src/apm_cli/policy/_help_text.py:21): Stale --why / apm policy explain forward reference. FOLDED IN by dropping the reference.

supply-chain-security-expert (active, 3 recommended)

  • recommended: Literal-tag regex accepts mutable git refs. Doc-comment that tag-pinning relies on lockfile SHA for integrity. Deferred as follow-up.
  • recommended: MCP deps are not passed through _check_pinned_constraints. Deferred as follow-up issue.
  • recommended: Docstring "direct deps only" vs "transitives also classified" -- FOLDED IN by updating the schema reference to clarify scope.

oss-growth-hacker (active, 2 nits + growth note)

  • nits: README inline-list amplification + reference-page cross-link. Deferred as launch amplification work.
  • growth_strategy_note: Theme 3 launch story is enterprise-shaped and ready; CHANGELOG entry should lead with the governance unlock. Captured above under Growth signal.

auth-expert (inactive)

  • inactive_reason: PR touches only src/apm_cli/policy/* and docs/tests -- pure constraint-classification logic with no token, credential, host classification, or remote-auth code paths.

doc-writer (active, 4 high + 2 low)

  • high: enterprise/policy-reference.md missing the new field across YAML scaffold, reference section, check-name row, inheritance merge row, line-552 Dependencies bullet, violation-class row. FOLDED IN.
  • high: enterprise/governance-guide.md missing scope-matrix row and merge-diagram caption. FOLDED IN (require_pinned_constraint=OR added).
  • high: enterprise/apm-policy-getting-started.md scaffold missing the field. FOLDED IN.
  • high: consumer/governance-on-the-consumer-ramp.md missing consumer-facing one-liner. FOLDED IN.
  • low: "direct" wording in reference page accuracy nit. FOLDED IN.
  • low: Pre-existing em-dash in unrelated content. Out of scope -- separable encoding cleanup pass.

test-coverage-expert (active, 0 findings)

  • evidence: 7 passed test references including e2e CliRunner integration-with-fixtures tier for block and warn modes, and a structural proof that policy_target_check filter (line 92) prevents double-emission.

Advisory pass generated by apm-review-panel. The panel surfaces signal; the maintainer ships. No labels modified.

@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

All panel blocking findings folded (panel reported zero blocking, all recommended/nits either folded in c22995f's history or explicitly deferred as post-merge follow-ups). CI green on the merge-with-main commit. Lint silent. Ready to merge.

Follow-ups from the apm-review-panel pass have landed. Summary:

  • python-architect nit (regex precompile at _constraint_pinning.py:99) -- resolved in 94ad4da (_PARTIAL_WILDCARD_RE module constant).
  • devx-ux nit (stale --why reference at _help_text.py:21) -- resolved in 94ad4da.
  • supply-chain-security recommended (docstring scope clarification: direct deps vs transitives) -- resolved in 94ad4da.
  • doc-writer high (sibling-page drift across enterprise/policy-reference.md, enterprise/governance-guide.md, enterprise/apm-policy-getting-started.md, consumer/governance-on-the-consumer-ramp.md) -- resolved in 94ad4da.
  • Merge-with-main resolved (c22995f) so CI evaluates on the same commit shape it would after merge.

Deferred as separable follow-ups (panel-tagged, not blocking):

  • supply-chain MCP-deps extension of _check_pinned_constraints -- post-merge issue.
  • supply-chain tag-mutability doc-comment (literal-tag relies on lockfile SHA for integrity) -- post-merge.
  • oss-growth README inline-list amplification + policy-reference.md cross-link -- launch amplification, deferred to land before the 0.15 release note.

Lint contract (all 4 silent / exit 0 on c22995f):

  • uv run --extra dev ruff check src/ tests/ -> "All checks passed!"
  • uv run --extra dev ruff format --check src/ tests/ -> "1105 files already formatted"
  • uv run --extra dev python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/ -> 10.00/10
  • bash scripts/lint-auth-signals.sh -> "[+] auth-signal lint clean"

Targeted policy test re-run (post-merge): pytest tests/unit/policy tests/integration -k "pinned or policy or constraint" -> 1434 passed, 16 skipped, 1 xfailed, 17 subtests passed. The single xfail is the #1488 sentinel for git-semver classification; it will flip to passing once PR #1496 merges.

CI evidence on c22995f: 13 SUCCESS, 1 SKIPPED (deploy), 0 FAILURE. Run: https://github.com/microsoft/apm/actions/runs/26506131520

Ready for maintainer review.

danielmeppiel and others added 2 commits May 27, 2026 13:10
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot review on #1494 flagged that _check_pinned_constraints()
iterated the full resolved set (including transitives) passed by
run_dependency_policy_checks(). A transitive package declaring
'main', '*', or '>=X' in its own manifest is not actionable by the
consumer, but enabling require_pinned_constraint would fail their
install.

Thread the direct-dep set (DependencyReference.get_unique_key()
values) through run_dependency_policy_checks into the pinned check.
Wire policy_gate, policy_target_check, and run_policy_preflight to
pass it; legacy/dep-only seams (direct_dep_keys=None) keep the
prior behavior. Audit wrapper already iterates direct-only manifest
deps, so no change there.

Regression test added with mutation-break gate verified: deleting
the filter line makes the new transitive-skip test fail, restoring
it passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

Copilot review follow-up: 1 thread addressed (require_pinned_constraint now restricted to direct deps via threaded direct_dep_keys from policy_gate / policy_target_check / run_policy_preflight; transitive constraints declared in a third-party manifest are no longer evaluated). Regression test landed with mutation-break gate verified. Commit 620fe31. CI green.

@danielmeppiel danielmeppiel merged commit 3dabdb2 into main May 27, 2026
14 checks passed
@danielmeppiel danielmeppiel deleted the feat/policy-require-pinned branch May 27, 2026 11:31
danielmeppiel added a commit that referenced this pull request May 27, 2026
…#1494) (#1505)

Adds 6 CliRunner-based e2e tests that close the user-promise gaps left
by #1494's existing unit + partial-e2e coverage:

- Promise B: pinned dep (caret range, bare exact version) passes the
  policy gate under enforcement=block + require_pinned_constraint=true.
- Promise C: policy_gate forwards direct_dep_keys to the runner --
  regression trap for the transitive bleed-through guard added in the
  #1494 Copilot follow-up.
- Promise D + G: block exits with code 1 and the diagnostic cites the
  offending dep ref, the pinning hint, and the check name inside the
  violation block (not just upstream resolver noise).
- Promise E: backward compat -- require_pinned_constraint=false (the
  default) does NOT block an unbounded dep, even at enforcement=block.
- Promise F: --dry-run previews a 'Would be blocked by policy' line
  citing the dep without aborting or mutating the filesystem.

Each test was verified with a mutation-break gate: removing the
production guard makes the corresponding test fail; restoring it
passes (see PR body for the 5 mutations + evidence).

Surfaces a separate observation worth filing: the '=1.2.3' alternate
exact-version syntax is currently classified as BARE_BRANCH by
_constraint_pinning.py (only bare '1.2.3' is recognized). The
e2e uses the bare form per the documented contract.

No production code changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(policy): add dependencies.require_pinned_constraint to ban unbounded ranges

2 participants