Skip to content

Phase 64 follow-ups: phase-language sweep + migrate fixes + behavioral tag catalog#27

Merged
tolgaergin merged 6 commits into
mainfrom
phase64-findings-tranche
May 6, 2026
Merged

Phase 64 follow-ups: phase-language sweep + migrate fixes + behavioral tag catalog#27
tolgaergin merged 6 commits into
mainfrom
phase64-findings-tranche

Conversation

@tolgaergin
Copy link
Copy Markdown
Contributor

Summary

Three coherent tranches addressing Phase 64 findings #8, #10, #29, #37, plus follow-up audits:

Plus three audit-driven follow-ups:

  • --allow-new was silently dropped on lpm install -g — now rejected with a clear "not supported yet" message + regression test.
  • .gitattributes orphan after migrate rollback (when migrate created it from scratch) — fixed by tracking unconditionally.
  • npm migration guide drifted from the actual CLI hint (lpm ci setup github-actions vs. real lpm migrate --ci) — corrected.
  • query.mdx drift test was asymmetric (no reverse-direction stray-tag check) — symmetrized.

Test plan

  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --check clean
  • cargo nextest run --workspace --exclude lpm-integration-tests — 5609 pass / 7 skipped
  • Drift test exercised end-to-end by injecting regressions in both security-audit.mdx and query.mdx
  • lpm.lockb rollback regression verified by reverting backup_file(&lockb_path) — test fails as expected
  • .gitattributes rollback regression verified the same way
  • Docs site: companion PRs on rust-client-docs already merged to main

🤖 Generated with Claude Code

tolgaergin and others added 6 commits May 6, 2026 21:26
…bility wording

