Skip to content

fix: honor per-dep targets and plural targets: in audit drift replay#1930

Merged
danielmeppiel merged 2 commits into
mainfrom
sergio-sisternes-epam-audit-drift-fix
Jun 27, 2026
Merged

fix: honor per-dep targets and plural targets: in audit drift replay#1930
danielmeppiel merged 2 commits into
mainfrom
sergio-sisternes-epam-audit-drift-fix

Conversation

@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

TL;DR

Two false-positive unintegrated drift bugs in apm audit --ci fixed
in a single, minimal change to src/apm_cli/install/drift.py.


Problem (WHY)

#1923 -- per-dependency targets: not honored in replay

apm install correctly narrows deployment for a dependency declared with
targets: [claude] in apm.yml -- only .claude/skills/ is written.
The lockfile stores target_subset: [claude] on the locked entry.

However, run_replay called integrate_package_primitives without
dep_target_subset, so the replay ran copilot primitives into
.agents/skills/ in scratch even though the dep was scoped to claude.
diff_scratch_against_project then found .agents/skills/<dep>/SKILL.md
in scratch but not in the project and reported false unintegrated drift.

#1924 -- plural targets: field ignored during replay

_read_apm_yml_target read data.get("target") (singular) and passed
it to parse_target_field. A project with targets: [claude, codex]
(plural list form, no copilot) returns None from the old lookup, so
resolve_targets fell through to directory-based auto-detection. With
.github/ present for unrelated CI workflows, copilot was auto-detected,
and the replay wrote .github/instructions/local.instructions.md into
scratch. The project tree had no such file, yielding false
unintegrated drift.


Approach (WHAT)

Two surgical fixes, both in src/apm_cli/install/drift.py:

  1. Pass dep_target_subset in run_replay -- thread
    lock_dep.target_subset or None into integrate_package_primitives
    so the per-dependency target filter that already works at install time
    is applied identically during replay.

  2. Use parse_targets_field in _read_apm_yml_target -- replace the
    hand-rolled data.get("target") + parse_target_field path with
    parse_targets_field from core/apm_yml.py, which already handles
    both target: (singular) and targets: (plural list) forms and
    validates tokens against the canonical set.


Implementation (HOW)

# src/apm_cli/install/drift.py

# Fix #1924: _read_apm_yml_target
-    raw = data.get("target")
-    if raw is None:
-        return None
-    try:
-        from apm_cli.core.target_detection import parse_target_field
-        return parse_target_field(raw, source_path=apm_yml)
-    except Exception:
-        return None
+    try:
+        from apm_cli.core.apm_yml import parse_targets_field
+        tokens = parse_targets_field(data)
+        return tokens if tokens else None
+    except Exception:
+        return None

# Fix #1923: run_replay
+    dep_target_subset=lock_dep.target_subset or None,

No CLI surface change. No new dependencies. No docs change required
(the targets: per-dependency semantics are already documented and
the fix restores intended behavior).


Architecture

flowchart TD
    A[apm audit --ci] --> B[_check_drift]
    B --> C[run_replay]
    C --> D[_read_apm_yml_target]
    D --> |"parse_targets_field (both forms)"| E[resolve_targets with explicit_target]
    C --> F[integrate_package_primitives]
    F --> |"dep_target_subset=lock_dep.target_subset"| G[filter_targets_for_dependency]
    G --> H[Write to scratch only for allowed targets]
    B --> I[diff_scratch_against_project]
    I --> J[No false unintegrated findings]
