fix(install): discard lockfile with git conflict markers, fail closed under --frozen - #3028
Lachlan Heywood (lachieh) wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Resolve the remaining lockfile path, decoding, frozen-guidance, and documentation issues.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds conflict-marker detection for lockfiles, regenerating them during full installs while failing closed for frozen and partial operations.
Changes:
- Updates install, lock, MCP, dry-run, and frozen-mode handling.
- Adds unit and integration coverage.
- Updates CLI documentation and changelog.
File summaries
| File | Description |
|---|---|
tests/unit/install/test_frozen.py |
Tests frozen conflict behavior. |
tests/unit/deps/test_lockfile_conflict_markers.py |
Tests marker detection and discard behavior. |
tests/integration/test_install_conflicted_lockfile_e2e.py |
Covers CLI recovery and failure modes. |
src/apm_cli/install/service.py |
Adds frozen-mode failure handling. |
src/apm_cli/install/presentation/dry_run.py |
Reports conflicts without modifying files. |
src/apm_cli/install/mcp/command.py |
Surfaces MCP lockfile errors. |
src/apm_cli/install/errors.py |
Refines frozen recovery guidance. |
src/apm_cli/deps/lockfile.py |
Detects and discards conflicted lockfiles. |
src/apm_cli/commands/lock.py |
Applies recovery and export handling. |
src/apm_cli/commands/install.py |
Regenerates conflicted files during full installs. |
docs/src/content/docs/troubleshooting/install-failures.md |
Documents recovery steps. |
docs/src/content/docs/reference/lockfile-spec.md |
Documents conflict semantics. |
docs/src/content/docs/reference/cli/lock.md |
Documents apm lock behavior. |
docs/src/content/docs/reference/cli/install.md |
Documents install and frozen behavior. |
CHANGELOG.md |
Records the fix. |
Review details
Suppressed comments (7)
CHANGELOG.md:29
- The
#2979suffix is the linked issue number, not this pull request's number. The changelog contract requires each entry to end with the actual PR number; replace this suffix with the PR number when it is known.
- A full `apm install` and `apm lock` now warn, discard `apm.lock.yaml`, and resolve from `apm.yml` when the lockfile still contains git merge conflict markers, instead of exiting with a YAML parse error. `apm install --frozen`, partial installs, and read-only commands such as `apm update` and `apm outdated` fail closed with an error that names the conflict and the next action, and the `--frozen` failure tip no longer points at commands that cannot read the lockfile. (#2979)
docs/src/content/docs/reference/cli/install.md:153
- The CLI behavior changed here, but the maintained
packages/apm-guide/.apm/skills/apm-usage/resources were not updated:commands.md:15still describes frozen mode only as missing/out-of-sync, andtroubleshooting.md:85-89has no merge-conflict recovery. Add the concise install/lock conflict behavior there so the package guidance does not give stale recovery instructions.
- **Frozen mode.** With `--frozen`, install resolves only what is in `apm.lock.yaml`. A missing lockfile, a direct dependency missing from it, or MCP config state that differs from `apm.yml` exits `1` before lockfile, target config, deployment, or cache mutation. Cold-cache installs (empty `apm_modules/`) with git `apm_package` deps are tolerated: MCP checks are skipped for absent package directories (the packages will be hydrated by the pipeline), and their MCP server configs are restored from the lockfile so no false drift is reported. Remote `claude_skill` dependencies declared at a repository root or subdirectory are also accepted from their locked type before materialization; once present, the lock type and detected skill shape must agree. Missing local paths still fail. A lockfile that contains git merge conflict markers also exits `1` and is never rewritten under `--frozen`. See [`config-consistency`](../../baseline-checks/#config-consistency) for the full manifest rule. Run normal `apm install` to create or repair MCP-only lock state, or to discard a conflicted lockfile and resolve from `apm.yml`, then retry frozen mode. Add-style invocations (`apm install PACKAGE` and `apm install --mcp NAME`) are rejected because they mutate `apm.yml`. Orphan package lock entries are tolerated; local-path deps are skipped. This is a structural check, not a content check -- run `apm audit --ci` for hash verification.
docs/src/content/docs/reference/lockfile-spec.md:398
- This "Every command" claim is broader than the current behavior:
commands/view.py::_lookup_lockfile_refandcommands/deps/cli.pycatchExceptionaroundLockFile.readand continue without lockfile metadata, so those readers still do not name this conflict. Narrow the sentence to commands that require the lockfile, or update those best-effort readers to surface the error.
conflict rather than a YAML error. Every command that reads the lockfile names
the file and the next action. A full `apm install` (no package arguments, no
src/apm_cli/commands/lock.py:313
apm lock exportis a read-only lockfile consumer, but this newLockFile.readcall still uses onlyget_lockfile_path. A project that has only the supported legacyapm.lockis therefore reported as having no lockfile, and a conflict in that file is never classified; route the path throughresolve_lockfile_path_for_read(project_root, read_only=True)as the other read-only consumers do (for example,commands/outdated.py:449).
lockfile = LockFile.read(lockfile_path)
src/apm_cli/deps/lockfile.py:1276
- This discard probe performs a second unguarded UTF-8 decode. A non-UTF-8 lockfile reaches it before the pipeline's
LockFile.read, so a full install reports a rawUnicodeDecodeErrorinstead of the normalized fail-closed lockfile error. Catch and normalize the decode here, leaving the file in place so it cannot be discarded as conflicted.
if not path.exists() or not has_conflict_markers(path.read_text(encoding="utf-8")):
return False
src/apm_cli/install/errors.py:103
- This new early return also applies to
FrozenInstallErrorfrom the generic unreadable-lockfile path inInstallService.enforce_frozen: that message only says--frozen could not read ...and does not tell the user how to recover. Preserve actionable guidance for unreadable (non-missing) lockfiles, or make that exception message include the normal-install repair action while keeping the missing-file case free of the obsoleteoutdatedtip.
if not error.reasons:
return ""
tests/integration/test_install_conflicted_lockfile_e2e.py:16
- This new integration module invokes the Click CLI in-process via
CliRunnerand touches a temporary filesystem, so it needs the module-levelcomponentbehavioral marker. Withoutpytestmark = pytest.mark.component, the new tests are left outside the repository's marker-only taxonomy and are not selected by component-scoped runs (seetests/quality/test_test_taxonomy.py:152-163).
import pytest
from click.testing import CliRunner
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Thank you for contributing this pull request. This PR is linked to #2979, which already carries maintainer status/accepted. Advisory triage recommendation is ready-for-review. That is not merge approval, not assignment, and not a request to run a review panel. CODEOWNERS already requested danielmeppiel and sergio-sisternes-epam. This note does not add or change review requests. A responsible human maintainer still needs to review the implementation against the accepted issue, including the chosen default: detect git conflict markers in the lockfile load owner, regenerate on a full non-frozen Generated by autopilot-pr-triage-worker. This comment is AI-generated and may contain errors. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 2 | 1 | Conflicted-lockfile detect is centralized, but discard-vs-fail-closed and the unlink still fork outside InstallTransaction. |
| CLI Logging Expert | 0 | 0 | 0 | Conflict named with next action; empty frozen tip is intentional so outdated/update are not suggested. |
| DevX UX Expert | 0 | 2 | 1 | Tighten regenerate vs fail-closed defaults, dry-run exit codes for CI, and non-transactional lockfile updates. |
| Supply Chain Security Expert | 0 | 0 | 0 | Conflicted-lock regen is the existing missing-lockfile path; --frozen and non-conflict corruption stay fail-closed. No integrity bypass. |
| Doc Writer | 0 | 2 | 1 | Conflict matrix is in the right pages; keep recover vs fail-closed explicit, canonicalize in lockfile-spec, and drop unrelated commands.md hermes churn. |
| Test Coverage Expert | 1 | 2 | 0 | PR adds focused unit + integration tests but misses failed-install-after-discard, --force, and lifecycle unlink-then-fail snapshot. |
| Performance Expert | 0 | 0 | 0 | Single multiline regex in LockFile.read on small YAML; no measurable perf impact; regex precompiled at module scope. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Test Coverage Expert] (blocking-severity) Add an integration fixture for failed install after conflicted-lock unlink/discard. -- Author-stated limitation: discard is outside the transaction, so durable state after unlink-then-fail is unguarded. git checkout recovers; the gap is a regression trap, not a new product switch.
- [Python Architect] Move conflicted-lock discard into InstallTransaction so a failed install can restore the prior file. -- Unlink today happens before resolve; later failure cannot roll the conflicted lock back through the existing transaction owner.
- [Doc Writer] Keep recover vs fail-closed explicit; do not claim every install rewrites a conflicted lock. -- Full install/lock recover; frozen/partial fail closed; dry-run warns and does not rewrite. Over-claim would fight the accepted default.
- [Doc Writer] Revert unrelated hermes/target wording churn in packages/apm-guide/.apm/skills/apm-usage/commands.md. -- A lockfile-conflict patch should not rewrite the hermes/target table; duplicate install rows already disagree.
- [Python Architect] Collapse discard vs fail-closed vs warn into one lockfile conflict policy used by install, lock, frozen, dry-run, and MCP. -- Detection is already centralized in LockFile.read; durable outcome is still recomputed at each call site.
Architecture
classDiagram
direction LR
class LockFile {
<<ValueObject>>
+read(path) LockFile
}
class has_conflict_markers {
<<Pure>>
}
class LockfileFormatError {
<<DomainError>>
}
class LockfileConflictError {
<<DomainError>>
+path Path
}
class discard_conflicted_lockfile {
<<IOBoundary>>
}
class InstallTransaction {
<<UnitOfWork>>
+commit(result) InstallResult
+fail(error) InstallResult
}
class InstallService {
+LockFile.read for frozen
}
class FrozenInstallError {
<<DomainError>>
}
LockfileConflictError --|> LockfileFormatError
LockFile ..> has_conflict_markers : detect
discard_conflicted_lockfile ..> has_conflict_markers : detect
LockFile ..> LockfileConflictError : raises
InstallService ..> LockFile : reads
InstallService ..> FrozenInstallError : raises
note for LockFile "Canonical detect: LockFile.read raises LockfileConflictError"
note for discard_conflicted_lockfile "Outcome fork: unlink on full install and apm lock"
note for InstallTransaction "Durable lockfile mutation belongs in this unit of work"
class LockFile:::touched
class LockfileConflictError:::touched
class discard_conflicted_lockfile:::touched
class InstallService:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
installEntry["commands/install.py:install"] --> packages["_install_apm_packages"]
packages --> migrate["[FS] migrate_lockfile_if_needed"]
migrate --> fullGate{"full_install: not frozen and not packages and InstallMode.ALL"}
fullGate -->|yes| discard["[FS] lockfile.py:discard_conflicted_lockfile path.unlink"]
fullGate -->|no| laterRead["[I/O] LockFile.read"]
discard --> resolve["_install_apm_dependencies"]
laterRead --> conflict{"LockfileConflictError?"}
conflict -->|frozen InstallService| frozenErr["raise FrozenInstallError"]
conflict -->|partial add or --only| failClosed["fail closed; lockfile left in place"]
frozenErr --> txnFail["InstallTransaction.fail"]
lockEntry["commands/lock.py:_run_lock"] --> lockDiscard["[FS] discard_conflicted_lockfile"]
lockDiscard --> lockResolve["_install_apm_dependencies"]
mcpEntry["install/mcp/command.py:run_mcp_install"] --> mcpWrite["[FS] add_mcp_to_apm_yml"]
mcpWrite --> mcpRead["[I/O] LockFile.read during integration"]
mcpRead -->|LockfileFormatError| mcpClick["raise click.ClickException; apm.yml already written"]
dryEntry["presentation/dry_run.py:render_and_exit"] --> dryRead["[I/O] LockFile.read"]
dryRead -->|LockfileConflictError| dryWarn["logger.warning; treat lock as missing"]
Recommendation
CODEOWNERS should confirm detect-in-load, regenerate on full non-frozen install/lock, and fail-closed under --frozen and partial installs. Fold the docs precision pass (no every-install recover claim; revert commands.md hermes churn) if it is still cheap in this PR. Track unlink-then-fail coverage and transactional discard as follow-ups; leave --force, CI autodetection, and dry-run exit codes out of scope.
Full per-persona findings
Python Architect
- [recommended] Conflicted-lockfile outcome is split across call sites instead of one owner. at
src/apm_cli/commands/install.py:1837
Detection is centralized in LockFile.read; durable outcome (unlink vs fail closed vs warn) is recomputed at install, lock, frozen, dry-run, and MCP.
Suggested: One lockfile conflict policy invoked from those call sites. - [recommended] discard_conflicted_lockfile unlinks apm.lock.yaml outside InstallTransaction. at
src/apm_cli/deps/lockfile.py:1283
unlink before resolve; later install failure cannot restore the conflicted file via InstallTransaction. - [nit] MCP install catches LockfileFormatError after writing apm.yml. at
src/apm_cli/install/mcp/command.py:308
Pre-existing order; conflicted lock can leave manifest write without regenerated lock.
CLI Logging Expert
No findings.
DevX UX Expert
- [recommended] Make regenerate vs fail-closed explicit for interactive vs CI
The PR implements both regenerate-on-full-install and fail-closed for frozen/partial installs. Users and CI need a clear rule about which behavior is the default. CEO: accepted scope already covers this; do not add CI autodetection. - [recommended] Clarify dry-run output and ensure machine-detectable exit codes
Dry-run currently reports human-readable outcomes like "would make no changes" vs warnings. Author documented this limitation. CEO: keep warn + exit 0. - [nit] Address non-transactional lockfile/regeneration risks in UX and docs
Regenerating without atomic replace risks partial state if interrupted.
Supply Chain Security Expert
No findings.
Doc Writer
- [recommended] Do not over-claim that every install recovers a conflicted lockfile at
docs/src/content/docs/reference/cli/install.md
Full install/lock recover; frozen/partial fail closed; dry-run does not rewrite. - [recommended] Revert unrelated hermes/target wording churn in commands.md at
packages/apm-guide/.apm/skills/apm-usage/commands.md:15
A lockfile-conflict patch should not rewrite the hermes/target table; duplicate install rows already disagree. - [nit] State conflict semantics once; point other pages at lockfile-spec at
docs/src/content/docs/reference/lockfile-spec.md
Canonical definition belongs in lockfile-spec; recovery steps in install-failures.md.
Test Coverage Expert
- [blocking] No test that a failed install after discard/unlink leaves durable state safe
Author-stated limitation: discard outside transaction.
Proof (missing at):tests/integration/test_install_failed_after_discard_unlink.py - [recommended] No explicit --force override test
Product intent: --force is unchanged and is NOT a lock regenerate switch. CEO dropped this follow-up.
Proof (missing at):tests/integration/test_install_force_override.py - [recommended] No ApmLifecycle snapshot for unlink-then-fail
Overlaps the unlink-then-fail coverage gap.
Proof (missing at):tests/integration/test_lifecycle_unlink_then_fail_snapshot.py
Performance Expert
No findings.
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Generated by autopilot-pr-review-worker. This comment is AI-generated and may contain errors.
|
Thank you Lachlan Heywood (@lachieh) I have enabled the merge queue for this PR. Please review the blocker actions from the APM Review Panel. Once the test coverage is fixed. Optionally, if you can take out the top 5 recommendations, that could help us reduce the technical debt. Thank you for your contribution. Sergio |
|
Thanks Sergio Sisternes (@sergio-sisternes-epam). The follow-ups are addressed in #3043. I left this PR as is so the merge queue can take it. I tried to stack on this branch, but cross-fork PRs can't target fork branches as the base so that branch will show 2 commits until this one merges. |
InstallTransaction now owns the conflicted-lockfile discard: it snapshots the bytes before unlinking and rollback puts the file back unless the attempt already wrote a new lockfile. apm lock runs under its own transaction so the same rule applies there. The module-level discard_conflicted_lockfile helper is removed. Follow-up to microsoft#3028 from the APM Review Panel.
Other lockfile format errors keep the redacted, verbose-only handling that path had before microsoft#3028 widened the except clause.
Head branch was pushed to by a user without write access
InstallTransaction now owns the conflicted-lockfile discard: it snapshots the bytes before unlinking and rollback puts the file back unless the attempt already wrote a new lockfile. apm lock runs under its own transaction so the same rule applies there. The module-level discard_conflicted_lockfile helper is removed. Follow-up to microsoft#3028 from the APM Review Panel.
… under --frozen LockFile.read raises LockfileConflictError, a LockfileFormatError, when apm.lock.yaml contains git merge conflict markers, so every command names the file and a next action instead of printing a YAML scanner error. A full apm install and apm lock warn, discard the conflicted file, and resolve from apm.yml. Partial installs (positional packages, --only, --mcp) and apm install --frozen fail closed with the named error, and the frozen tip no longer points at apm outdated when the lockfile cannot be read. Fixes microsoft#2979
…tions Keep the conflict-marker read inside LockFile.read's normalising try so a non-UTF-8 lockfile still fails through LockfileFormatError, and leave such a file in place instead of probing it for discard. The generic --frozen unreadable-lockfile error now names the repair step. apm lock export resolves a legacy apm.lock like the other read-only consumers. Narrow the docs claim to commands that require the lockfile, update the apm-guide skill resources, mark the new test modules as component tests, and cite the PR number in the changelog.
Other lockfile format errors keep the redacted, verbose-only handling that path had before microsoft#3028 widened the except clause.
50a59e5 to
4456bb6
Compare
InstallTransaction now owns the conflicted-lockfile discard: it snapshots the bytes before unlinking and rollback puts the file back unless the attempt already wrote a new lockfile. apm lock runs under its own transaction so the same rule applies there. The module-level discard_conflicted_lockfile helper is removed. Follow-up to microsoft#3028 from the APM Review Panel.
|
Rebased onto |
InstallTransaction now owns the conflicted-lockfile discard: it snapshots the bytes before unlinking and rollback puts the file back unless the attempt already wrote a new lockfile. apm lock runs under its own transaction so the same rule applies there. The module-level discard_conflicted_lockfile helper is removed. Follow-up to microsoft#3028 from the APM Review Panel.
…en apm lock Record InstallTransaction.discard_conflicted_lockfile as the canonical owner in .apm/architecture/owners/install-deployment.json with the install-deployment-conflicted-lockfile-discard guard, its linter check, and a mutation-matrix case proving rollback must restore the file. apm lock now migrates a legacy apm.lock before the discard probe and runs the transaction as a context manager so an interrupt after the discard still restores the conflicted lockfile. The MCP add path names only the conflict error at default verbosity; other lockfile format errors keep their redacted handling.
|
Sergio Sisternes (@sergio-sisternes-epam) the APM Review Panel's blocking item is now fixed in this PR rather than a stacked follow-up, so there is nothing left to merge separately.
Follow-up 3 (docs precision) is in the docs commits. On follow-up 4, the I originally split this into #3043, but GitHub cannot base a cross-fork PR on a fork branch and stacked PRs are not enabled here, so that PR's diff always included this one's commits and reviewers kept re-reviewing approved code. #3043 is closed and its work is here. |
Description
When
apm.lock.yamlstill contains git merge conflict markers, every command that reads it exits 1 with a raw PyYAML scanner error, and the--frozenfailure tip points atapm outdatedandapm update, which fail on the same error. The only recovery was deleting the file by hand.This PR implements the proposal in the issue comment:
LockFile.read(already the single lockfile load owner) scans for<<<<<<<,>>>>>>>, or|||||||at line start before parsing and raisesLockfileConflictError, aLockfileFormatErrorsubclass. The message names the file and the next action.apm update,apm outdated,apm lock export,apm install --dry-run, and every other command that requires the lockfile now report the real cause (best-effort readers such asapm viewkeep continuing without lock metadata). A bare=======line is not treated as a marker; a real conflict always carries<<<<<<<.apm install(no package arguments, no--only) andapm lockwarn (apm.lock.yaml contains git merge conflict markers; discarding it and resolving from apm.yml.), delete the conflicted file before the pipeline reads it, and let the pipeline write a fresh one. The ~15 downstreamLockFile.readsites see "no lockfile", which is what npm, pnpm, and Bun do ("treat as absent, regenerate").apm.ymlis the input a non-frozen install resolves from anyway, so no new trust path is added. Lockfiles that are invalid for any other reason keep failing closed.apm install PACKAGE,--only apm,--only mcp,--mcp NAME) never discard, because they would write a lockfile missing the entries they do not resolve (--only apmwould drop every MCP lock record,--only mcpevery package). They fail closed with the named error; the positional-add path restoresapm.ymlthrough the existing transaction rollback.apm install --dry-runreports the conflict as a warning and leaves the file alone; its closing line still says "would make no changes" because the dry-run plan does not model the lockfile rewrite, which is unchanged from today.apm install --frozenfails closed with--frozen cannot use apm.lock.yaml: it contains git merge conflict markers. Resolve the merge conflict, or run 'apm install' without --frozen to regenerate it from apm.yml.and leaves the file untouched.frozen_install_tipreturns nothing when the error carries no drift reasons (missing or unreadable lockfile). Those messages already name their next action, and the old tip named commands that cannot run.--forceis unchanged.The discard is owned by
InstallTransaction, which snapshots the conflicted bytes before unlinking;rollback()restores the file unless the attempt already wrote a new lockfile.apm lockruns under its own transaction (as a context manager, so an interrupt after the discard also restores) and migrates a legacyapm.lockbefore the discard probe. That decision is registered as an architecture owner (conflicted-lockfile-discardin.apm/architecture/owners/install-deployment.json) with theinstall-deployment-conflicted-lockfile-discardguard: the linter requires the transaction to define discard and restore and to call restore fromrollback(), requires both command callers to route through it, and flags any otherhas_conflict_markersuse ordiscard_conflicted_lockfiledefinition undersrc/. The mutation matrix proves the guard fires when rollback stops restoring.Limitations, stated deliberately:
apm installre-records them on deploy;apm lockwrites an empty deployment set, as it does today when no lockfile exists. A manifest with nothing to lock ends with no lockfile, which is why the warning says "discarding" rather than "regenerating".apm install --mcp NAMEstill writes the server toapm.ymlbefore it reads the lockfile (pre-existing ordering); the failure now prints the lockfile error at default verbosity instead of hiding it behind--verbose, while other lockfile format errors keep their redacted handling.InstallService.enforce_frozenand the dry-run preview do not resolve a legacyapm.lock. That predates [BUG]apm installshould support automatic merge conflict resolution through forced reinstallation. #2979 and is left for a separate issue.Issue and approved scope
Issue: #2979
Human scope-approval comment: the issue carries no
<!-- apm-scope:v1 -->comment.status/acceptedwas applied by Sergio Sisternes (@sergio-sisternes-epam) (core maintainer,projectremit) on 2026-09-18T15:05Z (timeline). The triage note recommendedneeds-designand asked for the default to be settled; the default implemented here is the one proposed in my comment, which also records what npm, Yarn, pnpm, Bun, and aube do. If a maintainer wants the default to fail closed instead, points 1 and 5 above stand and only the regeneration call inapm install/apm lockchanges.This PR completes the issue.
Fixes #2979
Type of change
Testing
Unit:
tests/unit/install/test_install_transaction.py(discard thenfail()restores byte-exact; rollback keeps a lockfile written after the discard; commit does not restore; valid, corrupt, undecodable, and missing files are never discarded),tests/unit/deps/test_lockfile_conflict_markers.py(read raises the named error for two-way and diff3 markers, corrupt-without-markers stays a plainLockfileFormatError, marker text mid-line is accepted,=======-only is not a conflict, discard removes only a conflicted file) and two cases intests/unit/install/test_frozen.py(frozen fails closed and leaves the file; noapm outdatedtip for missing or conflicted lockfiles).End to end via
CliRunner:tests/integration/test_install_conflicted_lockfile_e2e.pycoversapm installandapm lockdiscarding with the warning and recording local content;apm install --frozen,apm install ./pkg,--only apm,--only mcp, and--mcp NAMEexiting 1 with the named error and the file untouched;--dry-runnaming the conflict without touching the file; a corrupt lockfile without markers still failing closed on a full install; andapm update/apm outdated/apm lock exportraising the named error without a YAML trace.uv run pytest tests/unit tests/test_console.py: 22632 passed.bash scripts/lint-architecture-boundaries.shexits 0, and the owner-guard mutation matrix, linter entrypoint, andtests/qualitysuites pass.tests/integration+tests/spec_conformance: 9489 passed, 13 failed. All 13 fail identically onmainin this environment (network access to gitlab.com, a missingpwsh, real zsh startup enrolment, the local git factory determinism check, andtest_architecture_owner_rule_mutations.py::test_matrix_covers_every_registry_guard_exactly_once).ruff check,ruff format --checkclean;mypyon the touched modules adds no new errors (thelock exportpath was switched fromLockFile.from_yamltoLockFile.readso it goes through the load owner, and itsNonecheck replaces theis_file()check).Manual, in a scratch project with local
.apm/instructionsand a conflicted lockfile:apm installwarns and writes a lockfile byte-identical to a clean install;apm lockwarns and regenerates;apm install --frozen,apm install ./pkgA,--only mcp,--mcp foo,apm update,apm outdated, andapm lock exportexit 1 with the named error and leave the file untouched;--dry-runwarns and exits 0.apm lock exportnow resolves a legacyapm.lockthroughresolve_lockfile_path_for_readlike the other read-only consumers, the generic--frozen could not readerror names its repair step, and a non-UTF-8 lockfile still fails throughLockfileFormatError(it is never probed for discard). Theapm-guideskill resources carry the new frozen and recovery guidance.The APM Review Panel's blocking item (no coverage of a failed install after the discard) and its transactional-discard and single-owner follow-ups were originally split into a stacked PR; GitHub cannot base a cross-fork PR on a fork branch and stacked PRs are not enabled here, so that work is folded in as the last two commits and #3043 is closed.
An independent review pass against the first cut found the partial-install,
--only mcp,--mcp, and--dry-rungaps above; all four are fixed and covered by the e2e tests.Spec conformance (OpenAPM v0.1)
No normative statement covers invalid-lockfile handling.
req-pl-013(require_hashesfails closed when the lockfile is absent or unreadable) still holds:enforce_installed_hash_policyruns against the freshly written lockfile after regeneration, and--frozennever regenerates.