Skip to content

M6+M7: Upstream change detection, blast radius, and migration planner/generator - #66

Merged
kadireren7 merged 8 commits into
mainfrom
feat/m6-m7-upstream-change-migration
Sep 25, 2026
Merged

kadireren7 merged 8 commits into
mainfrom
feat/m6-m7-upstream-change-migration

Conversation

@kadireren7

Copy link
Copy Markdown
Owner

Summary

Second stage of PatchFrog's product-transition invariant change ->
impact -> evidence -> verification -> decision
, built entirely on M4+M5
(c66c562, already on main):

  • M6 — Upstream change detection + consumer impact / blast radius
    (patchfrog/upstream/, UPSTREAM_CHANGE_VERSION = 1): deterministic
    OpenAPI/SDK-surface contract diff, semver-aware version diff, explicit
    change hints as the only source of rename/replacement knowledge,
    consumer mapping from diff items to M5 usage sites (unmatched sites are
    ignored, never affected), bounded DIRECT/TRANSITIVE/POTENTIAL blast
    radius on the existing code graph, multi-repository impact via the M5
    registry, idempotent persistence, and a changes diff|analyze CLI. See
    docs/upstream-changes.md.
  • M7 — Migration planner + generated fix (patchfrog/migration/,
    MIGRATION_ENGINE_VERSION = 1): a planner assigning strategy +
    auto-fix eligibility per affected site (never inventing a value with no
    deterministic source), AST/lexical span rewrites, a pure and
    empirically idempotent patch generator, 8 deterministic safety
    gates, an optional narrowly-scoped model-assisted path (its own
    PatchOrigin, never merged with the deterministic patch, structurally
    guaranteed to be the only migration module that can import a
    provider), idempotent persistence with a PatchLinkage M8/M9 can key
    on, and a migrations plan|generate|demo CLI. See docs/migration.md.

Verification in this milestone is deterministic/structural only (the
safety gates). Executable/runtime verification is M8 and automated
migration PRs are M9 — neither is started here.

