fix(sync): reconcile branch-diff fast-path scope with issue's explicitly requested modules (#1980)#1983
Conversation
Pre-fix investigation summaryBug mechanics (verified on the epic base = current main):
Inventory / helper reuse: the flow already loads Prior art mined:
Sibling hunt — |
Red test output (commit 854ae13, before the fix)The repro integration test fails with exactly the under-scope from #1980, and the helper unit tests fail on the missing extractor. The two passing tests are the intentional behavior-unchanged guards (superset scope, prose non-inflation), which must pass both before and after the fix. |
Mocked-E2E evidence (hermetic real-CLI run, post-fix)The under-scope repro is exercised through the real code path ( Full suite transcript ( (Re-run after the lint refactor: Note on |
Codex adversarial review — PR #1983 @ 09e2905Verdict: DON'T MERGE P1 (blocking)
P2 (non-blocking)
Notes
(Posted by the orchestrator on behalf of |
Implementer response — round 2Thanks — all three findings are valid. Plan per finding: P1a (comment-only FILES_MODIFIED ignored) — Status: FIX. Extraction will scan the same signal-bearing content the identify path uses: P1b (duplicate-tail coverage masking) — Status: FIX. Two changes: (i) prompt-path tokens are converted to path-qualified module keys using the same P2 (hyphenated bare basenames never prose-matched) — Status: FIX (verified real). Checked the derivation: nothing normalizes hyphens away — Process: red tests first as a separate |
…nciliation (#1980) PR #1983 Codex review: - P1a: extraction must cover filtered issue comments (comment-only FILES_MODIFIED markers), not just title/body. - P1b: prompt-path tokens must keep path qualification, and tail-based scope coverage must only apply to globally-unambiguous tails so a diff touching extensions/a/src/page cannot mask an explicit request for extensions/b/src/page. - P2: bare hyphenated basenames (check-run) are valid inventory members and must prose-match like underscored ones. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, strict tail coverage (#1980) PR #1983 Codex review fixes: P1a: _extract_issue_named_modules gains a comments parameter and the step-7 call site passes the already-filtered signal-bearing comments (post-_filter_low_signal_comments), so comment-only FILES_MODIFIED markers now trigger reconciliation. Only comment bodies are scanned. P1b: prompt-path tokens now convert to path-qualified module keys via _module_key_from_prompt_path (same derivation as _detect_modules_from_branch_diff, #1675) instead of collapsing to the filename; _issue_modules_missing_from_scope takes the architecture inventory and accepts tail-based coverage ONLY for globally-unambiguous tails — duplicate-tail siblings now require exact-key coverage, so a diff touching extensions/a/src/page can no longer mask an explicit request for extensions/b/src/page. No looser path-suffix matching is attempted (it reintroduces masking through bare keys). P2: the bare prose matcher gate extends from underscore-only to underscore-or-hyphen tails (check-run); verified hyphenated basenames survive _basename_from_architecture_filename, and the existing boundary lookarounds already exclude hyphen-adjacent longer identifiers. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-2 red/green evidenceRed (commit 11c9122, tests-only, pushed before the fix) — 8 failed / 13 passed in Failures map 1:1 to the findings: P1a ( Green (commit 84ae8d0):
One deliberate deviation from the suggested P1b fix: instead of adding path-suffix coverage for path-qualified issue keys, coverage is now strictly exact full key OR globally-unambiguous tail. A path-suffix rule reintroduces the same masking through bare or partially-qualified keys (e.g. issue key |
Codex adversarial review — PR #1983 @ 84ae8d0Verdict: DON'T MERGE P1 (blocking)
P2 (non-blocking)
Notes
(Round-2 re-review; posted by the orchestrator on behalf of |
Implementer response — round 3All three findings are valid; all three are FIXING. The duplicate- P1#1 (duplicate-filename entries mask path-qualified requests) — Status: FIXING. Root cause: extraction/coverage consumed P1#2 (tokenizer drops P2 (surviving workflow comments / fenced noise inflate scope) — Status: FIXING. (a) Comments are no longer scanned as free text at all: after stripping fenced code blocks, only Consciously unfixed (ACCEPTED, recall-only): a backticked bare name whose tail is duplicated across real identities (e.g. Process: red tests first as a separate |
…ies, route brackets, comment/fence noise (#1980) PR #1983 Codex round-2 review: - P1#1: duplicate-filename architecture entries (pdd_cloud Next.js page_TypeScriptReact.prompt routes) must stay distinct identities; prompt-path requests resolve path-qualified and a diff touching one sibling must not cover the other. - P1#2: bracketed dynamic-route segments ([id]) must survive prompt-path tokenization. - P2: comments contribute only trusted FILES_MODIFIED marker payloads (fenced markers are logs); class-3 bare-name matching ignores fenced code blocks in title/body. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ths, trusted comment markers (#1980) PR #1983 Codex round-2 review fixes: P1#1: extraction and coverage now consume _architecture_identity_index, which keeps one identity per distinct architecture entry (duplicates preserved; runtime *_LLM entries excluded; true duplicates deduped by (identity, filepath)) plus exact-match aliases from _architecture_entry_aliases. Tail-based resolution/coverage requires the tail to list exactly ONE real entry, so two Next.js page_TypeScriptReact.prompt routes no longer collapse to a single 'page' that lets a diff on one route mask an explicit request for another. A path-qualified key derived from a prompt path is accepted directly as a scheduler key (same form the branch-diff detector emits, #1675); a bare name colliding with a duplicated tail resolves to nothing. P1#2: _ISSUE_PROMPT_PATH_RE captures full non-whitespace .prompt paths, with leading markdown punctuation trimmed by _prompt_path_tokens (a leading '[' is stripped only when no matching ']' follows, preserving dynamic-route segments like app/users/[id]/page_TypeScriptReact.prompt). P2: comments contribute ONLY FILES_MODIFIED marker payloads — parsed with the change orchestrator's _extract_marker_paths walker after stripping fenced code blocks (a fenced marker is a quoted log, not a directive) — and only as prompt-path tokens. Class-3 bare-name matching in title/body now runs on fence-stripped text; an unclosed fence conservatively drops the remainder. Classes 1-2 stay global in title/body. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-3 red/green evidenceRed (commit c7186e0, tests-only, pushed before the fix) — 8 failed / 23 passed in Failure-to-finding map: P1#1 — Green (commit 8bd1575):
Implementation notes: the identity index reuses the existing per-entry alias derivation ( |
Codex adversarial review — PR #1983 @ 8bd1575Verdict: DON'T MERGE P1 (blocking)
P2 (non-blocking)
Notes
(Round-3 re-review; posted by the orchestrator on behalf of |
…named modules (#1980) The #1868 E2E repro: issue explicitly names `greeter` and `textutil`, work-branch diff touches only greeter -> fast path syncs only greeter and reports Success. New tests assert the final scope must include both, with zero LLM calls, plus guards that a superset diff scope stays unchanged and prose words (e.g. a module named 'python') never inflate scope. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ules (#1980) The branch-diff fast path in run_agentic_sync step 7 committed to modules_to_sync = branch_modules unconditionally, silently under-scoping relative to the modules the issue explicitly requests (repro from the #1868 E2E validation: issue names greeter AND textutil, diff touches only greeter -> only greeter synced, greeter->textutil edge dropped, Success reported). Fix: deterministically extract explicitly-named known modules from the issue title/body and UNION any that the diff missed into the scope — still zero LLM calls. Extraction matches only high-precision token classes against the architecture inventory: 1. backticked inline-code tokens resolving to a known basename (.prompt / language suffixes stripped), 2. prompt-file path tokens (FILES_MODIFIED-style lists), 3. bare word-boundary mentions, only for underscored basenames that cannot be ordinary prose words (a module named 'python' is never pulled in by prose; single-word modules require backticks or a prompt path). When the diff already covers every issue-named module, behavior is byte-for-byte unchanged. The added modules go through the existing normalization / invalid-basename / ambiguity pipeline like every other scope source. The #1396 runtime-LLM-only boundary and the #1883 PDD_CHANGED_MODULES authoritative-override arm are untouched. The mocked E2E suite gains the under-scope repro (test_branch_diff_scope_reconciled_with_issue_named_modules); the resume test's state fixture now marks both fixture modules succeeded since the fixture issue explicitly names both prompts. Fixes #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nciliation (#1980) PR #1983 Codex review: - P1a: extraction must cover filtered issue comments (comment-only FILES_MODIFIED markers), not just title/body. - P1b: prompt-path tokens must keep path qualification, and tail-based scope coverage must only apply to globally-unambiguous tails so a diff touching extensions/a/src/page cannot mask an explicit request for extensions/b/src/page. - P2: bare hyphenated basenames (check-run) are valid inventory members and must prose-match like underscored ones. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, strict tail coverage (#1980) PR #1983 Codex review fixes: P1a: _extract_issue_named_modules gains a comments parameter and the step-7 call site passes the already-filtered signal-bearing comments (post-_filter_low_signal_comments), so comment-only FILES_MODIFIED markers now trigger reconciliation. Only comment bodies are scanned. P1b: prompt-path tokens now convert to path-qualified module keys via _module_key_from_prompt_path (same derivation as _detect_modules_from_branch_diff, #1675) instead of collapsing to the filename; _issue_modules_missing_from_scope takes the architecture inventory and accepts tail-based coverage ONLY for globally-unambiguous tails — duplicate-tail siblings now require exact-key coverage, so a diff touching extensions/a/src/page can no longer mask an explicit request for extensions/b/src/page. No looser path-suffix matching is attempted (it reintroduces masking through bare keys). P2: the bare prose matcher gate extends from underscore-only to underscore-or-hyphen tails (check-run); verified hyphenated basenames survive _basename_from_architecture_filename, and the existing boundary lookarounds already exclude hyphen-adjacent longer identifiers. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ies, route brackets, comment/fence noise (#1980) PR #1983 Codex round-2 review: - P1#1: duplicate-filename architecture entries (pdd_cloud Next.js page_TypeScriptReact.prompt routes) must stay distinct identities; prompt-path requests resolve path-qualified and a diff touching one sibling must not cover the other. - P1#2: bracketed dynamic-route segments ([id]) must survive prompt-path tokenization. - P2: comments contribute only trusted FILES_MODIFIED marker payloads (fenced markers are logs); class-3 bare-name matching ignores fenced code blocks in title/body. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ths, trusted comment markers (#1980) PR #1983 Codex round-2 review fixes: P1#1: extraction and coverage now consume _architecture_identity_index, which keeps one identity per distinct architecture entry (duplicates preserved; runtime *_LLM entries excluded; true duplicates deduped by (identity, filepath)) plus exact-match aliases from _architecture_entry_aliases. Tail-based resolution/coverage requires the tail to list exactly ONE real entry, so two Next.js page_TypeScriptReact.prompt routes no longer collapse to a single 'page' that lets a diff on one route mask an explicit request for another. A path-qualified key derived from a prompt path is accepted directly as a scheduler key (same form the branch-diff detector emits, #1675); a bare name colliding with a duplicated tail resolves to nothing. P1#2: _ISSUE_PROMPT_PATH_RE captures full non-whitespace .prompt paths, with leading markdown punctuation trimmed by _prompt_path_tokens (a leading '[' is stripped only when no matching ']' follows, preserving dynamic-route segments like app/users/[id]/page_TypeScriptReact.prompt). P2: comments contribute ONLY FILES_MODIFIED marker payloads — parsed with the change orchestrator's _extract_marker_paths walker after stripping fenced code blocks (a fenced marker is a quoted log, not a directive) — and only as prompt-path tokens. Class-3 bare-name matching in title/body now runs on fence-stripped text; an unclosed fence conservatively drops the remainder. Classes 1-2 stay global in title/body. Refs #1980 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8bd1575 to
e2e0323
Compare
Implementer response — round 4Branch rebased onto the updated epic base (263b492, post-#1982 squash) via clean cherry-pick; P1#1 (cross-project identity collapse) — Status: VERIFIED, FIXING. Confirmed against the source: P1#2 (dependency edges lost for prompt-path-derived keys) — Status: VERIFIED (empirically), FIXING. Reproduced directly: P2#1 (fenced title/body tokens widen scope via classes 1/2) — Status: FIXING. Mechanical: title/body is fence-stripped once, before all three classes (comments were already stripped before marker parsing). Test: fenced prior-run log containing a FILES_MODIFIED line and an inline-backticked module name must not widen scope; unfenced tokens still do. P2#2 (4+ char fences leak inner triple fences) — Status: FIXING. Mechanical: the stripper now records the opening fence character and length and closes only on a fence of the same character with >= length (CommonMark rule). Test: a ````-fenced block containing an inner ``` pair leaks nothing. Process: red tests first as a separate |
Fixes #1980. Part of epic PR #1981.
Root cause
Step 7 of the issue-sync flow (
run_agentic_sync,pdd/agentic_sync.py) committed tomodules_to_sync = branch_modulesunconditionally whenever_detect_modules_from_branch_diff()returned a non-empty set. The issuetitle/bodyand the loadedarchitectureinventory were already in scope at that point but never consulted, so the deterministic fast path silently under-scoped relative to what the issue explicitly requested. Repro (found by the #1868 E2E validation): issue explicitly asks to syncgreeterandtextutil; work-branch diff touches only greeter → only greeter synced, the greeter→textutil dependency edge dropped with a yellow warning, Success reported. The LLM-identify path scoped both correctly from the same issue.Chosen semantics: deterministic union (still 0 LLM calls)
Before committing to the fast path, deterministically extract explicitly-named known modules from the issue title/body (
_extract_issue_named_modules) and union any that the diff missed into the scope, with a clear log of which modules came from the diff vs the issue. Union was chosen over falling through to LLM identification because:_normalize_modules_for_sync,_filter_invalid_basenames,_drop_ambiguous_basenames(pdd sync should resolve modules by canonical path, not ambiguous short basenames #1677), dep-graph expansion), so nothing bypasses existing validation.When the diff-detected scope already covers every issue-named module, behavior is byte-for-byte unchanged: same scope, same output, no LLM calls.
Precision/recall trade-off in extraction
Only tokens that unambiguously resolve against the known architecture inventory count as "explicitly named" — precision over recall, because a false positive here inflates a write-mode sync scope:
`greeter`,`prompts/greeter_python.prompt`) resolving to a known basename after.prompt/language-suffix stripping.ci_validation): such names cannot be ordinary prose words. A single-word module named e.g.pythonis never pulled in by prose ("written in python"); requesting it requires backticks or its prompt path. Case-sensitive; ambiguous tails (multiple architecture entries) are skipped, consistent with pdd sync should resolve modules by canonical path, not ambiguous short basenames #1677.Accepted recall loss (documented on purpose): an issue that names a single-word module in plain prose without backticks or a prompt path does not trigger the union — behavior is then no worse than today. Extraction matches only title/body (not comments), mirroring where the issue's request lives; comments routinely quote logs naming unrelated modules.
Hard boundaries preserved
_branch_diff_is_runtime_llm_only(chore: PDD sync for #879 #1396): untouched arm; runtime*_LLMtemplates never resolve during extraction (both the inventory andextract_module_from_includeexclude them), covered bytest_runtime_llm_templates_never_extracted.PDD_CHANGED_MODULES(pdd sync: consume PDD_CHANGED_MODULES as authoritative module-identification fast path #1883): arm untouched — see sibling note in comments.Test plan (TDD: red pushed first, commit 854ae13)
tests/test_agentic_sync.py::TestBranchDiffIssueScopeReconciliation— 7 helper unit tests (backticks, prompt paths, prose guard for apython-named module, underscored-name prose match, longer-token non-match,*_LLMexclusion, no-architecture no-op) + 3run_agentic_syncintegration tests (union repro asserting both modules reach dry-run and the runner withrun_agentic_tasknever called; superset-unchanged; prose-non-inflation).tests/test_agentic_sync_mocked_e2e.py::test_branch_diff_scope_reconciled_with_issue_named_modules— hermetic real-CLI repro: work branch touches only greeter, fixture issue names both prompts → both child dry-run validations run, zero provider calls.tests/test_agentic_sync.py(247 passed) and full mocked E2E suite (5 passed);pylint pdd/agentic_sync.pyintroduces no new findings vs the epic baseline (verified by diffing pylint output; the resume E2E test's state fixture now marks both fixture modules succeeded because the fixture issue explicitly names both prompts).🤖 Generated with Claude Code