Skip to content

Fix phantom drift in apm audit after clean install (#1978)#1986

Open
sergio-sisternes-epam wants to merge 2 commits into
mainfrom
sergio-sisternes-epam-fix-audit-phantom-drift-1978
Open

Fix phantom drift in apm audit after clean install (#1978)#1986
sergio-sisternes-epam wants to merge 2 commits into
mainfrom
sergio-sisternes-epam-fix-audit-phantom-drift-1978

Conversation

@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

TL;DR

apm audit --ci reported false drift on every merge-hook target (claude, codex, cursor, gemini, windsurf) immediately after a clean apm install. Copilot was immune (per-file hook layout -- no _apm_source in JSON).

Problem (WHY)

During drift replay, run_replay() passed the scratch tmpdir as project_root to integrate_package_primitives. The hook integrator's _is_root_local_package compared package_info.install_path (real project dir) against project_root (scratch tmpdir) -- they never match, so _get_hook_source_marker returned the bare package name ("myapp") instead of the root-local marker ("_local/myapp").

The JSON written to disk during a real install contains "_apm_source": "_local/myapp". The scratch replay wrote "_apm_source": "myapp". The diff fired -- phantom drift on every target whose hooks live in a merged JSON config.

Approach (WHAT)

Thread real_project_root: Path | None = None through the hook integration call chain so ownership identity checks evaluate against the actual project directory while all file I/O still targets the scratch dir.

Implementation (HOW)

drift.py:run_replay()
  pass real_project_root=project_root (the real one, not scratch)
    -> integrate_package_primitives(real_project_root=...)
       -> _call_kwargs["real_project_root"] injected for hooks primitive only
          -> integrate_hooks_for_target(real_project_root=...)
             -> integrate_package_hooks / _integrate_merged_hooks / integrate_kiro_hooks
                _eff_root = real_project_root or project_root
                used for: _get_package_name, _get_hook_source_marker,
                          _is_root_local_package, _dependency_hook_sources
                project_root unchanged for: mkdir, read, write (still scratch)

All other integrators (prompt, agent, instruction, command, skill) are unaffected -- real_project_root is injected into _call_kwargs only when _prim_name == "hooks".

sequenceDiagram
    participant R as run_replay
    participant S as services.py
    participant H as HookIntegrator
    R->>S: integrate_package_primitives(scratch_root, real_project_root=project_root)
    S->>H: integrate_hooks_for_target(project_root=scratch, real_project_root=real)
    H->>H: _eff_root = real_project_root or project_root
    H->>H: _is_root_local_package(install_path, _eff_root) -> True
    H->>H: _get_hook_source_marker(...) -> "_local/myapp"
    H->>H: write to scratch/...
Loading

Trade-offs

  • real_project_root is None in all normal (non-replay) callers -- no behavioural change outside replay.
  • The C901 complexity noqa on integrate_package_primitives was extended (was PLR0913, now PLR0913,C901) since the if real_project_root is not None branch crossed the threshold. The function's complexity was already accepted; this is the minimal acceptable extension.

Validation evidence

pytest tests/unit/integration/test_hook_integrator_defect_regression.py -v
# 10 passed (6 pre-existing + 4 new #1978 tests)

pytest tests/integration/test_hook_root_source_drift_e2e.py -v
# 10 passed (5 pre-existing heal tests + 5 new phantom-drift tests)
# targets: claude, codex, cursor, gemini, windsurf

uv run --extra dev ruff check src/ tests/     # All checks passed
uv run --extra dev ruff format --check src/ tests/  # 1374 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

wc -l src/apm_cli/integration/hook_integrator.py
# 2051 (under 2450 guardrail)

How to test

# Minimal repro (any merge-hook target)
mkdir /tmp/myapp && cd /tmp/myapp
cat > apm.yml <<'EOF'
name: myapp
version: 0.0.0
targets:
  - claude
EOF
mkdir -p .apm/hooks
cat > .apm/hooks/pre.json <<'EOF'
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"echo test"}]}]}}
EOF

apm install
apm audit --ci  # was exit 1 before; must be exit 0 after this fix

Closes #1978

During drift replay, run_replay() passed the scratch tmpdir as
project_root to integrate_package_primitives. The hook integrator's
_is_root_local_package compared install_path (real project dir) against
project_root (scratch tmpdir) -- they never match, so the source marker
was computed as bare "name" instead of "_local/name". The JSON on disk
(written during real install) had _local/name; scratch had name -- the
diff fired and reported phantom drift.

Fix: thread real_project_root: Path | None = None through the hook
integration call chain so ownership identity checks evaluate against
the actual project directory while all file I/O still targets the
scratch dir.

Changed files:
- src/apm_cli/install/drift.py: pass real_project_root=project_root
  (the real project dir, not scratch) to integrate_package_primitives
- src/apm_cli/install/services.py: accept real_project_root param;
  inject into _call_kwargs for the hooks primitive only
- src/apm_cli/integration/hook_integrator.py: add real_project_root to
  integrate_hooks_for_target, integrate_package_hooks,
  _integrate_merged_hooks; use _eff_root = real_project_root or
  project_root for _get_package_name, _get_hook_source_marker,
  _is_root_local_package, _dependency_hook_sources
- src/apm_cli/integration/kiro_hook_integrator.py: same pattern for
  integrate_kiro_hooks

Tests:
- unit: test_get_hook_source_marker_uses_real_project_root_not_scratch
  and parametrized test_real_project_root_fixes_source_marker_for_merge_targets
  covering all merge-hook targets (claude/codex/cursor/gemini/windsurf)
- e2e: test_audit_no_drift_after_clean_install -- install then
  apm audit --ci must exit 0, parametrized across all merge-hook targets

Fixes #1978

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

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 a false-positive drift report in apm audit --ci immediately after a clean apm install by ensuring hook ownership identity checks compare against the real project root during drift replay (while replay file I/O still targets the scratch directory).

Changes:

  • Thread real_project_root: Path | None through the install integration pipeline and drift replay so hook integrators compute _apm_source consistently with install-time behavior.
  • Update hook integration paths (merged-hook targets + Copilot + Kiro) to use an effective root (real_project_root or project_root) for ownership/name decisions.
  • Add unit + integration regression tests covering the phantom drift scenario across merge-hook targets.

Reviewed changes

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

Show a summary per file
File Description
tests/unit/integration/test_hook_integrator_defect_regression.py Adds regression tests for #1978, covering source marker behavior during replay-like conditions.
tests/integration/test_hook_root_source_drift_e2e.py Adds end-to-end test ensuring audit --ci reports no drift right after install.
src/apm_cli/integration/kiro_hook_integrator.py Accepts real_project_root and uses it for package naming decisions during replay.
src/apm_cli/integration/hook_integrator.py Threads real_project_root through hook integration and uses it for ownership/source marker decisions.
src/apm_cli/install/services.py Passes real_project_root through to hook integration only (as a hooks-only kwarg).
src/apm_cli/install/drift.py Supplies real_project_root=project_root when replaying integrations into a scratch root.
Comment thread tests/unit/integration/test_hook_integrator_defect_regression.py Outdated
Comment thread tests/integration/test_hook_root_source_drift_e2e.py Outdated
- Drop tautological OR clause from pkg_name_scratch assertion; assert
  directly against pkg_info.install_path.name so the check can
  actually fail if fallback behaviour changes.
- Fix docstring in test_audit_no_drift_after_clean_install: the test
  runs apm audit --ci, not apm audit --check drift.

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

2 participants