Skip to content

Bundle: sync surface-contract enforcement + surgical regeneration (#1900, #1938)#1960

Open
Serhan-Asad wants to merge 18 commits into
mainfrom
bundle/sync-contract
Open

Bundle: sync surface-contract enforcement + surgical regeneration (#1900, #1938)#1960
Serhan-Asad wants to merge 18 commits into
mainfrom
bundle/sync-contract

Conversation

@Serhan-Asad

Copy link
Copy Markdown
Collaborator

Bundle: sync surface-contract enforcement + surgical regeneration

Rolls up the two sync-contract fixes plus the base-ref follow-up into one deployable branch off current main (post-#1954 deterministic continuous sync reconcile).

Constituent changes

  • fix(sync): prompt-declared <pdd-interface> as the public-surface contract (#1900) #1905 fix(sync): prompt-declared <pdd-interface> as the public-surface contract (#1900) — treats the prompt's declared <pdd-interface> as the authoritative public-surface contract, validates declared methods/constructors/underscore symbols against it, and catches callable→non-callable regressions so a sync can no longer silently drop declared surface.
  • fix(sync): surgical (edit-shaped) regeneration for mature modules + --fresh (#1938) #1940 fix(sync): surgical (edit-shaped) regeneration for mature modules + --fresh (#1938) — prefers surgical, edit-shaped regeneration for mature modules instead of full rebirth, with an explicit --fresh escape hatch to force complete regeneration.
  • base-ref follow-up fix(sync): base-branch original fallback for surgical regen (#1938/#1940) — when the pre-change prompt is already committed on-branch (cloud sync-after-change flow) and local history yields no distinct prior version, resolves the "original" prompt from the base branch (get_git_content_at_ref, PDD_SYNC_BASE_REF defaulting to origin/main) so surgical regen actually engages; otherwise falls back to an honest full regeneration surfaced by a single loud degradation notice.

Closes #1900
Closes #1938

Provenance

Surfaced during the PDD GitHub App E2E hardening campaign (staging lanes test_repo#4232 / test_repo#4233), where mature modules were repeatedly rebirthed on sync and declared symbols were dropped, producing consecutive surface-gate blocks.

Validation

Local unit suites pass on the bundle branch: test_issue_1900_surface_contract, test_code_generator_main, test_sync_orchestration, test_sync_main, test_one_session_sync, test_maintenance — 884 passed. Staging E2E evidence will be attached before merge.

Constituent PRs #1905 and #1940 will be closed as superseded when this bundle merges (left open for now).

🤖 Generated with Claude Code

Serhan-Asad and others added 15 commits July 7, 2026 16:31
…ace contract (#1900)

The public-surface gate compared OLD-vs-NEW generated code, so it could not
distinguish an intended interface change from generator drift, and its repair
directive had no stable target ("restore compatible signatures" — compatible
with the very code being regenerated). This dead-ended `pdd change -> pdd sync`
whenever a module's declared interface legitimately evolved (live case
pdd_cloud#2971: an added symbol plus unrelated `list_secret_metadata` drift).

Make `_verify_public_surface_regression` declaration-aware, per-symbol:

- Symbols declared in the prompt's <pdd-interface> (`type: module`) are validated
  against the DECLARED signature (a stable target) via `signature_entries_compatible`,
  with defaults resolved in the generated namespace (#1558). Binding-kind/async —
  which <pdd-interface> cannot express — stay anchored to the prior generation so
  async<->sync / function<->class drift is still caught; `BREAKING-CHANGE: change
  signature` remains the escape hatch for those.
- Undeclared symbols keep the old-code baseline (+ `BREAKING-CHANGE`), so
  backward-compat protection for helpers/re-exports is unchanged (no regression).
- `PublicSurfaceRegressionError` carries structured `signature_detail:` lines
  (full expected-vs-actual, no truncation); the agentic subprocess parser recovers
  them into the rebuilt repair directive and hard-failure block. `removed:` /
  `signature_changed:` lines stay byte-identical (cloud parser + tests depend on them).

Scoped to `type: module` function declarations; cli/command names and dotted
`Class.__init__`/new-class presence stay owned by the conformance gate.

Adds tests/test_issue_1900_surface_contract.py (18 gate-level tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…enerator_main

PDD-Auto-Heal-Checkpoint: success
… / parser hardening

Follow-up to the #1900 public-surface declaration contract (the preceding
auto-heal bot commit for examples/architecture/metadata is left untouched):

- Declared dotted names (`Class.method`, `Class.__init__`) are presence-only in the
  surface gate: `_snapshot_public_signatures` receiver-strips `self`/`cls`, so
  comparing a declared method signature that includes `self` false-positived on
  UNCHANGED code. Their parameter contract stays with the `<pdd-interface>`
  signature gate; top-level functions keep full declared-signature validation.
- `BREAKING-CHANGE: change signature <sym>` now relaxes ONLY the un-declarable
  binding-kind/async for a declared symbol, never its declared params/return — an
  added-required-param that violates the declaration is no longer bypassable by prose.
- `_parse_signature_detail_lines` tolerates ` | ` inside a signature/default
  (dropped the ordered-index precheck; right-anchored rsplit + try/except only), so
  union types / delimiter-like defaults keep the stable declared repair target.
- Sync README + the `code_generator_main` generator prompt (5b) to the
  declaration-aware behavior (incl. `PublicSurfaceRegressionError.signature_details`).

Adds 4 regression tests (22 total in tests/test_issue_1900_surface_contract.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…code baseline (#1900)

Review follow-up: making dotted-method / non-paren-class declared symbols
presence-only ALSO removed them from the old-code baseline, dropping the signature
protection they had before #1900 — a declared method could silently gain a required
arg or flip binding kind, and a declared class's constructor ABI could drift.

Only symbols actually validated against the declared signature (top-level names with
a parseable paren signature) are now recorded in `declared_validated` and excluded
from the old-code baseline; every other declared symbol (dotted methods,
`class Service`-style non-paren declarations) falls back to the previous-generation
old-vs-new comparison, which catches added-required-params, binding-kind flips, and
constructor-ABI drift, and still honors BREAKING-CHANGE + #1558. The receiver-strip
false positive stays fixed (the old-code path compares receiver-stripped snapshot
entries on both sides). README + generator prompt (5b) wording synced to match.

Adds 4 regression tests (method +required, staticmethod->instance flip, class ctor
drift, BREAKING-CHANGE opt-out) and strengthens the presence-only test that had
encoded the over-skip. 26 tests in tests/test_issue_1900_surface_contract.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the architecture.json conflict (main #1871/#1777 vs the auto-heal bot
commit) by taking main's registry; the auto-heal bot re-generates any real drift
for the changed modules on the next push. No source/test changes.
…face too (#1900)

Review follow-up: dotted methods and constructors were presence-only with an
old-code fallback, which left NEW methods/ctors unchecked (an added required arg
passed) and kept the `pdd change → sync` dead-end for method contract changes. Make
them first-class declared-contract citizens, exactly like top-level functions.

`_declared_signature_to_entry` gains `strip_receiver`: a declared method/ctor
signature is receiver-stripped (leading `self`/`cls` dropped) to match
`_snapshot_public_signatures`; `Class.__init__` maps to the class `[class]` entry
(constructor ABI); binding-kind/async come from the pre-generation entry when present
(else the generated one) so a `@staticmethod`→instance flip is still caught; and
`BREAKING-CHANGE: change signature` relaxes only those un-declarable facets, never the
declared params. Only symbols actually compared against the declaration join
`declared_validated` (keyed on the snapshot key) and are excluded from the old-code
baseline; description-only / non-paren declarations stay presence-only and fall back
to old-vs-new so no pre-#1900 protection is lost.

Net: a new declared method/ctor with an added required arg now raises; editing a
declared method contract is authorized without a `BREAKING-CHANGE` permit; the
declared-`self` false positive stays fixed. README + generator prompt (5b) synced.

Adds 3 tests and reworks 2 (29 in tests/test_issue_1900_surface_contract.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the <pdd-interface> conflict in code_generator_main_python.prompt: keeps
both PublicSurfaceRegressionError.signature_details (#1900) and the new
ProseOutputError entry (#1649). code_generator_main.py / agentic_sync_runner.py /
README auto-merged (disjoint regions). Full sweep 599 passed.
The auto-heal bot commit (3632d6a) regenerated context/agentic_sync_runner_example.py
in a way that dropped the top-level AsyncSyncRunner shim (breaking other prompts'
<include select="class:AsyncSyncRunner">), un-mocked _sync_one_module (so the example
spawned real pdd sync subprocesses), and left project_dependencies.csv stale with
trailing whitespace. The #1900 change is internal (surface gate + detail parser) and
does not alter the public interfaces these examples demonstrate, so restore main's
known-good generated artifacts. PR diff is now purely the #1900 source/tests/docs.
…allable, fix repair advice (#1900)

Review follow-up (three real gaps in the declared-interface surface contract):

- Declared `_`-prefixed helpers (e.g. agentic_common's `_extract_step_report`) were
  false-positived as removed: `declared_missing` checked presence against the public
  surface, which excludes underscore names. The declaration is authoritative (like
  `__all__`), so union the declared symbols actually defined in the code into the
  `patch_targets` fed to `_snapshot_public_surface` / `_snapshot_public_signatures`
  for both before and after — a present declared `_helper` is no longer flagged, a
  genuinely removed one still is, and its signature is now validated too.

- A declared callable regenerated as a non-callable (`def f` → `f = 1` /
  `from pkg import f as f`) passed: `signature_entries_compatible` returns None and the
  symbol was still marked validated, so the old-code baseline was skipped. Now only add
  to `declared_validated` when the comparison is conclusive (`compat is not None`); a
  None result falls through to the old-code baseline, which flags the
  `[function]`→`[assignment]`/`[import]` break (and does not false-positive on an
  unchanged callable-assignment).

- The hard-failure / repair advice told users to add `BREAKING-CHANGE`, which for a
  declared symbol only relaxes binding-kind/async, never params — a dead-end. Advice is
  now declaration-aware: a declared-param violation points at editing the
  `<pdd-interface>` declaration; the `BREAKING-CHANGE` guidance stays for
  undeclared/removed symbols.

Adds 7 tests (36 in tests/test_issue_1900_surface_contract.py); the generator prompt's
`<include>` now lists the new helpers; README deduped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ines, underscore-class ctor (#1900)

Review follow-up (three edge gaps in the declared-interface surface contract):

- A declared callable regenerated as a non-callable (`def f` → `f = 1` /
  `from pkg import f as f`) slipped through when the prompt also carried
  `BREAKING-CHANGE: change signature f` — the old-code fallback skipped it via
  `allowed_signature_changes`. Now, when the declared comparison is inconclusive
  (`compat is None`) and the OLD entry was a callable contract, flag the
  declared-callable-became-non-callable DIRECTLY, regardless of `BREAKING-CHANGE`
  (which authorizes parameter changes, not de-callable-ing a declared callable — use
  `BREAKING-CHANGE: remove` for that). An unchanged callable-assignment
  (`f = lambda: x`) still passes.

- `signature_detail:` lines are now JSON-encoded (`json.dumps` of
  `{symbol, expected, actual, source}`) at both emit sites and parsed via
  `json.loads`, ending the recurring field corruption when a signature default
  contains the ` | actual: ` / ` | source: ` delimiter substring.

- Declared underscore-CLASS constructor drift (`_Service.__init__`) was missed because
  the patch-target signature capture only built entries for top-level functions.
  Extracted `_class_constructor_signature` (shared with `_walk_class`) and used it so a
  patch-target class gets its `[class]` constructor-ABI entry; a declared underscore
  class / constructor is now validated like a public one.

Adds 5 tests (41 in tests/test_issue_1900_surface_contract.py); the generator prompt
spec notes the JSON detail-line format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sses; sync self-hosting metadata (#1900)

Review follow-up:

- Declared NESTED constructors whose class path contains an underscore behaved
  inconsistently — `_Outer.Inner.__init__` passed silently, `Outer._Inner.__init__` was
  mischaracterized as `removed`. They now behave like the public control:
  `_symbol_exists_in_module` resolves nested class paths (not just methods),
  `_resolve_class_node` walks dotted segments to a `ClassDef`, and the patch-target
  signature capture emits the `[class]` constructor-ABI entry for a nested declared class
  (keyed `Outer.Inner`) — so ctor drift raises a `changed` signature with a
  `pdd-interface` detail. Surgical: only DECLARED/patch-target nested classes are
  captured; the public `_walk_class` recursion (undeclared underscore-nested) is
  untouched, so no protection is broadened. `test_split_validation.py` confirms the
  `_symbol_exists_in_module` generalization didn't regress sibling-test patch resolution.

- Self-hosting metadata synced to the #1900 behavior so future self-regeneration keeps
  it: the `agentic_sync_runner` prompt documents the JSON `signature_detail:` parsing +
  declaration-aware repair; the `code_generator_main` prompt `<include>` lists the new
  helpers (`_class_constructor_signature`, `_resolve_class_node`,
  `_symbol_exists_in_module`, `_patch_target_signature_entry`); and architecture.json's
  `code_generator_main` + `agentic_sync_runner` descriptions carry the declared-contract /
  JSON-detail behavior.

Adds 4 tests (45 in tests/test_issue_1900_surface_contract.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-fresh (#1938)

pdd sync regenerated mature modules "rebirth-shaped" (full regeneration),
dropping declared public symbols. PDD already ships edit-shaped generation
(incremental_code_generator + code_patcher_LLM), but sync hardcoded
force_incremental_flag=False, so it deferred to the deliberately-conservative
diff_analyzer_LLM ("prefer complete regeneration"), which rebirthed modules on
ordinary small prompt deltas and lost symbols.

Make sync prefer surgical generation for mature modules, with a --fresh opt-out:
- New `pdd sync --fresh` flag (default off), threaded sync -> sync_main ->
  sync_orchestration and the one-session generate loop.
- Both sync generate call sites now pass force_incremental_flag=(not fresh):
  default -> surgical/edit-shaped (existing code + prompt delta -> minimal edit,
  declared symbols preserved); --fresh -> standard generation.
- Reuses existing infra only. Safety nets preserved: new/empty modules still
  full-generate; undeterminable original prompt falls back to full generation;
  conformance repair retries still force full regeneration (#1724); the
  public-surface / declared-interface gate (#1900) is unchanged and remains the
  guarantee.
- --fresh is single-module only: raises UsageError on global/agentic sync paths
  (mirrors --snapshot-context) instead of silently no-op'ing.

Prompt sources edited in tandem with generated .py (pdd is self-hosted);
sync_main's architecture.json signature aligned; README documents the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prompt-declared <pdd-interface> as the public-surface contract (#1900).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surgical (edit-shaped) regeneration for mature modules + --fresh (#1938).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
)

PR#1940 made `pdd sync` prefer surgical (edit-shaped) regeneration for mature
modules, which needs an "original" prompt to diff the current prompt against.
In the cloud sync-after-change flow the prompt edit is already committed on the
branch (on-disk == HEAD) and the pre-change version is not reliably found by the
local history search, so incremental resolution silently degraded to full
regeneration and re-dropped declared symbols (live failure: test_repo#4232, 5
consecutive surface-gate blocks).

Add a last-resort fallback in code_generator_main's original-prompt resolution:
when no explicit original, no uncommitted-vs-HEAD delta, and no distinct prior
version in local history resolve an original, read it from the base branch via
the existing get_git_content_at_ref() helper. Base ref is configurable via
PDD_SYNC_BASE_REF (default origin/main) and is only used when it differs from
the current prompt.

This replaces the prior degenerate fallback that set the "original" to the
current HEAD content (identical prompts), which produced a no-op incremental
patch that was then discarded for full regeneration anyway. When the base ref
also yields nothing usable, can_attempt_incremental now stays False and an
honest full regeneration runs, surfaced by a single unconditional (non-verbose)
degradation notice so operators are not blind to mature-module full regens.

Tests cover: base-ref resolves a distinct original -> incremental used;
base-ref missing (None) -> full generation + loud notice, no crash; base-ref
identical to current -> not used; PDD_SYNC_BASE_REF override honored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Staging E2E evidence (2026-07-09, staging1, stack/e2e-staging-v2 = 288d053)

Two full change→sync chains ran against real mirrors with this bundle deployed:

  • test_repo#4249 (frontend page): change ✅ → sync ✅ clean first pass (test_repo PR #4253, module green in 5 phases, $2.15).
  • test_repo#4248 (backend, incl. the 40-symbol worker_app module): change ✅ → sync gate correctly blocked twice — (1) annotation-level drift on two declared signatures (repair directive engaged as designed; convergence gap filed as pdd#1968, fix being added to this bundle), (2) genuine prompt/code drift on main from pdd_cloud#2350 (undeclared PROVIDER_QUEUE surface — the gate caught real hygiene debt; pdd_cloud#3187 / PR pdd_cloud#3195). After authorizing the declared-interface updates via the gate's own instructions, sync completed green including worker_app (test_repo PR #4254, 30.4 min).

Net: the contract gate blocks only for real reasons, the repair loop engages, and the authorize→relabel loop completes. Surface regression class from round 1 (silent symbol drops) did not recur.

Serhan-Asad and others added 3 commits July 9, 2026 16:16
…#1968)

The sync public-surface gate correctly rejected a regeneration that drifted
a declared `<pdd-interface>` signature only in annotation spelling (declared
`object`, regenerated `Any`; or a broadened parameter union), but the repair
retry re-emitted the identical "equivalent" spelling and never converged.

Two complementary fixes, building on PR#1905's self-repair mechanism:

1. Verbatim repair directive: for a `signature_changed` / `source: pdd-interface`
   violation, both the in-process `PublicSurfaceRegressionError.repair_directive`
   and the subprocess `agentic_sync_runner._parse_public_surface_failure`
   reconstruction now inject the declared signature as a hard constraint ("emit
   exactly this signature ...", plus an explicit anti-substitution rule: keep
   `object` as `object`, never `Any`; do not broaden a declared union), instead
   of merely describing the violation.

2. Deterministic annotation reconciliation: `_reconcile_declared_annotation_drift`
   runs BEFORE the gate re-checks and, when the ONLY drift on a declared symbol
   is annotation spelling (identical param names/order/kinds/defaults), rewrites
   the emitted annotation(s) back to the declared text via a byte-offset splice,
   so the gate passes with no further generation attempt. Fail-safe: only
   gate-INCOMPATIBLE annotations are rewritten, structural drift is left for the
   gate/repair loop, compatible aliases (`Dict` vs `dict`) are never churned, and
   the rewrite is discarded if the result no longer parses.
   `PDD_SKIP_ANNOTATION_RECONCILE=1` bypasses it.

Owning prompts (code_generator_main_python.prompt, agentic_sync_runner_python.prompt)
updated in sync, mirroring PR#1905's structure.

Tests: tests/test_issue_1968_annotation_convergence.py (9 cases) — verbatim
directive on both paths, object/Any + broadened-union convergence, method drift,
and regressions (added param / removed symbol / compatible alias / no declaration
are left untouched).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant