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