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