feat(php): positive alias binding via declared FQNs + incremental parity marker (#22, #23) - #35
Merged
Merged
Conversation
…#22) The #21 refusal made a claimed name decisive but could only delete edges. This is its additive counterpart: when the FQN a calling file claims for a receiver's type — via `use`, `use ... as`, a group `use`, or a qualified annotation — matches the name some in-corpus file DECLARES for a class (the #14 payload), PhpNameResolver binds it, selecting the imported one of several same-short-named classes and following a renaming alias to a class the written short name would never census. An FQN declared identically by two files poisons its index entry rather than guessing; an unclaimed short name still runs the unique-short-name fallback untouched, and every #16-style refusal stands where no declared FQN matches. Ships together with #23 (the persisted `_php_class_fqns` marker): without it, every edge this adds would exist on full builds only and silently vanish on the first incremental rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ty (#23) The php_class_fqns payload aligns 1:1 with the files dispatched THIS run, so every edge the #22 declared-FQN binding adds existed on full builds only and silently vanished on the first `graphify update` — the exact failure mode #11 closed for the interface refusal. Stamp each PHP file node with a persisted `_php_class_fqns` marker (the `_callable` / `_php_non_class_types` channel), hand it back on the resolution-context nodes in watch.py and cli.py, and re-inject it as synthetic per-file payload entries so the resolver rebuilds its index for files the incremental pass left untouched. A graph.json written before the marker fails closed, matching the established precedent: the #22 binding is simply absent — never redirected to a namesake, with the #16 refusals still standing on path evidence — until the defining file is re-extracted. Every #22 positive scenario is mirrored under _full_then_incremental, plus pre-marker variants pinning the fail-closed behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 6, 2026
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.
Fixes #22. Fixes #23. (Spec #18, tickets 4/5 and 5/5 — shipped together per decision 3: the binding and its carry-over marker must land in the same release.)
What
Two commits, reviewed best in order:
Commit 1 (#22) —
PhpNameResolvergains a declared-FQN index: casefoldeddeclared FQN -> definition node, built over the same node universe as the short-name census, sourced from the #14php_class_fqnspayload. Inresolve_type_name, once the calling file's claim is resolved to an FQN (viause,use ... as, groupuse, or a qualified annotation), a unique index hit binds before the single-candidate check. Two things now resolve that used to refuse:use App\Alpha\X;selectsApp\Alpha\Xamong several same-short-named classes, and a renaming aliasuse App\Alpha\X as Y;follows the alias to a class the written short name never censuses. An FQN declared identically by two files poisons its entry rather than guessing. On an index miss everything falls through to the #21 rules untouched — additive-only by construction, the mirror risk profile of #21's subtractive-only.Commit 2 (#23) — the
php_class_fqnspayload aligns 1:1 with files dispatched this run, so without persistence every #22 edge would exist on full builds only and silently vanish on the firstgraphify update(the #11 failure mode). Each PHP file node now persists_php_class_fqnsintograph.json(the_callable/_php_non_class_typeschannel),watch.pyandcli.pyhand it back on resolution-context nodes, andextract()re-injects it as synthetic per-file payload entries keyed by each context node's ownsource_file. A pre-markergraph.jsonfails closed: the binding is simply absent — never redirected to a namesake, with the #16 refusals still standing on path evidence — until the defining file is re-extracted.One existing test changed behavior deliberately:
test_duplicate_class_name_emits_no_edgehad its calleruse App\Services\LeadHunterService;with two namesakes — exactly #22's acceptance scenario 1, which now binds. It was repointed at the still-true refusal (nouseclaim at all), andtest_alias_renaming_to_an_unclaimed_short_name_stays_unresolved(the pinned #21/#22 boundary) now asserts the binding its docstring anticipated.Verification beyond the suite
Full suite: 4104 passed, 36 skipped (v8 baseline before the change: 4104 collected equivalents green; commit 1 also verified green in isolation, 4097 passed with the #23 files stashed).
Differential extract against
v8@cec9cddover a 25-file corpus covering the receiver-typing shapes (namesake pairs with plain/group/renaminguse, absolute and namespace-relative written FQNs, bare receiver with no claim, out-of-corpususe, duplicate declared FQN across two files, interface/enum/union refusals, inlinenew, first-class callable, non-PSR-4 layout, unique-short-name fallback): 6 edges added, 0 deleted, 0 re-pointed, 0 confidence changes. All six target exactlyApp\Alpha\X::sendat INFERRED 0.8 (fivecalls+ oneindirect_callthrough the FCC form); the baseline's 3 edges (inline-newEXTRACTED, non-PSR-4, unique fallback) are byte-identical.End-to-end incremental parity through the real CLI channel (not the test mirror):
graphify extracton that corpus → 9 call edges and 23 file nodes carrying_php_class_fqnsingraph.json; touch only the four caller files →graphify update .→ the same 9 edges, including every #22 edge whose defining file was left undispatched.🤖 Generated with Claude Code