From 5ab2451e9904c732e71da05844a7b96d8f78b7b9 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Mon, 17 Aug 2026 13:29:08 +0200 Subject: [PATCH] test(oxc-reference): expand primitive conformance corpus --- ...rimitive-literals-independently-in-rust.md | 17 +- docs/porting/migration-contract.md | 5 +- internal/oxc_reference/README.md | 20 +- .../occurrence-consumer/src/conformance.rs | 647 +++++++++++++++--- .../src/primitive_producer.rs | 137 +++- internal/oxc_reference/run-conformance.sh | 24 +- .../testdata/corpus/v0/README.md | 8 + .../v0/primitive-literal-budget/README.md | 4 + .../v0/primitive-literal-budget/case.json | 35 + .../v0/primitive-literal-budget/src/budget.ts | 3 + .../v0/primitive-literal-budget/tsconfig.json | 8 + .../primitive-literal-conformance/README.md | 5 + .../primitive-literal-conformance/case.json | 248 +++++++ .../src/exports.ts | 2 + .../src/matrix.ts | 29 + .../tsconfig.json | 10 + .../primitive-literal-independent/README.md | 4 + .../primitive-literal-independent/case.json | 126 +++- .../corpus/v0/recovery-budgets/README.md | 5 + .../corpus/v0/recovery-budgets/case.json | 39 +- rfcs/0001-semantic-facts-kernel.md | 9 + 21 files changed, 1238 insertions(+), 147 deletions(-) create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/README.md create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/case.json create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/src/budget.ts create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/tsconfig.json create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/README.md create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/case.json create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/src/exports.ts create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/src/matrix.ts create mode 100644 internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/tsconfig.json diff --git a/docs/adr/0019-compute-primitive-literals-independently-in-rust.md b/docs/adr/0019-compute-primitive-literals-independently-in-rust.md index cbbf46225..f1f603cb5 100644 --- a/docs/adr/0019-compute-primitive-literals-independently-in-rust.md +++ b/docs/adr/0019-compute-primitive-literals-independently-in-rust.md @@ -50,10 +50,14 @@ byte-identical. The executable shadow threshold is at least 15 complete supported records, 1,000,000 parts-per-million structured agreement, and zero semantic, -transport, or mapping differences. The tagged baseline contains 11 facts: 10 -supported facts plus one named unsupported object, 50 compared roots with 50 -identity matches, and 11 exact OXC mappings. Expected apparent-type truncation -and unsupported states remain visible outside the supported denominator. +transport, or unexplained mapping differences. The expanded tagged baseline +contains 28 classified facts: 20 supported, four named unsupported, one budget, +and three expected recovery-file mapping facts. It compares 29 supported graph +records and 100 roots with exact identity agreement. Twenty-five facts map to +typed OXC nodes; the three unmapped facts retain the stable `oxc-parse-error` +classification. Expected unsupported, budget, and mapping states remain visible +outside the supported denominator, and the full report must be byte-identical +across repeated runs. This producer remains an internal shadow implementation. The Go checker stays the semantic authority and production fallback. No TS7 producer protocol, @@ -65,8 +69,9 @@ part of this decision. - Primitive/literal conformance now measures independently computed Rust/OXC semantics instead of a Go-graph projection. - The narrow category has deterministic source-to-record and graph-identity - evidence, including contextual literals, literal unions, unsupported forms, - and budget truncation. + evidence, including declarations and expressions, widening, `as const`, + `satisfies`, contextual literals, literal unions, null-like values, imports, + enum-like values, unsupported forms, and budget truncation. - Graph inspection continues to describe Go responses; it no longer embeds a misleading projected Rust candidate. - Passing this shadow gate does not itself authorize a production authority diff --git a/docs/porting/migration-contract.md b/docs/porting/migration-contract.md index d6d17f35c..c93023a80 100644 --- a/docs/porting/migration-contract.md +++ b/docs/porting/migration-contract.md @@ -114,8 +114,9 @@ fallback behavior remain Go-authoritative. 8. `./internal/oxc_reference/run-evidence.sh --output ` produces stable repeated observations and passes ADR-0016's occurrence/attachment gate. 9. `./internal/oxc_reference/run-conformance.sh --output ` emits the - deterministic ADR-0019 report and fails on any semantic, transport, or - mapping mismatch while retaining named unsupported and budget cases. + byte-stable ADR-0019 report and fails on any unexplained semantic, transport, + or mapping mismatch while retaining named unsupported, budget, and expected + mapping cases outside the supported denominator. ## Upstream synchronization diff --git a/internal/oxc_reference/README.md b/internal/oxc_reference/README.md index 212a728f5..1143081d9 100644 --- a/internal/oxc_reference/README.md +++ b/internal/oxc_reference/README.md @@ -60,17 +60,21 @@ Run the deterministic Go-versus-Rust shadow conformance gate with: --output /tmp/ts7-rust-conformance.json ``` -The command runs the same corpus through the Go oracle and the version-2 Rust -primitive/literal candidate. Rust derives its records independently from the +The command runs every explicitly classified primitive/literal corpus fixture +through the Go oracle and the version-2 Rust producer using the internal +version-2 candidate schema. Rust derives its records independently from the project source and OXC semantic nodes; the Go graph is supplied only to the comparator. Its JSON compares fact identity, exact OXC mapping, all five roots, response-local graph identity through a Go-to-Rust ID bijection, structured -payloads, recovery, unsupported states, and truncation. Every entry is -classified as `semantic`, `transport`, `mapping`, `unsupported`, or `budget`. -Unexplained -semantic, transport, or mapping differences fail the command; named -unsupported and budget cases remain separately reported. The gate is a shadow -comparison and does not change the producer or production routing. See +payloads, recovery, unsupported states, and truncation. Every selected fixture +contains a structured Go expectation and exactly one Rust classification: +`supported`, `unsupported`, `budget`, or `mapping`. The three recovery-file +facts remain individually visible as expected `oxc-parse-error` mapping gaps. +Unexplained semantic, transport, or mapping differences fail the command; +expected unsupported, budget, and mapping cases remain separately reported. +The wrapper executes the complete report twice and requires byte-identical +files. The gate is a shadow comparison and does not change the producer or +production routing. See [ADR-0019](../../docs/adr/0019-compute-primitive-literals-independently-in-rust.md). The first test suite applies the Rust implementation of the portable contract diff --git a/internal/oxc_reference/crates/occurrence-consumer/src/conformance.rs b/internal/oxc_reference/crates/occurrence-consumer/src/conformance.rs index aec81b914..5913275e0 100644 --- a/internal/oxc_reference/crates/occurrence-consumer/src/conformance.rs +++ b/internal/oxc_reference/crates/occurrence-consumer/src/conformance.rs @@ -16,8 +16,8 @@ use crate::{ }, contract::{Occurrence, Span}, facts::{ - EntityState, OccurrenceTypeFacts, ProducerBudgetReport, SemanticSnapshot, TypeGraph, - TypeId, TypeKind, TypeViewState, + EntityState, LiteralValue, OccurrenceTypeFacts, ProducerBudgetReport, SemanticSnapshot, + TypeGraph, TypeId, TypeKind, TypeViewState, TypeViewStates, }, primitive_producer::{ IndependentPrimitiveLiteralOutput, PrimitiveLiteralSelection, PrimitiveProducerLimits, @@ -25,7 +25,7 @@ use crate::{ }, }; -pub const CONFORMANCE_SCHEMA_VERSION: u32 = 2; +pub const CONFORMANCE_SCHEMA_VERSION: u32 = 3; #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] @@ -35,11 +35,21 @@ pub struct ConformanceReport { pub candidate: &'static str, pub shadow_only: bool, pub threshold: CompatibilityThreshold, + pub corpus: CorpusCoverage, pub cases: Vec, pub summary: ConformanceSummary, pub passes: bool, } +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CorpusCoverage { + pub discovered_cases: usize, + pub selected_cases: usize, + pub selected_facts: usize, + pub excluded_cases: Vec, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompatibilityThreshold { @@ -55,9 +65,10 @@ pub struct CompatibilityThreshold { #[serde(rename_all = "camelCase")] pub struct ConformanceCase { pub name: String, - pub independent_scope: bool, pub facts: usize, pub repeated_rust_output_equal: bool, + pub classifications: ClassificationCoverage, + pub selections: Vec, pub go_oracle: GoOracleEvidence, pub rust_producer: RustProducerEvidence, pub roots: RootCoverage, @@ -69,6 +80,57 @@ pub struct ConformanceCase { pub differences: Vec, } +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClassificationCoverage { + pub supported: usize, + pub unsupported: usize, + pub budget: usize, + pub mapping: usize, +} + +impl ClassificationCoverage { + fn add(&mut self, classification: ExpectedClassification) { + match classification { + ExpectedClassification::Supported => self.supported += 1, + ExpectedClassification::Unsupported => self.unsupported += 1, + ExpectedClassification::Budget => self.budget += 1, + ExpectedClassification::Mapping => self.mapping += 1, + } + } + + fn merge(&mut self, other: Self) { + self.supported += other.supported; + self.unsupported += other.unsupported; + self.budget += other.budget; + self.mapping += other.mapping; + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SelectionEvidence { + pub fact_index: usize, + pub proves: String, + pub expected_classification: ExpectedClassification, + #[serde(skip_serializing_if = "Option::is_none")] + pub expected_code: Option, + pub expectation_matched: bool, + pub go_oracle: GoOracleFactObservation, + pub rust_candidate: PrimitiveLiteralCandidate, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GoOracleFactObservation { + pub occurrence: Occurrence, + pub complete: bool, + pub recovered: bool, + pub truncated: bool, + pub type_view_states: TypeViewStates, + pub actual: serde_json::Value, +} + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct GoOracleEvidence { @@ -115,7 +177,9 @@ pub struct ConformanceSummary { pub supported_records: usize, pub matched_supported_records: usize, pub supported_compatibility_ppm: u64, + pub classifications: ClassificationCoverage, pub differences_by_category: BTreeMap, + pub unexplained_differences_by_category: BTreeMap, pub expected_differences: usize, pub blocking_differences: usize, } @@ -129,6 +193,7 @@ impl Default for ConformanceSummary { supported_records: 0, matched_supported_records: 0, supported_compatibility_ppm: 0, + classifications: ClassificationCoverage::default(), differences_by_category: [ ("semantic".to_owned(), 0), ("transport".to_owned(), 0), @@ -138,6 +203,15 @@ impl Default for ConformanceSummary { ] .into_iter() .collect(), + unexplained_differences_by_category: [ + ("semantic".to_owned(), 0), + ("transport".to_owned(), 0), + ("mapping".to_owned(), 0), + ("unsupported".to_owned(), 0), + ("budget".to_owned(), 0), + ] + .into_iter() + .collect(), expected_differences: 0, blocking_differences: 0, } @@ -154,6 +228,12 @@ pub enum DifferenceCategory { Budget, } +struct NonSupportedObservation<'a> { + category: DifferenceCategory, + state_matches: bool, + explanation: &'a str, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConformanceDifference { @@ -179,8 +259,6 @@ struct CorpusManifest { project: String, capabilities: Vec, #[serde(default)] - coverage: Vec, - #[serde(default)] budgets: ProducerBudgetRequest, selections: Vec, } @@ -197,6 +275,52 @@ struct CorpusSelection { file: String, text: String, occurrence: usize, + proves: String, + #[serde(default)] + conformance: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct SelectionExpectation { + classification: ExpectedClassification, + #[serde(default)] + code: Option, + go_oracle: GoOracleExpectation, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ExpectedClassification { + Supported, + Unsupported, + Budget, + Mapping, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct GoOracleExpectation { + complete: bool, + recovered: bool, + truncated: bool, + type_view_states: TypeViewStates, + actual: GoOracleTypeExpectation, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct GoOracleTypeExpectation { + type_kind: TypeKind, + state: EntityState, + complete: bool, + truncated: bool, + #[serde(default)] + literal: Option, + #[serde(default)] + members: Option>, + #[serde(default)] + member_count: Option, } #[derive(Serialize)] @@ -226,9 +350,31 @@ pub fn run_conformance( let corpus_root = corpus_root .canonicalize() .map_err(|error| format!("transport: resolve {}: {error}", corpus_root.display()))?; + let case_directories = sorted_case_directories(&corpus_root)?; + let mut corpus = CorpusCoverage { + discovered_cases: case_directories.len(), + ..CorpusCoverage::default() + }; let mut cases = Vec::new(); - for case_directory in sorted_case_directories(&corpus_root)? { + for case_directory in case_directories { let manifest = read_manifest(&case_directory)?; + let expectation_count = manifest + .selections + .iter() + .filter(|selection| selection.conformance.is_some()) + .count(); + if expectation_count == 0 { + corpus.excluded_cases.push(manifest.name); + continue; + } + if expectation_count != manifest.selections.len() { + return Err(format!( + "transport: case {:?} has {expectation_count}/{} conformance expectations; selected cases must classify every fixture", + manifest.name, + manifest.selections.len() + )); + } + validate_expectations(&manifest)?; let request = build_request(&case_directory, &manifest)?; let snapshot = run_go_oracle(&tsfacts_binary, &case_directory, &request)?; let selections = request @@ -252,16 +398,30 @@ pub fn run_conformance( let first = produce_primitive_literals(&case_directory, &selections, limits)?; let repeated = produce_primitive_literals(&case_directory, &selections, limits)?; let repeated_equal = serde_json::to_vec(&first).ok() == serde_json::to_vec(&repeated).ok(); - let independent_scope = manifest - .coverage + let expectations = manifest + .selections + .iter() + .map(|selection| { + selection + .conformance + .clone() + .expect("selected cases require every expectation") + }) + .collect::>(); + let proves = manifest + .selections .iter() - .any(|item| item == "primitive-literals-independent"); + .map(|selection| selection.proves.clone()) + .collect::>(); + corpus.selected_cases += 1; + corpus.selected_facts += expectations.len(); cases.push(compare_case( manifest.name, - independent_scope, &snapshot, first, repeated_equal, + &expectations, + &proves, )); } @@ -274,6 +434,7 @@ pub fn run_conformance( candidate: "independent-primitive-literal-v2", shadow_only: true, threshold, + corpus, cases, summary, passes, @@ -301,7 +462,7 @@ fn threshold_passes(summary: &ConformanceSummary, threshold: CompatibilityThresh fn difference_count(summary: &ConformanceSummary, category: &str) -> usize { summary - .differences_by_category + .unexplained_differences_by_category .get(category) .copied() .unwrap_or_default() @@ -343,10 +504,11 @@ fn run_go_oracle( fn compare_case( name: String, - independent_scope: bool, snapshot: &SemanticSnapshot, output: IndependentPrimitiveLiteralOutput, repeated_equal: bool, + expectations: &[SelectionExpectation], + proves: &[String], ) -> ConformanceCase { let mut comparison = Comparison::new(snapshot, &output); if !repeated_equal { @@ -361,31 +523,8 @@ fn compare_case( )); } - if independent_scope { - comparison.compare_scoped_facts(); - } else { - comparison.differences.push(expected_difference( - DifferenceCategory::Unsupported, - "case-outside-independent-slice", - None, - "case.coverage", - None, - None, - "this pre-existing corpus case remains outside the selected primitive/literal slice", - )); - } - if output.truncated { - comparison.differences.push(expected_difference( - DifferenceCategory::Budget, - "rust-type-budget-truncated", - None, - "rustProducer.truncated", - json_value(&snapshot.budgets.truncated), - json_value(&true), - "the independent Rust response-local type budget was exhausted", - )); - } - comparison.finish(name, independent_scope, repeated_equal) + comparison.compare_scoped_facts(expectations, proves); + comparison.finish(name, repeated_equal) } struct Comparison<'a> { @@ -395,6 +534,8 @@ struct Comparison<'a> { roots: RootCoverage, mapping: MappingCoverage, candidate_states: CandidateSummary, + classifications: ClassificationCoverage, + selections: Vec, bijection: TypeBijection, compared_pairs: BTreeSet<(TypeId, TypeId)>, supported_records: usize, @@ -441,6 +582,8 @@ impl<'a> Comparison<'a> { ..MappingCoverage::default() }, candidate_states, + classifications: ClassificationCoverage::default(), + selections: Vec::new(), bijection: TypeBijection::default(), compared_pairs: BTreeSet::new(), supported_records: 0, @@ -449,58 +592,256 @@ impl<'a> Comparison<'a> { } } - fn compare_scoped_facts(&mut self) { - let mut candidates = self - .output - .candidates - .iter() - .map(|candidate| (candidate.occurrence.clone(), candidate)) - .collect::>(); - for (fact_index, facts) in self.snapshot.facts().iter().enumerate() { + fn compare_scoped_facts(&mut self, expectations: &[SelectionExpectation], proves: &[String]) { + self.mapping.facts = expectations.len(); + if self.snapshot.facts().len() != expectations.len() + || self.output.candidates.len() != expectations.len() + || proves.len() != expectations.len() + { + self.differences.push(mismatch( + DifferenceCategory::Transport, + "selection-count-mismatch", + None, + "selections", + json_value(&expectations.len()), + json_value(&( + self.snapshot.facts().len(), + self.output.candidates.len(), + proves.len(), + )), + "the manifest, Go oracle, and Rust producer must retain one observation per selection", + )); + } + + let mut failed_files = BTreeSet::new(); + let count = expectations + .len() + .min(self.snapshot.facts().len()) + .min(self.output.candidates.len()) + .min(proves.len()); + for fact_index in 0..count { + let facts = &self.snapshot.facts()[fact_index]; + let candidate = &self.output.candidates[fact_index]; + let expectation = &expectations[fact_index]; let occurrence = facts.occurrence(); - let Some(candidate) = candidates.remove(&occurrence) else { - self.mapping.unmapped += 1; - self.differences.push(mismatch( - DifferenceCategory::Mapping, - "missing-rust-candidate", - Some((fact_index, occurrence)), - "mapping", - json_value(&"one candidate"), - json_value(&"none"), - "every selected Go fact must map to one independent OXC candidate", - )); - continue; - }; - if candidate.oxc_node_id.is_some() { - self.mapping.mapped += 1; - } else { - self.mapping.unmapped += 1; + let difference_start = self.differences.len(); + self.classifications.add(expectation.classification); + self.validate_go_expectation(fact_index, facts, &expectation.go_oracle); + + if candidate.occurrence.file != occurrence.file + || candidate.occurrence.span != occurrence.span + { self.differences.push(mismatch( - DifferenceCategory::Mapping, - "missing-oxc-node", + DifferenceCategory::Transport, + "fact-identity-mismatch", Some((fact_index, occurrence.clone())), - "oxcNodeId", - json_value(&"typed OXC NodeId"), - None, - "independent facts must originate from an exact OXC semantic node", + "occurrence", + json_value(&occurrence), + json_value(&candidate.occurrence), + "Go and Rust observations must retain the requested file and UTF-8 span", )); } - self.compare_fact(fact_index, facts, candidate); + + match expectation.classification { + ExpectedClassification::Supported => { + if candidate.occurrence.syntax_kind != occurrence.syntax_kind { + self.differences.push(mismatch( + DifferenceCategory::Mapping, + "syntax-kind-mismatch", + Some((fact_index, occurrence.clone())), + "occurrence.syntaxKind", + json_value(&occurrence.syntax_kind), + json_value(&candidate.occurrence.syntax_kind), + "supported observations require the exact portable syntax kind", + )); + } + if candidate.oxc_node_id.is_some() { + self.mapping.mapped += 1; + self.compare_fact(fact_index, facts, candidate); + } else { + self.mapping.unmapped += 1; + self.differences.push(mismatch( + DifferenceCategory::Mapping, + "missing-oxc-node", + Some((fact_index, occurrence.clone())), + "oxcNodeId", + json_value(&"typed OXC NodeId"), + None, + "supported facts must originate from an exact OXC semantic node", + )); + } + } + ExpectedClassification::Unsupported => { + self.observe_expected_non_supported( + fact_index, + facts, + candidate, + expectation, + NonSupportedObservation { + category: DifferenceCategory::Unsupported, + state_matches: candidate.summary.unsupported > 0, + explanation: "the fixture pins an out-of-category Rust observation", + }, + ); + } + ExpectedClassification::Budget => { + self.observe_expected_non_supported( + fact_index, + facts, + candidate, + expectation, + NonSupportedObservation { + category: DifferenceCategory::Budget, + state_matches: candidate.summary.truncated > 0 && self.output.truncated, + explanation: "the fixture pins explicit response-local budget truncation", + }, + ); + } + ExpectedClassification::Mapping => { + self.mapping.unmapped += 1; + failed_files.insert(facts.file.clone()); + let code = expectation + .code + .as_deref() + .expect("mapping expectations require a code"); + let diagnostic_matches = + self.output.diagnostics.iter().any(|diagnostic| { + diagnostic.file == facts.file && diagnostic.code == code + }); + if candidate.oxc_node_id.is_none() + && candidate.summary.error > 0 + && diagnostic_matches + { + self.differences.push(expected_difference( + DifferenceCategory::Mapping, + code, + Some((fact_index, occurrence.clone())), + "classification", + json_value(&expectation.classification), + json_value(candidate), + "the known recovery-file OXC parser gap is explicit and stable", + )); + } else { + self.differences.push(mismatch( + DifferenceCategory::Mapping, + "mapping-classification-mismatch", + Some((fact_index, occurrence.clone())), + "classification", + json_value(expectation), + json_value(candidate), + "the expected mapping gap changed and its fixture classification must be reviewed", + )); + } + } + } + + let expectation_matched = self.differences[difference_start..] + .iter() + .all(|difference| difference.expected); + self.selections.push(SelectionEvidence { + fact_index, + proves: proves[fact_index].clone(), + expected_classification: expectation.classification, + expected_code: expectation.code.clone(), + expectation_matched, + go_oracle: GoOracleFactObservation { + occurrence, + complete: facts.complete, + recovered: facts.recovered, + truncated: facts.truncated, + type_view_states: facts.type_view_states.clone(), + actual: self + .snapshot + .graph() + .type_record(facts.actual()) + .map_or(serde_json::Value::Null, oracle_record_value), + }, + rust_candidate: candidate.clone(), + }); } - for (occurrence, _) in candidates { + self.mapping.failed_files = failed_files.len(); + } + + fn observe_expected_non_supported( + &mut self, + fact_index: usize, + facts: &OccurrenceTypeFacts, + candidate: &PrimitiveLiteralCandidate, + expectation: &SelectionExpectation, + observation: NonSupportedObservation<'_>, + ) { + let occurrence = facts.occurrence(); + if candidate.oxc_node_id.is_some() { + self.mapping.mapped += 1; + } else { self.mapping.unmapped += 1; + } + if observation.state_matches { + self.differences.push(expected_difference( + observation.category, + expectation + .code + .as_deref() + .expect("non-supported expectations require a code"), + Some((fact_index, occurrence)), + "classification", + json_value(&expectation.classification), + json_value(candidate), + observation.explanation, + )); + } else { self.differences.push(mismatch( - DifferenceCategory::Mapping, - "extra-rust-candidate", - None, - "mapping", - json_value(&"no extra candidate"), - json_value(&occurrence), - "the Rust producer must not invent facts outside the pinned request", + DifferenceCategory::Transport, + "classification-state-mismatch", + Some((fact_index, occurrence)), + "classification", + json_value(expectation), + json_value(candidate), + "the Rust candidate state no longer matches its explicit fixture classification", )); } } + fn validate_go_expectation( + &mut self, + fact_index: usize, + facts: &OccurrenceTypeFacts, + expectation: &GoOracleExpectation, + ) { + let actual_record = self.snapshot.graph().type_record(facts.actual()); + let fact_matches = facts.complete == expectation.complete + && facts.recovered == expectation.recovered + && facts.truncated == expectation.truncated + && facts.type_view_states == expectation.type_view_states; + let type_matches = actual_record.is_some_and(|record| { + oracle_type_matches_expectation( + self.snapshot.graph(), + record, + &expectation.actual, + &mut BTreeSet::new(), + ) + }); + if fact_matches && type_matches { + return; + } + let occurrence = facts.occurrence(); + self.differences.push(mismatch( + DifferenceCategory::Semantic, + "go-oracle-expectation-mismatch", + Some((fact_index, occurrence)), + "goOracle", + json_value(expectation), + json_value(&serde_json::json!({ + "complete": facts.complete, + "recovered": facts.recovered, + "truncated": facts.truncated, + "typeViewStates": facts.type_view_states, + "actual": actual_record.map(oracle_record_value), + })), + "the checked Go oracle observation drifted from the fixture's structured expectation", + )); + } + fn compare_fact( &mut self, fact_index: usize, @@ -524,14 +865,14 @@ impl<'a> Comparison<'a> { oracle_type_supported(self.snapshot.graph(), record, &mut BTreeSet::new()) }); if !actual_supported { - self.differences.push(expected_difference( - DifferenceCategory::Unsupported, - "actual-type-outside-primitive-literal-slice", + self.differences.push(mismatch( + DifferenceCategory::Semantic, + "supported-go-type-outside-primitive-literal-slice", context, "roots[actual]", actual_record.map(oracle_record_value), candidate.roots.first().and_then(json_value), - "the fixture intentionally retains an out-of-category selection", + "a fixture classified as supported must have an in-category complete Go actual type", )); return; } @@ -738,18 +1079,14 @@ impl<'a> Comparison<'a> { matched } - fn finish( - mut self, - name: String, - independent_scope: bool, - repeated_equal: bool, - ) -> ConformanceCase { + fn finish(mut self, name: String, repeated_equal: bool) -> ConformanceCase { sort_differences(&mut self.differences); ConformanceCase { name, - independent_scope, facts: self.snapshot.facts().len(), repeated_rust_output_equal: repeated_equal, + classifications: self.classifications, + selections: self.selections, go_oracle: GoOracleEvidence { diagnostic_count: self.snapshot.diagnostic_count, budgets: self.snapshot.budgets, @@ -838,6 +1175,42 @@ fn oracle_type_supported( } } +fn oracle_type_matches_expectation( + graph: &TypeGraph, + record: &crate::facts::TypeRecord, + expectation: &GoOracleTypeExpectation, + visiting: &mut BTreeSet, +) -> bool { + if record.type_kind != expectation.type_kind + || record.state != expectation.state + || record.complete != expectation.complete + || record.truncated != expectation.truncated + || record.literal != expectation.literal + || expectation + .member_count + .is_some_and(|count| record.members.len() != count) + { + return false; + } + let Some(expected_members) = &expectation.members else { + return true; + }; + if expected_members.len() != record.members.len() || !visiting.insert(record.id.clone()) { + return false; + } + let matched = record + .members + .iter() + .zip(expected_members) + .all(|(member, expectation)| { + graph.type_record(member).is_some_and(|record| { + oracle_type_matches_expectation(graph, record, expectation, visiting) + }) + }); + visiting.remove(&record.id); + matched +} + fn literal_wire_name(kind: LiteralKind) -> &'static str { match kind { LiteralKind::Boolean => "boolean", @@ -878,6 +1251,7 @@ fn summarize(cases: &[ConformanceCase]) -> ConformanceSummary { + case.candidate_states.error; summary.supported_records += case.supported_records; summary.matched_supported_records += case.matched_supported_records; + summary.classifications.merge(case.classifications); for difference in &case.differences { *summary .differences_by_category @@ -885,13 +1259,19 @@ fn summarize(cases: &[ConformanceCase]) -> ConformanceSummary { .or_default() += 1; if difference.expected { summary.expected_differences += 1; - } else if matches!( - difference.category, - DifferenceCategory::Semantic - | DifferenceCategory::Transport - | DifferenceCategory::Mapping - ) { - summary.blocking_differences += 1; + } else { + *summary + .unexplained_differences_by_category + .entry(category_name(difference.category).to_owned()) + .or_default() += 1; + if matches!( + difference.category, + DifferenceCategory::Semantic + | DifferenceCategory::Transport + | DifferenceCategory::Mapping + ) { + summary.blocking_differences += 1; + } } } } @@ -1006,6 +1386,57 @@ fn read_manifest(case_directory: &Path) -> Result { .map_err(|error| format!("transport: decode {}: {error}", path.display())) } +fn validate_expectations(manifest: &CorpusManifest) -> Result<(), String> { + for (index, selection) in manifest.selections.iter().enumerate() { + let expectation = selection + .conformance + .as_ref() + .expect("selected cases require every expectation"); + let requires_code = expectation.classification != ExpectedClassification::Supported; + if requires_code + != expectation + .code + .as_deref() + .is_some_and(|code| !code.is_empty()) + { + return Err(format!( + "transport: case {:?} selections[{index}] must {} an explicit classification code", + manifest.name, + if requires_code { "provide" } else { "omit" } + )); + } + if expectation.go_oracle.type_view_states.actual != TypeViewState::Available { + return Err(format!( + "transport: case {:?} selections[{index}] Go actual view must be available", + manifest.name + )); + } + validate_type_expectation(&manifest.name, index, &expectation.go_oracle.actual)?; + } + Ok(()) +} + +fn validate_type_expectation( + case_name: &str, + selection_index: usize, + expectation: &GoOracleTypeExpectation, +) -> Result<(), String> { + if let (Some(member_count), Some(members)) = (expectation.member_count, &expectation.members) + && member_count != members.len() + { + return Err(format!( + "transport: case {case_name:?} selections[{selection_index}] memberCount {member_count} does not match {} structured members", + members.len() + )); + } + if let Some(members) = &expectation.members { + for member in members { + validate_type_expectation(case_name, selection_index, member)?; + } + } + Ok(()) +} + fn build_request<'a>( case_directory: &Path, manifest: &'a CorpusManifest, @@ -1060,6 +1491,30 @@ mod tests { assert!(!threshold_passes(&summary, compatibility_threshold())); } + #[test] + fn expected_mapping_gaps_do_not_hide_or_block_supported_denominator() { + let mut summary = ConformanceSummary { + supported_records: 15, + matched_supported_records: 15, + supported_compatibility_ppm: 1_000_000, + ..ConformanceSummary::default() + }; + summary + .differences_by_category + .insert("mapping".to_owned(), 3); + summary.classifications = ClassificationCoverage { + supported: 10, + mapping: 3, + ..ClassificationCoverage::default() + }; + + assert!(threshold_passes(&summary, compatibility_threshold())); + summary + .unexplained_differences_by_category + .insert("mapping".to_owned(), 1); + assert!(!threshold_passes(&summary, compatibility_threshold())); + } + #[test] fn response_local_identity_comparison_is_bijective() { let mut bijection = TypeBijection::default(); diff --git a/internal/oxc_reference/crates/occurrence-consumer/src/primitive_producer.rs b/internal/oxc_reference/crates/occurrence-consumer/src/primitive_producer.rs index b669b7280..0668c1940 100644 --- a/internal/oxc_reference/crates/occurrence-consumer/src/primitive_producer.rs +++ b/internal/oxc_reference/crates/occurrence-consumer/src/primitive_producer.rs @@ -7,7 +7,7 @@ use std::{ use oxc_allocator::Allocator; use oxc_ast::{ AstKind, - ast::{Expression, TSLiteral, TSType, TSTypeName}, + ast::{Expression, TSLiteral, TSType, TSTypeName, VariableDeclarationKind}, }; use oxc_parser::{ParseOptions, Parser}; use oxc_semantic::{NodeId, Semantic, SemanticBuilder}; @@ -24,7 +24,7 @@ use crate::{ facts::{TypeId, TypeView, TypeViewState}, }; -pub const INDEPENDENT_PRIMITIVE_LITERAL_PRODUCER_VERSION: u32 = 1; +pub const INDEPENDENT_PRIMITIVE_LITERAL_PRODUCER_VERSION: u32 = 2; #[derive(Clone, Debug, Eq, PartialEq)] pub struct PrimitiveLiteralSelection { @@ -530,19 +530,29 @@ fn infer_declaration( resolving: &mut BTreeSet<(u32, u32)>, ) -> SemanticShape { match kind { - AstKind::VariableDeclarator(declarator) => declarator - .type_annotation - .as_ref() - .map(|annotation| infer_ts_type(&annotation.type_annotation, semantic, resolving)) - .or_else(|| { - declarator - .init - .as_ref() - .map(|expression| infer_expression(expression, semantic, resolving)) - }) - .unwrap_or_else(|| { + AstKind::VariableDeclarator(declarator) => { + if let Some(annotation) = &declarator.type_annotation { + infer_ts_type(&annotation.type_annotation, semantic, resolving) + } else if let Some(expression) = &declarator.init { + let shape = infer_expression(expression, semantic, resolving); + let declaration_kind = match semantic.nodes().parent_kind(declarator.node_id.get()) + { + AstKind::VariableDeclaration(declaration) => Some(declaration.kind), + _ => None, + }; + if matches!( + declaration_kind, + Some(VariableDeclarationKind::Let | VariableDeclarationKind::Var) + ) && !is_const_assertion(expression) + { + widen_literal(shape) + } else { + shape + } + } else { unavailable_span(declarator.span, CandidateReason::UnsupportedExpression) - }), + } + } AstKind::TSTypeAliasDeclaration(alias) => { let key = (alias.span.start, alias.span.end); if !resolving.insert(key) { @@ -655,6 +665,9 @@ fn infer_expression( Expression::ParenthesizedExpression(parenthesized) => { infer_expression(&parenthesized.expression, semantic, resolving) } + Expression::TSAsExpression(assertion) if is_const_type(&assertion.type_annotation) => { + infer_expression(&assertion.expression, semantic, resolving) + } Expression::TSAsExpression(assertion) => { infer_ts_type(&assertion.type_annotation, semantic, resolving) } @@ -674,15 +687,47 @@ fn contextual_shape( resolving: &mut BTreeSet<(u32, u32)>, ) -> Option { semantic.nodes().ancestor_kinds(node_id).find_map(|kind| { - let AstKind::VariableDeclarator(declarator) = kind else { - return None; + let shape = match kind { + AstKind::TSSatisfiesExpression(satisfies) => { + infer_ts_type(&satisfies.type_annotation, semantic, resolving) + } + AstKind::VariableDeclarator(declarator) => { + let annotation = declarator.type_annotation.as_ref()?; + infer_ts_type(&annotation.type_annotation, semantic, resolving) + } + _ => return None, }; - let annotation = declarator.type_annotation.as_ref()?; - let shape = infer_ts_type(&annotation.type_annotation, semantic, resolving); (!matches!(shape, SemanticShape::NullLike(NullLikeKind::Null))).then_some(shape) }) } +fn is_const_assertion(expression: &Expression<'_>) -> bool { + matches!(expression, Expression::TSAsExpression(assertion) if is_const_type(&assertion.type_annotation)) +} + +fn is_const_type(r#type: &TSType<'_>) -> bool { + matches!( + r#type, + TSType::TSTypeReference(reference) + if matches!( + &reference.type_name, + TSTypeName::IdentifierReference(identifier) if identifier.name == "const" + ) + ) +} + +fn widen_literal(shape: SemanticShape) -> SemanticShape { + match shape { + SemanticShape::Literal(literal, _) => SemanticShape::Primitive(match literal { + LiteralKind::Boolean => PrimitiveKind::Boolean, + LiteralKind::String => PrimitiveKind::String, + LiteralKind::Number => PrimitiveKind::Number, + LiteralKind::Bigint => PrimitiveKind::Bigint, + }), + shape => shape, + } +} + fn apparent_shape(actual: &SemanticShape) -> Option { let primitive = match actual { SemanticShape::Primitive(primitive) => Some(*primitive), @@ -1045,6 +1090,52 @@ unsupported; ); } + #[test] + fn widening_const_assertions_and_satisfies_context_are_independent() { + const MATRIX: &str = r#" +let widened = "wide"; +const asserted = "narrow" as const; +const checked = "checked" satisfies string; +widened; +asserted; +"#; + let sources = BTreeMap::from([("src/primitives.ts".to_owned(), MATRIX.to_owned())]); + let selections = [ + selection(MATRIX, "widened", 1), + selection(MATRIX, "asserted", 1), + selection(MATRIX, "\"checked\"", 0), + ]; + let output = produce_primitive_literals_from_sources( + &sources, + &selections, + PrimitiveProducerLimits::default(), + ); + + assert!(candidate_has_semantic( + &output.candidates[0], + &CandidateSemantic::Primitive { + primitive: PrimitiveKind::String, + } + )); + assert!(candidate_has_semantic( + &output.candidates[1], + &CandidateSemantic::Literal { + literal: LiteralKind::String, + value: "narrow".to_owned(), + } + )); + assert_eq!( + output.candidates[2].roots[1].state, + TypeViewState::Available + ); + assert!(candidate_has_semantic( + &output.candidates[2], + &CandidateSemantic::Primitive { + primitive: PrimitiveKind::String, + } + )); + } + #[test] fn recoverable_oxc_diagnostics_mark_independent_facts_recovered() { const RECOVERED_SOURCE: &str = "declare const value: null;\nvalue;\n\ @@ -1088,4 +1179,14 @@ unsupported; }, } } + + fn candidate_has_semantic( + candidate: &PrimitiveLiteralCandidate, + expected: &CandidateSemantic, + ) -> bool { + candidate + .types + .iter() + .any(|record| record.semantic.as_ref() == Some(expected)) + } } diff --git a/internal/oxc_reference/run-conformance.sh b/internal/oxc_reference/run-conformance.sh index 00fea06a6..3a6603e71 100755 --- a/internal/oxc_reference/run-conformance.sh +++ b/internal/oxc_reference/run-conformance.sh @@ -5,13 +5,13 @@ repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" conformance_tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/ts7-rust-conformance.XXXXXX")" trap 'rm -rf -- "$conformance_tmpdir"' EXIT -output_args=() +output_path="" if [[ $# -gt 0 ]]; then if [[ $# -ne 2 || $1 != "--output" ]]; then echo "usage: $0 [--output ]" >&2 exit 2 fi - output_args=("--output" "$2") + output_path="$2" fi cd "$repository_root" @@ -22,4 +22,22 @@ cargo run --quiet --locked \ --bin oxc-occurrence-map -- \ conformance "$conformance_tmpdir/tsfacts" \ internal/semanticfacts/testdata/corpus/v0 \ - "${output_args[@]}" + --output "$conformance_tmpdir/report-first.json" +cargo run --quiet --locked \ + --manifest-path internal/oxc_reference/Cargo.toml \ + --bin oxc-occurrence-map -- \ + conformance "$conformance_tmpdir/tsfacts" \ + internal/semanticfacts/testdata/corpus/v0 \ + --output "$conformance_tmpdir/report-repeated.json" + +if ! cmp -s "$conformance_tmpdir/report-first.json" "$conformance_tmpdir/report-repeated.json"; then + echo "conformance reports differ across repeated full-corpus runs" >&2 + cmp "$conformance_tmpdir/report-first.json" "$conformance_tmpdir/report-repeated.json" || true + exit 1 +fi + +if [[ -n "$output_path" ]]; then + cp "$conformance_tmpdir/report-first.json" "$output_path" +else + cat "$conformance_tmpdir/report-first.json" +fi diff --git a/internal/semanticfacts/testdata/corpus/v0/README.md b/internal/semanticfacts/testdata/corpus/v0/README.md index 6c15b0661..9b4217901 100644 --- a/internal/semanticfacts/testdata/corpus/v0/README.md +++ b/internal/semanticfacts/testdata/corpus/v0/README.md @@ -10,3 +10,11 @@ signatures, advanced type operators, occurrence contexts, and recovery/budget pressure. The manifest smoke test keeps every selection resolvable and every snapshot valid. Golden normalization and structural assertions build on these same cases in the conformance milestone. + +Primitive/literal shadow selections add a structured `conformance` expectation +to every selected fixture. The expectation pins the Go fact state, all five +type-view states, and the structured actual type, then classifies the Rust/OXC +observation as `supported`, `unsupported`, `budget`, or `mapping`. The +conformance report lists unrelated corpus cases as excluded and reports every +selected fact in exactly one class, so unsupported or recovery evidence cannot +silently disappear from a compatibility denominator. diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/README.md b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/README.md new file mode 100644 index 000000000..f88b5f74a --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/README.md @@ -0,0 +1,4 @@ +# Primitive and literal response budget + +Exercises explicit Go and Rust truncation when a literal union exceeds the +response-local type-node budget. diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/case.json b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/case.json new file mode 100644 index 000000000..906dce004 --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/case.json @@ -0,0 +1,35 @@ +{ + "name": "primitive-literal-budget", + "description": "Explicit primitive/literal response-local budget truncation", + "coverage": [ + "primitive-literal-response-budget" + ], + "project": "tsconfig.json", + "capabilities": [ + "occurrence.type-views", + "types.core-composite" + ], + "budgets": { + "maxTypeNodes": 2, + "maxTypeDepth": 16 + }, + "selections": [ + { + "file": "src/budget.ts", + "text": "budgetUnion", + "occurrence": 1, + "proves": "both producers expose a bounded literal-union response instead of hiding it", + "conformance": { + "classification": "budget", + "code": "type-budget-exceeded", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { "typeKind": "union", "state": "truncated", "complete": false, "truncated": true, "memberCount": 6 } + } + } + } + ] +} diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/src/budget.ts b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/src/budget.ts new file mode 100644 index 000000000..fbd8ac67f --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/src/budget.ts @@ -0,0 +1,3 @@ +type BudgetUnion = "ready" | 42 | true | 42n | null | undefined; +declare const budgetUnion: BudgetUnion; +budgetUnion; diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/tsconfig.json b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/tsconfig.json new file mode 100644 index 000000000..17b39622c --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-budget/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "noEmit": true, + "target": "esnext" + }, + "files": ["src/budget.ts"] +} diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/README.md b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/README.md new file mode 100644 index 000000000..667649b59 --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/README.md @@ -0,0 +1,5 @@ +# Primitive and literal conformance matrix + +Exercises declarations and expressions, widening, `as const`, `satisfies`, +enum-like values, imports, contextual values, literal unions, and null-like +values through the shared Go-oracle and independent Rust/OXC shadow path. diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/case.json b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/case.json new file mode 100644 index 000000000..a260f4b96 --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/case.json @@ -0,0 +1,248 @@ +{ + "name": "primitive-literal-conformance", + "description": "Expanded primitive/literal declarations, expressions, modifiers, imports, and null-like values", + "coverage": [ + "primitive-literal-as-const", + "primitive-literal-declarations", + "primitive-literal-enum-like", + "primitive-literal-expressions", + "primitive-literal-imports", + "primitive-literal-satisfies", + "primitive-literal-widening" + ], + "project": "tsconfig.json", + "capabilities": [ + "occurrence.type-views", + "types.core-composite" + ], + "budgets": { + "maxTypeNodes": 512, + "maxTypeDepth": 16 + }, + "selections": [ + { + "file": "src/matrix.ts", + "text": "literalDeclaration", + "occurrence": 1, + "proves": "literal declaration retains its annotated literal", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "declared" } } + } + } + }, + { + "file": "src/matrix.ts", + "text": "\"expression\"", + "occurrence": 0, + "proves": "standalone literal expression remains a literal", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "inapplicable" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "expression" } } + } + } + }, + { + "file": "src/matrix.ts", + "text": "widenedDeclaration", + "occurrence": 1, + "proves": "mutable declaration widens a string literal to string", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "string", "state": "complete", "complete": true, "truncated": false } + } + } + }, + { + "file": "src/matrix.ts", + "text": "constAssertion", + "occurrence": 1, + "proves": "as const prevents literal widening", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "const-asserted" } } + } + } + }, + { + "file": "src/matrix.ts", + "text": "\"satisfied\"", + "occurrence": 0, + "proves": "satisfies preserves the literal while supplying string context", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "available", "widened": "same-as-actual", "apparent": "available", "declared": "inapplicable" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "satisfied" } } + } + } + }, + { + "file": "src/matrix.ts", + "text": "\"context\"", + "occurrence": 0, + "proves": "annotated initializer retains literal and contextual primitive views", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "available", "widened": "same-as-actual", "apparent": "available", "declared": "inapplicable" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "context" } } + } + } + }, + { + "file": "src/matrix.ts", + "text": "enumLikeValue", + "occurrence": 1, + "proves": "enum-like literal values remain explicitly outside the narrow Rust category", + "conformance": { + "classification": "unsupported", + "code": "enum-literal-outside-category", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "available" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "enum", "value": "ready" } } + } + } + }, + { + "file": "src/matrix.ts", + "text": "importedLiteral", + "occurrence": 1, + "proves": "imported value literals are observed without pretending cross-file Rust resolution", + "conformance": { + "classification": "unsupported", + "code": "cross-file-import-outside-category", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "imported" } } + } + } + }, + { + "file": "src/matrix.ts", + "text": "importedType", + "occurrence": 1, + "proves": "imported literal aliases are observed without pretending cross-file Rust resolution", + "conformance": { + "classification": "unsupported", + "code": "cross-file-import-outside-category", + "goOracle": { + "complete": false, + "recovered": false, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "imported-type" } } + } + } + }, + { + "file": "src/matrix.ts", + "text": "literalUnion", + "occurrence": 1, + "proves": "literal union retains every primitive, literal, and null-like member", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": true, + "recovered": false, + "truncated": false, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { + "typeKind": "union", + "state": "complete", + "complete": true, + "truncated": false, + "members": [ + { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "ready" } }, + { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "number", "value": "42" } }, + { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "bigint", "value": "42" } }, + { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "boolean", "value": "true" } }, + { "typeKind": "null", "state": "complete", "complete": true, "truncated": false }, + { "typeKind": "undefined", "state": "complete", "complete": true, "truncated": false } + ] + } + } + } + }, + { + "file": "src/matrix.ts", + "text": "null", + "occurrence": 1, + "proves": "null expression remains null-like", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": true, + "recovered": false, + "truncated": false, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "inapplicable" }, + "actual": { "typeKind": "null", "state": "complete", "complete": true, "truncated": false } + } + } + }, + { + "file": "src/matrix.ts", + "text": "undefined", + "occurrence": 1, + "proves": "undefined expression remains null-like", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": true, + "recovered": false, + "truncated": false, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { "typeKind": "undefined", "state": "complete", "complete": true, "truncated": false } + } + } + }, + { + "file": "src/matrix.ts", + "text": "voidValue", + "occurrence": 1, + "proves": "void declaration remains null-like", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": true, + "recovered": false, + "truncated": false, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { "typeKind": "void", "state": "complete", "complete": true, "truncated": false } + } + } + } + ] +} diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/src/exports.ts b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/src/exports.ts new file mode 100644 index 000000000..1c289bd96 --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/src/exports.ts @@ -0,0 +1,2 @@ +export const importedLiteral = "imported" as const; +export type ImportedLiteral = "imported-type"; diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/src/matrix.ts b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/src/matrix.ts new file mode 100644 index 000000000..30ad34667 --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/src/matrix.ts @@ -0,0 +1,29 @@ +import { importedLiteral } from "./exports"; +import type { ImportedLiteral } from "./exports"; + +type LiteralUnion = "ready" | 42 | true | 42n | null | undefined; + +const literalDeclaration: "declared" = "declared"; +let widenedDeclaration = "widened"; +const constAssertion = "const-asserted" as const; +const satisfiesLiteral = "satisfied" satisfies string; +const contextualString: string = "context"; +enum Mode { + Ready = "ready", +} +const enumLikeValue = Mode.Ready; +declare const importedType: ImportedLiteral; +declare const literalUnion: LiteralUnion; +declare const voidValue: void; + +literalDeclaration; +"expression"; +widenedDeclaration; +constAssertion; +enumLikeValue; +importedLiteral; +importedType; +literalUnion; +null; +undefined; +voidValue; diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/tsconfig.json b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/tsconfig.json new file mode 100644 index 000000000..120c92722 --- /dev/null +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-conformance/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "noEmit": true, + "target": "esnext", + "module": "nodenext", + "moduleResolution": "nodenext" + }, + "files": ["src/exports.ts", "src/matrix.ts"] +} diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-independent/README.md b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-independent/README.md index 1561706a7..73f7dd284 100644 --- a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-independent/README.md +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-independent/README.md @@ -4,3 +4,7 @@ Exercises the Rust/OXC primitive/literal producer against the Go oracle with primitive annotations, literal expressions, a literal union, contextual literals, null-like values, recoverable duplicate-declaration diagnostics, and an out-of-category object type. + +Every selection pins a structured Go-oracle expectation. Supported Rust/OXC +observations remain in the exact semantic denominator, while the object is a +named expected unsupported case. diff --git a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-independent/case.json b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-independent/case.json index 1b53b6e1b..6baf5c851 100644 --- a/internal/semanticfacts/testdata/corpus/v0/primitive-literal-independent/case.json +++ b/internal/semanticfacts/testdata/corpus/v0/primitive-literal-independent/case.json @@ -24,67 +24,171 @@ "file": "src/primitives.ts", "text": "literalUnion", "occurrence": 1, - "proves": "literal union and response-local member identity" + "proves": "literal union and response-local member identity", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, + "recovered": true, + "truncated": false, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { + "typeKind": "union", + "state": "complete", + "complete": true, + "truncated": false, + "members": [ + { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "ready" } }, + { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "number", "value": "42" } }, + { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "bigint", "value": "42" } }, + { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "boolean", "value": "true" } }, + { "typeKind": "null", "state": "complete", "complete": true, "truncated": false }, + { "typeKind": "undefined", "state": "complete", "complete": true, "truncated": false } + ] + } + } + } }, { "file": "src/primitives.ts", "text": "booleanPrimitive", "occurrence": 1, - "proves": "boolean primitive" + "proves": "boolean primitive", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "boolean", "state": "complete", "complete": true, "truncated": false } + } + } }, { "file": "src/primitives.ts", "text": "stringPrimitive", "occurrence": 1, - "proves": "string primitive" + "proves": "string primitive", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "string", "state": "complete", "complete": true, "truncated": false } + } + } }, { "file": "src/primitives.ts", "text": "numberPrimitive", "occurrence": 1, - "proves": "number primitive" + "proves": "number primitive", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "number", "state": "complete", "complete": true, "truncated": false } + } + } }, { "file": "src/primitives.ts", "text": "bigintPrimitive", "occurrence": 1, - "proves": "bigint primitive" + "proves": "bigint primitive", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "available", "declared": "same-as-actual" }, + "actual": { "typeKind": "bigint", "state": "complete", "complete": true, "truncated": false } + } + } }, { "file": "src/primitives.ts", "text": "\"context\"", "occurrence": 0, - "proves": "contextual string literal" + "proves": "contextual string literal", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "available", "widened": "same-as-actual", "apparent": "available", "declared": "inapplicable" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "string", "value": "context" } } + } + } }, { "file": "src/primitives.ts", "text": "7", "occurrence": 0, - "proves": "contextual number literal" + "proves": "contextual number literal", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "available", "widened": "same-as-actual", "apparent": "available", "declared": "inapplicable" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "number", "value": "7" } } + } + } }, { "file": "src/primitives.ts", "text": "false", "occurrence": 0, - "proves": "contextual boolean literal" + "proves": "contextual boolean literal", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "available", "widened": "same-as-actual", "apparent": "available", "declared": "inapplicable" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "boolean", "value": "false" } } + } + } }, { "file": "src/primitives.ts", "text": "7n", "occurrence": 0, - "proves": "contextual bigint literal" + "proves": "contextual bigint literal", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "available", "widened": "same-as-actual", "apparent": "available", "declared": "inapplicable" }, + "actual": { "typeKind": "literal", "state": "complete", "complete": true, "truncated": false, "literal": { "kind": "bigint", "value": "7" } } + } + } }, { "file": "src/primitives.ts", "text": "null", "occurrence": 1, - "proves": "contextual null-like value" + "proves": "contextual null-like value", + "conformance": { + "classification": "supported", + "goOracle": { + "complete": false, "recovered": true, "truncated": false, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "inapplicable" }, + "actual": { "typeKind": "null", "state": "complete", "complete": true, "truncated": false } + } + } }, { "file": "src/primitives.ts", "text": "unsupported", "occurrence": 1, - "proves": "out-of-category type remains explicit" + "proves": "out-of-category type remains explicit", + "conformance": { + "classification": "unsupported", + "code": "object-outside-category", + "goOracle": { + "complete": false, "recovered": true, "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { "typeKind": "object", "state": "truncated", "complete": false, "truncated": true } + } + } } ], "expectations": { diff --git a/internal/semanticfacts/testdata/corpus/v0/recovery-budgets/README.md b/internal/semanticfacts/testdata/corpus/v0/recovery-budgets/README.md index a9904fd4b..3a5c6be24 100644 --- a/internal/semanticfacts/testdata/corpus/v0/recovery-budgets/README.md +++ b/internal/semanticfacts/testdata/corpus/v0/recovery-budgets/README.md @@ -2,3 +2,8 @@ Proves inspectable type and syntax recovery together with deterministic graph depth truncation pressure on a recursive advanced type. + +The three selected facts also pin the current OXC parser-panic boundary as +three expected `oxc-parse-error` mapping classifications. They remain visible +individually in the conformance report and any change to that classification +fails the gate until the fixture expectation is reviewed. diff --git a/internal/semanticfacts/testdata/corpus/v0/recovery-budgets/case.json b/internal/semanticfacts/testdata/corpus/v0/recovery-budgets/case.json index 654dbd7f9..115502bdd 100644 --- a/internal/semanticfacts/testdata/corpus/v0/recovery-budgets/case.json +++ b/internal/semanticfacts/testdata/corpus/v0/recovery-budgets/case.json @@ -24,19 +24,52 @@ "file": "src/recovery.ts", "text": "recovered", "occurrence": 1, - "proves": "type-error recovery remains an explicit fact state" + "proves": "type-error recovery remains an explicit fact state", + "conformance": { + "classification": "mapping", + "code": "oxc-parse-error", + "goOracle": { + "complete": false, + "recovered": true, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { "typeKind": "error", "state": "error", "complete": false, "truncated": false } + } + } }, { "file": "src/recovery.ts", "text": "syntax", "occurrence": 1, - "proves": "syntax-error recovery remains queryable" + "proves": "syntax-error recovery remains queryable", + "conformance": { + "classification": "mapping", + "code": "oxc-parse-error", + "goOracle": { + "complete": false, + "recovered": true, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { "typeKind": "error", "state": "error", "complete": false, "truncated": false } + } + } }, { "file": "src/recovery.ts", "text": "deep", "occurrence": 1, - "proves": "recursive advanced graph hits the configured depth budget" + "proves": "recursive advanced graph hits the configured depth budget", + "conformance": { + "classification": "mapping", + "code": "oxc-parse-error", + "goOracle": { + "complete": false, + "recovered": true, + "truncated": true, + "typeViewStates": { "actual": "available", "contextual": "unavailable", "widened": "same-as-actual", "apparent": "same-as-actual", "declared": "same-as-actual" }, + "actual": { "typeKind": "object", "state": "truncated", "complete": false, "truncated": true } + } + } } ], "expectations": { diff --git a/rfcs/0001-semantic-facts-kernel.md b/rfcs/0001-semantic-facts-kernel.md index d12c5b309..90eb78b87 100644 --- a/rfcs/0001-semantic-facts-kernel.md +++ b/rfcs/0001-semantic-facts-kernel.md @@ -279,6 +279,15 @@ identity, structured payloads, unsupported states, and truncation. CI requires semantic, transport, or mapping differences. This gate does not change the schema-v1 producer, external consumers, or production authority. +Issue #46 expands that gate to 28 explicitly classified facts covering +declarations, expressions, widening, `as const`, `satisfies`, enum-like values, +imports, contextual literals, literal unions, null-like values, recovery, and +budget truncation. Structured Go expectations are checked before differential +comparison. Expected unsupported, budget, and the three pinned recovery mapping +cases remain visible outside the 29-record supported denominator; unexplained +semantic, transport, or mapping differences still block CI, and repeated full +reports must be byte-identical. + ## Considered alternatives ### Prune TypeScript 7 immediately