Files
trufflepig/docs/php-contract.md
Joshua MoonandCodex 2a38fe7b5f feat(php): resolve XFCP chains
Add bounded ordered XFCP predecessor candidates and resolve literal parent calls through concrete PHP methods. Publish typed hierarchy edges, preserve priority ties as candidates, and version resolver projections independently from extraction caches.

Validation: 471 Rust tests, release build, 58 Python plugin tests, and XenForo acceptance across 7,550 PHP files (99.1% complete, 57 CLI calls, zero failures, 10 XFCP occurrences, 8 parent calls, 4 Forum tie pairs, 25 User tie pairs).

Co-authored-by: Codex <[email protected]>
2026-09-28 18:23:03 -04:00

9.7 KiB

PHP and XenForo navigation

PHP extraction

.php and .phtml map to php and use pinned tree-sitter-php 0.24.2. Extraction records namespaces, classes, interfaces, traits, enums, functions, methods, properties, constants, enum cases, parameters, variables, imports, and identifier occurrences. Definition spans cover declaration nodes; occurrence spans use original source bytes, including $ for PHP variables. Parse statuses are complete, parse_error, invalid_encoding, incompatible_grammar, cancelled, and depth_limit; parse errors retain safe observations but do not establish confident links.

Definition names stay short. Class-like declarations use their namespace as the container, functions use their namespace, and members use the containing class's fully qualified name. Namespace-relative and imported type/import path occurrences use normalized fully qualified names without a leading slash; explicit absolute names keep their leading slash. Normalized names can differ from the text at the occurrence span.

Only reads of a unique, unambiguous parameter declared in the same function can resolve. Assignment-bound variables remain candidates, even with one apparent assignment, because the extractor does not establish branch visibility, assignment order, or dynamic-variable behavior. Calls, imports, and type references remain candidates or unresolved, even when one target is found, including an absolute fully qualified type or import name matching exactly one definition. The PHP and XenForo resolver can add bounded, source-derived inheritance and literal parent::method() candidates as described below. These candidates do not make extraction compiler-equivalent or prove runtime dispatch. Trait-use references remain candidate or unresolved evidence; traits are not expanded. A conditional XFCP helper declaration does not bind a generated class. Cross-file lookup uses exact fully qualified names and does not guess from a short class name.

XenForo add-on metadata

The metadata resolver reads addon.json and _data/class_extensions.xml from indexed source content. It does not read live activation state from the XenForo database or discover files outside the index. Add-on requirement occurrences span the quoted key in addon.json's require object, not its version/value; XML metadata occurrences span their attribute values. A class-extension relationship cites the full <extension> element as its evidence span.

Known local add-on IDs in addon.json's require object produce xenforo_addon_requirement_candidate edges from the requiring add-on to the required local add-on. Unknown requirements, including PHP or XenForo framework requirements without a local add-on target, remain observations without an add-on edge.

Each class-extension declaration has a xenforo_class_extension definition and xenforo_class_extension_candidate links to exact indexed base-class candidates. Its to_class occurrence separately lists matching PHP class definitions. Provenance records active, execute_order, XML source, and runtime_xfcp_chain_unresolved. An inactive declaration remains metadata but has no active chain edge. The XML owner's add-on can differ from the add-on defining to_class; the resolver keeps those owners distinct. A xenforo_addon_extension_dependency_candidate, when emitted, links the XML owner to exact indexed base owners and requires unique implementation and base owner evidence. It does not infer the PHP implementation's owner from the XML or class-name prefixes. Malformed extension XML invalidates the whole file; when its owner is known, a file-scoped inheritance_issue cites its XML range, and valid declarations in other files remain usable.

These edges are navigation evidence, not proof that an add-on is installed or enabled, that XenForo will apply the declaration, or that an extension chain will execute in the recorded order. The source resolver treats registrations that pass the indexed metadata checks as applicable while constructing candidates; it cannot read runtime enabled-add-on state or database collation. runtime_xfcp_chain_unresolved continues to mark the gap between source candidates and the runtime chain.

Declared inheritance and literal parent calls

The inheritance resolver uses indexed PHP source and indexed XenForo registration source only. It does not query the XenForo database, load classes, run add-on code, or establish which add-ons are enabled. A candidate chain assumes the indexed registrations that pass their source checks apply; this is navigation evidence, not a claim about the live XFCP chain.

