bynk_check/analysis.rs
1//! The project-level analysis entry point (P4.1, #1115): discovery
2//! (`bynk-project`) → parse → resolve → check, returning the `bynk-ide`-facing
3//! analogue of `bynk-emit`'s `ProjectAnalysis` — without ever emitting.
4//!
5//! `bynk-ide` is repointed at [`analyse_project`] as of P4.2 (#1122) —
6//! `bynk_emit::project::analyse_project_with` was no longer reachable from
7//! `bynk-ide` from that point on (it had no `bynk-emit` dependency left).
8//! This entry point is what every real caller uses; #1541 deleted
9//! `analyse_project_with` and its single-tree wrapper `analyse_project`
10//! outright once nothing but `bynk-check/tests/differential_analysis.rs`'s
11//! own comparison still called them — that fixture is now a golden test of
12//! this entry point alone, not a differential against the retired one.
13//!
14//! ## The residual gap
15//!
16//! This entry point was diagnostically faithful to `bynk-emit`'s
17//! `run_checks`'s `Mode::Analyse` arm **minus seven categories** of
18//! whole-project checking at P4.2 (recorded on the tracking issue's own
19//! scope-correction comments, not silently assumed). Categories 2, 3, 4 and 6
20//! closed at P5.0/P5.1/P5.2 (`design/tracks/semantics-in-the-checker.md` §6)
21//! — [`crate::project_model::phase_messages_bundles`]/
22//! [`crate::project_model::phase_locale_bundle_ambiguity`]/
23//! [`crate::project_model::phase_event_subscriptions`]/
24//! [`crate::project_model::phase_function_type_boundaries`] are now called
25//! from [`analyse_project`] at the same points `run_checks` calls them.
26//! Categories 1 and 5 closed at P5.3, structurally rather than observably —
27//! both were already unreachable from the editor before P4.2 even shipped,
28//! so porting them changed nothing observable. Category 7 closed at P5.4, the
29//! last of the seven and the one this doc comment's own author flagged as
30//! needing more care (§9 of the design doc) — see below. All seven
31//! categories are now closed:
32//!
33//! 1. ~~Schema-registry reconciliation~~ — **closed at P5.3**.
34//! [`crate::schema_registry::reconcile`] is now called from
35//! [`analyse_project`], right after [`crate::project_model::phase_validate_providers`]
36//! (the same relative point `run_checks` calls it). Still unreachable on
37//! this path — it only ever fires under `SchemaLock::On`, and this entry
38//! point has no on-disk lock concept at all, so it always reconciles
39//! against an empty registry, which every event baselines against
40//! silently — so relocating it changed nothing observable; it now simply
41//! originates in `bynk-check`, per R3.5.
42//! 2. ~~`messages` bundle validation~~ — **closed at P5.0**, see above.
43//! 3. ~~Locale bundle ambiguity~~ — **closed at P5.0**, see above.
44//! 4. ~~Event-subscription validation~~ — **closed at P5.1**, see above.
45//! 5. ~~Platform-lock enforcement~~ — **closed at P5.3**.
46//! [`crate::project_model::phase_platform_lock`] is now called from
47//! [`analyse_project`], right after the per-unit compose/check loop (the
48//! same relative point `run_checks` calls it, gated the same way on a
49//! clean error sink so far). Still unreachable on this path, for the same
50//! reason as before the relocation: `analyse_project` hardcodes
51//! `Platform::default()` (Cloudflare) and `BuildTarget::Bundle`, and
52//! `bynk.cloudflare` is the only platform-native unit that exists
53//! (`firstparty::platform_of`) — so `lock_violation` can never find a
54//! native platform disagreeing with the selected one, for any project, on
55//! this path. No fixture can observe this category regressing (or
56//! improving) because it never fired through this path to begin with, both
57//! before and after this relocation.
58//! 6. ~~Function-type-boundary checks~~ — **closed at P5.2**. Formerly reached,
59//! in `bynk-emit`, only through `phase_group`'s optional boundary-check
60//! hook (`Some` from `run_checks`, `None` here); the hook is gone —
61//! [`crate::project_model::phase_group`] now calls
62//! [`crate::project_model::phase_function_type_boundaries`] directly, at
63//! the exact point the hook used to fire, so both callers see it in the
64//! same diagnostic-ordering position as before.
65//! 7. ~~Test/integration-suite processing~~
66//! (`process_tests`/`process_integration_tests`) — **closed at P5.4**.
67//! Unlike categories 2-6, these run *unconditionally* in `run_checks`, in
68//! `Mode::Analyse` too, and push into the same shared error sink (`bynk-emit`'s own
69//! `check_project_reports_a_test_body_error_past_an_earlier_structural_error`
70//! pins a `bynk.types.let_annotation_mismatch` originating inside a
71//! `suite`/`test integration` body). The two functions were emission-coupled
72//! (`Document`, `RunnableTest`, `ImportExt`, `contracts`, a shared
73//! `emitted_barrels` set) deeply enough that P5.4 split them at the
74//! check/emit boundary rather than porting the whole thing: their checking
75//! half relocated to [`crate::test_suites::phase_test_bodies`]/
76//! [`crate::test_suites::phase_integration_bodies`], now called from
77//! [`analyse_project`] right after the per-unit compose/check loop (the
78//! same relative point `run_checks` calls the originals, unconditionally —
79//! unlike categories 2-6, neither is gated on a clean error sink), while
80//! emission itself stays in `bynk-emit::project::tests_emit`, which now
81//! calls the relocated checking phase too rather than duplicating it. Both
82//! functions still take `&mut RefSink`, so every binding edge inside a
83//! `.bynk` suite file is populated here again too — go-to-definition
84//! inside a test file works through this entry point once more.
85//!
86//! Emission itself is orthogonal rather than a gap: this entry point never
87//! emits, by construction (it has no `BuildTarget`/`ImportExt`/`contracts`
88//! concept at all), so there is no diagnostic-agreement question to ask of it.
89//!
90//! A fixture that exercises none of the seven categories above saw identical
91//! diagnostics from this entry point and from `analyse_project_with` while
92//! both existed — that is why `bynk-check/tests/differential_analysis.rs`'s
93//! clean/broken golden cases still avoid all seven (#1541 retired the
94//! comparison, not the fixture shape). A third case
95//! (`new_entry_point_reports_a_test_bodys_own_type_error`, renamed from
96//! `new_entry_point_omits_test_body_diagnostics` at #1541) pinned category
97//! 7's divergence directly; now that P5.4 closed it, that test pins parity
98//! with what `analyse_project_with` used to report instead (see its own doc
99//! comment).
100//!
101//! ## Two sites outside the seven-category accounting
102//!
103//! `bynk-check/src/analysis.rs`'s own seven categories were `run_checks`'s
104//! whole-project checks; two more registered diagnostics were still
105//! constructed in `bynk-emit` and outside that accounting, found and closed
106//! at P5.5 (`design/tracks/semantics-in-the-checker.md` §6, §9):
107//!
108//! - `bynk.project.schema_registry_corrupt` — a malformed on-disk
109//! `bynk.schema.lock`. [`crate::schema_registry::parse_or_diagnose`] now
110//! constructs it. Unreachable from this entry point, same reason as
111//! category 1: no on-disk lock concept exists here.
112//! - `bynk.secrets.computed_name` — see
113//! [`crate::project_model::phase_secrets_computed_name`]'s own doc. Unlike
114//! the seven categories (scoped and confirmed live gaps or confirmed
115//! gap-in-name-only by this settling pass), this one's reachability from
116//! *this* entry point was still open at settling time — §9 named it a risk
117//! rather than a scoped item. It resolved the same way categories 1 and 5
118//! did: gap-in-name-only, since `run_checks`'s own gate
119//! (`target == BuildTarget::Workers`) can never pass against this entry
120//! point's hardcoded `BuildTarget::Bundle`.
121
122use std::collections::{HashMap, HashSet};
123use std::path::PathBuf;
124use std::sync::Arc;
125
126use crate::check_pipeline::{check_file_core, prepare_unit_check_ctx, record_analyse_types};
127use crate::checker::Types;
128use crate::expr_types::{ExprTypeSink, FileExprTypes};
129use crate::firstparty::Platform;
130use crate::hints::{FileHints, HintSink};
131use crate::index::{ProjectIndex, RefSink};
132use crate::locals::{FileLocals, LocalsSink};
133use crate::project_model::{
134 self, ErrorSink, assemble_unit_info, collect_unit_methods, compose_unit_symbols,
135 merge_consumed_exports, normalize_service_defaults,
136};
137use crate::requirements::{FileRequirements, RequirementSink};
138use crate::symbols::{assemble_index, build_cross_context_info, combined_types_for};
139use bynk_project::{AttributedError, Roots, UnitKind};
140use bynk_syntax::ast::{AgentDecl, ServiceDecl, TypeDecl};
141
142/// #846: the per-unit slice of resolution the sequence-diagram classifier
143/// needs — see [`ProjectAnalysis::sequence_info`]. Moved verbatim (Decision
144/// C, #1115) from `bynk-emit/src/project/diagnostics.rs`.
145#[derive(Debug, Clone, Default)]
146pub struct ContextSequenceInfo {
147 pub cross_context: crate::resolver::CrossContextInfo,
148 pub agents: HashMap<String, AgentDecl>,
149}
150
151/// #855: the per-unit slice of resolution the wire-contract peek needs —
152/// see [`ProjectAnalysis::boundary_info`]. A sibling of
153/// [`ContextSequenceInfo`], not a field on it: that struct is named and
154/// documented for #846, and this is a separate retained table serving a
155/// separate query (hover/panel over a single handler's boundary, not the
156/// sequence-diagram classifier). Moved verbatim (Decision C, #1115).
157#[derive(Debug, Clone, Default)]
158pub struct ContextBoundaryInfo {
159 /// `combined_types_for`: the unit's own declared types plus the types of
160 /// every commons it `uses` — the same table `own_contract_hashes` hashes
161 /// through, so the peek's hash and the emitted `X-Bynk-Contract` constant
162 /// cannot disagree.
163 pub types: HashMap<String, Arc<TypeDecl>>,
164 pub services: HashMap<String, ServiceDecl>,
165 pub agents: HashMap<String, AgentDecl>,
166}
167
168/// v0.24: the analyse-mode result — every discovered file's analysed text
169/// snapshot (positions must convert against the text that was analysed, not
170/// a newer buffer) plus the attributed diagnostics. Moved verbatim (Decision
171/// C, #1115) from `bynk-emit/src/project/diagnostics.rs`; `bynk-emit`
172/// re-exports this type at its old path (`bynk_emit::project::ProjectAnalysis`)
173/// so `bynk-ide`'s existing destructuring needs no field-by-field rewrite.
174pub struct ProjectAnalysis {
175 /// `(project-relative source path, analysed text)` for every file read,
176 /// including clean files (the LSP needs them to clear diagnostics).
177 pub snapshots: Vec<(PathBuf, String)>,
178 pub errors: Vec<AttributedError>,
179 /// v0.25 (ADR 0053): the project-wide binding index. Empty when the
180 /// pipeline bails before resolution (discovery/parse failures).
181 pub index: ProjectIndex,
182 /// v0.27 (ADR 0056): per-file inferred-type inlay hints — `(binding-name
183 /// span, label)`, span-ordered, harvested from the checker's binding
184 /// sites. Empty for files the pipeline never type-checked.
185 pub hints: FileHints,
186 /// v0.30.2 (ADR 0063): per-file expression types — `(expr span, Ty)`,
187 /// captured on the Ok path (a file that checks clean), for `.`-member
188 /// completion's receiver typing. Empty for files with errors (the
189 /// clean-file ceiling) and for synthetic files.
190 pub expr_types: FileExprTypes,
191 /// T3.6b (R4.1): the intern table every `TyId` in `expr_types` resolves
192 /// against — one table shared across the whole analysis.
193 pub ty_intern: Arc<Types>,
194 /// v0.31 (ADR 0064): per-file local bindings with their scope ranges —
195 /// `let`/`let <-`, fn/handler/lambda params — for the scope-at-offset
196 /// query backing locals completion + navigation. Synthetic files muted.
197 pub locals: FileLocals,
198 /// v0.99: per-file capability-requirement ledger — every
199 /// capability-consuming site (direct call, store op), covered or not,
200 /// with its provenance. Empty for files the pipeline never type-checked,
201 /// and for synthetic/test files (muted).
202 pub requirements: FileRequirements,
203 /// Slice 6b (ADR 0095): qualified unit name → the project source file(s)
204 /// that comprise it, in discovery order. Excludes synthetic
205 /// (toolchain-injected) units; empty when the pipeline bails before the
206 /// checker.
207 pub unit_sources: HashMap<String, Vec<PathBuf>>,
208 /// #846: qualified context/adapter unit name → the cross-context and
209 /// agent tables needed to classify a handler call as a lifeline for the
210 /// sequence-diagram query. Only contexts/adapters have an entry; empty
211 /// when the pipeline bails before the checker.
212 pub sequence_info: HashMap<String, ContextSequenceInfo>,
213 /// #855: qualified context/adapter unit name → the combined type table
214 /// and per-context service/agent tables the wire-contract peek needs.
215 /// Only contexts/adapters have an entry; empty when the pipeline bails
216 /// before the checker.
217 pub boundary_info: HashMap<String, ContextBoundaryInfo>,
218 /// #848: qualified unit name → its doc-comment intra-doc-link search
219 /// order — itself first, then its `uses` targets, then its `consumes`
220 /// targets, in that order. Empty when the pipeline bails before the
221 /// checker.
222 pub doc_scope: HashMap<String, Vec<String>>,
223}
224
225/// The `ProjectAnalysis` shape for every "bailed before the checker ran"
226/// exit — discovery/file-conflict/parse failures. Factored out so the three
227/// early-return sites in [`analyse_project`] agree by construction, the same
228/// role `RunChecks::Bailed` plays in `bynk-emit`'s `run_checks`.
229fn bailed(
230 errors: ErrorSink,
231 snapshots: Vec<(PathBuf, String)>,
232 mut hints: HintSink,
233 mut locals: LocalsSink,
234 mut exprs: ExprTypeSink,
235 mut requirements: RequirementSink,
236 tys: &Arc<Types>,
237) -> ProjectAnalysis {
238 ProjectAnalysis {
239 snapshots,
240 // ADR 0117: the LSP renders warnings alongside errors (severity is
241 // applied downstream), so analyse surfaces the full diagnostic list.
242 errors: errors.into_all(),
243 index: ProjectIndex::default(),
244 hints: hints.take_files(),
245 locals: locals.take_files(),
246 expr_types: exprs.take_files(),
247 ty_intern: Arc::clone(tys),
248 requirements: requirements.take_files(),
249 unit_sources: HashMap::new(),
250 sequence_info: HashMap::new(),
251 boundary_info: HashMap::new(),
252 doc_scope: HashMap::new(),
253 }
254}
255
256/// The `bynk-check`-native discovery→parse→resolve→check entry point (P4.1,
257/// #1115) — see this module's own doc comment for the residual-gap
258/// accounting against `bynk-emit`'s now-deleted (#1541) `analyse_project_with`.
259/// Mirrored its call shape exactly where the two overlapped:
260/// `BuildTarget::Bundle`-equivalent (this entry point has no build target at
261/// all — it never emits), `Platform::default()`, no schema-registry lock.
262///
263/// Identity is project-relative (ADR 0198): a file's `source_path` here is
264/// unique across `include` roots, same as `analyse_project_with` used to be.
265pub fn analyse_project(roots: &Roots, overlay: &HashMap<PathBuf, String>) -> ProjectAnalysis {
266 let tys = &Arc::new(Types::new());
267 let trees = roots.trees();
268 let excludes = roots.excludes();
269
270 let mut errors = ErrorSink::new();
271 let mut refs = RefSink::new();
272 let mut hints = HintSink::new();
273 let mut locals = LocalsSink::new();
274 let mut requirements = RequirementSink::new();
275 let mut exprs = ExprTypeSink::new();
276 let mut snapshots: Vec<(PathBuf, String)> = Vec::new();
277
278 // -- 1. Discovery. --
279 let file_lists = match project_model::phase_discovery(&trees, &excludes, &mut errors) {
280 Ok(files) => files,
281 Err(()) => return bailed(errors, snapshots, hints, locals, exprs, requirements, tys),
282 };
283 if project_model::check_discovered_files(&trees, &file_lists, &mut errors).is_err() {
284 return bailed(errors, snapshots, hints, locals, exprs, requirements, tys);
285 }
286
287 // -- 2. Parse. --
288 let (mut parsed, consumes_bynk, consumes_cloudflare, broken) =
289 match project_model::phase_parse(&trees, &file_lists, overlay, &mut errors, &mut snapshots)
290 {
291 Ok(out) => out,
292 Err(()) => return bailed(errors, snapshots, hints, locals, exprs, requirements, tys),
293 };
294
295 // -- 2b. Normalize service-level `by`/`given` defaults (v0.155). --
296 normalize_service_defaults(&mut parsed);
297 let parsed = parsed;
298
299 // -- 3. Group. P5.2: closes category 6 of this module's own residual-gap
300 // accounting (see doc comment above) — `phase_group` now also
301 // confines function types to non-boundary positions directly, at
302 // the point its old optional hook used to fire. --
303 let (groups, kinds, test_groups, integration_groups, _adapter_bindings, _npm_deps) =
304 project_model::phase_group(
305 &parsed,
306 &trees,
307 Platform::default(),
308 consumes_bynk,
309 consumes_cloudflare,
310 overlay,
311 &mut errors,
312 );
313
314 // -- 4. Per-unit combined symbol tables. --
315 let unit_tables = project_model::phase_symbol_tables(&groups, &kinds, &parsed, &mut errors);
316
317 // -- 5. `uses` resolution. --
318 let unit_uses =
319 project_model::phase_resolve_uses(&groups, &kinds, &parsed, &unit_tables, &mut errors);
320
321 // -- 5b. `consumes` resolution. --
322 let (unit_consumes, unit_flattened) = project_model::phase_resolve_consumes(
323 &groups,
324 &kinds,
325 &parsed,
326 &unit_tables,
327 &mut errors,
328 &mut refs,
329 );
330 let mut unit_tables = unit_tables;
331 crate::symbols::record_flattened_caps(&mut unit_tables, &unit_flattened);
332 let unit_tables = unit_tables;
333
334 // -- 5b'. `consumes` aliases. --
335 let unit_consumes_aliases =
336 project_model::phase_consumes_aliases(&groups, &kinds, &parsed, &unit_tables, &mut errors);
337
338 // -- 5b''. v0.173 (ADR 0196 D1), P5.5 (`design/tracks/semantics-in-the-checker.md`
339 // §6, §9): warn where a `bynk.Secrets` read names its secret with
340 // a computed expression — closes the "ninth gap" that §9 flagged
341 // as unresolved risk rather than a scoped relocation. Mirrors
342 // `run_checks`'s own call at the same relative point. Gated on
343 // the Workers target, same as `run_checks`; this entry point
344 // hardcodes `BuildTarget::Bundle` (inherited from the
345 // now-deleted `analyse_project_with`'s own hardcoding, see this
346 // function's doc comment), so the call
347 // closes the category structurally (R3.5 — the diagnostic now
348 // originates in `bynk-check`), not observably, the same as
349 // categories 1 and 5. --
350 project_model::phase_secrets_computed_name(
351 project_model::BuildTarget::Bundle,
352 &parsed,
353 &groups,
354 &kinds,
355 &unit_flattened,
356 &mut errors,
357 );
358
359 // -- 5c. `consumes` cycles. --
360 project_model::phase_detect_consumes_cycles(&groups, &parsed, &unit_consumes, &mut errors);
361
362 // -- 6. `uses` name-conflict detection. --
363 project_model::phase_uses_name_conflicts(
364 &unit_uses,
365 &unit_tables,
366 &parsed,
367 &groups,
368 &mut errors,
369 );
370
371 // -- 6a'. message-bundles slice 1 (#859): messages-block legality,
372 // @reference cardinality, within-block duplicate codes, and the
373 // `uses bynk.locale` dependency. P5.0: closes category 2 of this
374 // module's own residual-gap accounting. --
375 project_model::phase_messages_bundles(&parsed, &groups, &kinds, &unit_uses, &mut errors);
376
377 // -- 6a''. Locale capability track, slice 2 (#882): a context reaching
378 // two or more message-bundle commons while consuming `Locale`
379 // has no single bundle to negotiate against. P5.0: closes
380 // category 3. --
381 project_model::phase_locale_bundle_ambiguity(
382 &parsed,
383 &groups,
384 &kinds,
385 &unit_uses,
386 &unit_flattened,
387 &mut errors,
388 );
389
390 // -- 6a'''. Events track, slice 0 (spine #936): a `from Events(E)`
391 // subscription must name a real, declared event — needs
392 // `unit_tables` + `unit_consumes` together, so it runs here
393 // rather than in the per-context `check_service_protocols`.
394 // P5.1: closes category 4. --
395 project_model::phase_event_subscriptions(
396 &parsed,
397 &groups,
398 &kinds,
399 &unit_tables,
400 &unit_consumes,
401 &unit_uses,
402 &mut errors,
403 );
404
405 // -- 6b. Type exports. --
406 let exports_visibility = project_model::phase_validate_type_exports(
407 &groups,
408 &kinds,
409 &parsed,
410 &unit_tables,
411 &mut errors,
412 &mut refs,
413 );
414
415 // -- 6b'. Capability exports. --
416 project_model::phase_validate_capability_exports(
417 &groups,
418 &kinds,
419 &parsed,
420 &unit_tables,
421 &mut errors,
422 &mut refs,
423 );
424
425 // -- 6c. Provider matching. --
426 project_model::phase_validate_providers(
427 &unit_tables,
428 &groups,
429 &parsed,
430 &broken,
431 &mut errors,
432 tys,
433 );
434
435 // -- 6d. Events track, slice 3c (#980): schema-registry reconciliation.
436 // P5.3: closes category 1 of this module's own residual-gap
437 // accounting — `crate::schema_registry::reconcile` now runs here
438 // too, at the same point `run_checks` calls it. This entry point
439 // carries no on-disk schema lock (inherited from the now-deleted
440 // `analyse_project_with`'s own hardcoded `SchemaLock::Off`), so
441 // every event baselines
442 // silently against an empty registry — no diagnostic is reachable
443 // through this call, same as before the relocation.
444 //
445 // Cost (review #1133): this is a full sweep over every event in
446 // every unit on every analysis — `snapshot` clones each field name
447 // and runs `canon_type` per field, plus a sort and two `HashMap`
448 // inserts per event — for a diagnostic that can provably never
449 // fire on this path. R3.5 wants the check to *originate* in
450 // `bynk-check`; it does not require paying for it on the editor's
451 // hot path. Not measured against a large project before this
452 // landed — worth profiling (or skipping the call under a
453 // `unit_tables`-is-empty-of-events fast path) if LSP latency on a
454 // big project ever traces back here. --
455 let mut schema_errors: Vec<bynk_syntax::error::CompileError> = Vec::new();
456 crate::schema_registry::reconcile(
457 &bynk_project::schema_registry::SchemaRegistry::new(),
458 &unit_tables,
459 &mut schema_errors,
460 );
461 errors.extend_for(None, schema_errors);
462
463 // No bail gate: this entry point never bails after discovery (mirrors
464 // `Mode::Analyse` — independent unit groups resolve/check past another
465 // group's errors).
466
467 // -- 7. Per-unit file index. --
468 let unit_file_index = project_model::phase_file_index(&groups, &parsed);
469
470 // -- 7b. Assemble per-unit info. --
471 let unit_info = assemble_unit_info(
472 &groups,
473 &kinds,
474 &unit_tables,
475 &unit_uses,
476 &unit_consumes,
477 &unit_flattened,
478 &unit_consumes_aliases,
479 &exports_visibility,
480 &unit_file_index,
481 );
482
483 // -- 8. For each unit, compose the symbol space and resolve+check every
484 // file. Test/integration processing is the residual gap after this
485 // loop — see this module's own doc comment. Category 5
486 // (platform-lock) closes right after, below the loop, at the same
487 // relative point `run_checks` calls it (after its own per-unit
488 // checking, gated on a clean error sink so far). --
489 // #1702 review: `uses` targets first, so a generic callee's compared type
490 // parameters are known before an importer's calls are checked.
491 crate::checker::reset_compared_cache();
492 for name in project_model::uses_first_order(unit_info.keys(), &unit_uses) {
493 let info = &unit_info[name];
494 let kind = info.kind;
495 let indices = info.files.as_slice();
496 let local_table = &info.table;
497 let group_error_baseline = errors.len();
498
499 let (
500 mut combined_types,
501 combined_fns,
502 mut combined_methods,
503 mut imported_from,
504 mut imported_from_kind,
505 ) = compose_unit_symbols(name, local_table, &unit_info);
506 let consumed_types = merge_consumed_exports(
507 name,
508 &parsed,
509 &unit_info,
510 &mut combined_types,
511 &mut combined_methods,
512 &mut imported_from,
513 &mut imported_from_kind,
514 &mut errors,
515 );
516
517 if errors.len() > group_error_baseline {
518 continue;
519 }
520
521 let local_names: HashSet<String> = local_table.types.keys().cloned().collect();
522 let local_methods_for_type = collect_unit_methods(indices, &parsed);
523 let ctx = prepare_unit_check_ctx(
524 name,
525 kind,
526 &broken,
527 &unit_info,
528 &combined_types,
529 &imported_from_kind,
530 );
531
532 for &i in indices {
533 let pf = &parsed[i];
534 if let Some(crate::check_pipeline::FileCheckResult { typed, .. }) = check_file_core(
535 name,
536 kind,
537 pf,
538 &unit_info,
539 &combined_types,
540 &combined_fns,
541 &combined_methods,
542 &local_names,
543 &local_methods_for_type,
544 &consumed_types,
545 &imported_from,
546 &ctx,
547 &mut errors,
548 &mut refs,
549 &mut hints,
550 &mut locals,
551 &mut exprs,
552 &mut requirements,
553 tys,
554 ) {
555 // This entry point never emits — every clean file's exit is
556 // the Analyse-mode one (record best-effort/final types, move
557 // on to the next file). Mirrors `check_unit_files`'s own
558 // `mode == Mode::Analyse` clean-path branch.
559 record_analyse_types(
560 &mut exprs,
561 &pf.identity_path(),
562 pf.is_synthetic(),
563 &typed.expr_types,
564 );
565 }
566 }
567 }
568
569 // P5.4 (`design/tracks/semantics-in-the-checker.md` §6): test/
570 // integration-suite processing — closes category 7 of this module's own
571 // residual-gap accounting, the last of the seven. Mirrors `run_checks`'s
572 // own call shape: both run unconditionally (unlike categories 2-6 above,
573 // `run_checks` never gates these on a clean error sink), right after its
574 // own per-unit `check_unit_files` loop and before platform-lock — the
575 // same relative point this loop just occupied. Neither function's
576 // returned "ready for emission" map is needed here — this entry point
577 // never emits — only the diagnostic/`RefSink` side effects matter, so
578 // both are discarded. Diagnostics are file-unattributed (`extend_for(None,
579 // ...)`), matching `run_checks`'s own `#696`-noted gap (attributing them
580 // means threading a file through many internal push sites — out of scope
581 // here, same as there).
582 let mut test_errors: Vec<bynk_syntax::error::CompileError> = Vec::new();
583 let _ready_tests = crate::test_suites::phase_test_bodies(
584 &test_groups,
585 &parsed,
586 &kinds,
587 &unit_tables,
588 &exports_visibility,
589 &unit_consumes,
590 &unit_consumes_aliases,
591 &unit_uses,
592 &mut test_errors,
593 &mut refs,
594 tys,
595 );
596 errors.extend_attributed_by_span(&parsed, test_errors);
597
598 let mut integration_errors: Vec<bynk_syntax::error::CompileError> = Vec::new();
599 let _ready_integration = crate::test_suites::phase_integration_bodies(
600 &integration_groups,
601 &parsed,
602 &unit_tables,
603 &unit_consumes,
604 &unit_consumes_aliases,
605 &unit_uses,
606 &mut integration_errors,
607 &mut refs,
608 tys,
609 );
610 errors.extend_attributed_by_span(&parsed, integration_errors);
611
612 // v0.19 (decisions 0017/0024), P5.3: platform-lock enforcement — closes
613 // category 5 of this module's own residual-gap accounting. Inherited from
614 // the now-deleted `analyse_project_with`'s own hardcoded
615 // `Platform::default()` (Cloudflare) and `BuildTarget::Bundle`:
616 // `bynk.cloudflare` is the only
617 // platform-native unit that exists, and it matches the default
618 // selection, so `lock_violation` can never fire here, for any project
619 // (see `bynk-lsp/tests/analysis_residual_gap.rs`'s
620 // `platform_lock_diagnostic_stays_absent`) — this call closes the
621 // category structurally (R3.5), not observably.
622 //
623 // Cost (review #1133): a full provider-closure walk per context, same
624 // shape as `run_checks`'s own gate, for a diagnostic that is provably
625 // dead here. `collect_given_closure` is also unmemoised — a
626 // diamond-shaped provider graph re-walks shared subtrees, so this is
627 // worse than linear in the closure's depth, not just wasted. Same
628 // trade-off and same "worth profiling if it ever shows up" note as the
629 // schema-registry reconciliation call above.
630 if errors.is_empty() {
631 project_model::phase_platform_lock(
632 project_model::BuildTarget::Bundle,
633 Platform::default(),
634 &parsed,
635 &groups,
636 &kinds,
637 &unit_tables,
638 &unit_consumes,
639 &unit_consumes_aliases,
640 &unit_flattened,
641 &mut errors,
642 );
643 }
644
645 // -- Assemble the `ProjectAnalysis`. Mirrored the now-deleted
646 // `analyse_project_with`'s own `RunChecks::Checked` arm exactly. --
647 let index = assemble_index(
648 &parsed,
649 &unit_uses,
650 &unit_consumes,
651 std::mem::take(&mut refs),
652 );
653
654 let mut unit_sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
655 for pf in &parsed {
656 if pf.is_synthetic() {
657 continue;
658 }
659 unit_sources
660 .entry(pf.unit().name().joined())
661 .or_default()
662 .push(pf.identity_path());
663 }
664
665 let mut sequence_info: HashMap<String, ContextSequenceInfo> = HashMap::new();
666 let mut boundary_info: HashMap<String, ContextBoundaryInfo> = HashMap::new();
667 for (name, kind) in &kinds {
668 if !matches!(kind, UnitKind::Context | UnitKind::Adapter) {
669 continue;
670 }
671 let Some(table) = unit_tables.get(name) else {
672 continue;
673 };
674 let mut cross_context = build_cross_context_info(
675 name,
676 &unit_consumes,
677 &unit_consumes_aliases,
678 &unit_uses,
679 &unit_tables,
680 );
681 cross_context.flattened_caps = unit_flattened.get(name).cloned().unwrap_or_default();
682 let agents: HashMap<String, AgentDecl> = table.agents.clone();
683 sequence_info.insert(
684 name.clone(),
685 ContextSequenceInfo {
686 cross_context,
687 agents: agents.clone(),
688 },
689 );
690 boundary_info.insert(
691 name.clone(),
692 ContextBoundaryInfo {
693 types: combined_types_for(name, &unit_tables, &unit_uses),
694 services: table.services.clone(),
695 agents,
696 },
697 );
698 }
699
700 let mut doc_scope: HashMap<String, Vec<String>> = HashMap::new();
701 for name in unit_sources.keys() {
702 let mut scope = vec![name.clone()];
703 scope.extend(unit_uses.get(name).cloned().unwrap_or_default());
704 scope.extend(unit_consumes.get(name).cloned().unwrap_or_default());
705 doc_scope.insert(name.clone(), scope);
706 }
707
708 ProjectAnalysis {
709 snapshots,
710 errors: errors.into_all(),
711 index,
712 hints: hints.take_files(),
713 locals: locals.take_files(),
714 expr_types: exprs.take_files(),
715 ty_intern: Arc::clone(tys),
716 requirements: requirements.take_files(),
717 unit_sources,
718 sequence_info,
719 boundary_info,
720 doc_scope,
721 }
722}