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