Validation

  • Full pytest suite against real Postgres: 2810 passed, 5 skipped
    (expected no-op fixture cases)
  • ruff and mypy --strict: clean, repo-wide
  • Alembic: single head (0035_upstream_change_migration), fresh
    alembic upgrade head from scratch succeeds
  • Both Docker images (api, worker) build from the working tree;
    9/9 Celery tasks registered (mirrors CI's own build+check exactly)
  • M4/M5 regression guards unchanged from their pre-existing baselines:
    cost benchmark identical (23→10 calls, $0.046→$0.022, 2/2 findings
    preserved), beta-readiness eval identical (all metrics 1.0/0.0,
    reviewer calls 22 / critic calls 10 — confirms M6/M7 add zero calls to
    the normal review path)
  • Dependency discovery (35), contract-diff (35), impact/blast-radius
    (48), and migration planner/generator (75, 5 skipped) suites all green
    in isolation
  • End-to-end demo (migrations demo) walked and verified by hand: a
    breaking SDK change classified correctly, an 8-step plan (7 automatic,
    1 correctly left HUMAN_REQUIRED for a removed argument), all 8
    safety gates pass, and the emitted diff is a minimal, correct rewrite
  • Idempotency confirmed empirically: re-running migrations generate
    against an already-migrated checkout yields a zero-step plan and an
    empty diff — the planner itself finds no remaining affected usage
    sites, not just "the diff happens to be empty"
  • Secret-pattern scan over the branch diff only: no private keys, no
    token-shaped strings, no credential-shaped assignments (the one hit is
    a test asserting such values are rejected); this scans tracked-file
    diff content only, not terminal/UI/local display exposure

Full detail, per-suite numbers, and known limitations (JS/TS has no
caller graph for blast radius; cross-repo impact is bounded by what the
M5 registry has discovered; assist.py was exercised only against
FakeLLMProvider) in
validation/m6_m7_upstream_change_migration/latest-summary.md.

Test plan

  • ruff check .
  • mypy . --strict
  • alembic upgrade head against real Postgres, single head verified
  • Full pytest suite against real Postgres (2810 passed, 5 skipped)
  • Docker build api + worker targets; Celery task registration
    verified in-container (9/9)
  • M4 cost benchmark, beta-readiness eval, dependency discovery,
    contract-diff, impact/blast-radius, and migration
    planner/generator suites — no regression
  • End-to-end demo run by hand; idempotency re-run confirmed
  • Secret-pattern scan over the branch diff

🤖 Generated with Claude Code

kadireren7 and others added 8 commits September 25, 2026 15:52
- patchfrog/upstream/domain.py: provider-agnostic ExternalChangeEvent,
  ExternalChangeSource/Kind, ExternalContractRevision, DependencyRelease,
  DependencyTarget, ContractDiffItem (stable kind, location, subject,
  old/new representation, compatibility class, explanation, evidence,
  known replacement), CompatibilityClass and ChangeRisk.
- openapi_diff.py: diff over M5-normalized OpenAPI contracts (endpoints,
  methods, path moves via operationId or explicit hint, parameters,
  request bodies, responses, auth requirements/schemes/scopes,
  components). Consumer-direction aware: request-only vs response-only
  schemas classify differently; path parameters are positional.
- sdk_surface.py: minimal SDK surface document + diff for SDKs without
  full API contracts (symbols, arguments, enums, result fields, modules).
- package_version.py: version comparison; a version change alone is at
  most POTENTIALLY_BREAKING, never BREAKING.
- hints.py: explicit rename/move/replacement hints -- the only way a
  replacement becomes "known"; credential-shaped values refused.
- classify.py / events.py: event-level SAFE/LOW_RISK/REVIEW_REQUIRED/
  BREAKING with machine-readable reasons; events from contract pairs,
  registry snapshots, version pairs and release metadata; fingerprint
  excludes observation time (idempotent re-ingestion).

UPSTREAM_CHANGE_VERSION = 1 (new). No provider calls, no network.
- calls.py: locate an SDK call at a usage-site line and describe its
  arguments with exact character spans (Python via ast; JS/TS via a
  string/comment/template-aware lexer) -- shared with M7 rewriters.
- consumers.py: per consumer-affecting diff item, map M5 usage sites to
  consumers through a specific evidence path (exact SDK symbol, namespace,
  hinted operation->SDK bridge, HTTP path/method, schema reference,
  import/module, auth, credential config, package-level). Argument-level
  changes inspect the call: calls that provably do not use the changed
  argument are ruled out; unconfirmable ones (**kwargs, no source) are
  POTENTIAL, not DIRECT. Version-level evidence never widens the set when
  structural evidence exists. Unmatched sites are reported as unaffected.
- code_graph.py: local checkout -> the existing parse/resolve/test/
  build_graph pipeline without the database.
- blast_radius.py: DIRECT / TRANSITIVE (resolved call edges, bounded
  depth, decaying confidence) / POTENTIAL (never expanded); callees listed
  but not counted; related tests; coverage gaps for files without a call
  graph (JS/TS) reported instead of assumed empty.
- workspace.py: repository and multi-repository impact (affected /
  uncertain / unaffected) from local checkouts or from M5 registry rows
  alone; "already on the target version" detection; the repository's own
  discovered version as the "from" side of version-change events.
- M5 discovery: packages claimed by caller-supplied provider adapters are
  no longer also reported as generic packages (default behavior unchanged)
  -- lets an SDK described only by a surface document be tracked.
- Fixture corpus tests/fixtures/upstream_changes (10 required cases +
  demo) with case.yaml expectations; anti-cheat test that the core has no
  fixture-specific strings; no-provider-import test.
- Migration 0035_upstream_change_migration (single head): five bounded
  tables -- external_change_events (unique fingerprint, global),
  external_change_diff_items, external_change_impacts (per event x
  repository x matched dependency x analyzed commit), migration_plans and
  migration_patches (used by M7). Full normalized contracts are not
  duplicated: M5 contract snapshots own them. Deleting a repository
  removes its impacts/plans, never the global event; deleting a
  dependency row keeps the historical impact (SET NULL).
- upstream/store.py: idempotent event ingestion (re-ingestion only moves
  last_observed_at), per-commit impact history, and read access to the M5
  registry (latest contract snapshot as a diff's old side; all active
  dependencies + usage sites for registry-wide cross-repo impact).
- upstream/report.py: one JSON shape for CLI output and persisted JSON.
- CLI (patchfrog/cli_changes.py): `changes diff OLD NEW` and `changes
  analyze` with a contract pair, a package version change (from version
  taken from the repository when omitted; GitHub release metadata
  digested, never stored verbatim), or an M5 registry snapshot as the old
  side; local checkouts (repeatable --repo) or --registry; --persist.
- patchfrog/migration/domain.py: MigrationPlan / MigrationStep /
  MigrationTarget / MigrationStrategy / EditOperation / VersionConstraint,
  AutoFixEligibility (AUTO_SAFE, AUTO_WITH_REVIEW, HUMAN_REQUIRED,
  UNSUPPORTED), MigrationStatus (NOT_REQUIRED, PLANNED, PATCH_GENERATED,
  PARTIAL, HUMAN_REQUIRED, UNSUPPORTED, FAILED), PatchLinkage and result
  types. MIGRATION_ENGINE_VERSION = 1 (new).
- planner.py: one deterministic rule per (change kind, usage evidence):
  hinted symbol/argument renames, module moves and enum mappings are
  AUTO_SAFE; hinted required values, Python result-field renames and
  endpoint replacements are AUTO_WITH_REVIEW; missing value sources,
  removed APIs/arguments, response shape, auth and indirect arguments are
  HUMAN_REQUIRED; lockfile refresh, JS result fields and class renames are
  UNSUPPORTED. Values are never invented; credentials are never touched.
  Plan-level manifest version bump: AUTO_SAFE only for semver-compatible
  bumps or when every structural change is automatic; a major bump without
  structural evidence adds a HUMAN_REQUIRED VERIFY_UPGRADE step. Each step
  carries current usage, required change, proposed behavior, target
  file/symbol/line, related tests and residual uncertainty.
- Corpus expectations extended with the reviewed plan for every case.
- patchfrog/migration/edits.py: the one transformation primitive
  (TextEdit spans against original text, apply_edits with overlap
  detection, unified_diff) every rewriter and the generator share.
- python_rewrite.py / js_rewrite.py / manifest_rewrite.py: turn a planned
  EditOperation into character-span edits. Python via ast (exact spans:
  call chains, keywords, imports, result-field reads scoped to the
  enclosing function); JS/TS via the existing lexical call locator (call
  chains, object-literal keys, import/require specifiers, path literals
  inside template/string literals); manifests via single-clause version
  constraints only (requirements*.txt, pyproject.toml, package.json) --
  ranges/exclusions/URLs always need a human decision. Every rewriter
  raises NotNeeded when the code already has the target shape (the
  idempotency contract) and RewriteError when it cannot edit safely --
  never a guess, never invented text.
- patchfrog.upstream.calls: added chain-agnostic locate_calls_at_line
  (python_calls_at_line / js_calls_at_line) -- needed when a later
  step's obsolete-shape check must re-find a call whose chain an earlier
  step already renamed.
- generator.py: reads the checkout, never writes it. All of one file's
  steps are located against that file's *original* content (so one
  step's edit never has to re-find what another step already changed);
  edits are merged and diffed into a unified patch; conflicting edits at
  the same site fail only that step, not the whole file.
- safety.py (M7.5): allowed_files, edits_match_steps (every changed line
  inside an applied step's scope), no_secret_files (no secret-store file
  touched, no credential-shaped literal introduced), no_dependency_removed,
  bounded_diff, line_stability, syntax (ast/json/tomllib/JS bracket
  balance), obsolete_shape_gone. A patch is a candidate only when every
  gate passes -- not runtime-verified (M8).
- Corpus tests (tests/integration/test_migration_corpus.py): for every
  fixture case with automatic steps, the generated patch is a
  safety-gate-passing candidate, applies cleanly with `git apply
  --check`, and is idempotent (materializing it and re-running the whole
  pipeline produces zero further diff).

Full validation: ruff, mypy --strict, full pytest (2788 tests incl. the
sandboxed executable-verification corpus), M4 cost benchmark and M5
dependency-discovery suite all unchanged/green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sted path

MigrationStore mirrors patchfrog.upstream.store's fingerprint-keyed
idempotency: one migration_plans row per (change + exact repository
content state via a sha256 base_content_fingerprint), one
migration_patches row per (plan, patch fingerprint). report.py gives
plan/patch/linkage a single dict representation shared by --json CLI
output and the bounded JSON persistence columns.

assist.py adds the opt-in, narrowly-scoped model-assisted path for a
HUMAN_REQUIRED add_required_parameter/replace_enum_value step with a
single already-located call: one structured value field only, the same
deterministic rewriter and safety gates a hint would use, its own
PatchOrigin.MODEL_ASSISTED, never merged with the deterministic patch.
Nothing in the deterministic planner/generator imports it; a structural
test walks every other migration module's AST to enforce that.

generator.py's private _read_file becomes the public read_target_file
so the store's content-fingerprint computation reads target files the
same secret-store-safe, bounded way the generator does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds `patchfrog.cli migrations {plan,generate,demo}`:
- plan/generate reuse the same local/registry analysis path
  `changes analyze` already established (add_change_inputs, analyze_local)
  so a migration is planned from the identical event + workspace impact
  a user already inspected via `changes`.
- generate supports --write (materialize a candidate patch into the
  checkout, off by default), --output-dir (one unified diff per
  repository), and --persist (record event + plan + patch via
  MigrationStore, requires DATABASE_URL).
- demo runs the bundled, fully offline, fictional acme-ai SDK case
  (tests/fixtures/upstream_changes/demo) end to end with no live
  provider call and no real vendor described.

8/8 migration CLI integration tests passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds docs/upstream-changes.md and docs/migration.md (the same
per-package doc convention as dependency-discovery.md/
cost-aware-review.md). Updates docs/architecture.md's pipeline diagram
and docs/roadmap.md to reflect M4+M5 now merged to main and M6+M7
implemented (with M8 Executable/Runtime Verification and M9 Automated
Migration PRs as the next, not-started stages).

Appends the RESULT section to
validation/m6_m7_upstream_change_migration/latest-summary.md: full
validation results (2810 passed / 5 skipped against real Postgres, ruff/
mypy clean, both Docker images build with the working-tree code, 9/9
Celery tasks registered), M4/M5 regression status (cost benchmark and
beta-readiness eval both unchanged from their pre-M6/M7 baselines,
confirming zero calls added to the normal review path), the end-to-end
demo walkthrough, an empirically-confirmed idempotency result, known
limitations, and the M6/M7/M8-readiness completion assessment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kadireren7
kadireren7 merged commit 812ac5a into main Sep 25, 2026
1 check passed
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