Skip to main content

presolve_compiler/
lib.rs

1//! Core compiler data structures for the first `Presolve` learning slice.
2//!
3//! This crate deliberately does **not** parse TSX yet. It records a source summary,
4//! spans, obvious declarations, and diagnostics. That gives the project a stable
5//! place to learn compiler fundamentals before choosing a real parser backend.
6
7pub mod action_authority;
8pub mod action_field_lowering;
9pub mod application_publication;
10pub mod application_semantic_model;
11pub mod asm_validation;
12pub mod authored_semantics;
13pub mod binding_table;
14pub mod capability_projection;
15pub mod capture_escape;
16pub mod codec_protocol;
17pub mod compilation_unit;
18pub mod compiler_pass;
19pub mod component_composition;
20pub mod component_diagnostics;
21pub mod component_graph;
22pub mod component_inheritance_lowering;
23pub mod component_initialization;
24pub mod component_instance;
25pub mod component_instance_scope;
26pub mod component_invocation;
27pub mod component_ir;
28pub mod component_ir_optimization;
29pub mod component_scope;
30pub mod composition_typing;
31pub mod computed_getter_lowering;
32pub mod computed_instance_slots;
33pub mod computed_projection;
34pub mod computed_value;
35pub mod consumer;
36pub mod context;
37pub mod context_declaration_candidate;
38pub mod context_dependency;
39mod context_designator;
40pub mod context_diagnostics;
41pub mod context_evaluation;
42pub mod context_inspection;
43pub mod context_lifetime;
44pub mod context_ownership;
45pub mod context_projection;
46pub mod context_resolution;
47pub mod context_resume;
48pub mod context_resume_projection;
49pub mod context_typing;
50pub mod context_update;
51pub mod control_flow;
52pub mod document_template;
53pub mod effect;
54pub mod effect_capability;
55pub mod effect_diagnostics;
56pub mod effect_field_lowering;
57pub mod effect_inspection;
58pub mod effect_projection;
59pub mod effect_resume;
60pub mod environment_input;
61pub mod environment_ownership;
62pub mod environment_publication;
63pub mod environment_read_lowering;
64pub mod environment_read_ownership;
65pub mod explain;
66pub mod expression_graph;
67pub mod file_route_publication;
68pub mod form;
69pub mod form_binding;
70pub mod form_definition_lowering;
71pub mod form_diagnostics;
72pub mod form_field;
73pub mod form_field_definition_lowering;
74pub mod form_inspection;
75pub mod form_ir;
76pub mod form_ir_optimization;
77pub mod form_ownership;
78pub mod form_projection;
79pub mod form_reset;
80pub mod form_serialization;
81pub mod form_submission;
82pub mod form_submission_host;
83pub mod form_submission_projection;
84pub mod form_tracking;
85pub mod form_validation;
86pub mod form_validation_definition_lowering;
87pub mod form_validation_plan;
88pub mod function_summary;
89pub mod html_codegen;
90pub mod incremental_projection;
91pub mod instance_context;
92pub mod intermediate_representation;
93#[cfg(any(test, all(feature = "wasm", target_arch = "wasm32")))]
94mod language_service;
95pub mod layout_composition;
96pub mod layout_graph;
97pub mod lazy_action_chunks;
98pub mod legacy_decorator_lowering;
99pub mod metaframework_handoff;
100pub mod model;
101pub mod module_graph;
102pub mod opaque;
103pub mod ordinary_html_codegen;
104pub mod ordinary_template_instance;
105pub mod ordinary_template_integrity;
106pub mod page_codegen;
107pub mod persistent_cache;
108pub mod platform;
109pub mod production_audit;
110pub mod production_bootstrap;
111pub mod production_chunk_graph;
112pub mod production_cleanup;
113pub mod production_constant_pool;
114pub mod production_deduplication;
115pub mod production_diagnostics;
116pub mod production_elimination;
117pub mod production_module_emitter;
118pub mod production_optimization;
119pub mod production_reachability;
120pub mod production_reports;
121pub mod production_runtime_artifact;
122pub mod production_scheduler;
123pub mod production_validation;
124pub mod project_discovery;
125pub mod provider;
126pub mod purity_effect;
127pub mod resource;
128pub mod resource_projection;
129pub mod resume_activation;
130pub mod resume_anchor;
131pub mod resume_boot;
132pub mod resume_boundary;
133pub mod resume_capture;
134pub mod resume_chunk;
135pub mod resume_diagnostics;
136pub mod resume_explain;
137pub mod resume_identity;
138pub mod resume_instance;
139pub mod resume_liveness;
140pub mod resume_manifest;
141pub mod resume_plan;
142pub mod resume_restore;
143pub mod resume_schema;
144pub mod route_graph;
145pub mod route_loader;
146pub mod route_loader_projection;
147pub mod route_metadata;
148pub mod route_server_action;
149pub mod runtime_codegen;
150pub mod runtime_component;
151pub mod runtime_component_artifact;
152pub mod runtime_computed;
153pub mod runtime_computed_artifact;
154pub mod runtime_context;
155pub mod runtime_context_artifact;
156pub mod runtime_effect;
157pub mod runtime_effect_artifact;
158pub mod runtime_effect_instance;
159pub mod runtime_form_artifact;
160pub mod runtime_form_registry;
161pub mod runtime_opaque_artifact;
162pub mod runtime_package_invocation_artifact;
163pub mod runtime_resource_artifact;
164pub mod semantic_capability;
165pub mod semantic_graph;
166pub mod semantic_id;
167pub mod semantic_package;
168pub mod semantic_package_runtime;
169pub mod semantic_provenance;
170pub mod semantic_reference;
171pub mod semantic_type;
172pub mod server_action_projection;
173pub mod service;
174pub mod shared_chunk_candidate;
175pub mod slot;
176pub mod slot_binding;
177pub mod slot_content;
178pub mod slot_field_lowering;
179pub mod slot_projection;
180pub mod state_initializer_lowering;
181pub mod state_instance_storage;
182pub mod state_projection;
183pub mod structural_component;
184pub mod structural_occurrence_identity;
185pub mod summarize;
186pub mod symbol_table;
187pub mod template_graph;
188pub mod template_manifest;
189pub mod template_semantics;
190pub mod tooling_products;
191pub mod tooling_reader;
192pub mod tooling_schema;
193pub mod tsx_binding_projection;
194pub mod v2_authoring_lowering;
195pub mod v2_authority_request;
196pub mod v2_authority_response;
197pub mod validation_projection;
198pub use tooling_products::{
199    build_tooling_artifact_graph_v1, build_tooling_build_trace_v1,
200    build_tooling_compile_cost_report_v1, decode_tooling_artifact_graph_v1,
201    decode_tooling_build_trace_v1, decode_tooling_compile_cost_report_v1,
202    decode_tooling_query_snapshot_v1, tooling_artifact_graph_json_v1, tooling_build_trace_json_v1,
203    tooling_compile_cost_report_json_v1, tooling_query_snapshot_json_v1, ToolingArtifactGraphV1,
204    ToolingBuildTraceStageV1, ToolingBuildTraceV1, ToolingCompileCostReportV1,
205    ToolingProductValidationErrorV1, ToolingQueryDiagnosticSecondaryV1,
206    ToolingQueryDiagnosticSeverityV1, ToolingQueryDiagnosticV1, ToolingQueryRangeV1,
207    ToolingQueryReferenceKindV1, ToolingQueryReferenceV1, ToolingQuerySemanticKindV1,
208    ToolingQuerySemanticRecordV1, ToolingQuerySnapshotSourceUnitV1, ToolingQuerySnapshotV1,
209    ToolingTraceIdentityV1, ToolingTraceOutcomeV1, ToolingTraceStageKindV1,
210    ARTIFACT_GRAPH_TOOLING_SCHEMA_V1, BUILD_TRACE_TOOLING_SCHEMA_V1,
211    COMPILE_COST_TOOLING_SCHEMA_V1, QUERY_SNAPSHOT_TOOLING_SCHEMA_V1,
212};
213pub mod watch;
214pub mod workspace;
215
216#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
217mod wasm_binding;
218
219pub use action_authority::{
220    build_action_authority_v1, ActionAdmissionV1, ActionAuthorityV1, ActionCaptureCoverageV1,
221    ActionEnvironmentV1, ActionFactV1, ActionRecordV1, ACTION_AUTHORITY_SCHEMA_VERSION,
222};
223pub use action_field_lowering::{
224    action_field_sites_v1, lower_action_fields_v1, ActionFieldLoweringErrorV1,
225    ActionFieldLoweringV1, ActionFieldSiteV1, ResolvedActionFieldV1,
226    ResolvedTerminalPackageInvocationV1,
227};
228pub use application_publication::{
229    application_publication_manifest_json_v1, build_application_publication_product_from_asm_v1,
230    build_application_publication_product_v1, validate_application_publication_request_v1,
231    ApplicationPublicationArtifactV1, ApplicationPublicationErrorV1,
232    ApplicationPublicationManifestV1, ApplicationPublicationProductV1,
233    ApplicationPublicationProfileV1, ApplicationPublicationRequestErrorV1,
234    ApplicationPublicationRequestV1, ApplicationPublicationSourceV1,
235    ValidatedApplicationPublicationRequestV1, APPLICATION_PUBLICATION_COMPILER_CONTRACT_V1,
236    APPLICATION_PUBLICATION_MANIFEST_SCHEMA_VERSION,
237};
238pub use application_semantic_model::{
239    build_application_semantic_model, build_application_semantic_model_for_unit,
240    build_application_semantic_model_for_unit_with_packages,
241    build_application_semantic_model_from_component_graph,
242    build_file_route_application_semantic_model_for_route_with_packages,
243    build_file_route_application_semantic_model_for_route_with_packages_and_v2_authoring,
244    build_file_route_application_semantic_model_for_unit_with_packages,
245    build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring,
246    ApplicationSemanticModel, FileRouteApplicationModelErrorV1, SemanticEntity, SemanticEntityKind,
247};
248pub use asm_validation::{validate_application_semantic_model, AsmValidationDiagnostic};
249pub use authored_semantics::{
250    compose_authored_semantics_v1, normalize_authored_semantics_v1,
251    AuthoredSemanticCandidateKindV1, AuthoredSemanticCompositionErrorV1,
252    AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1,
253    CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredDeclarationV1,
254    CanonicalAuthoredSemanticModelV1, CanonicalIntrinsicKindV1, DerivedAuthoredEvidenceV2,
255    ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
256    CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
257};
258pub use binding_table::{
259    build_binding_table, build_binding_table_with_packages, BindingDiagnostic, BindingTable,
260    ExportBinding, ImportBinding, ImportBindingTarget, ModuleBindingTable,
261};
262pub use capture_escape::{
263    build_capture_escape_graph_v1, CaptureEscapeCoverageV1, CaptureEscapeErrorV1,
264    CaptureEscapeFactV1, CaptureEscapeFactsV1, CaptureEscapeGraphV1, CaptureEscapeKindV1,
265    FunctionCaptureEscapeV1, ResumeCaptureAdmissionV1, CAPTURE_ESCAPE_SCHEMA_VERSION,
266};
267pub use codec_protocol::{
268    build_codec_protocol_v1, CodecBehaviorV1, CodecClassificationV1, CodecClassificationsV1,
269    CodecDeclarationV1, CodecEnvironmentV1, CodecFailureBehaviorV1,
270    CodecProtocolDiagnosticReasonV1, CodecProtocolDiagnosticV1, CodecProtocolRecordV1,
271    CodecProtocolV1, CodecRepresentationV1, CODEC_PROTOCOL_SCHEMA_VERSION,
272};
273pub use compilation_unit::CompilationUnit;
274pub use compiler_pass::{
275    fold_component_graph, AnalysisPass, ConstantEvaluation, ConstantEvaluationPass,
276    ConstantFoldingPass, DependencyAnalysis, DependencyAnalysisPass, ImmutableAsmPass,
277};
278pub use component_composition::{
279    analyze_component_composition, ComponentCompositionAnalysis, ComponentCompositionCycle,
280};
281pub use component_diagnostics::{
282    collect_component_diagnostics, ComponentDiagnosticContract, COMPONENT_DIAGNOSTIC_CONTRACTS,
283};
284pub use component_graph::{
285    build_component_graph, build_component_graph_for_module, build_v2_component_graph_for_module,
286    is_valid_opaque_action_fact, ActionEndpoint, ArithmeticEvaluationError, ArithmeticExpression,
287    ArithmeticExpressionKind, ArithmeticOperator, AuthoredComponentHeritage,
288    AuthoredContextDeclarationCandidate, AuthoredDeclarationKind, AuthoredOpaqueActionFact,
289    AuthoredRouteLoaderDeclarationFact, AuthoredServerActionFact, AuthoredSlotDeclarationCandidate,
290    AuthoredStandardSchemaValidationFact, AuthoredSubmissionDeclarationFact,
291    AuthoredTerminalPackageInvocationFact, AuthoredValidationRuleArgument,
292    AuthoredValidationRuleArgumentKind, AuthoredValidationRuleDeclarationFact,
293    AuthoredValidationRuleExpression, AuthoredValidationRuleExpressionKind, ComparisonOperator,
294    ComponentAction, ComponentDiagnostic, ComponentDiagnosticSeverity, ComponentEffectField,
295    ComponentGraph, ComponentMethod, ComponentNode, ComputedExpression, ComputedExpressionKind,
296    ConstantEvaluationError, ConstantExpression, ConstantExpressionKind, ConsumerDeclaration,
297    ContextDeclaration, ContextDeclarationCandidateKind, ContextDeclarationViolation,
298    ContextDesignator, DeclaredStateType, DeclaredStateTypeKind, DiagnosticSecondaryLabel,
299    EffectBodySyntax, EffectCleanupSyntax, EffectExpression, EffectExpressionKind,
300    EffectStatementSyntax, EffectStatementSyntaxKind, FormDeclarationCandidate,
301    FormDeclarationStatus, FormDeclarationViolation, FormDesignatorFact,
302    FormFieldDeclarationCandidate, FormFieldDeclarationViolation, LogicalOperator, MethodCall,
303    MethodLocalVariable, MethodParameter, RenderAttribute, RenderAttributeValue, RenderChild,
304    RenderEventHandler, RenderFragment, RenderList, RenderModel, SerializableValue,
305    SlotDeclaration, SlotDeclarationViolation, SlotKind, StateField, StateOperation,
306    UnsupportedEffectStatementKind, UnsupportedFormDesignatorFact,
307};
308pub use component_inheritance_lowering::{
309    component_inheritance_sites_v1, lower_component_inheritance_v1,
310    ComponentInheritanceLoweringErrorV1, ComponentInheritanceLoweringV1,
311    ComponentInheritanceSiteV1, ResolvedComponentInheritanceV1,
312};
313pub use component_initialization::{
314    plan_component_initialization, ComponentInitializationPlan, ComponentInstanceBatch,
315    SlotBindingBatch,
316};
317pub use component_instance::{
318    plan_component_instances, plan_component_instances_with_virtual_invocations,
319    structural_template_entity_for_region, BlockedComponentInstancePlan,
320    BlockedComponentInstanceReason, ComponentBuildRoot, ComponentBuildRootKind, ComponentInstance,
321    ComponentInstancePlan, ComponentInstanceStatus,
322};
323pub use component_instance_scope::{
324    build_component_instance_scope_graph, validate_component_instance_scope_graph,
325    ComponentInstanceScopeDiagnostic, ComponentInstanceScopeGraph, ComponentInstanceScopeNode,
326    ComponentInstanceScopeViolation,
327};
328pub use component_invocation::{
329    collect_component_invocations, ComponentInvocationEntity, ComponentInvocationResolutionStatus,
330};
331pub use component_ir::{
332    lower_component_ir, validate_component_ir, ComponentIrInstruction, ComponentIrOperation,
333    ComponentIrReport,
334};
335pub use component_ir_optimization::{
336    optimize_component_ir, validate_optimized_component_ir, OptimizedComponentIrReport,
337};
338pub use component_scope::{ComponentScopeDiagnostic, ComponentScopeGraph};
339pub use composition_typing::{
340    collect_composition_type_products, ComponentInvocationTypeRecord, CompositionCompatibility,
341    CompositionTypeProducts, InstanceContextBindingTypeRecord, SlotBindingTypeRecord,
342};
343pub use computed_getter_lowering::{
344    computed_getter_sites_v1, lower_computed_getters_v1, ComputedGetterLoweringErrorV1,
345    ComputedGetterLoweringV1, ComputedGetterSiteV1,
346};
347pub use computed_instance_slots::{
348    build_computed_instance_slot_registry, validate_computed_instance_slot_registry,
349    ComputedInstanceSlotRecord, ComputedInstanceSlotRegistry,
350    COMPUTED_INSTANCE_SLOT_REGISTRY_VERSION,
351};
352pub use computed_projection::{
353    build_computed_projection_v1, ComputedProjectionRecordV1, ComputedProjectionV1,
354    COMPUTED_PROJECTION_SCHEMA_VERSION,
355};
356pub use computed_value::{
357    collect_computed_values, ComputedCachePolicy, ComputedDiagnosticCode, ComputedPurity,
358    ComputedPurityViolation, ComputedPurityViolationKind, ComputedValue,
359};
360pub use consumer::{collect_consumer_entities, ConsumerEntity, ContextResolutionState};
361pub use context::{collect_context_entities, ContextEntity};
362pub use context_declaration_candidate::{
363    collect_context_declaration_candidates, ContextDeclarationCandidate,
364    ContextDeclarationCandidateRegistry, ContextDeclarationStatus, ContextSemanticEntityId,
365};
366pub use context_dependency::{
367    collect_context_dependency_graph, ContextDependencyCompatibility, ContextDependencyEdge,
368    ContextDependencyEdgeKind, ContextDependencyGraph, ContextDependencyNode,
369    ContextDependencyNodeId, ContextDependencyNodeKind,
370};
371pub use context_diagnostics::collect_context_diagnostics;
372pub use context_evaluation::{
373    collect_context_evaluation_plan, ContextConsumerAvailabilityEntry,
374    ContextConsumerAvailabilityStatus, ContextEvaluationBatch, ContextEvaluationBatchId,
375    ContextEvaluationPlan, ContextEvaluationPlanId, ContextSourceBlockReason,
376    ContextSourcePlanEntry, ContextSourcePlanStatus, ContextValueSourceId,
377};
378pub use context_inspection::{
379    build_context_inspection_registry, ContextInspection, ContextInspectionRegistry,
380};
381pub use context_lifetime::{
382    collect_context_lifetime_analysis, ContextBindingLifetimeRecord, ContextBindingLifetimeSource,
383    ContextBindingLifetimeStatus, ContextDefaultLifetimeRecord, ContextDependencyLifetimeRecord,
384    ContextEntityLifetimeRecord, ContextLifetimeAnalysis, ContextLifetimeEntityId,
385    ContextLifetimeId, LifetimeCompatibilityStatus, ProviderLifetimeRecord,
386};
387pub use context_ownership::{
388    collect_context_ownership_graph, ContextOwnedEntities, ContextOwnershipEdge,
389    ContextOwnershipEdgeKind, ContextOwnershipGraph, ContextOwnershipNode, ContextOwnershipNodeId,
390    ContextOwnershipNodeKind, ContextOwnershipOwnerId, ContextOwnershipTargetId,
391};
392pub use context_projection::{
393    build_context_projection_v1, ContextProjectionV1, ContextProviderRecordV1,
394    ContextTokenRecordV1, CONTEXT_PROJECTION_SCHEMA_VERSION,
395};
396pub use context_resolution::{
397    collect_context_resolutions, ContextResolution, ContextResolutionResult,
398};
399pub use context_resume::{
400    build_context_resume_plan, ContextResumePlan, ContextResumeRecord, ContextResumeSlotId,
401    ContextSlotResumeStatus,
402};
403pub use context_resume_projection::{
404    build_context_resume_projection_v1, ContextConsumerResolutionV1,
405    ContextResumeProjectionRecordV1, ContextResumeProjectionV1,
406    CONTEXT_RESUME_PROJECTION_SCHEMA_VERSION,
407};
408pub use context_typing::{
409    collect_context_type_products, CompatibilityStatus, ConsumerTypeRecord,
410    ContextBindingCompatibility, ContextBindingTypeRecord, ContextSerializationCompatibility,
411    ContextTypeProducts, ContextTypeRecord, ProviderTypeRecord,
412};
413pub use context_update::{build_context_update_plan, ContextActionUpdatePlan, ContextUpdatePlan};
414pub use control_flow::{
415    build_control_flow_graph_v1, ControlFlowAccessKindV1, ControlFlowAccessV1, ControlFlowBlockV1,
416    ControlFlowBranchArmV1, ControlFlowBranchEdgeV1, ControlFlowCoverageStatusV1,
417    ControlFlowCoverageV1, ControlFlowFunctionV1, ControlFlowGraphV1, ControlFlowLoopV1,
418    ControlFlowProvenanceV1, CONTROL_FLOW_SCHEMA_VERSION,
419};
420pub use document_template::{apply_document_template_v1, DocumentTemplateErrorV1};
421pub use effect::{
422    analyze_effect_reactivity, collect_effects, derive_effect_trigger_plan, lower_effect_bodies,
423    plan_effect_execution, validate_effects, ActionBatch, ActionBatchEffectTrigger,
424    ActionEffectExecutionPlan, Effect, EffectBody, EffectComputedPrerequisiteBatch,
425    EffectDeclaration, EffectExecutionBatch, EffectExecutionPlan, EffectExecutionPolicy,
426    EffectReactiveAnalysis, EffectRenderBoundary, EffectSemanticViolation,
427    EffectSemanticViolationKind, EffectStatement, EffectStatementKind, EffectTriggerPlan,
428    EffectValidation, InitialEffectExecutionPlan, UnplannedEffect, UnplannedEffectReason,
429};
430pub use effect_capability::{
431    ArgumentSerializationPolicy, BuiltinCapabilityProvenance, CapabilityDefinition, CapabilityId,
432    CapabilityOperation, CapabilityOperationId, CapabilityOperationKind, CapabilityParameters,
433    CapabilityResultPolicy, CapabilitySignature, CapabilityValueContract, EffectCapabilityRegistry,
434    RuntimeCapabilityLowering, StaticCapabilityPath, EFFECT_CAPABILITY_REGISTRY,
435    EFFECT_CAPABILITY_REGISTRY_VERSION,
436};
437pub use effect_diagnostics::{collect_effect_diagnostics, EffectDiagnosticCode};
438pub use effect_field_lowering::{
439    effect_field_sites_v1, lower_effect_fields_v1, EffectFieldLoweringErrorV1,
440    EffectFieldLoweringV1, EffectFieldSiteV1, ResolvedEffectFieldV1,
441};
442pub use effect_inspection::{
443    build_effect_inspection_registry, validate_effect_inspection_registry, EffectInspection,
444    EffectInspectionActionTrigger, EffectInspectionCapability, EffectInspectionDependencies,
445    EffectInspectionInitialTrigger, EffectInspectionIr, EffectInspectionPrerequisiteBatch,
446    EffectInspectionProvenance, EffectInspectionRegistry, EffectInspectionResumability,
447    EffectInspectionRuntime, EffectInspectionSchedule, EffectInspectionScheduledAction,
448    EffectInspectionUnplanned, EffectInspectionValidation, EffectInspectionValidationDiagnostic,
449    EffectInspectionViolation,
450};
451pub use effect_projection::{
452    build_effect_projection_v1, EffectProjectionRecordV1, EffectProjectionV1,
453    EFFECT_PROJECTION_SCHEMA_VERSION,
454};
455pub use effect_resume::{
456    build_effect_resume_plan, validate_effect_resume_plan, EffectActivationSlotId,
457    EffectActivationStatus, EffectInitialResumeMembership, EffectResumePlan, EffectResumeRecord,
458    EffectResumeValidationDiagnostic,
459};
460pub use environment_input::{
461    build_environment_input_manifest_v1, environment_input_manifest_from_json_v1,
462    environment_input_manifest_json_v1, validate_environment_input_manifest_v1,
463    EnvironmentInputErrorV1, EnvironmentInputManifestV1, ENVIRONMENT_INPUT_SCHEMA_VERSION,
464};
465pub use environment_ownership::{
466    build_environment_ownership_graph_v1, EnvironmentClassV1, EnvironmentOwnershipDiagnosticV1,
467    EnvironmentOwnershipEdgeKindV1, EnvironmentOwnershipEdgeV1, EnvironmentOwnershipErrorV1,
468    EnvironmentOwnershipFactsV1, EnvironmentOwnershipGraphV1, EnvironmentOwnershipNodeV1,
469    EnvironmentOwnershipViolationV1, LifetimeClassV1, ENVIRONMENT_OWNERSHIP_SCHEMA_VERSION,
470};
471pub use environment_publication::{
472    build_environment_publication_artifact_v1, environment_publication_artifact_json_v1,
473    validate_environment_publication_artifact_v1, EnvironmentPublicationArtifactV1,
474    EnvironmentPublicationErrorV1, ENVIRONMENT_PUBLICATION_SCHEMA_VERSION,
475};
476pub use environment_read_lowering::{
477    lower_environment_reads_v1, EnvironmentReadDiagnosticCodeV1, EnvironmentReadDiagnosticV1,
478    EnvironmentReadLoweringV1, EnvironmentReadRecordV1, ENVIRONMENT_READ_LOWERING_SCHEMA_VERSION,
479};
480pub use environment_read_ownership::{
481    build_environment_read_ownership_v1, EnvironmentReadOwnershipErrorV1,
482};
483pub use explain::{explain_json, explain_text};
484pub use expression_graph::{ExpressionGraph, ExpressionNode, ExpressionNodeKind};
485pub use file_route_publication::{
486    build_file_route_publication_v1, file_route_artifact_root_v1,
487    file_route_publication_manifest_json_v1, resolve_file_route_request_match_v1,
488    resolve_file_route_request_v1, FileRoutePublicationErrorV1, FileRoutePublicationManifestV1,
489    FileRoutePublicationProductV1, FileRoutePublicationRequestV1, FileRoutePublicationRouteV1,
490    FileRouteRequestMatchV1, FileRouteRequestTargetV1, FILE_ROUTE_ENVIRONMENT_ARTIFACT_PATH_V1,
491    FILE_ROUTE_PUBLICATION_COMPILER_CONTRACT_V1, FILE_ROUTE_PUBLICATION_MANIFEST_SCHEMA_VERSION,
492};
493pub use form::{collect_form_entities, FormEntity};
494pub use form_binding::{
495    collect_form_field_binding_products, FormControlChannel, FormControlCompatibility,
496    FormControlNormalization, FormFieldBinding, FormFieldBindingCandidate,
497    FormFieldBindingEvidence, FormFieldBindingEvidenceKind, FormFieldBindingExpressionFact,
498    FormFieldBindingProducts, FormFieldBindingViolation, FormInputKind,
499};
500pub use form_definition_lowering::{
501    form_definition_sites_v1, lower_form_definitions_v1, FormDefinitionLoweringErrorV1,
502    FormDefinitionLoweringV1, FormDefinitionSiteV1, ResolvedFormDefinitionV1,
503};
504pub use form_diagnostics::{
505    collect_form_diagnostics, FormDiagnosticReservation, FORM_DIAGNOSTIC_RESERVATIONS,
506};
507pub use form_field::{collect_form_field_products, FormFieldEntity, FormFieldProducts};
508pub use form_field_definition_lowering::{
509    form_field_definition_sites_v1, lower_form_field_definitions_v1,
510    FormFieldDefinitionLoweringErrorV1, FormFieldDefinitionLoweringV1, FormFieldDefinitionSiteV1,
511    ResolvedFormFieldDefinitionV1, ResolvedFormFieldValueClassificationV1,
512};
513pub use form_inspection::{build_form_inspection_registry, FormInspection, FormInspectionRegistry};
514pub use form_ir::{
515    lower_form_ir, FormInstanceIr, FormIrOperation, FormIrReport, FormRuntimeStorage,
516};
517pub use form_ir_optimization::{
518    optimize_form_ir, FormIrOptimizationMetrics, OptimizedFormIrReport,
519};
520pub use form_ownership::{
521    collect_form_ownership_graph, validate_form_ownership_graph, FormOwnershipEdge,
522    FormOwnershipEdgeKind, FormOwnershipGraph, FormOwnershipIntegrityDiagnostic,
523    FormOwnershipIntegrityKind, FormOwnershipNode, FormOwnershipNodeKey, FormOwnershipValidation,
524    FormReferenceEdge, FormReferenceKind,
525};
526pub use form_reset::{
527    collect_reset_products, FieldResetOperation, FieldResetStep, FormResetPlan, ResetProducts,
528};
529pub use form_serialization::{
530    collect_serialization_products, FormFieldSerializationConversion, FormSerializationFormat,
531    FormSerializationPlan, SerializationDeclarationFact, SerializationPlanStatus,
532    SerializationProducts, SerializedFieldPlan,
533};
534pub use form_submission::{
535    collect_submission_products, FormSubmissionCapability, FormSubmissionPlan,
536    SubmissionDeclarationCandidate, SubmissionDeclarationViolation, SubmissionProducts,
537    SubmitResetPolicy,
538};
539pub use form_submission_host::{
540    collect_submission_host_products, SubmissionHost, SubmissionHostCandidate,
541    SubmissionHostProducts, SubmissionHostViolation,
542};
543pub use form_tracking::{
544    collect_form_tracking_products, structurally_equal_serializable_values,
545    validate_dirty_tracking_graph, validate_touched_tracking_graph, DirtyTrackingGraph,
546    DirtyTrackingPlan, DirtyTransitionCause, DirtyTransitionPlan, FieldDirtyTracking,
547    FieldTouchedTracking, FormTrackingIntegrityDiagnostic, FormTrackingIntegrityKind,
548    FormTrackingProducts, FormTrackingValidation, TouchedTrackingGraph, TouchedTrackingPlan,
549    TouchedTransitionCause, TouchedTransitionPlan,
550};
551pub use form_validation::{
552    collect_validation_graph, collect_validation_products, validate_validation_graph,
553    ValidationCompatibility, ValidationDependencyCycle, ValidationDependencyDesignator,
554    ValidationGraph, ValidationGraphEdge, ValidationGraphEdgeKind,
555    ValidationGraphIntegrityDiagnostic, ValidationGraphIntegrityKind, ValidationGraphNode,
556    ValidationGraphNodeKey, ValidationGraphValidation, ValidationProducts, ValidationRule,
557    ValidationRuleArgument, ValidationRuleCandidate, ValidationRuleKind, ValidationRuleViolation,
558};
559pub use form_validation_definition_lowering::{
560    form_validation_definition_sites_v1, lower_form_validation_definitions_v1,
561    FormValidationDefinitionLoweringErrorV1, FormValidationDefinitionSiteV1,
562    ResolvedFormValidationDefinitionKindV1, ResolvedFormValidationDefinitionV1,
563};
564pub use form_validation_plan::{
565    collect_validation_dependency_plans, validate_validation_dependency_plans,
566    BlockedFieldValidationDependency, FieldChangeSet, FieldChangeValidationSchedule,
567    FieldDependencyBlockReason, FieldValidationChangePlan, FieldValidationDependency,
568    FieldValidationSourceEntry, FieldValidationTargetEntry, FormValidationDependencyPlan,
569    ValidationDependencyPlanIntegrityDiagnostic, ValidationDependencyPlanIntegrityKind,
570    ValidationDependencyPlanValidation, ValidationDependencyPlans, ValidationPlanningStatus,
571};
572pub use function_summary::{
573    build_function_summary_graph_v1, FunctionCallCoverageV1, FunctionCallFactV1,
574    FunctionCallFactsV1, FunctionSummaryErrorV1, FunctionSummaryGraphV1, FunctionSummaryV1,
575    FUNCTION_SUMMARY_SCHEMA_VERSION,
576};
577pub use html_codegen::generate_static_html;
578pub use instance_context::{
579    collect_instance_context_registry, ConsumerInstanceId, ConsumerInstanceRecord,
580    ContextDefaultSourceInstanceId, ContextSourceInstanceId, ContextSourceInstanceOwner,
581    InstanceContextRegistry, InstanceContextResolution, InstanceContextResolutionStatus,
582    InstanceContextValueSlotId, ProviderInstanceId, ProviderInstanceRecord,
583};
584pub use intermediate_representation::{
585    analyze_constant_propagation, analyze_dead_assignments, analyze_definition_uses,
586    analyze_liveness, analyze_reachability, analyze_reactive_cycles,
587    analyze_reactive_transitive_graph, analyze_use_definitions, build_reactive_graph,
588    compute_dominators, compute_post_dominators, computed_optimization_pipeline, inspect_dom_nodes,
589    lower_components_to_ir, optimize_computed_ir, optimize_context_ir, optimize_effect_ir,
590    plan_computed_evaluation, validate_context_ir, validate_effect_ir,
591    validate_intermediate_representation, validate_optimized_context_ir, ContextConsumerLoadId,
592    ContextIrReport, ContextSourceFunctionId, ContextValueSlotId, IntermediateRepresentation,
593    IrBinaryOperation, IrBlock, IrBlockId, IrBranchArm, IrBranchEdge, IrCfgCleanupPass,
594    IrCommonSubexpressionEliminationPass, IrComputedEvaluation, IrComputedEvaluationPlan,
595    IrConstant, IrConstantFoldingPass, IrConstantPropagationAnalysis, IrContextConsumerBinding,
596    IrContextLoad, IrContextSourceEvaluation, IrCopyPropagationPass, IrDeadAssignmentAnalysis,
597    IrDeadCodeEliminationPass, IrDefinitionUseAnalysis, IrDomAttribute, IrDomAttributeValue,
598    IrDomBinding, IrDomConditional, IrDomEvent, IrDomInspection, IrDomList, IrDomNode, IrDomNodeId,
599    IrDomNodeKind, IrDomText, IrDominatorTree, IrEffectCompletion, IrEffectExecution, IrFunction,
600    IrInstruction, IrInstructionId, IrInstructionKind, IrInstructionSimplificationPass,
601    IrLivenessAnalysis, IrLoop, IrLoopId, IrModule, IrOperand, IrOptimizationMetrics,
602    IrOptimizationPass, IrOptimizationPassReport, IrOptimizationPipeline, IrOptimizationReport,
603    IrPassManager, IrPostDominatorTree, IrReachabilityAnalysis, IrReactiveCycle,
604    IrReactiveCycleAnalysis, IrReactiveEdge, IrReactiveEdgeKind, IrReactiveGraph, IrReactiveNode,
605    IrReactiveNodeKind, IrReactiveTransitiveAnalysis, IrSchedulerInspection, IrStorage,
606    IrStorageId, IrTemplateEntrypoint, IrUnaryOperation, IrUpdateScheduler, IrUse, IrUseDefinition,
607    IrValidationDiagnostic, IrValue, IrValueDefinition, IrValueId, OptimizedContextIrReport,
608    OptimizedIrContextSourceEvaluation,
609};
610pub use layout_composition::{
611    build_layout_composition_plan_from_components_v1, build_layout_composition_plan_v1,
612    layout_composition_virtual_invocations_from_provenance_v1,
613    layout_composition_virtual_invocations_v1, LayoutCompositionEdgeV1, LayoutCompositionErrorV1,
614    LayoutCompositionPlanV1, LayoutCompositionRouteV1,
615};
616pub use legacy_decorator_lowering::{
617    legacy_decorator_sites_v1, lower_legacy_decorators_v1, LegacyDecoratorLoweringErrorV1,
618    LegacyDecoratorLoweringV1, LegacyDecoratorResolutionV1, LegacyDecoratorSiteV1,
619};
620pub use metaframework_handoff::{
621    build_deployable_release_manifest_v1, build_static_request_handoff_v1,
622    deployable_release_manifest_json_v1, static_request_handoff_json_v1, DeployableArtifactV1,
623    DeployableReleaseManifestV1, StaticRequestHandoffV1, StaticRequestRouteV1,
624};
625pub use model::{
626    ClassSummary, DecoratorSummary, Diagnostic, RenderMethodSummary, Severity, SourceSummary, Span,
627};
628pub use module_graph::{
629    build_module_graph, ModuleEdge, ModuleEdgeKind, ModuleGraph, ModuleNode, ModuleTarget,
630};
631pub use opaque::{OpaqueActionResolution, OpaqueActionResolutionOutcome, OpaqueTerminalBinding};
632pub use ordinary_html_codegen::{
633    generate_ordinary_instance_html, generate_ordinary_instance_html_for_component,
634    generate_structural_conditional_host_fragments, generate_structural_keyed_host_fragments,
635    generate_structural_template_instance_html,
636};
637pub use ordinary_template_instance::{
638    build_ordinary_template_instance_registry, validate_ordinary_template_instance_registry,
639    OrdinaryTemplateBindingKind, OrdinaryTemplateInstanceBindingRecord,
640    OrdinaryTemplateInstanceEventRecord, OrdinaryTemplateInstanceRegistry,
641    OrdinaryTemplateInstanceTargetRecord, OrdinaryTemplateTargetKind,
642    ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION,
643};
644pub use ordinary_template_integrity::{
645    ComputedInstanceSlotIntegrityCode, OrdinaryTemplateIntegrityCode,
646    StateInstanceStorageIntegrityCode,
647};
648pub use page_codegen::{
649    embed_opaque_runtime_artifact, embed_package_invocation_runtime_artifact,
650    generate_standalone_page, generate_standalone_page_with_component_runtime,
651    generate_standalone_page_with_component_runtime_and_forms,
652    generate_standalone_page_with_computed_runtime, generate_standalone_page_with_context_runtime,
653    generate_standalone_page_with_effect_runtime, generate_standalone_page_with_resume_runtime,
654    generate_standalone_page_with_resume_runtime_and_resources,
655};
656pub use production_audit::{
657    build_production_audit_report_v1, production_audit_report_json_v1, ProductionAuditErrorV1,
658    ProductionAuditReportV1, ProductionRuntimeAuthority, PRODUCTION_AUDIT_REPORT_SCHEMA_VERSION,
659    PRODUCTION_RUNTIME_AUTHORITIES, PRODUCTION_RUNTIME_REFINEMENT_INVARIANTS,
660};
661pub use production_bootstrap::{
662    build_production_bootstrap_plan, ProductionBootstrapBlock, ProductionBootstrapPlan,
663    ProductionRuntimeIndexes,
664};
665pub use production_chunk_graph::{
666    extract_production_chunk_graph, validate_production_chunk_graph, ProductionActivationChunkPlan,
667    ProductionChunkDependency, ProductionChunkExtractionError, ProductionChunkExtractionReport,
668    ProductionChunkGraph, ProductionChunkGraphValidationError, ProductionChunkKind,
669    ProductionChunkRecord, ProductionRootChunkInput, ProductionSharedChunkFailurePolicy,
670};
671pub use production_cleanup::{
672    build_production_destroy_plan, validate_production_cleanup_closure,
673    ProductionCleanupClosureViolation, ProductionCleanupKind, ProductionDestroyPlan,
674    ProductionOwnedRuntimeRecord,
675};
676pub use production_constant_pool::{
677    pool_production_constants, ConstantPoolConsumer, ConstantPoolingDecision,
678    ConstantPoolingReport, ProductionConstantCandidate, ProductionConstantPool,
679    ProductionConstantPoolEntry,
680};
681pub use production_deduplication::{
682    deduplicate_generated_programs, DeduplicatedProgramRegistry, ExecutableProgramCandidate,
683    ExecutableProgramCanonicalStream, ExecutableProgramFingerprintRegistry, ProgramAliasRecord,
684    ProgramDeduplicationReport,
685};
686pub use production_diagnostics::{
687    project_production_diagnostics, ProductionDiagnosticContract, ProductionDiagnosticFact,
688    ProductionDiagnosticKind, ProductionDiagnosticProvenance, ProductionProjectedDiagnostic,
689    PRODUCTION_DIAGNOSTIC_CATALOG,
690};
691pub use production_elimination::{
692    eliminate_unreachable_production_records, DeadProductEliminationReason,
693    DeadProductEliminationReport, OptimizationDecision, ProductionArtifactProjection,
694    ProductionExecutionRecord,
695};
696pub use production_module_emitter::{
697    emit_production_modules, validate_production_module, ProductionModuleLayout,
698    ProductionModuleRecord, ProductionModuleValidationError,
699};
700pub use production_optimization::{
701    BenchmarkFixtureId, ConstantPoolEntryId, ExecutableProgramFingerprint, OptimizationDecisionId,
702    OptimizationPolicyId, OptimizationReportId, PerformanceBudgetId, ProductionArtifactId,
703    ProductionChunkId, ProductionOptimizationDiagnosticReservation,
704    ProductionOptimizationIdentityParseError, ProductionOptimizationPolicy,
705    ProductionOptimizationPolicyV1, RuntimeTableId, SharedChunkCandidateId,
706    PRODUCTION_OPTIMIZATION_DIAGNOSTIC_RESERVATIONS,
707    PRODUCTION_OPTIMIZATION_INTEGRITY_RESERVATION_END,
708    PRODUCTION_OPTIMIZATION_INTEGRITY_RESERVATION_START,
709};
710pub use production_reachability::{
711    build_production_reachability_graph, ProductionExecutableRoot, ProductionProgramEdge,
712    ProductionReachabilityBlock, ProductionReachabilityGraph, ProductionReachabilityReason,
713    ProductionUnreachableRecord,
714};
715pub use production_reports::{
716    build_production_reports, optimization_report_json, runtime_cost_report_json,
717    OptimizationInspectionQuery, OptimizationReportV1, ProductionReportInputs,
718    RuntimeCostInspectionQuery, RuntimeCostReportV1, OPTIMIZATION_REPORT_SCHEMA_VERSION,
719    RUNTIME_COST_REPORT_SCHEMA_VERSION,
720};
721pub use production_runtime_artifact::{
722    build_production_runtime_artifact, build_production_runtime_table,
723    parse_production_runtime_artifact_v1, production_runtime_artifact_json,
724    validate_production_runtime_artifact, ProductionArtifactActivationEntry,
725    ProductionArtifactChunkRecord, ProductionArtifactEntry, ProductionArtifactIntegrity,
726    ProductionArtifactIntegrityViolation, ProductionOrdinalMapping, ProductionOrdinalWidth,
727    ProductionRuntimeArtifactV1, ProductionRuntimeTable, ProductionRuntimeTableRegistry,
728    PRODUCTION_RUNTIME_ARTIFACT_SCHEMA_VERSION, PRODUCTION_RUNTIME_TABLE_SCHEMA_VERSION,
729};
730pub use production_scheduler::{
731    build_production_patch_schedule, coalesce_production_binding_writes,
732    BindingWriteCoalescingDecision, BindingWriteCoalescingReport, ProductionPatchBatchKind,
733    ProductionPatchOperation, ProductionPatchSchedule,
734};
735pub use production_validation::{
736    validate_production_runtime_pipeline, ProductionRuntimeFailure, ProductionValidationEvidence,
737    ProductionValidationPhase, ProductionValidationResult,
738};
739pub use project_discovery::{
740    discover_project_v1, discover_semantic_packages_v1, DiscoveredProjectSourceV1,
741    DiscoveredProjectV1, ProjectDiscoveryErrorV1,
742};
743pub use provider::{collect_provider_entities, DuplicateProviderDeclaration, ProviderEntity};
744pub use purity_effect::{
745    build_purity_effect_graph_v1, FunctionEffectFactV1, FunctionEffectKindV1,
746    FunctionPurityEffectV1, FunctionPurityV1, PurityEffectErrorV1, PurityEffectGraphV1,
747    PURITY_EFFECT_SCHEMA_VERSION,
748};
749pub use resource::{
750    ResourceActivation, ResourceDeclaration, ResourceDeclarationError, ResourceEndpointBinding,
751    ResourceEndpointResolution, ResourceEndpointResolutionOutcome, ResourceInvalidationPolicy,
752    ResourceLifecycleError, ResourceLifecycleEvent, ResourceLifecycleState, ResourceRetryPolicy,
753};
754pub use resume_activation::{
755    build_resume_activation_plan, validate_resume_activation_plan, ResumeActivationBlock,
756    ResumeActivationBlockReason, ResumeActivationIntegrityCode,
757    ResumeActivationIntegrityDiagnostic, ResumeActivationPlan, ResumeActivationPolicy,
758    ResumeActivationPolicyDecision, ResumeActivationPrerequisite, RESUME_ACTIVATION_PLAN_VERSION,
759};
760pub use resume_anchor::{
761    build_resume_anchor_plan, validate_resume_anchor_plan, validate_resume_marker_html,
762    ResumeAnchorIntegrityCode, ResumeAnchorIntegrityDiagnostic, ResumeAnchorKind,
763    ResumeAnchorPlacement, ResumeAnchorPlan, ResumeAnchorPlanRecord, ResumeEventMarkerRecord,
764    RESUME_ANCHOR_PLAN_VERSION,
765};
766pub use resume_boundary::{
767    build_resume_boundary_graph, validate_resume_boundary_graph, ResumeBoundary,
768    ResumeBoundaryActivationIdentity, ResumeBoundaryActivationProgram,
769    ResumeBoundaryActivationReference, ResumeBoundaryBlock, ResumeBoundaryBlockSource,
770    ResumeBoundaryGraph, ResumeBoundaryIntegrityCode, ResumeBoundaryIntegrityDiagnostic,
771    ResumeBoundaryOwner, ResumeBoundaryOwnershipEdge, RESUME_BOUNDARY_GRAPH_VERSION,
772};
773pub use resume_capture::{
774    build_resume_capture_plan, capture_resume_snapshot, encode_resume_value,
775    resume_snapshot_artifact_json, resume_snapshot_json, validate_resume_capture_plan,
776    ResumeCaptureBlock, ResumeCaptureBlockReason, ResumeCaptureError, ResumeCaptureErrorKind,
777    ResumeCaptureInstruction, ResumeCaptureIntegrityCode, ResumeCaptureIntegrityDiagnostic,
778    ResumeCapturePlan, ResumeCaptureProgram, ResumeEncodedValue, ResumeEnvelopeWriterPlan,
779    ResumeSnapshotBoundaryV1, ResumeSnapshotStructuralOccurrenceV2,
780    ResumeSnapshotStructuralStateV2, ResumeSnapshotV1, ResumeSnapshotV2,
781    ResumeSnapshotValueRecordV1, RuntimeQuiescenceState, RESUME_CAPTURE_MANIFEST_VERSION,
782    RESUME_CAPTURE_PLAN_VERSION, RESUME_SNAPSHOT_SCHEMA_VERSION,
783};
784pub use resume_chunk::{
785    build_resume_chunk_graph, validate_resume_chunk_graph, ResumeChunk, ResumeChunkBlock,
786    ResumeChunkBlockReason, ResumeChunkGraph, ResumeChunkIntegrityCode,
787    ResumeChunkIntegrityDiagnostic, ResumeChunkModulePlan, ResumeChunkProgram,
788    ResumeChunkProgramInclusion, ResumeChunkRootKind, RESUME_CHUNK_GRAPH_VERSION,
789};
790pub use resume_diagnostics::{
791    build_resume_diagnostic_products, project_resume_diagnostics,
792    project_resume_diagnostics_from_products, ResumeDiagnosticProducts,
793    ResumeDiagnosticReservation, ResumeProjectedDiagnostic, RESUME_DIAGNOSTIC_RESERVATIONS,
794    RESUME_INTEGRITY_RESERVATION_END, RESUME_INTEGRITY_RESERVATION_START,
795};
796pub use resume_identity::{
797    ComputedInstanceCacheSlotId, ComputedInstanceDirtySlotId, ResumeActivationId,
798    ResumeActivationRootKind, ResumeAnchorId, ResumeBoundaryId, ResumeBoundaryKind, ResumeBuildId,
799    ResumeCaptureProgramId, ResumeChunkGroupId, ResumeChunkId, ResumeEventId,
800    ResumeIdentityParseError, ResumeRestoreProgramId, ResumeSchemaId, ResumeSlotId,
801    ResumeSnapshotId, ResumeValueRecordId, StateInstanceSlotId, TemplateInstanceBindingId,
802    TemplateInstanceTargetId,
803};
804pub use resume_liveness::{
805    build_resume_liveness_plan, validate_resume_liveness_plan, ResumeExcludedSlot,
806    ResumeExistingSlot, ResumeLivenessBlock, ResumeLivenessBlockReason,
807    ResumeLivenessClassificationKind, ResumeLivenessClassificationRef, ResumeLivenessIntegrityCode,
808    ResumeLivenessIntegrityDiagnostic, ResumeLivenessOwner, ResumeLivenessPlan, ResumeLivenessSlot,
809    ResumeRecomputableSlot, ResumeRecomputationProof, ResumeRetainedSlot, ResumeRetentionReason,
810    RESUME_LIVENESS_PLAN_VERSION,
811};
812pub use resume_manifest::{
813    build_resume_manifest, compute_resume_build_id, parse_resume_manifest_v6,
814    parse_resume_manifest_v7, resume_manifest_json, validate_resume_manifest, ResumeManifest,
815    ResumeManifestActivationPolicy, ResumeManifestActivationRecord, ResumeManifestAnchorRecord,
816    ResumeManifestBoundaryKind, ResumeManifestBoundaryRecord, ResumeManifestCaptureInstruction,
817    ResumeManifestCaptureProgram, ResumeManifestChunkRecord, ResumeManifestContextSlotRecord,
818    ResumeManifestEffectRecord, ResumeManifestEventRecord,
819    ResumeManifestPhaseIComponentResumeRecord, ResumeManifestRestoreInstruction,
820    ResumeManifestRestoreInstructionRecord, ResumeManifestRestoreProgram,
821    ResumeManifestSlotSchemaRecord, ResumeManifestValidationDiagnostic,
822    RESUME_MANIFEST_SCHEMA_VERSION, RESUME_RUNTIME_PROTOCOL_VERSION,
823};
824pub use resume_plan::{
825    build_resume_plan, ComponentInstanceResumePlan, FormFieldResumePlan, FormInstanceResumePlan,
826    ResumeComponentPlan, ResumeComputedPlan, ResumePlan, SlotBindingResumePlan,
827    StructuralRegionResumePlan,
828};
829pub use resume_restore::{
830    build_resume_restore_plan, validate_resume_restore_plan, ResumeRestoreApplicationSchedule,
831    ResumeRestoreBlock, ResumeRestoreBlockReason, ResumeRestoreInstruction,
832    ResumeRestoreInstructionRecord, ResumeRestoreIntegrityCode, ResumeRestoreIntegrityDiagnostic,
833    ResumeRestorePhase, ResumeRestorePlan, ResumeRestoreProgram, ResumeRestoreSchedulePhase,
834    ResumeRestoreSlotAssignment, RESUME_RESTORE_PLAN_VERSION,
835};
836pub use resume_schema::{
837    build_resume_schema_registry, resume_value_codec, validate_resume_schema_registry,
838    ResumeBoundarySchema, ResumeObjectPropertyCodec, ResumeSchemaBlock, ResumeSchemaBlockReason,
839    ResumeSchemaIntegrityCode, ResumeSchemaIntegrityDiagnostic, ResumeSchemaRegistry,
840    ResumeSlotSchema, ResumeValueCodec, RESUME_SCHEMA_REGISTRY_VERSION,
841};
842pub use route_graph::{
843    build_file_route_graph_from_components_v1, build_file_route_graph_v1, build_route_graph,
844    build_static_route_publication_v1, build_validated_file_route_graph_from_components_v1,
845    build_validated_file_route_graph_v1, build_validated_route_graph_v1, route_manifest_json_v1,
846    route_manifest_v1, FileRouteGraphV1, FileRouteNodeV1, RouteGraph, RouteGraphError,
847    RouteManifestEntryV1, RouteManifestV1, RouteNode,
848};
849pub use route_loader::{
850    build_route_loader_plan_v1, route_loader_plan_json_v1, RouteLoaderBindingV1,
851    RouteLoaderPlanErrorV1, RouteLoaderPlanV1, RouteLoaderRouteV1,
852    ROUTE_LOADER_PLAN_SCHEMA_VERSION,
853};
854pub use route_metadata::{
855    build_route_metadata_manifest_v1, route_metadata_manifest_json_v1, RouteMetadataErrorV1,
856    RouteMetadataInputV1, RouteMetadataManifestV1, RouteMetadataRecordV1,
857    ROUTE_METADATA_SCHEMA_VERSION,
858};
859pub use route_server_action::{
860    build_route_server_action_plan_v1, route_server_action_plan_json_v1,
861    RouteServerActionBindingV1, RouteServerActionPlanErrorV1, RouteServerActionPlanV1,
862    RouteServerActionRouteV1, ROUTE_SERVER_ACTION_PLAN_SCHEMA_VERSION,
863};
864pub use runtime_codegen::generate_runtime_stub;
865pub use runtime_component::{
866    build_runtime_component_registry, RuntimeComponentContextBindingRecord,
867    RuntimeComponentDefinitionRecord, RuntimeComponentInitializationBatch,
868    RuntimeComponentInstanceRecord, RuntimeComponentRegistry, RuntimeComponentSlotBindingRecord,
869    RUNTIME_COMPONENT_REGISTRY_SCHEMA_CONTRACT_VERSION,
870};
871pub use runtime_component_artifact::{
872    build_runtime_component_artifact, runtime_component_artifact_json,
873    validate_runtime_component_artifact, RuntimeComponentArtifact,
874    RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION,
875};
876pub use runtime_computed::{
877    build_runtime_computed_registry, ComputedCacheSlotId, ComputedDirtyFlagId,
878    RuntimeComputedCacheSlot, RuntimeComputedDirtyFlag, RuntimeComputedRecord,
879    RuntimeComputedRegistry,
880};
881pub use runtime_computed_artifact::{
882    build_runtime_computed_artifact, runtime_computed_artifact_json, RuntimeComputedArtifact,
883    RuntimeComputedArtifactDirtyFlag, RuntimeComputedArtifactEvaluation,
884    RuntimeComputedArtifactInstruction, RuntimeComputedArtifactInvalidation,
885    RuntimeComputedArtifactResourceInvalidation, RuntimeComputedArtifactSerialization,
886    RUNTIME_COMPUTED_ARTIFACT_SCHEMA_VERSION,
887};
888pub use runtime_context::{
889    build_runtime_context_registry, validate_runtime_context_registry,
890    RuntimeContextConsumerRecord, RuntimeContextEvaluationBatch, RuntimeContextRegistry,
891    RuntimeContextRegistryValidationDiagnostic, RuntimeContextSourceKind,
892    RuntimeContextSourceRecord, RUNTIME_CONTEXT_REGISTRY_SCHEMA_CONTRACT_VERSION,
893};
894pub use runtime_context_artifact::{
895    build_runtime_context_artifact, runtime_context_artifact_json, RuntimeContextArtifact,
896    SerializedContextActionUpdatePlan, SerializedContextBatchId, SerializedContextConsumerBinding,
897    SerializedContextEvaluationBatch, SerializedContextExecutionBoundary,
898    SerializedContextInstruction, SerializedContextInstructionKind, SerializedContextProgram,
899    SerializedContextSource, SerializedContextSourceKind, RUNTIME_CONTEXT_ARTIFACT_SCHEMA_VERSION,
900};
901pub use runtime_effect::{
902    build_runtime_effect_registry, RuntimeActionBatchEffectTrigger, RuntimeEffectRecord,
903    RuntimeEffectRegistry, RuntimeInitialEffectTrigger,
904};
905pub use runtime_effect_artifact::{
906    build_runtime_effect_artifact, runtime_effect_artifact_json, RuntimeEffectArtifact,
907    RuntimeEffectArtifactActionTrigger, RuntimeEffectArtifactCapabilityInstructionKind,
908    RuntimeEffectArtifactCapabilityOperation, RuntimeEffectArtifactEffect,
909    RuntimeEffectArtifactExecutionBoundary, RuntimeEffectArtifactExecutionPolicy,
910    RuntimeEffectArtifactInitialTrigger, RuntimeEffectArtifactInstance,
911    RuntimeEffectArtifactInstruction, RuntimeEffectArtifactPrerequisiteBatch,
912    RuntimeEffectArtifactProgram, RuntimeEffectArtifactRenderBoundary,
913    RuntimeEffectArtifactStructuralTemplate, RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION,
914};
915pub use runtime_effect_instance::{
916    build_runtime_effect_instance_registry, build_runtime_effect_structural_template_registry,
917    RuntimeEffectInstanceRecord, RuntimeEffectInstanceRegistry,
918    RuntimeEffectStructuralTemplateRecord, RUNTIME_EFFECT_INSTANCE_REGISTRY_VERSION,
919};
920pub use runtime_form_artifact::{
921    build_runtime_forms_artifact, runtime_forms_artifact_json, validate_runtime_forms_artifact,
922    RuntimeFormsArtifact, RuntimeFormsArtifactBinding, RuntimeFormsArtifactDependency,
923    RuntimeFormsArtifactField, RuntimeFormsArtifactFieldProgram, RuntimeFormsArtifactFieldSlots,
924    RuntimeFormsArtifactForm, RuntimeFormsArtifactInstance, RuntimeFormsArtifactPrograms,
925    RuntimeFormsArtifactReset, RuntimeFormsArtifactRule, RuntimeFormsArtifactRuleArgument,
926    RuntimeFormsArtifactSerialization, RuntimeFormsArtifactStandardSchemaModule,
927    RuntimeFormsArtifactSubmission, RuntimeFormsArtifactValidation,
928    RUNTIME_FORM_ARTIFACT_SCHEMA_VERSION,
929};
930pub use runtime_form_registry::{
931    build_runtime_form_registry, RuntimeFormInstanceRecord, RuntimeFormRecord, RuntimeFormRegistry,
932    RUNTIME_FORM_REGISTRY_VERSION,
933};
934pub use runtime_opaque_artifact::{
935    build_runtime_opaque_artifact, build_runtime_opaque_artifact_with_modules,
936    runtime_opaque_artifact_json, validate_runtime_opaque_artifact, RuntimeOpaqueArtifact,
937    RuntimeOpaqueArtifactActivation, RuntimeOpaqueArtifactBuildError,
938    RuntimeOpaqueArtifactValidationError, RUNTIME_OPAQUE_ARTIFACT_SCHEMA_VERSION,
939};
940pub use runtime_package_invocation_artifact::{
941    build_runtime_package_invocation_artifact, runtime_package_invocation_artifact_json,
942    validate_runtime_package_invocation_artifact, RuntimePackageInvocation,
943    RuntimePackageInvocationArtifact, RuntimePackageInvocationArtifactValidationError,
944    PACKAGE_INVOCATION_REGISTRY_PATH, RUNTIME_PACKAGE_INVOCATION_ARTIFACT_SCHEMA_VERSION,
945};
946pub use runtime_resource_artifact::{
947    build_runtime_resource_artifact, build_runtime_resource_artifact_with_modules,
948    runtime_resource_artifact_json, validate_runtime_resource_artifact, RuntimeResourceArtifact,
949    RuntimeResourceArtifactActivation, RuntimeResourceArtifactBuildError,
950    RuntimeResourceArtifactDeclaration, RuntimeResourceArtifactEndpoint,
951    RuntimeResourceArtifactValidationError, RUNTIME_RESOURCE_ARTIFACT_SCHEMA_VERSION,
952};
953pub use semantic_capability::{
954    build_semantic_capability_registry, semantic_capability_matrix_text,
955    semantic_capability_migration_text, semantic_capability_registry_json, SemanticCapability,
956    SemanticCapabilityClass, SemanticCapabilityRegistry, SemanticCapabilityStatus,
957    SEMANTIC_CAPABILITY_REGISTRY_SCHEMA_VERSION,
958};
959pub use semantic_graph::{
960    build_semantic_graph, semantic_graph_json, SemanticGraph, SemanticGraphConsumer,
961    SemanticGraphContext, SemanticGraphEdge, SemanticGraphEdgeKind, SemanticGraphNode,
962    SemanticGraphNodeKind, SemanticGraphProvenance, SemanticGraphProvider,
963    SEMANTIC_GRAPH_SCHEMA_VERSION,
964};
965pub use semantic_id::{
966    ComponentInstanceId, ComponentInvocationId, ComponentRootId, ComponentStructuralRegionId,
967    ConsumerId, ContextDeclarationCandidateId, ContextId, DirtyTrackingPlanId, EffectId,
968    EffectInstanceId, EffectStatementId, FieldBindingId, FieldDependencyId, FieldId,
969    FieldResetOperationId, FieldTrackingId, FormDeclarationCandidateId,
970    FormFieldBindingCandidateId, FormFieldDeclarationCandidateId, FormFieldDirtySlotId,
971    FormFieldTouchedSlotId, FormFieldValidationSlotId, FormFieldValueSlotId, FormId,
972    FormInstanceId, FormOwnershipGraphId, FormSubmissionStateSlotId, FormValidationAggregateSlotId,
973    ProviderId, ResetPlanId, ResourceActivationId, ResourceId, SemanticId, SemanticOwner,
974    SerializationPlanId, SlotBindingId, SlotContentFragmentId, SlotDeclarationCandidateId, SlotId,
975    SlotOutletId, SubmissionDeclarationCandidateId, SubmissionHostCandidateId, SubmissionHostId,
976    SubmissionPlanId, TemplatePositionId, TouchedTrackingPlanId, ValidationDependencyCycleId,
977    ValidationGraphId, ValidationPlanId, ValidationRuleCandidateId, ValidationRuleId,
978};
979pub use semantic_package::{
980    parse_semantic_package_contract, SemanticPackageContract, SemanticPackageContractError,
981    SemanticPackageExport, SemanticPackageFormSubmission,
982    SemanticPackageFormSubmissionCancellation, SemanticPackageFormSubmissionExecutionBoundary,
983    SemanticPackageFormSubmissionInput, SemanticPackageFormSubmissionResult, SemanticPackageKind,
984    SemanticPackageOpaqueExecutionBoundary, SemanticPackageOpaqueResumePolicy,
985    SemanticPackageOpaqueTerminal, SemanticPackagePureOperation, SemanticPackageResolutionTable,
986    SemanticPackageResourceCancellation, SemanticPackageResourceEndpoint,
987    SemanticPackageResourceExecutionBoundary, SemanticPackageResourceResumePolicy,
988    SemanticPackageRouteLoader, SemanticPackageRouteLoaderFailure, SemanticPackageRouteLoaderInput,
989    SemanticPackageServerAction, SemanticPackageServerActionInput,
990    SemanticPackageServerActionResponse, SemanticPackageServerCachePolicy,
991    SemanticPackageServerCacheScope, SEMANTIC_PACKAGE_CONTRACT_SCHEMA_VERSION,
992};
993pub use semantic_package_runtime::{
994    SemanticPackageRuntimeModuleError, SemanticPackageRuntimeModuleKey,
995    SemanticPackageRuntimeModuleTable,
996};
997pub use semantic_provenance::SourceProvenance;
998pub use semantic_reference::{SemanticReference, SemanticReferenceKind};
999pub use semantic_type::{
1000    boundary_compatibility, dom_binding_contract, infer_serializable_value_type, is_assignable,
1001    is_state_initializer_assignable, operator_result_type, semantic_type_text,
1002    serialization_compatibility, state_initializer_value_type, BoundaryCompatibility,
1003    BuiltinTypeAuthority, ComputedValueType, DomBindingContract, DomBindingKind,
1004    EffectCompatibility, EffectOperationClassification, EffectStatementTypeRecord,
1005    ExecutionBoundary, ObjectType, ResolvedDeclaredSemanticType, ResourceExecutionBoundary,
1006    ResourceType, SemanticOperator, SemanticType, SemanticTypeAlias, SemanticTypeAssignment,
1007    SemanticTypeId, SemanticTypeModel, SemanticTypeStatus, SerializationCompatibility,
1008    TypeDiagnosticCode, TypeDiagnosticFamily,
1009};
1010pub use shared_chunk_candidate::{
1011    plan_shared_lazy_chunk_candidates, SharedChunkCandidate, SharedChunkCandidatePlan,
1012    SharedChunkConsumerRoot, SharedChunkProgramOccurrence, SharedChunkRejectionReason,
1013    SharedChunkSavingsCalculation,
1014};
1015pub use slot::{collect_slot_entities, SlotEntity};
1016pub use slot_binding::{
1017    collect_slot_bindings, collect_slot_bindings_with_virtual_invocations, SlotBinding,
1018    SlotBindingRegistry, SlotBindingStatus,
1019};
1020pub use slot_content::{
1021    collect_slot_composition, SlotCompositionRegistry, SlotContentFragment,
1022    SlotContentFragmentStatus, SlotContentFragmentViolation, SlotOutlet, SlotOutletStatus,
1023    SlotOutletViolation,
1024};
1025pub use slot_field_lowering::{
1026    lower_slot_fields_v1, slot_field_sites_v1, ResolvedSlotFieldV1, SlotFieldLoweringErrorV1,
1027    SlotFieldLoweringV1, SlotFieldSiteV1,
1028};
1029pub use slot_projection::{
1030    build_slot_projection_graph_v1, SlotProjectionGraphV1, SlotProjectionRecordV1,
1031    SlotProjectionStatusV1, SlotResumabilityCoverageV1, SLOT_PROJECTION_SCHEMA_VERSION,
1032};
1033pub use state_initializer_lowering::{
1034    lower_state_initializers_v1, state_initializer_sites_v1, ResolvedStateInitializerV1,
1035    StateInitializerLoweringErrorV1, StateInitializerLoweringV1, StateInitializerSiteV1,
1036};
1037pub use state_instance_storage::{
1038    build_state_instance_storage_registry, validate_state_instance_storage_registry,
1039    StateInstanceStorageRecord, StateInstanceStorageRegistry,
1040    STATE_INSTANCE_STORAGE_REGISTRY_VERSION,
1041};
1042pub use state_projection::{
1043    build_state_projection_v1, StateProjectionRecordV1, StateProjectionV1, StateResumeAdmissionV1,
1044    StateUpdateCoverageV1, STATE_PROJECTION_SCHEMA_VERSION,
1045};
1046pub use structural_component::{
1047    build_structural_component_graph_v1, ComponentInheritanceStatusV1, ComponentPropsResolutionV1,
1048    StructuralComponentDiagnosticReasonV1, StructuralComponentDiagnosticV1,
1049    StructuralComponentFactV1, StructuralComponentGraphV1, StructuralComponentPropV1,
1050    StructuralComponentRecordV1, STRUCTURAL_COMPONENT_SCHEMA_VERSION,
1051};
1052pub use structural_occurrence_identity::{
1053    decode_structural_occurrence_identity, encode_structural_occurrence_identity,
1054    StructuralOccurrenceIdentity,
1055};
1056pub use summarize::summarize_source;
1057pub use symbol_table::{
1058    build_symbol_table, ModuleSymbol, ModuleSymbolTable, SymbolDiagnostic, SymbolKind, SymbolTable,
1059};
1060pub use template_graph::{
1061    build_template_graph, AttributeValue, ConditionalNode, ElementNode, FragmentNode, ListNode,
1062    TemplateAttribute, TemplateChild, TemplateGraph, TemplateNode, TemplateNodeId,
1063};
1064pub use template_manifest::{
1065    build_template_manifest, build_template_manifest_from_asm, template_manifest_json,
1066    validate_template_manifest, ManifestAction, ManifestBindingTarget, ManifestComponent,
1067    ManifestEvent, ManifestEventKind, ManifestFormBinding, ManifestFormHost, ManifestNode,
1068    ManifestOperation, ManifestOrdinaryBinding, ManifestOrdinaryEvent, ManifestOrdinaryTarget,
1069    ManifestTemplate, TemplateManifest, TEMPLATE_MANIFEST_SCHEMA_VERSION,
1070};
1071pub use template_semantics::{
1072    build_template_semantic_entities, TemplateSemanticEntity, TemplateSemanticKind,
1073    TemplateSemanticScope,
1074};
1075pub use v2_authoring_lowering::{
1076    lower_v2_authoring_v1, V2AuthoringLoweringErrorV1, V2AuthoringLoweringV1,
1077    V2AuthoringResolutionsV1,
1078};
1079pub use v2_authority_request::{
1080    build_v2_authority_component_request_v1, build_v2_authority_request_v1,
1081    build_v2_environment_authority_request_v1, V2AuthorityCanonicalV1, V2AuthorityFormFieldSiteV1,
1082    V2AuthorityMemberSiteV1, V2AuthorityPositionV1, V2AuthorityRequestErrorV1,
1083    V2AuthorityRequestV1, V2AuthoritySiteV1, V2_AUTHORITY_REQUEST_SCHEMA_VERSION,
1084};
1085pub use v2_authority_response::{
1086    v2_authoring_resolutions_from_response_v1, v2_environment_public_resolutions_from_response_v1,
1087    validate_v2_authority_response_v1, ResolvedEnvironmentPublicReadV1,
1088    V2AuthorityFormFieldResolutionV1, V2AuthorityFormFieldValueClassificationV1,
1089    V2AuthorityIdentityV1, V2AuthorityResolutionV1, V2AuthorityResponseErrorV1,
1090    V2AuthorityResponseV1, V2_AUTHORITY_RESPONSE_SCHEMA_VERSION,
1091};
1092
1093#[cfg(test)]
1094mod tests {
1095    use super::*;
1096    use std::{collections::BTreeMap, path::Path};
1097
1098    #[test]
1099    fn summarizes_component_decorator_class_and_render_method() {
1100        let source = r#"
1101@component("x-counter")
1102class Counter extends Component {
1103  render() {
1104    return <button>Count</button>;
1105  }
1106}
1107"#;
1108
1109        let summary = summarize_source("Counter.tsx", source);
1110
1111        assert_eq!(summary.component_decorators.len(), 1);
1112        assert_eq!(summary.component_classes.len(), 1);
1113        assert_eq!(
1114            summary.component_decorators[0].argument.as_deref(),
1115            Some("x-counter")
1116        );
1117        assert_eq!(summary.class_declarations.len(), 1);
1118        assert_eq!(summary.class_declarations[0].name, "Counter");
1119        assert_eq!(summary.render_methods.len(), 1);
1120        assert!(summary.has_tsx_like_syntax);
1121    }
1122
1123    #[test]
1124    fn recognizes_decorator_free_component_classes_without_ps0100() {
1125        let source = r#"
1126import { Component } from "presolve";
1127
1128export default class Home extends Component {
1129  render() {
1130    return <main>Home</main>;
1131  }
1132}
1133"#;
1134
1135        let summary = summarize_source("app/routes/index.tsx", source);
1136
1137        assert_eq!(summary.component_classes.len(), 1);
1138        assert_eq!(summary.component_classes[0].name, "Home");
1139        assert!(summary.component_decorators.is_empty());
1140        assert!(!summary
1141            .diagnostics
1142            .iter()
1143            .any(|diagnostic| diagnostic.code == "PS0100"));
1144    }
1145
1146    #[test]
1147    fn emits_diagnostics_for_empty_source() {
1148        let summary = summarize_source("Empty.tsx", "");
1149        assert!(summary
1150            .diagnostics
1151            .iter()
1152            .any(|diagnostic| diagnostic.code == "PS0001"));
1153    }
1154
1155    #[test]
1156    fn fixture_0001_source_summary_explain_text_matches_expected() {
1157        let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
1158            .join("../..")
1159            .join("fixtures/0001-source-summary");
1160
1161        let input_path = fixture_root.join("input/Counter.tsx");
1162        let expected_path = fixture_root.join("expected/explain.txt");
1163
1164        let source = std::fs::read_to_string(&input_path).expect("failed to read fixture input");
1165        let expected = std::fs::read_to_string(&expected_path)
1166            .expect("failed to read expected explain output");
1167
1168        let summary = summarize_source("fixtures/0001-source-summary/input/Counter.tsx", &source);
1169
1170        let actual = explain_text(&summary);
1171
1172        assert_eq!(actual, expected);
1173    }
1174
1175    #[test]
1176    fn fixture_0001_source_summary_explain_json_matches_expected() {
1177        let fixture_root = Path::new(env!("CARGO_MANIFEST_DIR"))
1178            .join("../..")
1179            .join("fixtures/0001-source-summary");
1180
1181        let input_path = fixture_root.join("input/Counter.tsx");
1182        let expected_path = fixture_root.join("expected/explain.json");
1183
1184        let source = std::fs::read_to_string(&input_path).expect("failed to read fixture input");
1185        let expected = std::fs::read_to_string(&expected_path)
1186            .expect("failed to read expected JSON explain output");
1187
1188        let summary = summarize_source("fixtures/0001-source-summary/input/Counter.tsx", &source);
1189
1190        let actual = explain_json(&summary);
1191
1192        let actual_json: serde_json::Value =
1193            serde_json::from_str(&actual).expect("actual explain JSON is invalid");
1194        let expected_json: serde_json::Value =
1195            serde_json::from_str(&expected).expect("expected explain JSON fixture is invalid");
1196
1197        assert_eq!(actual_json, expected_json);
1198    }
1199
1200    #[test]
1201    #[allow(clippy::too_many_lines)]
1202    fn builds_component_graph_from_parsed_counter() {
1203        let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
1204
1205        let parsed =
1206            presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
1207
1208        let graph = build_component_graph(&parsed);
1209
1210        assert!(graph.diagnostics.is_empty());
1211
1212        let component = graph.components.first().expect("expected component");
1213
1214        assert_eq!(component.class_name, "Counter");
1215        assert_eq!(component.id.as_str(), "component:x-counter");
1216        assert_eq!(component.owner, SemanticOwner::Application);
1217        assert_eq!(component.element_name.as_deref(), Some("x-counter"));
1218        assert_eq!(component.route_path.as_deref(), Some("/counter"));
1219
1220        assert_eq!(component.state_fields.len(), 1);
1221        assert_eq!(component.state_fields[0].name, "count");
1222        assert_eq!(
1223            component.state_fields[0].id.as_str(),
1224            "component:x-counter/state:count"
1225        );
1226        assert_eq!(
1227            component.state_fields[0].owner,
1228            SemanticOwner::entity(component.id.clone())
1229        );
1230        assert_eq!(
1231            component.state_fields[0].initial_value,
1232            Some(SerializableValue::Number("0".to_string()))
1233        );
1234
1235        let method_names = component
1236            .methods
1237            .iter()
1238            .map(|method| method.name.as_str())
1239            .collect::<Vec<_>>();
1240
1241        assert_eq!(method_names, vec!["increment", "render"]);
1242        assert_eq!(
1243            component.methods[0].id.as_str(),
1244            "component:x-counter/method:increment"
1245        );
1246        assert_eq!(
1247            component.methods[0].owner,
1248            SemanticOwner::entity(component.id.clone())
1249        );
1250        assert_eq!(
1251            component.actions,
1252            vec![ComponentAction {
1253                id: SemanticId::component(Some("x-counter"), "Counter").action("increment", 0),
1254                owner: SemanticOwner::entity(
1255                    SemanticId::component(Some("x-counter"), "Counter").method("increment"),
1256                ),
1257                method: "increment".to_string(),
1258                operation: StateOperation::AddAssign(SerializableValue::Number("1".to_string())),
1259                field: "count".to_string(),
1260            }]
1261        );
1262
1263        let render = component.render.as_ref().expect("expected render model");
1264
1265        assert_eq!(render.root_element.as_deref(), Some("button"));
1266        assert_eq!(render.attributes.len(), 1);
1267        assert_eq!(render.attributes[0].name, "onClick");
1268        assert!(matches!(
1269            render.attributes[0].value,
1270            RenderAttributeValue::Expression(_)
1271        ));
1272        assert_eq!(render.bindings, vec!["this.count"]);
1273        assert_eq!(render.root_span.expect("expected root span").line, 12);
1274        assert_eq!(render.root_span.expect("expected root span").column, 7);
1275        assert_eq!(render.event_handlers.len(), 1);
1276        assert_eq!(
1277            render.event_handlers[0].id.as_str(),
1278            "component:x-counter/event:click:0"
1279        );
1280        assert_eq!(
1281            render.event_handlers[0].owner,
1282            SemanticOwner::entity(component.id.template())
1283        );
1284        assert_eq!(render.event_handlers[0].event, "click");
1285        assert_eq!(render.event_handlers[0].handler, "this.increment");
1286        assert_eq!(render.event_handlers[0].span.line, 12);
1287        assert_eq!(render.event_handlers[0].span.column, 15);
1288        assert_eq!(render.children.len(), 2);
1289
1290        let RenderChild::Text { value, span } = &render.children[0] else {
1291            panic!("expected text child");
1292        };
1293        assert_eq!(value, "Count:");
1294        assert_eq!(span.line, 13);
1295        assert_eq!(span.column, 9);
1296
1297        let RenderChild::Binding { expression, span } = &render.children[1] else {
1298            panic!("expected binding child");
1299        };
1300        assert_eq!(expression, "this.count");
1301        assert_eq!(span.line, 13);
1302        assert_eq!(span.column, 16);
1303
1304        assert_eq!(graph.references.len(), 2);
1305        assert_eq!(graph.references[0].kind, SemanticReferenceKind::ActionState);
1306        assert_eq!(
1307            graph.references[0].source,
1308            SemanticId::component(Some("x-counter"), "Counter").action("increment", 0)
1309        );
1310        assert_eq!(
1311            graph.references[0].target,
1312            SemanticId::component(Some("x-counter"), "Counter").state_field("count")
1313        );
1314        assert_eq!(
1315            graph.references[0].provenance.path,
1316            Path::new("fixtures/0001-source-summary/input/Counter.tsx")
1317        );
1318        assert_eq!(graph.references[0].provenance.span.line, 7);
1319
1320        assert_eq!(graph.references[1].kind, SemanticReferenceKind::EventMethod);
1321        assert_eq!(
1322            graph.references[1].source,
1323            SemanticId::component(Some("x-counter"), "Counter").event_handler("click", 0)
1324        );
1325        assert_eq!(
1326            graph.references[1].target,
1327            SemanticId::component(Some("x-counter"), "Counter").method("increment")
1328        );
1329        assert_eq!(
1330            graph.references[1].provenance.path,
1331            Path::new("fixtures/0001-source-summary/input/Counter.tsx")
1332        );
1333        assert_eq!(graph.references[1].provenance.span.line, 12);
1334
1335        assert_eq!(graph.provenance[&component.id].span.line, 1);
1336        assert_eq!(graph.provenance[&component.state_fields[0].id].span.line, 4);
1337        assert_eq!(graph.provenance[&component.methods[0].id].span.line, 6);
1338        assert_eq!(graph.provenance[&component.actions[0].id].span.line, 7);
1339        assert_eq!(graph.provenance[&component.id.template()].span.line, 10);
1340        assert_eq!(graph.provenance[&render.event_handlers[0].id].span.line, 12);
1341    }
1342
1343    #[test]
1344    fn lowers_method_parameters_into_canonical_method_metadata() {
1345        let parsed = presolve_parser::parse_file(
1346            "src/Parameters.tsx",
1347            r#"
1348@component("x-parameters")
1349class Parameters extends Component {
1350  save(title: string, retries?: number) {}
1351}
1352"#,
1353        );
1354
1355        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1356        let method = &graph.components[0].methods[0];
1357
1358        assert_eq!(method.name, "save");
1359        assert_eq!(
1360            method
1361                .parameters
1362                .iter()
1363                .map(|parameter| parameter.name.as_str())
1364                .collect::<Vec<_>>(),
1365            vec!["title", "retries"]
1366        );
1367        assert_eq!(method.parameters[0].span.line, 4);
1368        assert_eq!(method.parameters[1].span.line, 4);
1369    }
1370
1371    #[test]
1372    fn assembles_application_semantic_model_from_existing_graphs() {
1373        let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
1374        let parsed =
1375            presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
1376
1377        let asm = build_application_semantic_model(&parsed);
1378        let component = &asm.components[0];
1379
1380        assert!(asm.diagnostics.is_empty());
1381        assert_eq!(asm.templates.len(), 1);
1382        assert!(asm.template_entities.len() >= 3);
1383        assert_eq!(asm.references.len(), 4);
1384        assert_eq!(asm.ownership.len(), asm.provenance.len());
1385        assert_eq!(asm.ownership[&component.id], SemanticOwner::Application);
1386        assert_eq!(
1387            asm.ownership[&component.state_fields[0].id],
1388            SemanticOwner::entity(component.id.clone())
1389        );
1390        assert_eq!(
1391            asm.ownership[&component.actions[0].id],
1392            SemanticOwner::entity(component.methods[0].id.clone())
1393        );
1394        assert_eq!(
1395            asm.ownership[&asm.templates[0].id],
1396            SemanticOwner::entity(component.id.clone())
1397        );
1398        assert_eq!(asm.provenance[&asm.templates[0].id].span.line, 10);
1399
1400        assert!(matches!(
1401            asm.entity(&component.id),
1402            Some(SemanticEntity::Component(_))
1403        ));
1404        assert!(matches!(
1405            asm.entity(&component.state_fields[0].id),
1406            Some(SemanticEntity::StateField(_))
1407        ));
1408        assert_eq!(asm.component(&component.id), Some(component));
1409        assert_eq!(asm.template(&asm.templates[0].id), Some(&asm.templates[0]));
1410        assert_eq!(
1411            asm.owner(&component.actions[0].id),
1412            Some(&component.actions[0].owner)
1413        );
1414        assert_eq!(asm.provenance(&asm.templates[0].id).unwrap().span.line, 10);
1415        let template_binding = asm
1416            .template_entities
1417            .iter()
1418            .find(|entity| entity.kind == TemplateSemanticKind::Binding)
1419            .expect("template binding entity");
1420        assert!(matches!(
1421            asm.entity(&template_binding.id),
1422            Some(SemanticEntity::TemplateEntity(_))
1423        ));
1424        assert_eq!(
1425            asm.template_entities_for(&asm.templates[0].id).len(),
1426            asm.template_entities.len()
1427        );
1428        assert_eq!(asm.references_from(&component.actions[0].id).len(), 1);
1429        assert_eq!(asm.references_to(&component.state_fields[0].id).len(), 2);
1430        assert_eq!(asm.references_to(&component.methods[0].id).len(), 2);
1431        assert!(validate_application_semantic_model(&asm).is_empty());
1432
1433        let mut invalid = asm.clone();
1434        invalid.provenance.remove(&component.actions[0].id);
1435        let diagnostics = validate_application_semantic_model(&invalid);
1436        let codes = diagnostics
1437            .iter()
1438            .map(|diagnostic| diagnostic.code.as_str())
1439            .collect::<Vec<_>>();
1440        assert!(codes.contains(&"PSASM1002"));
1441        assert!(codes.contains(&"PSASM1006"));
1442
1443        let mut invalid_type = asm.clone();
1444        let state_id = component.state_fields[0].id.clone();
1445        invalid_type
1446            .semantic_types
1447            .assignments
1448            .get_mut(&state_id)
1449            .expect("state type")
1450            .id = SemanticTypeId::for_subject(&component.id);
1451        let diagnostics = validate_application_semantic_model(&invalid_type);
1452        assert!(diagnostics
1453            .iter()
1454            .any(|diagnostic| diagnostic.code == "PSASM1102"));
1455
1456        let dependencies = DependencyAnalysisPass.analyze(&asm);
1457        assert_eq!(dependencies.dependencies[&component.actions[0].id].len(), 1);
1458        assert_eq!(
1459            dependencies.dependents[&component.state_fields[0].id].len(),
1460            2
1461        );
1462    }
1463
1464    #[test]
1465    fn carries_declared_state_types_into_component_and_asm_data() {
1466        let parsed = presolve_parser::parse_file(
1467            "src/Panel.tsx",
1468            r#"
1469@component("x-panel")
1470class Panel extends Component {
1471  count: number = state(0);
1472}
1473"#,
1474        );
1475
1476        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1477        let graph_type = graph.components[0].state_fields[0]
1478            .declared_type
1479            .as_ref()
1480            .expect("declared state type");
1481
1482        assert_eq!(graph_type.text, "number");
1483        assert_eq!(graph_type.provenance.path, Path::new("src/Panel.tsx"));
1484        assert_eq!(graph_type.provenance.span.line, 4);
1485        assert_eq!(graph_type.provenance.span.column, 8);
1486        assert_eq!(graph_type.kind, Some(DeclaredStateTypeKind::Number));
1487
1488        let asm = build_application_semantic_model(&parsed);
1489        assert_eq!(
1490            asm.components[0].state_fields[0].declared_type,
1491            Some(graph_type.clone())
1492        );
1493    }
1494
1495    #[test]
1496    fn lowers_and_evaluates_constant_arithmetic_state_initializers() {
1497        let parsed = presolve_parser::parse_file(
1498            "src/ArithmeticState.tsx",
1499            r#"
1500@component("x-arithmetic-state")
1501class ArithmeticState extends Component {
1502  total: number = state((1 + 2) * 3);
1503  difference: number = state(10 - 3);
1504  quotient: number = state(10 / 2);
1505  remainder: number = state(10 % 3);
1506
1507  render() {
1508    return <output>{this.total}</output>;
1509  }
1510}
1511"#,
1512        );
1513        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1514        let field = &graph.components[0].state_fields[0];
1515
1516        assert_eq!(
1517            field
1518                .initial_expression
1519                .as_ref()
1520                .map(ToString::to_string)
1521                .as_deref(),
1522            Some("((1 + 2) * 3)")
1523        );
1524        assert_eq!(
1525            field.initial_value,
1526            Some(SerializableValue::Number("9".to_string()))
1527        );
1528        assert_eq!(
1529            graph.components[0].state_fields[1].initial_value,
1530            Some(SerializableValue::Number("7".to_string()))
1531        );
1532        assert_eq!(
1533            graph.components[0].state_fields[2].initial_value,
1534            Some(SerializableValue::Number("5".to_string()))
1535        );
1536        assert_eq!(
1537            graph.components[0].state_fields[3].initial_value,
1538            Some(SerializableValue::Number("1".to_string()))
1539        );
1540        assert!(graph.diagnostics.is_empty());
1541    }
1542
1543    #[test]
1544    fn reports_invalid_constant_arithmetic_state_initializers() {
1545        let parsed = presolve_parser::parse_file(
1546            "src/ArithmeticState.tsx",
1547            r#"
1548@component("x-arithmetic-state")
1549class ArithmeticState extends Component {
1550  total: number = state(10 / 0);
1551}
1552"#,
1553        );
1554        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1555        let diagnostic = graph
1556            .diagnostics
1557            .iter()
1558            .find(|diagnostic| diagnostic.code == "PSC1022")
1559            .expect("arithmetic diagnostic");
1560
1561        assert!(diagnostic.message.contains("division or remainder by zero"));
1562        assert_eq!(
1563            diagnostic
1564                .provenance
1565                .as_ref()
1566                .map(|provenance| provenance.span.line),
1567            Some(4)
1568        );
1569    }
1570
1571    #[test]
1572    fn lowers_and_evaluates_constant_comparison_state_initializers() {
1573        let parsed = presolve_parser::parse_file(
1574            "src/ComparisonState.tsx",
1575            r#"
1576@component("x-comparison-state")
1577class ComparisonState extends Component {
1578  equal: boolean = state(3 === 3);
1579  notEqual: boolean = state(3 !== 4);
1580  lessThan: boolean = state(2 < 3);
1581  lessThanOrEqual: boolean = state(3 <= 3);
1582  greaterThan: boolean = state(4 > 3);
1583  ready: boolean = state(((1 + 2) * 3) >= 9);
1584
1585  render() {
1586    return <output>{this.ready}</output>;
1587  }
1588}
1589"#,
1590        );
1591        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1592        let fields = &graph.components[0].state_fields;
1593
1594        assert_eq!(
1595            fields[0]
1596                .initial_expression
1597                .as_ref()
1598                .map(ToString::to_string)
1599                .as_deref(),
1600            Some("(3 === 3)")
1601        );
1602        for field in fields {
1603            assert_eq!(field.initial_value, Some(SerializableValue::Boolean(true)));
1604        }
1605        assert_eq!(
1606            fields[5]
1607                .initial_expression
1608                .as_ref()
1609                .map(ToString::to_string)
1610                .as_deref(),
1611            Some("(((1 + 2) * 3) >= 9)")
1612        );
1613        assert!(graph.diagnostics.is_empty());
1614    }
1615
1616    #[test]
1617    fn reports_invalid_constant_comparison_state_initializers() {
1618        let parsed = presolve_parser::parse_file(
1619            "src/ComparisonState.tsx",
1620            r#"
1621@component("x-comparison-state")
1622class ComparisonState extends Component {
1623  ready: boolean = state((10 / 0) >= 1);
1624}
1625"#,
1626        );
1627        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1628        let diagnostic = graph
1629            .diagnostics
1630            .iter()
1631            .find(|diagnostic| diagnostic.code == "PSC1023")
1632            .expect("comparison diagnostic");
1633
1634        assert!(diagnostic.message.contains("division or remainder by zero"));
1635        assert_eq!(
1636            diagnostic
1637                .provenance
1638                .as_ref()
1639                .map(|provenance| provenance.span.line),
1640            Some(4)
1641        );
1642    }
1643
1644    #[test]
1645    fn lowers_and_evaluates_constant_logical_state_initializers() {
1646        let parsed = presolve_parser::parse_file(
1647            "src/LogicalState.tsx",
1648            r#"
1649@component("x-logical-state")
1650class LogicalState extends Component {
1651  both: boolean = state((1 < 2) && (3 >= 3));
1652  either: boolean = state(false || (10 !== 4));
1653  shortAnd: boolean = state(false && ((10 / 0) > 1));
1654  shortOr: boolean = state(true || ((10 / 0) > 1));
1655
1656  render() {
1657    return <output>{this.both}</output>;
1658  }
1659}
1660"#,
1661        );
1662        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1663        let fields = &graph.components[0].state_fields;
1664
1665        assert_eq!(
1666            fields[0]
1667                .initial_expression
1668                .as_ref()
1669                .map(ToString::to_string)
1670                .as_deref(),
1671            Some("((1 < 2) && (3 >= 3))")
1672        );
1673        assert_eq!(
1674            fields
1675                .iter()
1676                .map(|field| field.initial_value.clone())
1677                .collect::<Vec<_>>(),
1678            vec![
1679                Some(SerializableValue::Boolean(true)),
1680                Some(SerializableValue::Boolean(true)),
1681                Some(SerializableValue::Boolean(false)),
1682                Some(SerializableValue::Boolean(true)),
1683            ]
1684        );
1685        assert!(graph.diagnostics.is_empty());
1686    }
1687
1688    #[test]
1689    fn reports_evaluated_invalid_constant_logical_state_initializers() {
1690        let parsed = presolve_parser::parse_file(
1691            "src/LogicalState.tsx",
1692            r#"
1693@component("x-logical-state")
1694class LogicalState extends Component {
1695  ready: boolean = state(true && ((10 / 0) > 1));
1696}
1697"#,
1698        );
1699        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1700        let diagnostic = graph
1701            .diagnostics
1702            .iter()
1703            .find(|diagnostic| diagnostic.code == "PSC1024")
1704            .expect("logical diagnostic");
1705
1706        assert!(diagnostic.message.contains("division or remainder by zero"));
1707        assert_eq!(
1708            diagnostic
1709                .provenance
1710                .as_ref()
1711                .map(|provenance| provenance.span.line),
1712            Some(4)
1713        );
1714    }
1715
1716    #[test]
1717    fn lowers_and_evaluates_constant_nullish_state_initializers() {
1718        let parsed = presolve_parser::parse_file(
1719            "src/NullishState.tsx",
1720            r#"
1721@component("x-nullish-state")
1722class NullishState extends Component {
1723  label: string = state(null ?? "fallback");
1724  total: number = state(5 ?? (10 / 0));
1725
1726  render() {
1727    return <output>{this.label}</output>;
1728  }
1729}
1730"#,
1731        );
1732        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1733        let fields = &graph.components[0].state_fields;
1734
1735        assert_eq!(
1736            fields[0]
1737                .initial_expression
1738                .as_ref()
1739                .map(ToString::to_string)
1740                .as_deref(),
1741            Some("(null ?? \"fallback\")")
1742        );
1743        assert_eq!(
1744            fields[0].initial_value,
1745            Some(SerializableValue::String("fallback".to_string()))
1746        );
1747        assert_eq!(
1748            fields[1].initial_value,
1749            Some(SerializableValue::Number("5".to_string()))
1750        );
1751        assert!(graph.diagnostics.is_empty());
1752    }
1753
1754    #[test]
1755    fn lowers_and_evaluates_constant_unary_state_initializers() {
1756        let parsed = presolve_parser::parse_file(
1757            "src/UnaryState.tsx",
1758            r#"
1759@component("x-unary-state")
1760class UnaryState extends Component {
1761  negated: boolean = state(!(1 < 2));
1762  signed: number = state(-(1 + 2));
1763  render() { return <output>{this.signed}</output>; }
1764}
1765"#,
1766        );
1767        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1768        assert_eq!(
1769            graph.components[0].state_fields[0].initial_value,
1770            Some(SerializableValue::Boolean(false))
1771        );
1772        assert_eq!(
1773            graph.components[0].state_fields[1].initial_value,
1774            Some(SerializableValue::Number("-3".to_string()))
1775        );
1776    }
1777
1778    #[test]
1779    fn reports_reached_invalid_constant_nullish_state_initializers() {
1780        let parsed = presolve_parser::parse_file(
1781            "src/NullishState.tsx",
1782            r#"
1783@component("x-nullish-state")
1784class NullishState extends Component {
1785  total: number = state(null ?? (10 / 0));
1786}
1787"#,
1788        );
1789        let graph = fold_component_graph(&build_component_graph_for_module(&parsed));
1790        let diagnostic = graph
1791            .diagnostics
1792            .iter()
1793            .find(|diagnostic| diagnostic.code == "PSC1025")
1794            .expect("nullish diagnostic");
1795        assert!(diagnostic.message.contains("division or remainder by zero"));
1796    }
1797
1798    #[test]
1799    fn classifies_exact_primitive_declared_state_types() {
1800        let source =
1801            include_str!("../../../fixtures/0025-typed-state-annotations/input/TypedState.tsx");
1802        let parsed = presolve_parser::parse_file(
1803            "fixtures/0025-typed-state-annotations/input/TypedState.tsx",
1804            source,
1805        );
1806        let graph = build_component_graph_for_module(&parsed);
1807        let kinds = graph.components[0]
1808            .state_fields
1809            .iter()
1810            .map(|field| {
1811                (
1812                    field.name.as_str(),
1813                    field
1814                        .declared_type
1815                        .as_ref()
1816                        .and_then(|declared_type| declared_type.kind),
1817                )
1818            })
1819            .collect::<Vec<_>>();
1820
1821        assert_eq!(
1822            kinds,
1823            vec![
1824                ("count", Some(DeclaredStateTypeKind::Number)),
1825                ("status", None),
1826                ("title", Some(DeclaredStateTypeKind::String)),
1827                ("enabled", Some(DeclaredStateTypeKind::Boolean)),
1828                ("empty", Some(DeclaredStateTypeKind::Null)),
1829            ]
1830        );
1831    }
1832
1833    #[test]
1834    fn reports_primitive_declared_state_initializer_mismatches() {
1835        let source = include_str!(
1836            "../../../fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx"
1837        );
1838        let parsed = presolve_parser::parse_file(
1839            "fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx",
1840            source,
1841        );
1842        let graph = build_component_graph_for_module(&parsed);
1843        let folded = ConstantFoldingPass.transform(
1844            &build_application_semantic_model_from_component_graph(&graph),
1845        );
1846        let diagnostics = folded
1847            .diagnostics
1848            .iter()
1849            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1850            .collect::<Vec<_>>();
1851
1852        assert_eq!(diagnostics.len(), 6);
1853        assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1016"));
1854
1855        let provenance = folded.diagnostics[0]
1856            .provenance
1857            .as_ref()
1858            .expect("mismatch diagnostic provenance");
1859        assert_eq!(
1860            provenance.path,
1861            Path::new("fixtures/0027-declared-state-type-diagnostics/input/InvalidTypedState.tsx")
1862        );
1863        assert_eq!(provenance.span.line, 3);
1864        assert_eq!(provenance.span.column, 8);
1865    }
1866
1867    #[test]
1868    fn reports_primitive_declared_state_action_assignment_mismatches() {
1869        let source = include_str!(
1870            "../../../fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx"
1871        );
1872        let parsed = presolve_parser::parse_file(
1873            "fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx",
1874            source,
1875        );
1876        let graph = build_component_graph_for_module(&parsed);
1877        let folded = ConstantFoldingPass.transform(
1878            &build_application_semantic_model_from_component_graph(&graph),
1879        );
1880        let diagnostics = folded
1881            .diagnostics
1882            .iter()
1883            .filter(|diagnostic| diagnostic.code == "PSC1017")
1884            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1885            .collect::<Vec<_>>();
1886
1887        assert_eq!(diagnostics.len(), 6);
1888        assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1017"));
1889        assert!(diagnostics.iter().any(|(_, message)| {
1890            message.contains("state field `status`") && message.contains("assigns `number`")
1891        }));
1892        assert!(diagnostics.iter().any(|(_, message)| {
1893            message.contains("state field `collection`") && message.contains("assigns `tuple`")
1894        }));
1895
1896        let provenance = folded
1897            .diagnostics
1898            .iter()
1899            .find(|diagnostic| diagnostic.code == "PSC1017")
1900            .expect("action mismatch diagnostic")
1901            .provenance
1902            .as_ref()
1903            .expect("action mismatch diagnostic provenance");
1904        assert_eq!(
1905            provenance.path,
1906            Path::new(
1907                "fixtures/0028-primitive-action-type-diagnostics/input/InvalidTypedActions.tsx"
1908            )
1909        );
1910        assert_eq!(provenance.span.line, 11);
1911        assert_eq!(provenance.span.column, 5);
1912    }
1913
1914    #[test]
1915    fn reports_non_boolean_primitive_toggle_actions() {
1916        let source = include_str!(
1917            "../../../fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx"
1918        );
1919        let parsed = presolve_parser::parse_file(
1920            "fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx",
1921            source,
1922        );
1923        let graph = build_component_graph_for_module(&parsed);
1924        let folded = ConstantFoldingPass.transform(
1925            &build_application_semantic_model_from_component_graph(&graph),
1926        );
1927        let diagnostics = folded
1928            .diagnostics
1929            .iter()
1930            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1931            .collect::<Vec<_>>();
1932
1933        assert_eq!(diagnostics.len(), 4);
1934        assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1018"));
1935
1936        let provenance = folded.diagnostics[0]
1937            .provenance
1938            .as_ref()
1939            .expect("toggle diagnostic provenance");
1940        assert_eq!(
1941            provenance.path,
1942            Path::new(
1943                "fixtures/0029-primitive-toggle-type-diagnostics/input/InvalidTypedToggles.tsx"
1944            )
1945        );
1946        assert_eq!(provenance.span.line, 10);
1947        assert_eq!(provenance.span.column, 5);
1948    }
1949
1950    #[test]
1951    fn reports_non_numeric_primitive_increment_and_decrement_actions() {
1952        let source = include_str!(
1953            "../../../fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx"
1954        );
1955        let parsed = presolve_parser::parse_file(
1956            "fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx",
1957            source,
1958        );
1959        let graph = build_component_graph_for_module(&parsed);
1960        let folded = ConstantFoldingPass.transform(
1961            &build_application_semantic_model_from_component_graph(&graph),
1962        );
1963        let diagnostics = folded
1964            .diagnostics
1965            .iter()
1966            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
1967            .collect::<Vec<_>>();
1968
1969        assert_eq!(diagnostics.len(), 4);
1970        assert!(diagnostics.iter().all(|(code, _)| *code == "PSC1019"));
1971
1972        let provenance = folded.diagnostics[0]
1973            .provenance
1974            .as_ref()
1975            .expect("numeric action diagnostic provenance");
1976        assert_eq!(
1977            provenance.path,
1978            Path::new(
1979                "fixtures/0030-primitive-numeric-action-type-diagnostics/input/InvalidTypedNumericActions.tsx"
1980            )
1981        );
1982        assert_eq!(provenance.span.line, 10);
1983        assert_eq!(provenance.span.column, 5);
1984    }
1985
1986    #[test]
1987    fn reports_compound_numeric_action_target_and_operand_mismatches() {
1988        let source = include_str!(
1989            "../../../fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx"
1990        );
1991        let parsed = presolve_parser::parse_file(
1992            "fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx",
1993            source,
1994        );
1995        let graph = build_component_graph_for_module(&parsed);
1996        let folded = ConstantFoldingPass.transform(
1997            &build_application_semantic_model_from_component_graph(&graph),
1998        );
1999        let diagnostics = folded
2000            .diagnostics
2001            .iter()
2002            .map(|diagnostic| (diagnostic.code.as_str(), diagnostic.message.as_str()))
2003            .collect::<Vec<_>>();
2004
2005        assert_eq!(diagnostics.len(), 7);
2006        assert_eq!(
2007            diagnostics
2008                .iter()
2009                .filter(|(code, _)| *code == "PSC1020")
2010                .count(),
2011            3
2012        );
2013        assert_eq!(
2014            diagnostics
2015                .iter()
2016                .filter(|(code, _)| *code == "PSC1021")
2017                .count(),
2018            4
2019        );
2020
2021        let provenance = folded.diagnostics[0]
2022            .provenance
2023            .as_ref()
2024            .expect("compound action diagnostic provenance");
2025        assert_eq!(
2026            provenance.path,
2027            Path::new(
2028                "fixtures/0031-primitive-compound-action-type-diagnostics/input/InvalidTypedCompoundActions.tsx"
2029            )
2030        );
2031        assert_eq!(provenance.span.line, 9);
2032        assert_eq!(provenance.span.column, 5);
2033    }
2034
2035    #[test]
2036    fn assembles_application_semantic_model_from_multiple_files() {
2037        let unit = CompilationUnit::parse_sources([
2038            (
2039                "src/Zeta.tsx",
2040                r#"
2041@component("x-zeta")
2042class Zeta extends Component {
2043  render() {
2044    return <div>Zeta</div>;
2045  }
2046}
2047"#,
2048            ),
2049            (
2050                "src/Alpha.tsx",
2051                r#"
2052@component("x-alpha")
2053class Alpha extends Component {
2054  render() {
2055    return <div>Alpha</div>;
2056  }
2057}
2058"#,
2059            ),
2060        ]);
2061
2062        let asm = build_application_semantic_model_for_unit(&unit);
2063
2064        assert_eq!(
2065            unit.files()
2066                .iter()
2067                .map(|file| file.path.to_string_lossy().into_owned())
2068                .collect::<Vec<_>>(),
2069            vec!["src/Alpha.tsx", "src/Zeta.tsx"]
2070        );
2071        assert_eq!(
2072            asm.components
2073                .iter()
2074                .map(|component| component.id.as_str())
2075                .collect::<Vec<_>>(),
2076            vec![
2077                "module:src/Alpha.tsx/component:x-alpha",
2078                "module:src/Zeta.tsx/component:x-zeta"
2079            ]
2080        );
2081        assert!(asm.diagnostics.is_empty());
2082        assert!(validate_application_semantic_model(&asm).is_empty());
2083    }
2084
2085    #[test]
2086    fn component_graph_reports_semantic_errors() {
2087        let source =
2088            include_str!("../../../fixtures/0003-semantic-errors/input/BrokenSemantics.tsx");
2089
2090        let parsed = presolve_parser::parse_file(
2091            "fixtures/0003-semantic-errors/input/BrokenSemantics.tsx",
2092            source,
2093        );
2094
2095        let graph = build_component_graph(&parsed);
2096
2097        let codes = graph
2098            .diagnostics
2099            .iter()
2100            .map(|diagnostic| diagnostic.code.as_str())
2101            .collect::<Vec<_>>();
2102
2103        assert!(codes.contains(&"PSC1001"));
2104        assert!(codes.contains(&"PSC1003"));
2105        assert!(codes.contains(&"PSC1004"));
2106        assert!(graph.references.is_empty());
2107    }
2108
2109    #[test]
2110    fn component_graph_reports_unsupported_event_errors() {
2111        let source = r#"
2112@component("x-counter")
2113class Counter extends Component {
2114  count = state(0);
2115
2116  increment() {
2117    this.count++;
2118  }
2119
2120  render() {
2121    return <button onMouseover={() => this.increment()}>Count: {this.count}</button>;
2122  }
2123}
2124"#;
2125
2126        let parsed = presolve_parser::parse_file("UnsupportedEvent.tsx", source);
2127
2128        let graph = build_component_graph(&parsed);
2129
2130        assert!(graph
2131            .diagnostics
2132            .iter()
2133            .any(|diagnostic| diagnostic.code == "PSC1005"));
2134    }
2135
2136    #[test]
2137    fn component_graph_validates_static_action_parameter_bindings() {
2138        let source = r#"
2139@component("x-parameterized")
2140class Parameterized extends Component {
2141  label = state("Ready");
2142
2143  @action() setLabel(value: string) {
2144    this.label = value;
2145  }
2146
2147  render() {
2148    return <button onClick={() => this.setLabel(1)}>{this.label}</button>;
2149  }
2150}
2151"#;
2152
2153        let parsed = presolve_parser::parse_file("Parameterized.tsx", source);
2154        let graph = build_component_graph(&parsed);
2155        let codes = graph
2156            .diagnostics
2157            .iter()
2158            .map(|diagnostic| diagnostic.code.as_str())
2159            .collect::<Vec<_>>();
2160
2161        assert!(codes.contains(&"PSC1043"));
2162        assert!(!codes.contains(&"PSC1041"));
2163        assert!(!codes.contains(&"PSC1042"));
2164    }
2165
2166    #[test]
2167    fn component_graph_rejects_unbound_action_parameters() {
2168        let source = r#"
2169@component("x-parameterized")
2170class Parameterized extends Component {
2171  label = state("Ready");
2172
2173  @action() setLabel(value: string) {
2174    this.label = value;
2175  }
2176
2177  render() {
2178    return <button onClick={this.setLabel}>{this.label}</button>;
2179  }
2180}
2181"#;
2182
2183        let parsed = presolve_parser::parse_file("UnboundParameterized.tsx", source);
2184        let graph = build_component_graph(&parsed);
2185
2186        assert!(graph
2187            .diagnostics
2188            .iter()
2189            .any(|diagnostic| diagnostic.code == "PSC1042"));
2190    }
2191
2192    #[test]
2193    fn component_graph_requires_action_decorator_for_parameter_state_assignment() {
2194        let source = r#"
2195@component("x-parameterized")
2196class Parameterized extends Component {
2197  label = state("Ready");
2198
2199  setLabel(value: string) {
2200    this.label = value;
2201  }
2202
2203  render() {
2204    return <button onClick={() => this.setLabel("Locked")}>{this.label}</button>;
2205  }
2206}
2207"#;
2208
2209        let parsed = presolve_parser::parse_file("UndecoratedParameterized.tsx", source);
2210        let graph = build_component_graph(&parsed);
2211
2212        assert!(graph.diagnostics.iter().any(|diagnostic| {
2213            diagnostic.code == "PSC1041" && diagnostic.message.contains("requires @action()")
2214        }));
2215    }
2216
2217    #[test]
2218    fn component_graph_rejects_action_parameter_state_type_mismatch() {
2219        let source = r#"
2220@component("x-parameterized")
2221class Parameterized extends Component {
2222  count = state(0);
2223
2224  @action() setCount(value: string) {
2225    this.count = value;
2226  }
2227
2228  render() {
2229    return <button onClick={() => this.setCount("Locked")}>{this.count}</button>;
2230  }
2231}
2232"#;
2233
2234        let parsed = presolve_parser::parse_file("MismatchedParameterized.tsx", source);
2235        let graph = build_component_graph(&parsed);
2236
2237        assert!(graph
2238            .diagnostics
2239            .iter()
2240            .any(|diagnostic| diagnostic.code == "PSC1044"));
2241    }
2242
2243    #[test]
2244    fn component_graph_lowers_serializable_action_local_to_state_assignment() {
2245        let source = r#"
2246@component("x-action-local")
2247class ActionLocal extends Component {
2248  label = state("Ready");
2249
2250  @action() lock() {
2251    const next = "Locked";
2252    this.label = next;
2253  }
2254
2255  render() {
2256    return <button onClick={this.lock}>{this.label}</button>;
2257  }
2258}
2259"#;
2260
2261        let parsed = presolve_parser::parse_file("ActionLocal.tsx", source);
2262        let graph = build_component_graph(&parsed);
2263
2264        assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2265        assert!(matches!(
2266            graph.components[0].actions[0].operation,
2267            StateOperation::Assign(SerializableValue::String(ref value)) if value == "Locked"
2268        ));
2269    }
2270
2271    #[test]
2272    fn component_graph_lowers_structured_serializable_action_local_to_state_assignment() {
2273        let source = r#"
2274@component("x-action-local-record")
2275class ActionLocalRecord extends Component {
2276  profile = state({ name: "Ready", roles: ["reader"] });
2277
2278  @action() promote() {
2279    const next = { name: "Locked", roles: ["writer", "admin"] };
2280    this.profile = next;
2281  }
2282
2283  render() {
2284    return <button onClick={this.promote}>Promote</button>;
2285  }
2286}
2287"#;
2288
2289        let parsed = presolve_parser::parse_file("ActionLocalRecord.tsx", source);
2290        let graph = build_component_graph(&parsed);
2291
2292        assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2293        assert!(matches!(
2294            graph.components[0].actions[0].operation,
2295            StateOperation::Assign(SerializableValue::Object(_))
2296        ));
2297    }
2298
2299    #[test]
2300    fn component_graph_retains_resource_declaration_facts_before_package_resolution() {
2301        let parsed = presolve_parser::parse_file(
2302            "ResourceFact.tsx",
2303            r#"
2304@component("x-resource-fact")
2305class ResourceFact extends Component {
2306  @resource("profile") profile!: string;
2307  render() { return <div>Profile</div>; }
2308}
2309"#,
2310        );
2311        let graph = build_component_graph(&parsed);
2312        let facts = &graph.components[0].resource_declaration_candidates;
2313        assert_eq!(facts.len(), 1);
2314        assert_eq!(facts[0].field, "profile");
2315        assert!(facts[0].decorator_invoked);
2316        assert_eq!(facts[0].decorator_argument_count, 1);
2317        assert_eq!(facts[0].endpoint_designator.as_deref(), Some("profile"));
2318        assert!(graph.diagnostics.is_empty(), "{:?}", graph.diagnostics);
2319    }
2320
2321    #[test]
2322    fn component_graph_retains_and_validates_opaque_action_declarations() {
2323        let parsed = presolve_parser::parse_file(
2324            "OpaqueActions.tsx",
2325            r#"
2326@component("x-opaque-actions")
2327class OpaqueActions extends Component {
2328  @action() @opaque("@acme/analytics", "trackPurchase")
2329  track(): void {}
2330
2331  @action() @opaque("@acme/analytics")
2332  missingExport(): void {}
2333
2334  @opaque("@acme/analytics", "trackPurchase")
2335  missingAction(): void {}
2336
2337  @action() @opaque("@acme/analytics", "trackPurchase")
2338  writesState(): void { this.count++; }
2339
2340  count = state(0);
2341
2342  render() { return <button onClick={this.track}>Buy</button>; }
2343}
2344"#,
2345        );
2346
2347        let graph = build_component_graph(&parsed);
2348        let component = &graph.components[0];
2349        assert_eq!(component.opaque_action_facts.len(), 4);
2350
2351        let track = component
2352            .opaque_action_facts
2353            .iter()
2354            .find(|fact| fact.method_name == "track")
2355            .expect("retained track declaration");
2356        assert_eq!(
2357            track.id.as_str(),
2358            "component:x-opaque-actions/opaque-activation:track"
2359        );
2360        assert_eq!(track.package.as_deref(), Some("@acme/analytics"));
2361        assert_eq!(track.export.as_deref(), Some("trackPurchase"));
2362        assert!(track.is_action);
2363        assert!(track.action_invoked);
2364        assert!(!track.has_body_effects);
2365
2366        assert_eq!(
2367            graph
2368                .diagnostics
2369                .iter()
2370                .filter(|diagnostic| diagnostic.code == "PSC1130")
2371                .count(),
2372            3,
2373            "{:?}",
2374            graph.diagnostics
2375        );
2376    }
2377
2378    #[test]
2379    fn resolves_opaque_action_through_an_integrity_checked_terminal_package_contract() {
2380        let unit = CompilationUnit::parse_sources([(
2381            "src/Checkout.tsx",
2382            r#"
2383import { trackPurchase } from "@acme/analytics";
2384
2385@component("x-checkout")
2386class Checkout extends Component {
2387  @action() @opaque("@acme/analytics", "trackPurchase")
2388  track(): void {}
2389
2390  render() { return <button onClick={this.track}>Buy</button>; }
2391}
2392"#,
2393        )]);
2394        let contract = parse_semantic_package_contract(
2395            r#"{"schema_version":1,"package":"@acme/analytics","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"trackPurchase":{"kind":"opaque","type_signature":"() -> void","runtime_module":"dist/track.js","resume_policy":"cold_fallback","opaque_terminal":{"execution_boundary":"client","resume":"cold_fallback"}}}}"#,
2396        )
2397        .expect("opaque terminal contract");
2398        let mut packages = SemanticPackageResolutionTable::default();
2399        packages
2400            .insert("@acme/analytics".into(), contract)
2401            .expect("unique contract");
2402
2403        let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2404        assert_eq!(model.opaque_action_resolutions.len(), 1);
2405        let resolution = &model.opaque_action_resolutions[0];
2406        assert_eq!(
2407            resolution.activation.as_str(),
2408            "module:src/Checkout.tsx/component:x-checkout/opaque-activation:track"
2409        );
2410        let OpaqueActionResolutionOutcome::Resolved(binding) = &resolution.outcome else {
2411            panic!(
2412                "expected resolved opaque terminal: {:?}",
2413                resolution.outcome
2414            );
2415        };
2416        assert_eq!(binding.package, "@acme/analytics");
2417        assert_eq!(binding.version, "1.2.3");
2418        assert_eq!(binding.export, "trackPurchase");
2419        assert_eq!(binding.runtime_module, "dist/track.js");
2420        assert!(model
2421            .diagnostics
2422            .iter()
2423            .all(|diagnostic| diagnostic.code != "PSC1130" && diagnostic.code != "PSC1131"));
2424    }
2425
2426    #[test]
2427    fn opaque_action_rejects_a_nonopaque_package_export() {
2428        let unit = CompilationUnit::parse_sources([(
2429            "src/Checkout.tsx",
2430            r#"
2431import { format } from "date-kit";
2432
2433@component("x-checkout")
2434class Checkout extends Component {
2435  @action() @opaque("date-kit", "format")
2436  track(): void {}
2437
2438  render() { return <button onClick={this.track}>Buy</button>; }
2439}
2440"#,
2441        )]);
2442        let contract = parse_semantic_package_contract(
2443            r#"{"schema_version":1,"package":"date-kit","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"(Date) -> string","runtime_module":"dist/format.js","resume_policy":"input_only"}}}"#,
2444        )
2445        .expect("pure contract");
2446        let mut packages = SemanticPackageResolutionTable::default();
2447        packages.insert("date-kit".into(), contract).unwrap();
2448
2449        let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2450        assert!(matches!(
2451            model.opaque_action_resolutions[0].outcome,
2452            OpaqueActionResolutionOutcome::NonOpaqueBinding {
2453                kind: SemanticPackageKind::Pure,
2454                ..
2455            }
2456        ));
2457        assert!(model
2458            .diagnostics
2459            .iter()
2460            .any(|diagnostic| diagnostic.code == "PSC1131"));
2461    }
2462
2463    #[test]
2464    fn resolves_resource_source_designator_through_integrity_checked_package_contract() {
2465        let unit = CompilationUnit::parse_sources([(
2466            "src/Profile.tsx",
2467            r#"
2468import { loadProfile } from "profile-service";
2469
2470@component("x-profile")
2471class Profile extends Component {
2472  @resource("loadProfile") profile!: Resource<string, string>;
2473  render() { return <div>Profile</div>; }
2474}
2475"#,
2476        )]);
2477        let contract = parse_semantic_package_contract(
2478            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot","resource_endpoint":{"execution_boundary":"shared","cancellation":"abort","resume":"snapshot"}}}}"#,
2479        )
2480        .expect("resource contract");
2481        let mut packages = SemanticPackageResolutionTable::default();
2482        packages
2483            .insert("profile-service".into(), contract)
2484            .expect("unique package contract");
2485
2486        let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2487        assert_eq!(model.resource_endpoint_resolutions.len(), 1);
2488        let ResourceEndpointResolutionOutcome::Resolved(endpoint) =
2489            &model.resource_endpoint_resolutions[0].outcome
2490        else {
2491            panic!("expected resolved resource endpoint");
2492        };
2493        assert_eq!(endpoint.local_name, "loadProfile");
2494        assert_eq!(endpoint.package, "profile-service");
2495        assert_eq!(endpoint.version, "1.2.3");
2496        assert_eq!(endpoint.export, "loadProfile");
2497        assert_eq!(
2498            endpoint.endpoint.execution_boundary,
2499            SemanticPackageResourceExecutionBoundary::Shared
2500        );
2501        assert_eq!(model.resource_declarations.len(), 1);
2502        let declaration = model
2503            .resource_declarations
2504            .values()
2505            .next()
2506            .expect("resource declaration");
2507        assert_eq!(declaration.name, "profile");
2508        assert_eq!(declaration.data_type, SemanticType::String);
2509        assert_eq!(declaration.error_type, SemanticType::String);
2510        assert_eq!(
2511            declaration.execution_boundary,
2512            ResourceExecutionBoundary::Shared
2513        );
2514        assert_eq!(model.resource_activations.len(), 1);
2515        assert!(model.resource_activations.values().all(|activation| {
2516            activation.declaration == declaration.id
2517                && activation.state == ResourceLifecycleState::Idle
2518        }));
2519        assert!(
2520            model
2521                .diagnostics
2522                .iter()
2523                .all(|diagnostic| { diagnostic.code != "PSC1046" && diagnostic.code != "PSC1128" }),
2524            "{:?}",
2525            model.diagnostics
2526        );
2527        assert!(matches!(
2528            model
2529                .semantic_types
2530                .assignments
2531                .get(declaration.id.as_semantic_id())
2532                .map(|assignment| &assignment.semantic_type),
2533            Some(SemanticType::Resource(resource))
2534                if resource.data.as_ref() == &SemanticType::String
2535                    && resource.error.as_ref() == &SemanticType::String
2536        ));
2537    }
2538
2539    #[test]
2540    fn lowers_direct_computed_resource_data_projection_with_exact_declaration_identity() {
2541        let unit = CompilationUnit::parse_sources([(
2542            "src/Profile.tsx",
2543            r#"
2544import { loadProfile } from "profile-service";
2545
2546@component("x-profile")
2547class Profile extends Component {
2548  @resource("loadProfile") profile!: Resource<string, string>;
2549  @computed() get profileName(): string | null { return this.profile.data; }
2550  render() { return <div>{this.profileName}</div>; }
2551}
2552"#,
2553        )]);
2554        let contract = parse_semantic_package_contract(
2555            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot","resource_endpoint":{"execution_boundary":"shared","cancellation":"abort","resume":"snapshot"}}}}"#,
2556        )
2557        .expect("resource contract");
2558        let mut packages = SemanticPackageResolutionTable::default();
2559        packages
2560            .insert("profile-service".into(), contract)
2561            .expect("unique package contract");
2562
2563        let model = build_application_semantic_model_for_unit_with_packages(&unit, &packages);
2564        assert!(model.diagnostics.is_empty(), "{:?}", model.diagnostics);
2565        let declaration = model
2566            .resource_declarations
2567            .values()
2568            .next()
2569            .expect("resource declaration");
2570        let computed = model.components[0].id.computed("profileName");
2571        assert!(model.references.iter().any(|reference| {
2572            reference.kind == SemanticReferenceKind::ComputedResource
2573                && reference.source == computed
2574                && reference.target == *declaration.id.as_semantic_id()
2575        }));
2576        assert_eq!(
2577            model.semantic_types.computed_values[&computed].semantic_type,
2578            SemanticType::Union(vec![SemanticType::Null, SemanticType::String])
2579        );
2580
2581        let ir = lower_components_to_ir(&model);
2582        assert!(ir
2583            .modules
2584            .iter()
2585            .flat_map(|module| &module.functions)
2586            .any(|function| {
2587                function
2588                    .blocks
2589                    .iter()
2590                    .flat_map(|block| &block.instructions)
2591                    .any(|instruction| {
2592                        matches!(
2593                            &instruction.kind,
2594                            IrInstructionKind::LoadResource { declaration: loaded }
2595                                if loaded == declaration.id.as_semantic_id()
2596                        )
2597                    })
2598            }));
2599        let artifact = build_runtime_computed_artifact(&model, &ir);
2600        assert!(artifact.resource_invalidations.iter().any(|invalidation| {
2601            invalidation.declaration == declaration.id.as_str()
2602                && invalidation.dependents == vec![computed.to_string()]
2603        }));
2604        assert!(artifact.evaluations.iter().any(|evaluation| {
2605            evaluation.computed == computed.as_str()
2606                && evaluation.program.instructions.iter().any(|instruction| {
2607                    matches!(
2608                        instruction,
2609                        RuntimeComputedArtifactInstruction::LoadResource { declaration: loaded, .. }
2610                            if loaded == declaration.id.as_str()
2611                    )
2612                })
2613        }));
2614
2615        let chained_unit = CompilationUnit::parse_sources([(
2616            "src/ChainedProfile.tsx",
2617            r#"
2618import { loadProfile } from "profile-service";
2619
2620@component("x-chained-profile")
2621class ChainedProfile extends Component {
2622  @resource("loadProfile") profile!: Resource<string, string>;
2623  @computed() get invalid() { return this.profile.data.length; }
2624  render() { return <div>Profile</div>; }
2625}
2626"#,
2627        )]);
2628        let chained =
2629            build_application_semantic_model_for_unit_with_packages(&chained_unit, &packages);
2630        assert!(chained.diagnostics.iter().any(|diagnostic| {
2631            diagnostic.code == ComputedDiagnosticCode::UnsupportedBody.as_str()
2632                && diagnostic.message.contains("may only directly project")
2633        }));
2634    }
2635
2636    #[test]
2637    fn component_graph_reports_duplicate_event_errors() {
2638        let parsed = presolve_parser::ParsedFile {
2639            path: "DuplicateEvent.tsx".into(),
2640            syntax: presolve_parser::ParsedSourceAst {
2641                source: String::new(),
2642                estree_json: "{}".to_owned(),
2643                span: test_span(),
2644            },
2645            diagnostics: Vec::new(),
2646            imports: Vec::new(),
2647            exports: Vec::new(),
2648            call_expressions: Vec::new(),
2649            type_aliases: Vec::new(),
2650            local_type_bindings: Vec::new(),
2651            local_value_bindings: Vec::new(),
2652            classes: vec![presolve_parser::ParsedClass {
2653                name: "DuplicateEvent".to_string(),
2654                span: test_span(),
2655                heritage: None,
2656                decorators: vec![presolve_parser::ParsedDecorator {
2657                    name: "component".to_string(),
2658                    is_invoked: true,
2659                    arguments: vec![Some("x-duplicate-event".to_string())],
2660                    argument: Some("x-duplicate-event".to_string()),
2661                    argument_count: 1,
2662                    argument_spans: vec![test_span()],
2663                    static_member_argument: None,
2664                    this_member_argument: None,
2665                    validation_rule_expression: None,
2666                    span: test_span(),
2667                }],
2668                properties: Vec::new(),
2669                methods: vec![presolve_parser::ParsedMethod {
2670                    name: "render".to_string(),
2671                    span: test_span(),
2672                    decorators: Vec::new(),
2673                    is_getter: false,
2674                    is_setter: false,
2675                    is_async: false,
2676                    is_static: false,
2677                    jsx_roots: vec![presolve_parser::ParsedJsxNode::Element(
2678                        presolve_parser::ParsedJsxElement {
2679                            name: "button".to_string(),
2680                            name_span: test_span(),
2681                            span: test_span(),
2682                            attributes: Vec::new(),
2683                            event_handlers: vec![
2684                                presolve_parser::ParsedEventHandler {
2685                                    event: "click".to_string(),
2686                                    handler: "this.render".to_string(),
2687                                    arguments: Vec::new(),
2688                                    span: test_span(),
2689                                },
2690                                presolve_parser::ParsedEventHandler {
2691                                    event: "click".to_string(),
2692                                    handler: "this.render".to_string(),
2693                                    arguments: Vec::new(),
2694                                    span: test_span(),
2695                                },
2696                            ],
2697                            children: Vec::new(),
2698                        },
2699                    )],
2700                    bindings: Vec::new(),
2701                    state_updates: Vec::new(),
2702                    local_variables: Vec::new(),
2703                    parameters: Vec::new(),
2704                    return_type_annotation: None,
2705                    return_values: Vec::new(),
2706                    computed_expression: None,
2707                    effect_body: None,
2708                    calls: Vec::new(),
2709                }],
2710            }],
2711        };
2712
2713        let graph = build_component_graph(&parsed);
2714
2715        assert!(graph
2716            .diagnostics
2717            .iter()
2718            .any(|diagnostic| diagnostic.code == "PSC1006"));
2719    }
2720
2721    fn test_span() -> presolve_parser::SourceSpan {
2722        presolve_parser::SourceSpan {
2723            start: 0,
2724            end: 0,
2725            line: 1,
2726            column: 1,
2727        }
2728    }
2729
2730    #[test]
2731    fn builds_increment_action_from_parsed_method_update() {
2732        let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
2733
2734        let parsed =
2735            presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
2736
2737        let graph = build_component_graph(&parsed);
2738        let component = graph.components.first().expect("expected component");
2739
2740        assert_eq!(
2741            component.actions,
2742            vec![ComponentAction {
2743                id: SemanticId::component(Some("x-nested-counter"), "NestedCounter")
2744                    .action("increment", 0),
2745                owner: SemanticOwner::entity(
2746                    SemanticId::component(Some("x-nested-counter"), "NestedCounter")
2747                        .method("increment"),
2748                ),
2749                method: "increment".to_string(),
2750                operation: StateOperation::Increment,
2751                field: "count".to_string(),
2752            }]
2753        );
2754    }
2755
2756    #[test]
2757    fn builds_decrement_action_from_parsed_method_update() {
2758        let source =
2759            include_str!("../../../fixtures/0009-decrement-counter/input/DecrementCounter.tsx");
2760
2761        let parsed = presolve_parser::parse_file(
2762            "fixtures/0009-decrement-counter/input/DecrementCounter.tsx",
2763            source,
2764        );
2765
2766        let graph = build_component_graph(&parsed);
2767        let component = graph.components.first().expect("expected component");
2768
2769        assert_eq!(
2770            component.actions,
2771            vec![ComponentAction {
2772                id: SemanticId::component(Some("x-decrement-counter"), "DecrementCounter")
2773                    .action("decrement", 0),
2774                owner: SemanticOwner::entity(
2775                    SemanticId::component(Some("x-decrement-counter"), "DecrementCounter")
2776                        .method("decrement"),
2777                ),
2778                method: "decrement".to_string(),
2779                operation: StateOperation::Decrement,
2780                field: "count".to_string(),
2781            }]
2782        );
2783    }
2784
2785    #[test]
2786    fn builds_add_and_subtract_assign_actions_from_parsed_method_updates() {
2787        let source =
2788            include_str!("../../../fixtures/0010-add-subtract-assign/input/StepCounter.tsx");
2789
2790        let parsed = presolve_parser::parse_file(
2791            "fixtures/0010-add-subtract-assign/input/StepCounter.tsx",
2792            source,
2793        );
2794
2795        let graph = build_component_graph(&parsed);
2796        let component = graph.components.first().expect("expected component");
2797
2798        assert_eq!(
2799            component.actions,
2800            vec![
2801                ComponentAction {
2802                    id: SemanticId::component(Some("x-step-counter"), "StepCounter")
2803                        .action("addTwo", 0),
2804                    owner: SemanticOwner::entity(
2805                        SemanticId::component(Some("x-step-counter"), "StepCounter")
2806                            .method("addTwo"),
2807                    ),
2808                    method: "addTwo".to_string(),
2809                    operation: StateOperation::AddAssign(SerializableValue::Number(
2810                        "2".to_string()
2811                    )),
2812                    field: "count".to_string(),
2813                },
2814                ComponentAction {
2815                    id: SemanticId::component(Some("x-step-counter"), "StepCounter")
2816                        .action("subtractThree", 0),
2817                    owner: SemanticOwner::entity(
2818                        SemanticId::component(Some("x-step-counter"), "StepCounter")
2819                            .method("subtractThree"),
2820                    ),
2821                    method: "subtractThree".to_string(),
2822                    operation: StateOperation::SubtractAssign(SerializableValue::Number(
2823                        "3".to_string()
2824                    )),
2825                    field: "count".to_string(),
2826                }
2827            ]
2828        );
2829    }
2830
2831    #[test]
2832    fn builds_direct_assignment_action_from_parsed_method_update() {
2833        let source =
2834            include_str!("../../../fixtures/0011-direct-assignment/input/ResetCounter.tsx");
2835
2836        let parsed = presolve_parser::parse_file(
2837            "fixtures/0011-direct-assignment/input/ResetCounter.tsx",
2838            source,
2839        );
2840
2841        let graph = build_component_graph(&parsed);
2842        let component = graph.components.first().expect("expected component");
2843
2844        assert_eq!(
2845            component.actions,
2846            vec![ComponentAction {
2847                id: SemanticId::component(Some("x-reset-counter"), "ResetCounter")
2848                    .action("reset", 0),
2849                owner: SemanticOwner::entity(
2850                    SemanticId::component(Some("x-reset-counter"), "ResetCounter").method("reset"),
2851                ),
2852                method: "reset".to_string(),
2853                operation: StateOperation::Assign(SerializableValue::Number("0".to_string())),
2854                field: "count".to_string(),
2855            }]
2856        );
2857    }
2858
2859    #[test]
2860    fn builds_boolean_toggle_action_from_parsed_method_update() {
2861        let source = include_str!("../../../fixtures/0012-boolean-toggle/input/ToggleFlag.tsx");
2862
2863        let parsed = presolve_parser::parse_file(
2864            "fixtures/0012-boolean-toggle/input/ToggleFlag.tsx",
2865            source,
2866        );
2867
2868        let graph = build_component_graph(&parsed);
2869        let component = graph.components.first().expect("expected component");
2870
2871        assert_eq!(
2872            component.actions,
2873            vec![ComponentAction {
2874                id: SemanticId::component(Some("x-toggle-flag"), "ToggleFlag").action("toggle", 0),
2875                owner: SemanticOwner::entity(
2876                    SemanticId::component(Some("x-toggle-flag"), "ToggleFlag").method("toggle"),
2877                ),
2878                method: "toggle".to_string(),
2879                operation: StateOperation::Toggle,
2880                field: "enabled".to_string(),
2881            }]
2882        );
2883    }
2884
2885    #[test]
2886    fn builds_multi_step_actions_from_parsed_method_updates_in_source_order() {
2887        let source =
2888            include_str!("../../../fixtures/0013-multi-step-action/input/BatchActionCounter.tsx");
2889
2890        let parsed = presolve_parser::parse_file(
2891            "fixtures/0013-multi-step-action/input/BatchActionCounter.tsx",
2892            source,
2893        );
2894
2895        let graph = build_component_graph(&parsed);
2896        let component = graph.components.first().expect("expected component");
2897
2898        assert_eq!(
2899            component.actions,
2900            vec![
2901                ComponentAction {
2902                    id:
2903                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2904                            .action("apply", 0),
2905                    owner:
2906                        SemanticOwner::entity(
2907                            SemanticId::component(
2908                                Some("x-batch-action-counter"),
2909                                "BatchActionCounter",
2910                            )
2911                            .method("apply"),
2912                        ),
2913                    method: "apply".to_string(),
2914                    operation: StateOperation::AddAssign(SerializableValue::Number(
2915                        "2".to_string()
2916                    )),
2917                    field: "count".to_string(),
2918                },
2919                ComponentAction {
2920                    id:
2921                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2922                            .action("apply", 1),
2923                    owner:
2924                        SemanticOwner::entity(
2925                            SemanticId::component(
2926                                Some("x-batch-action-counter"),
2927                                "BatchActionCounter",
2928                            )
2929                            .method("apply"),
2930                        ),
2931                    method: "apply".to_string(),
2932                    operation: StateOperation::Decrement,
2933                    field: "count".to_string(),
2934                },
2935                ComponentAction {
2936                    id:
2937                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2938                            .action("apply", 2),
2939                    owner:
2940                        SemanticOwner::entity(
2941                            SemanticId::component(
2942                                Some("x-batch-action-counter"),
2943                                "BatchActionCounter",
2944                            )
2945                            .method("apply"),
2946                        ),
2947                    method: "apply".to_string(),
2948                    operation: StateOperation::Assign(SerializableValue::Number("8".to_string())),
2949                    field: "count".to_string(),
2950                },
2951                ComponentAction {
2952                    id:
2953                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2954                            .action("apply", 3),
2955                    owner:
2956                        SemanticOwner::entity(
2957                            SemanticId::component(
2958                                Some("x-batch-action-counter"),
2959                                "BatchActionCounter",
2960                            )
2961                            .method("apply"),
2962                        ),
2963                    method: "apply".to_string(),
2964                    operation: StateOperation::Increment,
2965                    field: "count".to_string(),
2966                },
2967                ComponentAction {
2968                    id:
2969                        SemanticId::component(Some("x-batch-action-counter"), "BatchActionCounter",)
2970                            .action("apply", 4),
2971                    owner:
2972                        SemanticOwner::entity(
2973                            SemanticId::component(
2974                                Some("x-batch-action-counter"),
2975                                "BatchActionCounter",
2976                            )
2977                            .method("apply"),
2978                        ),
2979                    method: "apply".to_string(),
2980                    operation: StateOperation::Toggle,
2981                    field: "enabled".to_string(),
2982                }
2983            ]
2984        );
2985    }
2986
2987    #[test]
2988    fn generates_static_html_from_template_graph() {
2989        let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
2990
2991        let parsed =
2992            presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
2993
2994        let component_graph = build_component_graph(&parsed);
2995        let template_graph = build_template_graph(&component_graph);
2996        let html = generate_static_html(&template_graph);
2997
2998        assert_eq!(
2999            html,
3000            "<button data-presolve-node=\"n0\" data-presolve-on-click=\"this.increment\" data-presolve-bindings=\"this.count\">Count:<!-- presolve-binding:n1:this.count -->0</button>\n"
3001        );
3002    }
3003
3004    #[test]
3005    fn lowers_jsx_html_attribute_aliases_before_html_and_manifest_generation() {
3006        let source = r#"
3007@component("x-attribute-alias")
3008class AttributeAlias extends Component {
3009  target = state("profile-name");
3010  render() {
3011    return <label className={this.target} htmlFor={this.target}>Profile</label>;
3012  }
3013}
3014"#;
3015        let parsed = presolve_parser::parse_file("AttributeAlias.tsx", source);
3016        let component_graph = build_component_graph(&parsed);
3017        let template_graph = build_template_graph(&component_graph);
3018        let html = generate_static_html(&template_graph);
3019        let manifest = build_template_manifest(&component_graph, &template_graph);
3020        let model = build_application_semantic_model_from_component_graph(&component_graph);
3021        let runtime = build_runtime_component_artifact(&model, &model.component_ir_optimization);
3022
3023        assert!(html.contains("class=\"profile-name\""));
3024        assert!(html.contains("for=\"profile-name\""));
3025        assert!(!html.contains("className="));
3026        assert!(!html.contains("htmlFor="));
3027        assert_eq!(
3028            manifest.components[0]
3029                .template
3030                .nodes
3031                .iter()
3032                .filter_map(|node| match node {
3033                    ManifestNode::Binding { attribute, .. } => attribute.as_deref(),
3034                    _ => None,
3035                })
3036                .collect::<Vec<_>>(),
3037            vec!["class", "for"]
3038        );
3039        assert_eq!(
3040            runtime
3041                .ordinary_template_bindings
3042                .iter()
3043                .filter_map(|binding| binding.attribute_name.as_deref())
3044                .collect::<Vec<_>>(),
3045            vec!["class", "for"]
3046        );
3047    }
3048
3049    #[test]
3050    fn preserves_string_state_literals_in_template_outputs() {
3051        let source = include_str!("../../../fixtures/0006-string-state/input/StringGreeting.tsx");
3052
3053        let parsed = presolve_parser::parse_file(
3054            "fixtures/0006-string-state/input/StringGreeting.tsx",
3055            source,
3056        );
3057
3058        let component_graph = build_component_graph(&parsed);
3059        let component = component_graph
3060            .components
3061            .first()
3062            .expect("expected component");
3063
3064        assert_eq!(
3065            component.state_fields[0].initial_value,
3066            Some(SerializableValue::String("Austin & <Zero>".to_string()))
3067        );
3068
3069        let template_graph = build_template_graph(&component_graph);
3070        let html = generate_static_html(&template_graph);
3071
3072        assert_eq!(
3073            html,
3074            "<p data-presolve-node=\"n0\" data-presolve-bindings=\"this.name\">Name:<!-- presolve-binding:n1:this.name -->Austin &amp; &lt;Zero&gt;</p>\n"
3075        );
3076
3077        let manifest = build_template_manifest(&component_graph, &template_graph);
3078
3079        assert_eq!(
3080            manifest.components[0].template.nodes,
3081            vec![
3082                ManifestNode::Element {
3083                    id: "n0".to_string(),
3084                    tag: "p".to_string(),
3085                },
3086                ManifestNode::Binding {
3087                    id: "n1".to_string(),
3088                    expression: "this.name".to_string(),
3089                    initial_value: Some(SerializableValue::String("Austin & <Zero>".to_string())),
3090                    target: None,
3091                    element: None,
3092                    attribute: None,
3093                }
3094            ]
3095        );
3096
3097        let manifest_json = template_manifest_json(&manifest);
3098        let manifest_value: serde_json::Value =
3099            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3100
3101        assert_eq!(
3102            manifest_value["components"][0]["template"]["nodes"][1]["initial_value"],
3103            serde_json::json!("Austin & <Zero>")
3104        );
3105    }
3106
3107    #[test]
3108    fn decodes_jsx_text_character_references_before_static_html_escaping() {
3109        let parsed = presolve_parser::parse_file(
3110            "CodeExample.tsx",
3111            r#"
3112@component("x-code-example")
3113class CodeExample extends Component {
3114  render() {
3115    return <pre><code>&lt;button&gt;A &amp; B&#123;ok&#125;&lt;/button&gt;</code></pre>;
3116  }
3117}
3118"#,
3119        );
3120
3121        let component_graph = build_component_graph(&parsed);
3122        assert!(component_graph.diagnostics.is_empty());
3123
3124        let html = generate_static_html(&build_template_graph(&component_graph));
3125
3126        assert!(html.contains("&lt;button&gt;A &amp; B{ok}&lt;/button&gt;"));
3127        assert!(!html.contains("&amp;lt;button&amp;gt;"));
3128        assert!(!html.contains("&amp;#123;"));
3129    }
3130
3131    #[test]
3132    fn preserves_inline_jsx_text_boundaries_in_static_html() {
3133        let parsed = presolve_parser::parse_file(
3134            "CodeExample.tsx",
3135            r#"
3136@component("x-code-example")
3137class CodeExample extends Component {
3138  render() {
3139    return <pre><code><span>import &#123; </span><span className="syntax-type">Component</span><span> &#125; from </span><span>&quot;presolve&quot;</span>;</code></pre>;
3140  }
3141}
3142"#,
3143        );
3144
3145        let component_graph = build_component_graph(&parsed);
3146        assert!(component_graph.diagnostics.is_empty());
3147
3148        let html = generate_static_html(&build_template_graph(&component_graph));
3149
3150        assert!(html.contains("import { </span><span data-presolve-node"));
3151        assert!(html.contains("class=\"syntax-type\">Component</span><span"));
3152        assert!(html.contains("> } from </span><span"));
3153        assert!(html.contains(">\"presolve\"</span>;"));
3154        assert!(!html.contains("data-presolve-bindings"));
3155    }
3156
3157    #[test]
3158    fn preserves_static_jsx_attributes_in_template_outputs() {
3159        let source =
3160            include_str!("../../../fixtures/0014-static-attributes/input/StaticAttributePanel.tsx");
3161
3162        let parsed = presolve_parser::parse_file(
3163            "fixtures/0014-static-attributes/input/StaticAttributePanel.tsx",
3164            source,
3165        );
3166
3167        let component_graph = build_component_graph(&parsed);
3168        assert!(component_graph.diagnostics.is_empty());
3169
3170        let template_graph = build_template_graph(&component_graph);
3171        let root = template_graph.templates[0]
3172            .root
3173            .as_ref()
3174            .expect("expected root");
3175
3176        assert_eq!(root.attributes.len(), 3);
3177        assert_eq!(root.attributes[0].name, "id");
3178        assert_eq!(
3179            root.attributes[0].value,
3180            AttributeValue::Static("panel-root".to_string())
3181        );
3182        assert_eq!(root.attributes[1].name, "aria-label");
3183        assert_eq!(
3184            root.attributes[1].value,
3185            AttributeValue::Static("Status \"Panel\"".to_string())
3186        );
3187        assert_eq!(root.attributes[2].name, "hidden");
3188        assert_eq!(root.attributes[2].value, AttributeValue::Boolean);
3189
3190        let TemplateChild::Element(button) = &root.children[0] else {
3191            panic!("expected button child");
3192        };
3193
3194        assert_eq!(button.attributes.len(), 4);
3195        assert_eq!(button.attributes[0].name, "type");
3196        assert_eq!(
3197            button.attributes[0].value,
3198            AttributeValue::Static("button".to_string())
3199        );
3200        assert_eq!(button.attributes[1].name, "data-mode");
3201        assert_eq!(
3202            button.attributes[1].value,
3203            AttributeValue::Static("safe & sound".to_string())
3204        );
3205        assert_eq!(button.attributes[2].name, "title");
3206        assert_eq!(
3207            button.attributes[2].value,
3208            AttributeValue::Static("Use <carefully>".to_string())
3209        );
3210        assert_eq!(button.attributes[3].name, "data-presolve-bindings");
3211
3212        let html = generate_static_html(&template_graph);
3213
3214        assert_eq!(
3215            html,
3216            "<section data-presolve-node=\"n0\" id=\"panel-root\" aria-label=\"Status &quot;Panel&quot;\" hidden><button data-presolve-node=\"n1\" type=\"button\" data-mode=\"safe &amp; sound\" title=\"Use &lt;carefully&gt;\" data-presolve-bindings=\"this.label\">Label:<!-- presolve-binding:n2:this.label -->Ready</button></section>\n"
3217        );
3218    }
3219
3220    #[test]
3221    fn reports_static_attribute_semantic_errors() {
3222        let source = r#"
3223@component("x-bad-attrs")
3224class BadAttrs extends Component {
3225  disabled = state(false);
3226
3227  render() {
3228    return <button type="button" type="submit" title={label} {...props}>Go</button>;
3229  }
3230}
3231"#;
3232
3233        let parsed = presolve_parser::parse_file("BadAttrs.tsx", source);
3234        let graph = build_component_graph(&parsed);
3235        let codes = graph
3236            .diagnostics
3237            .iter()
3238            .map(|diagnostic| diagnostic.code.as_str())
3239            .collect::<Vec<_>>();
3240
3241        assert!(codes.contains(&"PSC1007"));
3242        assert!(codes.contains(&"PSC1008"));
3243        assert!(codes.contains(&"PSC1009"));
3244    }
3245
3246    #[test]
3247    fn builds_dynamic_attribute_bindings_in_template_outputs() {
3248        let source = include_str!(
3249            "../../../fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx"
3250        );
3251
3252        let parsed = presolve_parser::parse_file(
3253            "fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx",
3254            source,
3255        );
3256
3257        let component_graph = build_component_graph(&parsed);
3258        assert!(component_graph.diagnostics.is_empty());
3259
3260        let template_graph = build_template_graph(&component_graph);
3261        let root = template_graph.templates[0]
3262            .root
3263            .as_ref()
3264            .expect("expected root");
3265
3266        assert_eq!(root.attributes[0].name, "disabled");
3267        assert_eq!(
3268            root.attributes[0].value,
3269            AttributeValue::Binding {
3270                id: TemplateNodeId("n1".to_string()),
3271                expression: "this.disabled".to_string(),
3272                initial_value: Some(SerializableValue::Boolean(false)),
3273            }
3274        );
3275        assert_eq!(root.attributes[1].name, "title");
3276        assert_eq!(
3277            root.attributes[1].value,
3278            AttributeValue::Binding {
3279                id: TemplateNodeId("n2".to_string()),
3280                expression: "this.label".to_string(),
3281                initial_value: Some(SerializableValue::String("Ready".to_string())),
3282            }
3283        );
3284
3285        let html = generate_static_html(&template_graph);
3286
3287        assert_eq!(
3288            html,
3289            "<button data-presolve-node=\"n0\" title=\"Ready\" data-presolve-on-click=\"this.lock\" data-presolve-bindings=\"this.label\">Status:<!-- presolve-binding:n3:this.label -->Ready</button>\n"
3290        );
3291    }
3292
3293    #[test]
3294    fn preserves_boolean_state_literals_in_template_outputs() {
3295        let source = include_str!("../../../fixtures/0007-boolean-state/input/BooleanFlags.tsx");
3296
3297        let parsed = presolve_parser::parse_file(
3298            "fixtures/0007-boolean-state/input/BooleanFlags.tsx",
3299            source,
3300        );
3301
3302        let component_graph = build_component_graph(&parsed);
3303        let component = component_graph
3304            .components
3305            .first()
3306            .expect("expected component");
3307
3308        assert_eq!(
3309            component.state_fields[0].initial_value,
3310            Some(SerializableValue::Boolean(true))
3311        );
3312        assert_eq!(
3313            component.state_fields[1].initial_value,
3314            Some(SerializableValue::Boolean(false))
3315        );
3316
3317        let template_graph = build_template_graph(&component_graph);
3318        let html = generate_static_html(&template_graph);
3319
3320        assert_eq!(
3321            html,
3322            "<section data-presolve-node=\"n0\"><p data-presolve-node=\"n1\" data-presolve-bindings=\"this.enabled\">Enabled:<!-- presolve-binding:n2:this.enabled -->true</p><p data-presolve-node=\"n3\" data-presolve-bindings=\"this.disabled\">Disabled:<!-- presolve-binding:n4:this.disabled -->false</p></section>\n"
3323        );
3324
3325        let manifest = build_template_manifest(&component_graph, &template_graph);
3326
3327        assert_eq!(
3328            manifest.components[0].template.nodes,
3329            vec![
3330                ManifestNode::Element {
3331                    id: "n0".to_string(),
3332                    tag: "section".to_string(),
3333                },
3334                ManifestNode::Element {
3335                    id: "n1".to_string(),
3336                    tag: "p".to_string(),
3337                },
3338                ManifestNode::Binding {
3339                    id: "n2".to_string(),
3340                    expression: "this.enabled".to_string(),
3341                    initial_value: Some(SerializableValue::Boolean(true)),
3342                    target: None,
3343                    element: None,
3344                    attribute: None,
3345                },
3346                ManifestNode::Element {
3347                    id: "n3".to_string(),
3348                    tag: "p".to_string(),
3349                },
3350                ManifestNode::Binding {
3351                    id: "n4".to_string(),
3352                    expression: "this.disabled".to_string(),
3353                    initial_value: Some(SerializableValue::Boolean(false)),
3354                    target: None,
3355                    element: None,
3356                    attribute: None,
3357                }
3358            ]
3359        );
3360
3361        let manifest_json = template_manifest_json(&manifest);
3362        let manifest_value: serde_json::Value =
3363            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3364
3365        assert_eq!(
3366            manifest_value["components"][0]["template"]["nodes"][2]["initial_value"],
3367            serde_json::json!(true)
3368        );
3369        assert_eq!(
3370            manifest_value["components"][0]["template"]["nodes"][4]["initial_value"],
3371            serde_json::json!(false)
3372        );
3373    }
3374
3375    #[test]
3376    fn preserves_null_state_literals_in_template_outputs() {
3377        let source = include_str!("../../../fixtures/0008-null-state/input/NullSelection.tsx");
3378
3379        let parsed =
3380            presolve_parser::parse_file("fixtures/0008-null-state/input/NullSelection.tsx", source);
3381
3382        let component_graph = build_component_graph(&parsed);
3383        let component = component_graph
3384            .components
3385            .first()
3386            .expect("expected component");
3387
3388        assert_eq!(
3389            component.state_fields[0].initial_value,
3390            Some(SerializableValue::Null)
3391        );
3392
3393        let template_graph = build_template_graph(&component_graph);
3394        let html = generate_static_html(&template_graph);
3395
3396        assert_eq!(
3397            html,
3398            "<p data-presolve-node=\"n0\" data-presolve-bindings=\"this.selection\">Selection:<!-- presolve-binding:n1:this.selection --></p>\n"
3399        );
3400
3401        let manifest = build_template_manifest(&component_graph, &template_graph);
3402
3403        assert_eq!(
3404            manifest.components[0].template.nodes,
3405            vec![
3406                ManifestNode::Element {
3407                    id: "n0".to_string(),
3408                    tag: "p".to_string(),
3409                },
3410                ManifestNode::Binding {
3411                    id: "n1".to_string(),
3412                    expression: "this.selection".to_string(),
3413                    initial_value: Some(SerializableValue::Null),
3414                    target: None,
3415                    element: None,
3416                    attribute: None,
3417                }
3418            ]
3419        );
3420
3421        let manifest_json = template_manifest_json(&manifest);
3422        let manifest_value: serde_json::Value =
3423            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
3424
3425        assert_eq!(
3426            manifest_value["components"][0]["template"]["nodes"][1]["initial_value"],
3427            serde_json::Value::Null
3428        );
3429    }
3430
3431    #[test]
3432    fn builds_template_graph_from_component_graph() {
3433        let source = include_str!("../../../fixtures/0001-source-summary/input/Counter.tsx");
3434
3435        let parsed =
3436            presolve_parser::parse_file("fixtures/0001-source-summary/input/Counter.tsx", source);
3437
3438        let component_graph = build_component_graph(&parsed);
3439        let template_graph = build_template_graph(&component_graph);
3440
3441        assert_eq!(template_graph.templates.len(), 1);
3442
3443        let template = &template_graph.templates[0];
3444        assert_eq!(template.component_name, "Counter");
3445        assert_eq!(template.id.as_str(), "component:x-counter/template:render");
3446        assert_eq!(
3447            template.provenance.path,
3448            Path::new("fixtures/0001-source-summary/input/Counter.tsx")
3449        );
3450        assert_eq!(template.provenance.span.line, 10);
3451        assert_eq!(
3452            template.owner,
3453            SemanticOwner::entity(SemanticId::component(Some("x-counter"), "Counter"))
3454        );
3455
3456        let root = template.root.as_ref().expect("expected template root");
3457
3458        assert_eq!(root.id.0, "n0");
3459        assert_eq!(root.tag_name, "button");
3460        assert_eq!(root.span.line, 12);
3461        assert_eq!(root.span.column, 7);
3462
3463        assert_eq!(root.attributes.len(), 2);
3464        assert_eq!(root.attributes[0].name, "data-presolve-on-click");
3465        assert_eq!(
3466            root.attributes[0].span.expect("expected event span").line,
3467            12
3468        );
3469        assert_eq!(
3470            root.attributes[0].span.expect("expected event span").column,
3471            15
3472        );
3473        assert_eq!(
3474            root.attributes[0].value,
3475            AttributeValue::EventHandler {
3476                event: "click".to_string(),
3477                handler: "this.increment".to_string(),
3478                arguments: Vec::new(),
3479            }
3480        );
3481
3482        assert_eq!(root.attributes[1].name, "data-presolve-bindings");
3483        assert_eq!(root.attributes[1].span, None);
3484        assert_eq!(
3485            root.attributes[1].value,
3486            AttributeValue::BindingList(vec!["this.count".to_string()])
3487        );
3488
3489        assert_eq!(root.children.len(), 2);
3490        let TemplateChild::Text { value, span } = &root.children[0] else {
3491            panic!("expected text child");
3492        };
3493        assert_eq!(value, "Count:");
3494        assert_eq!(span.line, 13);
3495        assert_eq!(span.column, 9);
3496
3497        let TemplateChild::Binding {
3498            id,
3499            expression,
3500            initial_value,
3501            span,
3502        } = &root.children[1]
3503        else {
3504            panic!("expected binding child");
3505        };
3506        assert_eq!(id.0, "n1");
3507        assert_eq!(expression, "this.count");
3508        assert_eq!(
3509            initial_value,
3510            &Some(SerializableValue::Number("0".to_string()))
3511        );
3512        assert_eq!(span.line, 13);
3513        assert_eq!(span.column, 16);
3514    }
3515
3516    #[test]
3517    fn semantic_ids_are_stable_when_component_declaration_order_changes() {
3518        let alpha = r#"
3519@component("x-alpha")
3520class Alpha extends Component {
3521  count = state(0);
3522
3523  increment() {
3524    this.count++;
3525  }
3526
3527  render() {
3528    return <p>{this.count}</p>;
3529  }
3530}
3531"#;
3532        let beta = r#"
3533@component("x-beta")
3534class Beta extends Component {
3535  enabled = state(false);
3536
3537  toggle() {
3538    this.enabled = !this.enabled;
3539  }
3540
3541  render() {
3542    return <p>{this.enabled}</p>;
3543  }
3544}
3545"#;
3546
3547        let ids_for = |source: &str| {
3548            let parsed = presolve_parser::parse_file("App.tsx", source);
3549            let component_graph = build_component_graph(&parsed);
3550            let template_graph = build_template_graph(&component_graph);
3551            let template_ids = template_graph
3552                .templates
3553                .iter()
3554                .map(|template| (template.component_name.as_str(), template.id.to_string()))
3555                .collect::<BTreeMap<_, _>>();
3556
3557            component_graph
3558                .components
3559                .iter()
3560                .map(|component| {
3561                    let ids = component
3562                        .state_fields
3563                        .iter()
3564                        .map(|field| field.id.to_string())
3565                        .chain(component.methods.iter().map(|method| method.id.to_string()))
3566                        .chain(component.actions.iter().map(|action| action.id.to_string()))
3567                        .chain(std::iter::once(
3568                            template_ids
3569                                .get(component.class_name.as_str())
3570                                .expect("expected component template")
3571                                .clone(),
3572                        ))
3573                        .collect::<Vec<_>>();
3574
3575                    (
3576                        component.class_name.clone(),
3577                        (component.id.to_string(), ids),
3578                    )
3579                })
3580                .collect::<BTreeMap<_, _>>()
3581        };
3582
3583        assert_eq!(
3584            ids_for(&format!("{alpha}\n{beta}")),
3585            ids_for(&format!("{beta}\n{alpha}"))
3586        );
3587    }
3588
3589    #[test]
3590    fn carries_source_spans_into_nested_template_nodes() {
3591        let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
3592
3593        let parsed =
3594            presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
3595
3596        let component_graph = build_component_graph(&parsed);
3597        let template_graph = build_template_graph(&component_graph);
3598        let root = template_graph.templates[0]
3599            .root
3600            .as_ref()
3601            .expect("expected root");
3602
3603        assert_eq!(root.tag_name, "section");
3604        assert_eq!(root.span.line, 12);
3605        assert_eq!(root.span.column, 7);
3606
3607        let TemplateChild::Element(button) = &root.children[0] else {
3608            panic!("expected nested button");
3609        };
3610
3611        assert_eq!(button.tag_name, "button");
3612        assert_eq!(button.span.line, 13);
3613        assert_eq!(button.span.column, 9);
3614        assert_eq!(button.attributes[0].name, "data-presolve-on-click");
3615        assert_eq!(
3616            button.attributes[0].span.expect("expected event span").line,
3617            13
3618        );
3619        assert_eq!(
3620            button.attributes[0]
3621                .span
3622                .expect("expected event span")
3623                .column,
3624            17
3625        );
3626        assert_eq!(button.attributes[1].name, "data-presolve-bindings");
3627        assert_eq!(button.attributes[1].span, None);
3628
3629        let TemplateChild::Text { value, span } = &button.children[0] else {
3630            panic!("expected nested text");
3631        };
3632        assert_eq!(value, "Count: ");
3633        assert_eq!(span.line, 13);
3634        assert_eq!(span.column, 50);
3635
3636        let TemplateChild::Binding {
3637            expression, span, ..
3638        } = &button.children[1]
3639        else {
3640            panic!("expected nested binding");
3641        };
3642        assert_eq!(expression, "this.count");
3643        assert_eq!(span.line, 13);
3644        assert_eq!(span.column, 57);
3645    }
3646
3647    #[test]
3648    fn preserves_fragment_siblings_without_wrapper_elements() {
3649        let source = include_str!("../../../fixtures/0016-fragments/input/FragmentPanel.tsx");
3650
3651        let parsed =
3652            presolve_parser::parse_file("fixtures/0016-fragments/input/FragmentPanel.tsx", source);
3653
3654        let component_graph = build_component_graph(&parsed);
3655        assert!(component_graph.diagnostics.is_empty());
3656
3657        let template_graph = build_template_graph(&component_graph);
3658        let template = &template_graph.templates[0];
3659        let fragment = template
3660            .root_fragment
3661            .as_ref()
3662            .expect("expected fragment root");
3663
3664        assert!(template.root.is_none());
3665        assert_eq!(fragment.id.0, "n0");
3666        assert_eq!(fragment.children.len(), 2);
3667
3668        let TemplateChild::Element(heading) = &fragment.children[0] else {
3669            panic!("expected heading child");
3670        };
3671        assert_eq!(heading.id.0, "n1");
3672        assert_eq!(heading.tag_name, "h1");
3673
3674        let TemplateChild::Fragment(nested) = &fragment.children[1] else {
3675            panic!("expected nested fragment child");
3676        };
3677        assert_eq!(nested.id.0, "n2");
3678
3679        let TemplateChild::Element(paragraph) = &nested.children[0] else {
3680            panic!("expected paragraph child");
3681        };
3682        assert_eq!(paragraph.id.0, "n3");
3683        assert_eq!(paragraph.tag_name, "p");
3684
3685        let html = generate_static_html(&template_graph);
3686        assert_eq!(
3687            html,
3688            "<h1 data-presolve-node=\"n1\">Title</h1><p data-presolve-node=\"n3\" data-presolve-bindings=\"this.label\">Status: <!-- presolve-binding:n4:this.label -->Ready</p><span data-presolve-node=\"n5\">Done</span>\n"
3689        );
3690
3691        let manifest = build_template_manifest(&component_graph, &template_graph);
3692        assert_eq!(
3693            manifest.components[0].template.nodes,
3694            vec![
3695                ManifestNode::Element {
3696                    id: "n1".to_string(),
3697                    tag: "h1".to_string(),
3698                },
3699                ManifestNode::Element {
3700                    id: "n3".to_string(),
3701                    tag: "p".to_string(),
3702                },
3703                ManifestNode::Binding {
3704                    id: "n4".to_string(),
3705                    expression: "this.label".to_string(),
3706                    initial_value: Some(SerializableValue::String("Ready".to_string())),
3707                    target: None,
3708                    element: None,
3709                    attribute: None,
3710                },
3711                ManifestNode::Element {
3712                    id: "n5".to_string(),
3713                    tag: "span".to_string(),
3714                },
3715            ]
3716        );
3717    }
3718
3719    #[test]
3720    fn builds_conditional_template_boundaries_and_manifest_branch_html() {
3721        let source = include_str!(
3722            "../../../fixtures/0017-conditional-rendering/input/ConditionalStatus.tsx"
3723        );
3724
3725        let parsed = presolve_parser::parse_file(
3726            "fixtures/0017-conditional-rendering/input/ConditionalStatus.tsx",
3727            source,
3728        );
3729
3730        let component_graph = build_component_graph(&parsed);
3731        assert!(component_graph.diagnostics.is_empty());
3732
3733        let template_graph = build_template_graph(&component_graph);
3734        let root = template_graph.templates[0]
3735            .root
3736            .as_ref()
3737            .expect("expected root element");
3738
3739        assert_eq!(root.id.0, "n0");
3740        assert_eq!(root.attributes[1].name, "data-presolve-bindings");
3741
3742        let TemplateChild::Conditional(conditional) = &root.children[0] else {
3743            panic!("expected conditional child");
3744        };
3745
3746        assert_eq!(conditional.id.0, "n1");
3747        assert_eq!(conditional.start_id.0, "n2");
3748        assert_eq!(conditional.end_id.0, "n3");
3749        assert_eq!(conditional.condition, "this.enabled");
3750        assert_eq!(
3751            conditional.initial_value,
3752            Some(SerializableValue::Boolean(true))
3753        );
3754
3755        let TemplateChild::Element(when_true) = &conditional.when_true[0] else {
3756            panic!("expected true branch element");
3757        };
3758        assert_eq!(when_true.id.0, "n4");
3759        assert_eq!(when_true.tag_name, "span");
3760
3761        let TemplateChild::Element(when_false) = &conditional.when_false[0] else {
3762            panic!("expected false branch element");
3763        };
3764        assert_eq!(when_false.id.0, "n5");
3765        assert_eq!(when_false.tag_name, "span");
3766
3767        let html = generate_static_html(&template_graph);
3768        assert_eq!(
3769            html,
3770            "<button data-presolve-node=\"n0\" data-presolve-on-click=\"this.toggle\" data-presolve-bindings=\"this.enabled\"><!-- presolve-conditional-start:n2:this.enabled --><span data-presolve-node=\"n4\">On</span><!-- presolve-conditional-end:n3 --></button>\n"
3771        );
3772
3773        let manifest = build_template_manifest(&component_graph, &template_graph);
3774        assert_eq!(
3775            manifest.components[0].template.nodes,
3776            vec![
3777                ManifestNode::Element {
3778                    id: "n0".to_string(),
3779                    tag: "button".to_string(),
3780                },
3781                ManifestNode::Conditional {
3782                    id: "n1".to_string(),
3783                    start: "n2".to_string(),
3784                    end: "n3".to_string(),
3785                    condition: "this.enabled".to_string(),
3786                    initial_value: Some(SerializableValue::Boolean(true)),
3787                    when_true_html: "<span data-presolve-node=\"n4\">On</span>".to_string(),
3788                    when_false_html: "<span data-presolve-node=\"n5\">Off</span>".to_string(),
3789                },
3790            ]
3791        );
3792    }
3793
3794    #[test]
3795    fn builds_logical_and_conditional_with_empty_false_branch() {
3796        let source = include_str!(
3797            "../../../fixtures/0018-logical-and-conditional/input/LogicalAndStatus.tsx"
3798        );
3799
3800        let parsed = presolve_parser::parse_file(
3801            "fixtures/0018-logical-and-conditional/input/LogicalAndStatus.tsx",
3802            source,
3803        );
3804
3805        let component_graph = build_component_graph(&parsed);
3806        assert!(component_graph.diagnostics.is_empty());
3807
3808        let template_graph = build_template_graph(&component_graph);
3809        let root = template_graph.templates[0]
3810            .root
3811            .as_ref()
3812            .expect("expected root element");
3813
3814        let TemplateChild::Conditional(conditional) = &root.children[0] else {
3815            panic!("expected conditional child");
3816        };
3817
3818        assert_eq!(conditional.when_true.len(), 1);
3819        assert!(conditional.when_false.is_empty());
3820
3821        let manifest = build_template_manifest(&component_graph, &template_graph);
3822        assert_eq!(
3823            manifest.components[0].template.nodes[1],
3824            ManifestNode::Conditional {
3825                id: "n1".to_string(),
3826                start: "n2".to_string(),
3827                end: "n3".to_string(),
3828                condition: "this.enabled".to_string(),
3829                initial_value: Some(SerializableValue::Boolean(true)),
3830                when_true_html: "<span data-presolve-node=\"n4\">On</span>".to_string(),
3831                when_false_html: String::new(),
3832            }
3833        );
3834    }
3835
3836    #[test]
3837    fn builds_empty_keyed_list_from_a_serializable_array() {
3838        let source =
3839            include_str!("../../../fixtures/0019-keyed-list-semantics/input/KeyedList.tsx");
3840
3841        let parsed = presolve_parser::parse_file(
3842            "fixtures/0019-keyed-list-semantics/input/KeyedList.tsx",
3843            source,
3844        );
3845
3846        let component_graph = build_component_graph(&parsed);
3847        assert!(component_graph.diagnostics.is_empty());
3848
3849        let template_graph = build_template_graph(&component_graph);
3850        let root = template_graph.templates[0]
3851            .root
3852            .as_ref()
3853            .expect("expected root element");
3854
3855        assert_eq!(root.id.0, "n0");
3856        assert_eq!(root.attributes[0].name, "data-presolve-bindings");
3857
3858        let TemplateChild::List(list) = &root.children[0] else {
3859            panic!("expected keyed list child");
3860        };
3861
3862        assert_eq!(list.id.0, "n1");
3863        assert_eq!(list.start_id.0, "n2");
3864        assert_eq!(list.end_id.0, "n3");
3865        assert_eq!(list.iterable, "this.items");
3866        assert_eq!(
3867            list.initial_value,
3868            Some(SerializableValue::Array(Vec::new()))
3869        );
3870        assert_eq!(list.item_variable, "item");
3871        assert_eq!(list.index_variable.as_deref(), Some("index"));
3872        assert_eq!(list.key_expression, "item.id");
3873
3874        let TemplateChild::Element(item_template) = &list.item_template[0] else {
3875            panic!("expected list item element");
3876        };
3877        assert_eq!(item_template.id.0, "n4");
3878        assert_eq!(item_template.tag_name, "li");
3879
3880        assert_eq!(
3881            generate_static_html(&template_graph),
3882            "<ul data-presolve-node=\"n0\" data-presolve-bindings=\"this.items\"><!-- presolve-list-start:n2:this.items --><!-- presolve-list-end:n3 --></ul>\n"
3883        );
3884
3885        let manifest = build_template_manifest(&component_graph, &template_graph);
3886        assert_eq!(
3887            manifest.components[0].template.nodes,
3888            vec![
3889                ManifestNode::Element {
3890                    id: "n0".to_string(),
3891                    tag: "ul".to_string(),
3892                },
3893                ManifestNode::List {
3894                    id: "n1".to_string(),
3895                    start: "n2".to_string(),
3896                    end: "n3".to_string(),
3897                    iterable: "this.items".to_string(),
3898                    initial_value: Some(SerializableValue::Array(Vec::new())),
3899                    item_variable: "item".to_string(),
3900                    index_variable: Some("index".to_string()),
3901                    key_expression: "item.id".to_string(),
3902                    item_root: "n4".to_string(),
3903                    item_template_html: "<li data-presolve-node=\"n4:__ez_list_key__\" data-presolve-bindings=\"index,item.label\"><!-- presolve-binding:n5:__ez_list_key__:index -->__ez_list_index__<!-- presolve-list-binding-end:n5:__ez_list_key__ -->: <!-- presolve-binding:n6:__ez_list_key__:item.label --><!-- presolve-list-binding-end:n6:__ez_list_key__ --></li>".to_string(),
3904                },
3905            ]
3906        );
3907    }
3908
3909    #[test]
3910    fn preserves_recursive_object_values_in_component_and_manifest_models() {
3911        let source = include_str!(
3912            "../../../fixtures/0023-recursive-object-values/input/RecursiveObjectValues.tsx"
3913        );
3914        let parsed = presolve_parser::parse_file(
3915            "fixtures/0023-recursive-object-values/input/RecursiveObjectValues.tsx",
3916            source,
3917        );
3918
3919        let component_graph = build_component_graph(&parsed);
3920        assert!(component_graph.diagnostics.is_empty());
3921
3922        let profile = SerializableValue::Object(BTreeMap::from([
3923            (
3924                "name".to_string(),
3925                SerializableValue::String("North".to_string()),
3926            ),
3927            (
3928                "settings".to_string(),
3929                SerializableValue::Object(BTreeMap::from([
3930                    ("enabled".to_string(), SerializableValue::Boolean(true)),
3931                    (
3932                        "tags".to_string(),
3933                        SerializableValue::Array(vec![
3934                            SerializableValue::String("compiler".to_string()),
3935                            SerializableValue::Object(BTreeMap::from([
3936                                (
3937                                    "name".to_string(),
3938                                    SerializableValue::String("runtime".to_string()),
3939                                ),
3940                                (
3941                                    "rank".to_string(),
3942                                    SerializableValue::Number("2".to_string()),
3943                                ),
3944                            ])),
3945                        ]),
3946                    ),
3947                ])),
3948            ),
3949        ]));
3950        assert_eq!(
3951            component_graph.components[0].state_fields[0].initial_value,
3952            Some(profile.clone())
3953        );
3954
3955        let template_graph = build_template_graph(&component_graph);
3956        let manifest = build_template_manifest(&component_graph, &template_graph);
3957        assert_eq!(
3958            manifest.components[0].template.nodes[3],
3959            ManifestNode::Binding {
3960                id: "n3".to_string(),
3961                expression: "this.profile".to_string(),
3962                initial_value: Some(profile),
3963                target: None,
3964                element: None,
3965                attribute: None,
3966            }
3967        );
3968        assert!(matches!(
3969            manifest.components[0].actions[0].operand,
3970            Some(SerializableValue::Object(_))
3971        ));
3972    }
3973
3974    #[test]
3975    fn renders_static_object_list_members_and_keys() {
3976        let source = include_str!(
3977            "../../../fixtures/0024-static-object-keyed-list/input/StaticObjectKeyedList.tsx"
3978        );
3979        let parsed = presolve_parser::parse_file(
3980            "fixtures/0024-static-object-keyed-list/input/StaticObjectKeyedList.tsx",
3981            source,
3982        );
3983        let component_graph = build_component_graph(&parsed);
3984        assert!(component_graph.diagnostics.is_empty());
3985
3986        let template_graph = build_template_graph(&component_graph);
3987        assert_eq!(
3988            generate_static_html(&template_graph),
3989            "<ol data-presolve-node=\"n0\" data-presolve-bindings=\"this.items\"><!-- presolve-list-start:n2:this.items --><li data-presolve-node=\"n4:north\" data-presolve-bindings=\"index,item.label,item.details.region\"><!-- presolve-binding:n5:north:index -->0<!-- presolve-list-binding-end:n5:north -->: <!-- presolve-binding:n6:north:item.label -->North<!-- presolve-list-binding-end:n6:north --> (<!-- presolve-binding:n7:north:item.details.region -->west<!-- presolve-list-binding-end:n7:north -->)</li><li data-presolve-node=\"n4:south\" data-presolve-bindings=\"index,item.label,item.details.region\"><!-- presolve-binding:n5:south:index -->1<!-- presolve-list-binding-end:n5:south -->: <!-- presolve-binding:n6:south:item.label -->South<!-- presolve-list-binding-end:n6:south --> (<!-- presolve-binding:n7:south:item.details.region -->east<!-- presolve-list-binding-end:n7:south -->)</li><!-- presolve-list-end:n3 --></ol>\n"
3990        );
3991    }
3992
3993    #[test]
3994    fn renders_initial_keyed_list_items_from_a_serializable_array() {
3995        let source =
3996            include_str!("../../../fixtures/0020-static-keyed-list/input/StaticKeyedList.tsx");
3997
3998        let parsed = presolve_parser::parse_file(
3999            "fixtures/0020-static-keyed-list/input/StaticKeyedList.tsx",
4000            source,
4001        );
4002        let component_graph = build_component_graph(&parsed);
4003        assert!(component_graph.diagnostics.is_empty());
4004
4005        let template_graph = build_template_graph(&component_graph);
4006        let root = template_graph.templates[0]
4007            .root
4008            .as_ref()
4009            .expect("expected root element");
4010        let TemplateChild::List(list) = &root.children[0] else {
4011            panic!("expected keyed list child");
4012        };
4013
4014        assert_eq!(
4015            list.initial_value,
4016            Some(SerializableValue::Array(vec![
4017                SerializableValue::String("North".to_string()),
4018                SerializableValue::String("South".to_string()),
4019            ]))
4020        );
4021
4022        assert_eq!(
4023            generate_static_html(&template_graph),
4024            "<ol data-presolve-node=\"n0\" data-presolve-bindings=\"this.labels\"><!-- presolve-list-start:n2:this.labels --><li data-presolve-node=\"n4:North\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:North:index -->0<!-- presolve-list-binding-end:n5:North -->: <!-- presolve-binding:n6:North:label -->North<!-- presolve-list-binding-end:n6:North --></li><li data-presolve-node=\"n4:South\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:South:index -->1<!-- presolve-list-binding-end:n5:South -->: <!-- presolve-binding:n6:South:label -->South<!-- presolve-list-binding-end:n6:South --></li><!-- presolve-list-end:n3 --></ol>\n"
4025        );
4026
4027        let manifest = build_template_manifest(&component_graph, &template_graph);
4028        assert_eq!(
4029            manifest.components[0].template.nodes,
4030            vec![
4031                ManifestNode::Element {
4032                    id: "n0".to_string(),
4033                    tag: "ol".to_string(),
4034                },
4035                ManifestNode::List {
4036                    id: "n1".to_string(),
4037                    start: "n2".to_string(),
4038                    end: "n3".to_string(),
4039                    iterable: "this.labels".to_string(),
4040                    initial_value: Some(SerializableValue::Array(vec![
4041                        SerializableValue::String("North".to_string()),
4042                        SerializableValue::String("South".to_string()),
4043                    ])),
4044                    item_variable: "label".to_string(),
4045                    index_variable: Some("index".to_string()),
4046                    key_expression: "label".to_string(),
4047                    item_root: "n4".to_string(),
4048                    item_template_html: "<li data-presolve-node=\"n4:__ez_list_key__\" data-presolve-bindings=\"index,label\"><!-- presolve-binding:n5:__ez_list_key__:index -->__ez_list_index__<!-- presolve-list-binding-end:n5:__ez_list_key__ -->: <!-- presolve-binding:n6:__ez_list_key__:label -->__ez_list_item__<!-- presolve-list-binding-end:n6:__ez_list_key__ --></li>".to_string(),
4049                },
4050            ]
4051        );
4052    }
4053
4054    #[test]
4055    fn builds_template_manifest_for_nested_jsx() {
4056        let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
4057
4058        let parsed =
4059            presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
4060
4061        let component_graph = build_component_graph(&parsed);
4062        let template_graph = build_template_graph(&component_graph);
4063        let manifest = build_template_manifest(&component_graph, &template_graph);
4064
4065        assert_eq!(manifest.schema_version, 1);
4066        assert_eq!(manifest.components.len(), 1);
4067
4068        let component = &manifest.components[0];
4069        assert_eq!(component.name, "NestedCounter");
4070
4071        assert_eq!(
4072            component.template.nodes,
4073            vec![
4074                ManifestNode::Element {
4075                    id: "n0".to_string(),
4076                    tag: "section".to_string(),
4077                },
4078                ManifestNode::Element {
4079                    id: "n1".to_string(),
4080                    tag: "button".to_string(),
4081                },
4082                ManifestNode::Binding {
4083                    id: "n2".to_string(),
4084                    expression: "this.count".to_string(),
4085                    initial_value: Some(SerializableValue::Number("0".to_string())),
4086                    target: None,
4087                    element: None,
4088                    attribute: None,
4089                }
4090            ]
4091        );
4092
4093        assert_eq!(
4094            component.template.events,
4095            vec![ManifestEvent {
4096                node: "n1".to_string(),
4097                kind: None,
4098                event: "click".to_string(),
4099                handler: "this.increment".to_string(),
4100                arguments: Vec::new(),
4101                method_id: None,
4102                action_batch_id: None,
4103            }]
4104        );
4105
4106        assert_eq!(
4107            component.actions,
4108            vec![ManifestAction {
4109                method: "increment".to_string(),
4110                method_id: None,
4111                action_batch_id: None,
4112                operation: ManifestOperation::Increment,
4113                field: "count".to_string(),
4114                storage_id: None,
4115                operand: None,
4116            }]
4117        );
4118
4119        let manifest_json = template_manifest_json(&manifest);
4120        let manifest_value: serde_json::Value =
4121            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4122
4123        assert_eq!(manifest_value["schema_version"], serde_json::json!(1));
4124    }
4125
4126    #[test]
4127    fn builds_template_manifest_for_decrement_action() {
4128        let source =
4129            include_str!("../../../fixtures/0009-decrement-counter/input/DecrementCounter.tsx");
4130
4131        let parsed = presolve_parser::parse_file(
4132            "fixtures/0009-decrement-counter/input/DecrementCounter.tsx",
4133            source,
4134        );
4135
4136        let component_graph = build_component_graph(&parsed);
4137        let template_graph = build_template_graph(&component_graph);
4138        let manifest = build_template_manifest(&component_graph, &template_graph);
4139
4140        assert_eq!(
4141            manifest.components[0].actions,
4142            vec![ManifestAction {
4143                method: "decrement".to_string(),
4144                method_id: None,
4145                action_batch_id: None,
4146                operation: ManifestOperation::Decrement,
4147                field: "count".to_string(),
4148                storage_id: None,
4149                operand: None,
4150            }]
4151        );
4152
4153        let manifest_json = template_manifest_json(&manifest);
4154        let manifest_value: serde_json::Value =
4155            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4156
4157        assert_eq!(
4158            manifest_value["components"][0]["actions"][0]["operation"],
4159            serde_json::json!("decrement")
4160        );
4161    }
4162
4163    #[test]
4164    fn builds_template_manifest_for_add_and_subtract_assign_actions() {
4165        let source =
4166            include_str!("../../../fixtures/0010-add-subtract-assign/input/StepCounter.tsx");
4167
4168        let parsed = presolve_parser::parse_file(
4169            "fixtures/0010-add-subtract-assign/input/StepCounter.tsx",
4170            source,
4171        );
4172
4173        let component_graph = build_component_graph(&parsed);
4174        let template_graph = build_template_graph(&component_graph);
4175        let manifest = build_template_manifest(&component_graph, &template_graph);
4176
4177        assert_eq!(
4178            manifest.components[0].actions,
4179            vec![
4180                ManifestAction {
4181                    method: "addTwo".to_string(),
4182                    method_id: None,
4183                    action_batch_id: None,
4184                    operation: ManifestOperation::AddAssign,
4185                    field: "count".to_string(),
4186                    storage_id: None,
4187                    operand: Some(SerializableValue::Number("2".to_string())),
4188                },
4189                ManifestAction {
4190                    method: "subtractThree".to_string(),
4191                    method_id: None,
4192                    action_batch_id: None,
4193                    operation: ManifestOperation::SubtractAssign,
4194                    field: "count".to_string(),
4195                    storage_id: None,
4196                    operand: Some(SerializableValue::Number("3".to_string())),
4197                }
4198            ]
4199        );
4200
4201        let manifest_json = template_manifest_json(&manifest);
4202        let manifest_value: serde_json::Value =
4203            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4204
4205        assert_eq!(
4206            manifest_value["components"][0]["actions"][0]["operation"],
4207            serde_json::json!("add_assign")
4208        );
4209        assert_eq!(
4210            manifest_value["components"][0]["actions"][0]["operand"],
4211            serde_json::json!("2")
4212        );
4213        assert_eq!(
4214            manifest_value["components"][0]["actions"][1]["operation"],
4215            serde_json::json!("subtract_assign")
4216        );
4217        assert_eq!(
4218            manifest_value["components"][0]["actions"][1]["operand"],
4219            serde_json::json!("3")
4220        );
4221    }
4222
4223    #[test]
4224    fn builds_template_manifest_for_direct_assignment_action() {
4225        let source =
4226            include_str!("../../../fixtures/0011-direct-assignment/input/ResetCounter.tsx");
4227
4228        let parsed = presolve_parser::parse_file(
4229            "fixtures/0011-direct-assignment/input/ResetCounter.tsx",
4230            source,
4231        );
4232
4233        let component_graph = build_component_graph(&parsed);
4234        let template_graph = build_template_graph(&component_graph);
4235        let manifest = build_template_manifest(&component_graph, &template_graph);
4236
4237        assert_eq!(
4238            manifest.components[0].actions,
4239            vec![ManifestAction {
4240                method: "reset".to_string(),
4241                method_id: None,
4242                action_batch_id: None,
4243                operation: ManifestOperation::Assign,
4244                field: "count".to_string(),
4245                storage_id: None,
4246                operand: Some(SerializableValue::Number("0".to_string())),
4247            }]
4248        );
4249
4250        let manifest_json = template_manifest_json(&manifest);
4251        let manifest_value: serde_json::Value =
4252            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4253
4254        assert_eq!(
4255            manifest_value["components"][0]["actions"][0]["operation"],
4256            serde_json::json!("assign")
4257        );
4258        assert_eq!(
4259            manifest_value["components"][0]["actions"][0]["operand"],
4260            serde_json::json!("0")
4261        );
4262    }
4263
4264    #[test]
4265    fn builds_template_manifest_for_boolean_toggle_action() {
4266        let source = include_str!("../../../fixtures/0012-boolean-toggle/input/ToggleFlag.tsx");
4267
4268        let parsed = presolve_parser::parse_file(
4269            "fixtures/0012-boolean-toggle/input/ToggleFlag.tsx",
4270            source,
4271        );
4272
4273        let component_graph = build_component_graph(&parsed);
4274        let template_graph = build_template_graph(&component_graph);
4275        let manifest = build_template_manifest(&component_graph, &template_graph);
4276
4277        assert_eq!(
4278            manifest.components[0].actions,
4279            vec![ManifestAction {
4280                method: "toggle".to_string(),
4281                method_id: None,
4282                action_batch_id: None,
4283                operation: ManifestOperation::Toggle,
4284                field: "enabled".to_string(),
4285                storage_id: None,
4286                operand: None,
4287            }]
4288        );
4289
4290        let manifest_json = template_manifest_json(&manifest);
4291        let manifest_value: serde_json::Value =
4292            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4293
4294        assert_eq!(
4295            manifest_value["components"][0]["actions"][0]["operation"],
4296            serde_json::json!("toggle")
4297        );
4298        assert!(manifest_value["components"][0]["actions"][0]
4299            .get("operand")
4300            .is_none());
4301    }
4302
4303    #[test]
4304    fn builds_template_manifest_for_multi_step_action_in_source_order() {
4305        let source =
4306            include_str!("../../../fixtures/0013-multi-step-action/input/BatchActionCounter.tsx");
4307
4308        let parsed = presolve_parser::parse_file(
4309            "fixtures/0013-multi-step-action/input/BatchActionCounter.tsx",
4310            source,
4311        );
4312
4313        let component_graph = build_component_graph(&parsed);
4314        let template_graph = build_template_graph(&component_graph);
4315        let manifest = build_template_manifest(&component_graph, &template_graph);
4316
4317        assert_eq!(
4318            manifest.components[0].actions,
4319            vec![
4320                ManifestAction {
4321                    method: "apply".to_string(),
4322                    method_id: None,
4323                    action_batch_id: None,
4324                    operation: ManifestOperation::AddAssign,
4325                    field: "count".to_string(),
4326                    storage_id: None,
4327                    operand: Some(SerializableValue::Number("2".to_string())),
4328                },
4329                ManifestAction {
4330                    method: "apply".to_string(),
4331                    method_id: None,
4332                    action_batch_id: None,
4333                    operation: ManifestOperation::Decrement,
4334                    field: "count".to_string(),
4335                    storage_id: None,
4336                    operand: None,
4337                },
4338                ManifestAction {
4339                    method: "apply".to_string(),
4340                    method_id: None,
4341                    action_batch_id: None,
4342                    operation: ManifestOperation::Assign,
4343                    field: "count".to_string(),
4344                    storage_id: None,
4345                    operand: Some(SerializableValue::Number("8".to_string())),
4346                },
4347                ManifestAction {
4348                    method: "apply".to_string(),
4349                    method_id: None,
4350                    action_batch_id: None,
4351                    operation: ManifestOperation::Increment,
4352                    field: "count".to_string(),
4353                    storage_id: None,
4354                    operand: None,
4355                },
4356                ManifestAction {
4357                    method: "apply".to_string(),
4358                    method_id: None,
4359                    action_batch_id: None,
4360                    operation: ManifestOperation::Toggle,
4361                    field: "enabled".to_string(),
4362                    storage_id: None,
4363                    operand: None,
4364                }
4365            ]
4366        );
4367
4368        let manifest_json = template_manifest_json(&manifest);
4369        let manifest_value: serde_json::Value =
4370            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4371
4372        let actions = manifest_value["components"][0]["actions"]
4373            .as_array()
4374            .expect("manifest actions should be an array");
4375
4376        assert_eq!(
4377            actions
4378                .iter()
4379                .map(|action| action["operation"].as_str().expect("operation is a string"))
4380                .collect::<Vec<_>>(),
4381            vec!["add_assign", "decrement", "assign", "increment", "toggle"]
4382        );
4383        assert_eq!(actions[0]["method"], serde_json::json!("apply"));
4384        assert_eq!(actions[4]["method"], serde_json::json!("apply"));
4385        assert!(actions[1].get("operand").is_none());
4386        assert!(actions[3].get("operand").is_none());
4387        assert!(actions[4].get("operand").is_none());
4388    }
4389
4390    #[test]
4391    fn builds_template_manifest_for_dynamic_attribute_bindings() {
4392        let source = include_str!(
4393            "../../../fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx"
4394        );
4395
4396        let parsed = presolve_parser::parse_file(
4397            "fixtures/0015-dynamic-attributes/input/DynamicAttributeButton.tsx",
4398            source,
4399        );
4400
4401        let component_graph = build_component_graph(&parsed);
4402        let template_graph = build_template_graph(&component_graph);
4403        let manifest = build_template_manifest(&component_graph, &template_graph);
4404        let manifest_json = template_manifest_json(&manifest);
4405        let manifest_value: serde_json::Value =
4406            serde_json::from_str(&manifest_json).expect("manifest JSON should parse");
4407
4408        assert_eq!(
4409            manifest.components[0].template.nodes[1],
4410            ManifestNode::Binding {
4411                id: "n1".to_string(),
4412                expression: "this.disabled".to_string(),
4413                initial_value: Some(SerializableValue::Boolean(false)),
4414                target: Some(ManifestBindingTarget::Attribute),
4415                element: Some("n0".to_string()),
4416                attribute: Some("disabled".to_string()),
4417            }
4418        );
4419        assert_eq!(
4420            manifest_value["components"][0]["template"]["nodes"][1]["target"],
4421            serde_json::json!("attribute")
4422        );
4423        assert_eq!(
4424            manifest_value["components"][0]["template"]["nodes"][1]["element"],
4425            serde_json::json!("n0")
4426        );
4427        assert_eq!(
4428            manifest_value["components"][0]["template"]["nodes"][1]["attribute"],
4429            serde_json::json!("disabled")
4430        );
4431        assert!(manifest_value["components"][0]["template"]["nodes"][3]
4432            .get("target")
4433            .is_none());
4434    }
4435
4436    #[test]
4437    fn generates_standalone_page_with_embedded_manifest() {
4438        let source = include_str!("../../../fixtures/0004-nested-jsx/input/NestedCounter.tsx");
4439
4440        let parsed =
4441            presolve_parser::parse_file("fixtures/0004-nested-jsx/input/NestedCounter.tsx", source);
4442
4443        let component_graph = build_component_graph(&parsed);
4444        let template_graph = build_template_graph(&component_graph);
4445        let html = generate_static_html(&template_graph);
4446        let manifest = build_template_manifest(&component_graph, &template_graph);
4447        let page = generate_standalone_page("NestedCounter", &html, &manifest);
4448
4449        assert!(page.starts_with("<!doctype html>\n"));
4450        assert!(page.contains("<title>NestedCounter</title>"));
4451        assert!(page.contains("<section data-presolve-node=\"n0\">"));
4452        assert!(page.contains("id=\"presolve-template-manifest\""));
4453        assert!(page.contains("\"name\": \"NestedCounter\""));
4454        assert!(page.contains("<script src=\"./runtime.js\" defer></script>"));
4455    }
4456}