Repository navigation
M6+M7: Upstream change detection, blast radius, and migration planner/generator - #66
Merged
Merged
Conversation
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second stage of PatchFrog's product-transition invariant change ->
impact -> evidence -> verification -> decision, built entirely on M4+M5
(
c66c562, already onmain):(
patchfrog/upstream/,UPSTREAM_CHANGE_VERSION = 1): deterministicOpenAPI/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|analyzeCLI. Seedocs/upstream-changes.md.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, structurallyguaranteed to be the only migration module that can import a
provider), idempotent persistence with a
PatchLinkageM8/M9 can keyon, and a
migrations plan|generate|demoCLI. Seedocs/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
(expected no-op fixture cases)
mypy --strict: clean, repo-wide0035_upstream_change_migration), freshalembic upgrade headfrom scratch succeedsapi,worker) build from the working tree;9/9 Celery tasks registered (mirrors CI's own build+check exactly)
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)
(48), and migration planner/generator (75, 5 skipped) suites all green
in isolation
migrations demo) walked and verified by hand: abreaking SDK change classified correctly, an 8-step plan (7 automatic,
1 correctly left
HUMAN_REQUIREDfor a removed argument), all 8safety gates pass, and the emitted diff is a minimal, correct rewrite
migrations generateagainst 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"
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.pywas exercised only againstFakeLLMProvider) invalidation/m6_m7_upstream_change_migration/latest-summary.md.Test plan
ruff check .mypy . --strictalembic upgrade headagainst real Postgres, single head verifiedpytestsuite against real Postgres (2810 passed, 5 skipped)api+workertargets; Celery task registrationverified in-container (9/9)
contract-diff, impact/blast-radius, and migration
planner/generator suites — no regression
🤖 Generated with Claude Code