Skip to main content

presolve_compiler/
lib.rs

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