Skip to content

[BUG] update/compile must reconcile deployed_files when the target universe contracts#2144

Closed
danielmeppiel wants to merge 3 commits into
mainfrom
apm-rt-fix-2139
Closed

[BUG] update/compile must reconcile deployed_files when the target universe contracts#2144
danielmeppiel wants to merge 3 commits into
mainfrom
apm-rt-fix-2139

Conversation

@danielmeppiel

Copy link
Copy Markdown
Collaborator

fix(reconciliation): remove deployments for contracted targets

TL;DR

apm update and apm compile now reconcile deployed files and lockfile ownership against the current declared target universe, including no-op dependency updates. The implementation shares install's target governance and hash-gated deletion path.

Fixes #2139. This is the update/compile sibling to #2059 and #2114.

Problem (WHY)

  • Install a package for copilot,windsurf, then contract apm.yml to copilot.
  • apm update --yes --target copilot exited 0 while .windsurf/rules/demo.md and its lockfile entries survived.
  • apm compile --target copilot had the same false-success state.
  • Only a later apm install repaired disk and lockfile state.

Approach (WHAT)

  • Promote deployed-state reconciliation into command-neutral operations in manifest_reconcile.py.
  • Route install's dependency and root/local lockfile blocks through the shared block reconciler.
  • Invoke project reconciliation after successful update and compile entrypoints, including no-op updates.
  • Keep target ownership vendor-neutral through TargetProfile governance and preserve legacy multi-target state when no target universe is declared.
  • Route every deletion through remove_stale_deployed_files() so path, hash, and user-edit gates remain unchanged.

Implementation (HOW)

Area Change
Reconciliation owner Resolves the declared target universe, reconciles dependency and local blocks, removes safely attributable stale files, and persists changed lockfiles.
Install Uses the shared block operation for per-dependency and root/local manifests.
Update Reconciles after accepted plans and successful no-op updates; dry-run and declined plans remain mutation-free.
Compile Reconciles after successful non-dry-run generation, with separate manifest, deployment, and lock roots for --root.
Tests/docs Adds hermetic update/compile contraction regressions, a no-declared-universe preservation trap, and command-reference updates.

Legend: the dashed stage is the shared behavior introduced by this change.

flowchart LR
    U[apm update] --> R[ReconcileDeployedState]
    C[apm compile] --> R
    I[apm install] --> R
    R --> G[TargetProfile governance]
    G --> D[Hash-gated disk cleanup]
    G --> L[Lockfile reconciliation]
    classDef new stroke-dasharray: 5 5;
    class R new;
Loading

Trade-offs

  • Reconciliation is conservative when apm.yml declares no target universe, preserving existing multi-target state rather than inferring contraction from stale directories.
  • Failed deletions remain lockfile-owned for retry; user-edited files follow the existing install cleanup semantics.
  • Dynamic and gated targets remain legitimate siblings so canonical target contraction does not erase unrelated dynamic deployment ownership.

Benefits

  1. Update and compile no longer report success with removed-target artifacts still deployed.
  2. Disk state and both lockfile ownership blocks converge in the same command run.
  3. Install, update, and compile use one deletion and target-governance implementation.

Validation evidence

TDD regression

The two new command scenarios failed on base at the stale-file assertion:

2 failed, 1 deselected
AssertionError: assert not True

After the fix:

300 passed in 4.13s

Scenario evidence

Scenario Principle Test
Update removes Windsurf state after contraction to Copilot Vendor-neutral, DevX test_materializing_command_reconciles_contracted_target[update-args0]
Compile removes Windsurf state after contraction to Copilot Vendor-neutral, DevX test_materializing_command_reconciles_contracted_target[compile-args1]
No declared universe preserves legitimate sibling targets Multi-harness test_state_reconcile_without_declared_universe_preserves_sibling

Lint

ruff check: passed
ruff format --check: passed
pylint R0801: 10.00/10
lint-auth-signals.sh: clean
file-length and portable relative-path guards: clean

How to test

  1. Install a fixture with apm install --target copilot,windsurf --no-policy.
  2. Remove windsurf from apm.yml and run apm update --yes --target copilot.
  3. Confirm .windsurf/rules/<name>.md and its deployed_files / deployed_file_hashes rows are gone.
  4. Repeat from the two-target state with apm compile --target copilot and confirm the same result.
  5. Run uv run --extra dev python -m pytest tests/integration/test_inactive_target_ghost_e2e.py.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 21:47

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

This PR makes apm update and apm compile reconcile on-disk deployed artifacts and lockfile deployed_files ownership against the currently declared target universe (so contracting targets: removes removed-target artifacts/rows), reusing install's hash/user-edit gated deletion path.

Changes:

  • Adds shared reconciliation helpers in install/manifest_reconcile.py and routes install lockfile/local blocks through them.
  • Invokes post-success reconciliation from apm update (including successful no-op updates) and apm compile (non-dry-run).
  • Adds unit + hermetic integration regressions for contracted-target cleanup and “no declared universe preserves siblings”, and updates command/reference docs.
Show a summary per file
File Description
tests/unit/install/phases/test_lockfile_union.py Adds regression ensuring reconciliation preserves legacy multi-target state when no declared universe exists.
tests/integration/test_inactive_target_ghost_e2e.py Adds hermetic end-to-end tests asserting update/compile remove contracted-target artifacts and lock entries.
src/apm_cli/install/phases/targets.py Delegates declared-universe resolution to shared manifest reconciliation helper.
src/apm_cli/install/phases/post_deps_local.py Switches local lockfile deployed-files union logic to shared reconcile helper.
src/apm_cli/install/phases/lockfile.py Switches per-dep deployed-files union logic to shared reconcile helper and threads diagnostics.
src/apm_cli/install/manifest_reconcile.py Introduces declared-universe resolution + shared deployed-block/state reconciliation and persistence helper.
src/apm_cli/commands/update.py Runs reconciliation after successful updates, including successful no-op updates.
src/apm_cli/commands/compile/cli.py Runs reconciliation after successful non-dry-run compilation.
packages/apm-guide/.apm/skills/apm-usage/commands.md Updates command reference blurbs to mention post-success reconciliation behavior.
docs/src/content/docs/reference/cli/update.md Documents that successful updates reconcile target contraction even without dep ref changes.
docs/src/content/docs/reference/cli/compile.md Documents post-success compile reconciliation semantics.

Review details

  • Files reviewed: 11/11 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment on lines +184 to +192
from apm_cli.core.apm_yml import CANONICAL_TARGETS, parse_targets_field
from apm_cli.integration.targets import KNOWN_TARGETS
from apm_cli.utils.yaml_io import load_yaml

try:
data = load_yaml(project_root / "apm.yml")
names = parse_targets_field(data) if isinstance(data, dict) else None
except (AttributeError, KeyError, OSError, TypeError, ValueError):
return None
danielmeppiel and others added 2 commits July 11, 2026 10:23
apm-spec-waiver: Deployment reconciliation gives update/compile the parity install already has; enforces existing lockfile ownership semantics, no new OpenAPM wire behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

Superseded by #2155. The reliability campaign consolidated all 13 point fixes into a single architectural cure PR that re-homes each fix under its canonical owner module and adds owner-invariant guards. The fix for #2139 is folded into #2155 and re-verified there (full CI green). Closing this point PR in favor of the consolidated cure; the branch is preserved and this can be reopened if #2155 is not merged.

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.

[BUG] apm update/compile leave stale deployed_files + artifacts when the target universe contracts (only install reconciles)

2 participants