Phase numbers are internal planning artifacts that don't mean anything to
end users. Strip every "Phase X.Y" reference from runtime error messages,
clap help, doc comments rendered into --help, npmrc warning text, and
workflow tests asserting on those messages. Replace with stable
capability-oriented wording ("not supported yet", "isn't supported on
Windows yet", "no global-scope cooldown overrides yet", etc.) that
remains true regardless of internal milestones.

Touched user-visible surfaces:
  * lpm patch — range-version rejection
  * lpm install -g — --min-release-age + --ignore-provenance-drift gates
  * lpm install --policy=triage — LLM-triage availability note
  * lpm rebuild / lpm doctor — sandbox unsupported-platform warning
  * lpm install --filter — substring-vs-glob diagnostic
  * .npmrc parser — per-origin TLS / mTLS unsupported warnings
  * lpm-workspace package.json schema — patchedDependencies field doc

Test asserts flipped to encode the user-facing contract instead of the
internal phase number, per the project's test-asserts-user-contracts rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related contract bugs in lpm migrate:

1. The CLI advertised "yes implies force" in --help, the dispatcher
   collapsed them at main.rs, and the migrate flow itself has no prompt
   path. Result: a CI engineer reaching for `-y` to make migrate
   non-interactive silently widened the blast radius into clobbering an
   existing lpm.lock, a behavior no other peer PM exposes through `-y`.
   Decouple them: `-y` is now reserved as a non-interactive flag (no-op
   today) and does NOT imply --force. Scripts that already pass `-y`
   continue to parse cleanly. To overwrite an existing lockfile the
   user must pass --force explicitly.

2. The rollback path's lpm.lockb cleanup at run_rollback used
   `restored.iter().any(|f| f == "lpm.lock")`, but the v2 backup manifest
   appends " (removed)" to every newly-created file's restored entry,
   so the exact-match comparison failed for fresh migrations and left
   lpm.lockb orphaned on disk after `lpm migrate --rollback`.

   Real fix: track lpm.lockb explicitly in MigrationBackup alongside
   lpm.lock. Lockfile::write_all writes both files atomically, so the
   backup chain needs both entries to round-trip cleanly. The brittle
   suffix-string fallback is dropped — the backup layer now handles the
   round-trip uniformly for every file the migration touched.

Folded in: append a "Need to undo?" hint to the migrate success summary
so the rollback escape hatch is discoverable on the success path, not
only on the verification-failure path it already shows up in.

Test coverage: extends migrate_rollback_restores_original to assert
lpm.lockb is also gone (regression test for the orphan bug — verified
empirically by reverting the backup_file(&lockb_path) call: test fails
with "lpm.lockb should be removed after rollback"). New
migrate_yes_alone_does_not_overwrite_existing_lockfile pins the -y vs
--force decoupling end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t guard

The 22 behavioral tags emitted by lpm audit / lpm query were defined as
Rust struct fields across three sibling modules, with their CLI tokens
in a separate enum and their human-readable descriptions hand-maintained
in two distinct doc pages (security-audit.mdx + query.mdx). Every drift
class — token rename, description rewrite, count drift in prose,
out-of-sync severity claim — could ship undetected because nothing in
the build asserted parity across the surfaces.

This change makes lpm_security::query the single source of truth:

  * New `BehavioralTagInfo { token, group, severity, description }`
    struct + `behavioral_tag_catalog()` accessor compose the catalog
    from `PseudoClass::all_behavioral()`, `group()`, `severity()`, and
    a new `description()` method.

  * `PseudoClass::group()` returns `Option<TagGroup>` — `Source` /
    `SupplyChain` / `Manifest` for the 22 behavioral tags, `None` for
    state / severity / special selectors.

  * `PseudoClass::description()` returns `Option<&'static str>`. Only
    behavioral tags carry a description; state/severity/special return
    `None`. The behavioral entries are non-empty by contract.

  * 4 unit tests in lpm-security pin the catalog invariants: every
    `all_behavioral` entry round-trips through the catalog accessor,
    descriptions are non-empty, tokens round-trip through `from_name`,
    and the catalog partitions cleanly into three groups.

  * 3 cross-repo drift tests in lpm-workflows assert exact parity
    between the catalog and both doc pages (security-audit.mdx +
    query.mdx). The drift test mirrors the schema_drift gate's resolver
    pattern: `LPM_DOCS_DIR=<path>` for explicit enforcement,
    `LPM_DOCS_DIR=skip` for opt-out, otherwise walk-up from
    CARGO_MANIFEST_DIR with a silent skip when the docs repo isn't
    checked out next to rust-client.

Empirically verified: deliberately rewriting one tag description in
security-audit.mdx made `security_audit_mdx_mirrors_catalog` fail with
the exact drift pointed at the offending row. With the catalog as the
single source of truth, future tag additions, renames, or rewrites
land in one place and CI enforces the doc tables follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…track deferred surfaces

Three follow-ups to the migrate + behavioral-tag tranche, plus a
tracking doc for surfaces we marked "not supported yet":

1. **migrate rollback: .gitattributes orphan.** `ensure_gitattributes`
   creates the file when it's missing and modifies it when present, but
   the backup chain only registered the file with `backup_file` inside
   `if exists()`. Result: a fresh migration with no pre-existing
   .gitattributes left it on disk after `lpm migrate --rollback`,
   violating the "restore original state" contract the docs promise.
   Fix: register the file unconditionally — the backup layer's
   created-vs-restored distinction handles both paths uniformly. The
   regression test now plants the fixture-NOT-having-.gitattributes
   precondition, asserts migrate creates it, then asserts rollback
   removes it. Verified empirically by running the new assertion
   against pre-fix code (failed) and post-fix (passed).

2. **migrating-from-npm.mdx CI hint drift.** The guide said the migrate
   flow "prints a hint pointing at `lpm ci setup github-actions`", but
   the actual CLI hint at migrate.rs:506 says `lpm migrate --ci`.
   Drop the wrong command; describe the platform detection + the
   actual `--ci` invocation.

3. **query.mdx drift test asymmetric.** The reverse-direction
   stray-tag check existed for security-audit.mdx but not query.mdx,
   so a bogus extra row added to query.mdx would pass CI. Add the
   parallel check; tighten `parse_tag_rows` to reject combinator
   syntax samples (`:a, :b`, `:not(:a)`, `:root > :child`) so the
   Combinators table doesn't trip false positives. Verified by
   injecting a `:bogus-tag` row: drift test fails with the
   exact-token-named diagnostic.

4. **DEFERRED_SURFACES.md.** New engineering tracking doc inventorying
   every "not supported yet" / "isn't supported yet" / "doesn't extend
   to globals yet" surface this tranche locked into the user-visible
   contract. Each entry names the source-of-truth string, why the
   feature is deferred, and the workaround today. Covers patch range
   selectors, patch renames, Windows sandbox, global-scope cooldown +
   provenance overrides + sandbox parity, the LLM triage layer,
   .npmrc mTLS / per-origin TLS, workspace deploy local-package
   injection, and the reserved `lpm migrate -y` flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GPT's audit caught two contract bugs introduced alongside the
deferred-surfaces tracking doc:

1. **`--allow-new` silently dropped on `lpm install -g`.** The
   global-install validator only rejected `--min-release-age` and the
   provenance-drift flags; the `-g` dispatch path then explicitly
   discarded `allow_new` without telling the user. The tracking doc
   advertised the rejection, but the code shipped a silent-drop —
   exactly the contract dishonesty the doc was meant to prevent. Fix:
   thread `allow_new` through `validate_global_install_project_scoped_flags`
   and reject with the same not-supported-yet wording the cooldown
   override uses. New regression test
   `install_global_rejects_allow_new_flag` pins the parser-level
   rejection symmetric with the existing `--min-release-age` and
   provenance-drift tests.

2. **DEFERRED_SURFACES.md TLS section overstated the gap.** The entry
   said `cafile` / `certfile` / `keyfile` (both global and per-origin)
   are "parsed and warned" and that requests use the global TLS config
   regardless. That's wrong for global `cafile`: npmrc.rs:1181 parses
   it, stores it in `tls.extra_roots`, and client.rs attaches those
   roots to the HTTP client. The genuinely-deferred surfaces are mTLS
   client certs (global + per-origin) and per-origin TLS overrides.
   Doc rewritten to scope the deferral honestly + name the global
   `cafile` / `ca` working path as the today-workaround.

3. **DEFERRED_SURFACES.md cooldown section corrected.** Now says both
   `--min-release-age` and `--allow-new` are rejected on `-g`, matching
   the validator's actual behavior post-fix.

CI gate at HEAD: workspace clippy clean, fmt clean, **5609/5609 tests
pass / 7 skipped** (+1 vs. previous: install_global_rejects_allow_new_flag),
Fumadocs build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DEFERRED_SURFACES.md is engineering-internal tracking — not a
contract artifact, not a contributor-facing reference. Move it under
/private/ alongside other internal docs and gitignore that path so
the tree stays for local use without being pushed.

The "not supported yet" source-of-truth strings still live in code
and CLI surfaces; only the engineering-internal aggregator moved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tolgaergin tolgaergin merged commit 0a502c5 into main May 6, 2026
3 checks passed
@tolgaergin tolgaergin deleted the phase64-findings-tranche branch May 6, 2026 21:26
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.

1 participant