Ordinary PHP extends names use namespace/import resolution followed by exact indexed class lookup. XF metadata alias mapping is separate: supported literal alias hooks in indexed XF.php canonicalize only metadata base names; an absent hook means identity, while a present unsupported hook blocks mapping. The hooks are not run and aliases are not inferred. PHP import aliases are resolved before matching an extends XFCP_* parent; metadata alias rules never rewrite PHP imports. The raw XML to_class spelling defines extender identity and its generated proxy spelling.

Framework extension declarations produce framework_parent_candidate edges from an implementation class to its immediate predecessor candidate. Ordinary PHP declarations produce php_extends_candidate edges. Duplicate actual PHP class identities block framework/proxy targets and emit inheritance_issue; metadata lookup can retain the matching class candidates. Reusing one actual implementation under different raw proxy names blocks composition for that base and creates no self-edges. Missing classes are omitted, never spliced into a shortcut to a later ancestor.

Only XML active="0" and active="1" are recognized. Zero remains metadata without an active edge; one is eligible source evidence; absent or other values remain unknown and block composition. Negative execute_order is invalid and blocks composition for that base. Strictly different valid numeric orders establish order; equal orders remain candidate alternatives marked by priority_tie, without a database-collation or lexical tie-break. Missing, nonnumeric, or negative order produces an inheritance_issue. If base alias mapping fails, independent raw implementation evidence remains available but does not compose into a chain.

A literal parent::method() call can produce a php_parent_call_candidate to the nearest ancestor's own concrete public or protected declaration of that method. The search follows candidate framework and ordinary PHP parent edges. It does not bind $this->method(), static::method(), dynamic calls, or other general member references. It does not flatten trait methods or search interfaces. A private or abstract method declaration, trait boundary, incomplete or conditional class/method declaration, parse failure, missing parent source, or ambiguous parent path is an inheritance_issue barrier; traversal does not guess past it. Duplicate or reused proxy names and unknown extension active state also produce issue evidence when anchored. Conditional or generated runtime class definitions do not become source declarations.

The three parent-link kinds remain candidate evidence; naming a target class does not establish the exact runtime chain. inheritance_issue reports incomplete evidence, not an alternative parent. Expansion is bounded to 64 classes per chain including the base, 64 parent-call candidates, and 64 visited classes per traversal including the caller. A 200,000-item derived-stage budget covers bounded preparation and publication and is charged before retaining edges and candidate IDs. ctx includes at most 100 graph edges within its normal token budget, so a context response can end before the full candidate graph. Reaching a bound or a source barrier is reported as incomplete evidence, not as proof that no parent exists. A global derived-stage failure aborts the staged publication, leaving the last complete publication available with no partial expansion. Internal php_markers remain private and do not appear as definitions, occurrences, relationships, or provenance. Public relationship kinds and provenance, including runtime_xfcp_chain_unresolved, remain visible.

Use sym: with a short class name and file: to find a definition, then inspect ctx or refs candidate details and read the source registrations.

Real checkout acceptance

The XenForo acceptance script reads the checkout in place and leaves its source read-only. It exercises search, show, map, refs, and ctx across XenForo core and add-on files, and reports aggregate PHP parse statuses. Index coverage follows the normal scanner ignore policy and source bounds: ignored files are not indexed or counted, so the report describes published PHP files rather than every PHP file on disk. The gate requires at least 5,000 indexed PHP files, at least 95% complete status, and complete status for its core and add-on fixture files. Its JSON report includes the status counts and failed checks. Build the binary separately. With --new-build, the runner scopes the system directory and spool to the cache root, starts private root and system services with that binary, waits for index publication, then routes CLI requests through its private router. It stops and reaps only those two owned processes when the run ends.

Run it with a dedicated cache under the disk-backed scratch root:

python3 evaluation/php_xenforo.py \
  --binary target/release/trufflepig \
  --xf-root /home/josh/Source/xf_kiwifarms \
  --cache-root /home/josh/.cache/codex-tmp/xf-php-final \
  --new-build

The script keeps its index and temporary files under the supplied cache root. The caller removes that dedicated cache directory after the run when its results are no longer needed.