@@ -214,100 +214,17 @@ Each item must include:
214214
215215# Outstanding items
216216
217- Recommended order: do tasks 2 → 3 → 4 in that order (4 renames fields
218- across the whole crate, so it goes last to avoid churning code that 2
219- and 3 are about to move).
217+ Recommended order: 3 → 4 → 5. Tasks 4 and 5 both churn the whole crate
218+ (4 renames ` Backend ` fields, 5 moves the type-engine modules), so they
219+ go last to avoid re-touching code that 3 moves; do 4 before 5 so 5's
220+ module moves don't collide with 4's field renames.
220221
221- All three tasks are pure internal refactors. None of them gets a
222+ All three remaining tasks are pure internal refactors. None gets a
222223changelog entry unless the work uncovers and fixes a concrete
223224user-visible bug. Run the Rust CI checks (` cargo nextest run ` , `cargo
224225clippy -- -D warnings` , ` cargo clippy --tests -- -D warnings` , ` cargo
225226fmt`) after each task.
226227
227- ## 2. Split ` diagnostics/mod.rs ` around its thin orchestrator
228-
229- ** What to do.** The actual orchestrator (` collect_slow_diagnostics ` ,
230- at ~ line 240, ~ 85 lines) is fine, but it is buried in a 3,391-line
231- ` src/diagnostics/mod.rs ` . This is pure code motion plus one shared
232- context struct — no behavior changes. Rust allows ` impl Backend `
233- blocks in any file of the crate, so moving a method means moving it
234- into a new ` impl Backend ` block in the new file; nothing needs to
235- become free functions.
236-
237- ** Step 1 — external-tool pipelines → ` diagnostics/external/ ` .**
238- Create ` src/diagnostics/external/{mod,phpstan,phpcs,mago}.rs ` and move
239- the four subprocess spawn/debounce/parse pipelines out of ` mod.rs ` :
240- ` schedule_phpstan ` (~ line 1299), ` schedule_phpcs ` (~ 1474),
241- ` schedule_mago_lint ` (~ 1632), and ` schedule_mago_analyze ` (~ 1764,
242- both mago pipelines go in ` mago.rs ` ) — roughly 730 lines — each with
243- the private helpers only it uses. Helpers shared by two or more
244- pipelines go in ` external/mod.rs ` . Do ** not** move
245- ` schedule_diagnostics ` , ` schedule_external_diagnostics ` , or
246- ` schedule_diagnostics_for_open_files ` (~ 1077–1298): those are the
247- native debounce orchestration and stay in ` mod.rs ` .
248-
249- ** Step 2 — staleness reconciliation → ` diagnostics/stale.rs ` .**
250- Move ` is_stale_phpstan_diagnostic ` (~ line 467) plus every helper only
251- it uses (~ 335 lines reconciling cached external diagnostics with
252- edits).
253-
254- ** Step 3 — post-processing policy → ` diagnostics/suppression.rs ` .**
255- Move ` filter_suppressed ` (~ line 2034), ` suppress_imprecise_overlaps `
256- (~ 2071), and ` is_full_line_range ` (~ 2159) — ~ 230 lines.
257-
258- ** Step 4 — shared ` FileDiagnosticContext ` .** Seven symbol-span
259- collectors (` unknown_classes.rs ` , ` unknown_functions.rs ` ,
260- ` deprecated.rs ` , ` implementation_errors.rs ` , ` invalid_class_kind.rs ` ,
261- ` unknown_members/mod.rs ` , ` unused_imports.rs ` ) open with variations of
262- the same lock-gathering preamble. Define, next to the existing
263- ` diagnostics/helpers.rs ` utilities, a struct holding the per-file
264- snapshot they gather: ` symbol_map: Arc<SymbolMap> ` ,
265- ` resolved_names: Option<Arc<OwnedResolvedNames>> ` , the file's use map
266- (from ` file_imports ` ), its ` Vec<NamespaceSpan> ` (from
267- ` file_namespaces ` ), and its local classes (from ` uri_classes_index ` ).
268- Give it a constructor like `FileDiagnosticContext::gather(&Backend,
269- uri) -> Option<Self >` (returning ` None` when no symbol map exists,
270- which is every collector's current early-out). Build it ** once** in
271- ` collect_slow_diagnostics ` and pass ` &FileDiagnosticContext ` to the
272- seven collectors; collectors that need only some fields just ignore
273- the rest. Before changing collector signatures, grep for callers
274- outside ` collect_slow_diagnostics ` (the ` analyse/ ` CLI path, code
275- actions, and tests call some collectors directly); any such caller
276- builds its own context via ` gather ` . Besides deduplication, the shared
277- snapshot guarantees all collectors observe consistent state instead of
278- re-reading locks that a concurrent parse may update between
279- collectors.
280-
281- ** Step 5 — split ` type_errors.rs ` .** Convert
282- ` src/diagnostics/type_errors.rs ` (1,538 lines) into a directory and
283- move ` is_type_compatible ` (starts at ~ line 76, ~ 730 lines) plus the
284- predicates only it uses (` is_bare_array ` , ` is_refined_scalar_pair ` ,
285- etc.) to ` type_errors/compatibility.rs ` . It is the diagnostic-policy
286- layer over ` crate::class_lookup::is_subtype_of_typed ` .
287-
288- ** Step 6 — file the MAYBE audit, do not act on it.** While moving
289- ` is_type_compatible ` , list its "MAYBE escape hatches" (IntRange,
290- union handling, iterable/Traversable, bare Closure/callable, bare
291- array, nullable → non-nullable) and assess which ones describe real
292- subtype relationships that belong in
293- ` class_lookup::is_subtype_of_typed ` where all callers would benefit,
294- versus diagnostic-only leniency that must stay in the policy layer.
295- Do ** not** move any semantics in this task — file the candidates with
296- reasoning in ` docs/todo/type-inference.md ` .
297-
298- ** Acceptance.** ` cargo nextest run ` passes with no test edits (except
299- mechanical import-path updates); ` mod.rs ` shrinks to the orchestrator,
300- scheduling, and publishing logic; behavior is unchanged.
301-
302- ** Why it matters.** Diagnostics is the most actively developed area;
303- new collectors keep landing in a module whose entry file is 75%
304- unrelated scheduling and filtering code. The ` external/ ` split also
305- directly de-risks the scheduled D10 task (PHPMD proxy — a fifth
306- external-tool pipeline that gets a formulaic home instead of growing
307- ` mod.rs ` further).
308-
309- ---
310-
311228## 3. Move workspace init/indexing out of ` server.rs ` and ` references/mod.rs `
312229
313230** What to do.** ` src/server.rs ` (4,007 lines) ends with an
@@ -413,3 +330,62 @@ the rest makes the Backend's state graph legible and shrinks the
413330constructors.
414331
415332---
333+
334+ ## 5. Extract the shared type engine out of ` completion/ `
335+
336+ ** What to do.** Despite the name, ` src/completion/ ` houses the
337+ project's single type-resolution engine — the code that answers "what
338+ is the type of this expression here?" — consumed by diagnostics, hover,
339+ go-to-definition, and signature help, not just completion. The name
340+ misleads every new contributor and buries the most load-bearing
341+ subsystem inside a feature module. Move the engine into a top-level
342+ module (proposed ` src/type_engine/ ` ; name negotiable but ** not**
343+ ` resolve ` /` resolution ` , which collide with the existing ` resolution.rs `
344+ class/function * name* lookup), leaving ` completion/ ` with only
345+ completion-specific code. This is pure code motion — ` impl Backend `
346+ blocks can live in any file of the crate, so methods move without
347+ becoming free functions.
348+
349+ ** Engine subtrees to move** (whole directories): ` completion/resolver/ ` ,
350+ ` completion/call_resolution/ ` , ` completion/types/ ` , and the
351+ type-resolution half of ` completion/variable/ ` — ` resolution.rs ` ,
352+ ` forward_walk/ ` , ` rhs_resolution/ ` , ` foreach_resolution.rs ` ,
353+ ` closure_resolution.rs ` , ` class_string_resolution.rs ` ,
354+ ` raw_type_inference.rs ` . The root-level ` subject_expr.rs ` ,
355+ ` subject_extraction.rs ` , and ` subject_resolution.rs ` are part of the
356+ engine too — fold them in.
357+
358+ ** Stays in ` completion/ ` :** ` handler/ ` , ` context/ ` , ` phpdoc/ ` ,
359+ ` builder.rs ` , ` target.rs ` , ` array_shape.rs ` , ` array_callable.rs ` ,
360+ ` named_args.rs ` , ` use_edit.rs ` , ` eloquent_string.rs ` ,
361+ ` laravel_route_controller.rs ` , ` laravel_string_keys.rs ` , and the one
362+ non-clean split: ` completion/variable/completion.rs ` is variable * name*
363+ completion (scope collection for the completion list), not type
364+ resolution, so it stays while its sibling type-resolution files move.
365+
366+ ** Steps.**
367+ 1 . Create the new module and move the engine subtrees into it, updating
368+ ` mod ` /` use ` declarations.
369+ 2 . Split ` completion/variable/ ` : keep ` completion.rs ` (and any
370+ scope-collection helpers only it uses) under ` completion/ ` ; move the
371+ rest under ` <engine>/variable/ ` .
372+ 3 . Update imports crate-wide (` crate::completion::resolver::… ` →
373+ ` crate::type_engine::resolver::… ` , etc.). Add thin ` pub use `
374+ re-exports in ` completion/mod.rs ` only if they meaningfully shrink
375+ the diff; otherwise fix call sites directly.
376+ 4 . Update the module map and the "shared type engine lives under
377+ ` completion/ ` " note in ` docs/ARCHITECTURE.md ` , and the Project
378+ Structure landmark in ` AGENTS.md ` , to point at the new module.
379+
380+ ** Acceptance.** ` cargo nextest run ` passes with only mechanical
381+ import-path updates; behavior is unchanged; ` completion/ ` contains only
382+ completion features; the type engine has one obvious top-level home.
383+
384+ ** Why it matters.** Anti-pattern #6 in ` AGENTS.md ` ("do not build a
385+ second type-resolution path") depends on the engine being
386+ discoverable. While it hides under ` completion/ ` , contributors reach
387+ for a "simpler" local resolver instead of finding the shared one. This
388+ is the largest churn of the set (crate-wide import moves), so it goes
389+ last.
390+
391+ ---
0 commit comments