Loading
sequenceDiagram
    participant Audit as apm audit --ci
    participant Replay as run_replay
    participant Parse as _read_apm_yml_target
    participant Integrate as integrate_package_primitives
    participant Filter as filter_targets_for_dependency

    Audit->>Replay: run_replay(config)
    Replay->>Parse: _read_apm_yml_target(project_root)
    Note over Parse: parse_targets_field handles<br/>target: and targets: (#1924)
    Parse-->>Replay: ["claude", "codex"]
    Replay->>Integrate: ...dep_target_subset=["claude"]
    Note over Integrate: filter_targets_for_dependency<br/>narrows to claude only (#1923)
    Integrate-->>Replay: scratch written to .claude/ only
    Replay-->>Audit: scratch_root
    Note over Audit: diff finds no phantom .agents/ entries
Loading

Trade-offs

  • _read_apm_yml_target now returns list[str] instead of str | None.
    resolve_targets(explicit_target=...) already accepts a list, so no
    downstream change is needed. Updated tests reflect the new return type.
  • Using parse_targets_field means an invalid token in targets: now
    raises inside _read_apm_yml_target's except Exception guard and
    falls back to auto-detect (same safe fallback as before).

Validation evidence

uv run --extra dev pytest tests/unit/install/test_drift.py \
  tests/unit/install/test_drift_detection.py \
  tests/unit/install/test_drift_phase3.py -q
# 1688 passed

uv run --extra dev ruff check src/ tests/ && \
  uv run --extra dev ruff format --check src/ tests/ && \
  uv run --extra dev python -m pylint --disable=all --enable=R0801 \
    --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/ && \
  bash scripts/lint-auth-signals.sh
# All clean / 10.00/10

How to test

  1. Create a project with targets: [claude, codex] and a .github/
    directory (no copilot target).
  2. Install any package that contains instruction primitives.
  3. Run apm audit --ci -- should report no drift (was false
    unintegrated for .github/instructions/ before this fix).

For #1923:

  1. Add a dep with targets: [claude] in apm.yml.
  2. Install (apm install) -- only .claude/skills/ is written.
  3. Run apm audit --ci -- should report no drift (was false
    unintegrated for .agents/skills/<dep>/SKILL.md before this fix).

Closes #1923
Closes #1924

Fixes two false-positive 'unintegrated' drift bugs in apm audit --ci:

#1923 -- dep_target_subset not threaded into replay
  run_replay called integrate_package_primitives without
  dep_target_subset, so a dependency narrowed to targets: [claude]
  via per-dependency 'targets:' in apm.yml would still have copilot
  replay skills into .agents/skills/ in the scratch tree, causing
  false 'unintegrated' findings for paths the install never wrote.

  Fix: pass dep_target_subset=lock_dep.target_subset or None to
  integrate_package_primitives from the loop in run_replay.

#1924 -- _read_apm_yml_target read 'target:' but not 'targets:'
  _read_apm_yml_target used data.get('target') directly and
  parse_target_field, missing the plural 'targets:' list form.
  A project with 'targets: [claude, codex]' (no copilot) that also
  has .github/ for unrelated CI workflows had copilot auto-detected
  during replay, writing .github/instructions/ into scratch and
  reporting false 'unintegrated' drift.

  Fix: replace the hand-rolled single-key lookup with
  parse_targets_field from core/apm_yml.py, which handles both
  'target:' (singular) and 'targets:' (plural list) forms and
  validates tokens against the canonical target set.

Tests: add test_run_replay_threads_dep_target_subset; add
test_apm_yml_with_targets_list_returns_list; update stale
test_apm_yml_with_target_returns_value / test_parse_target_field_*
tests in test_drift_detection.py and test_drift_phase3.py to
reflect that _read_apm_yml_target now returns list[str] via
parse_targets_field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 26, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes two false-positive unintegrated drift reports in apm audit --ci by ensuring drift replay uses the same target resolution and per-dependency target narrowing as apm install.

Changes:

  • Thread lock_dep.target_subset into integrate_package_primitives(..., dep_target_subset=...) during drift replay to honor per-dependency targets: narrowing (#1923).
  • Update _read_apm_yml_target() to use parse_targets_field() so both target: and targets: forms are honored during replay (#1924).
  • Adjust unit tests to reflect the new _read_apm_yml_target() return shape (list[str] | None) and add coverage for per-dep target subset forwarding.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/apm_cli/install/drift.py Aligns drift replay with install-time target parsing (targets:) and per-dependency target filtering (dep_target_subset).
tests/unit/install/test_drift.py Adds a unit test asserting dep_target_subset is forwarded into integration during replay.
tests/unit/install/test_drift_phase3.py Updates _read_apm_yml_target expectations to the new list-return behavior and patches parse_targets_field.
tests/unit/install/test_drift_detection.py Adds coverage for plural targets: parsing and updates singular/exception tests for the list-return behavior.

Comment thread tests/unit/install/test_drift.py Outdated
Comment thread tests/unit/install/test_drift.py Outdated
…et aware

Address Copilot code-review suggestions on test_run_replay_threads_dep_target_subset:

- Use an explicit apm.yml with 'targets: [claude, copilot]' instead of
  relying on directory-based auto-detection via .github/ presence.  This
  makes target resolution deterministic and matches the real #1923 scenario
  where a copilot target is active but the dep is scoped to claude only.

- Expand the spy to capture both the resolved 'targets' list (target names)
  and 'dep_target_subset'.  The test now also asserts that 'copilot' is in
  the resolved target set, confirming the subset actually narrows
  integration (not just a no-op subset on a single-target replay).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added this pull request to the merge queue Jun 27, 2026
Merged via the queue into main with commit 724a078 Jun 27, 2026
13 checks passed
@danielmeppiel danielmeppiel deleted the sergio-sisternes-epam-audit-drift-fix branch June 27, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants