diff --git a/docs/adr/0017-project-primitive-literal-candidates-from-go-graph-identity.md b/docs/adr/0017-project-primitive-literal-candidates-from-go-graph-identity.md new file mode 100644 index 000000000..082c2c4c2 --- /dev/null +++ b/docs/adr/0017-project-primitive-literal-candidates-from-go-graph-identity.md @@ -0,0 +1,69 @@ +# ADR-0017: Project primitive/literal candidates from Go graph identity + +- Status: accepted +- Date: 2026-08-17 +- Deciders: TypeScript Semantic Kernel maintainers +- Supersedes: — +- Superseded by: — + +## Context + +ADR-0016 selected primitive and literal type construction as the first +independent Rust semantic candidate, while keeping TypeScript 7's Go checker as +the semantic oracle. The existing OXC reference consumer already attaches all +five TypeFacts roots to arena-local `NodeId`s and inspects the shared, +response-local graph without copying its identity. + +A candidate record must be useful for later Go-versus-Rust comparison without +mistaking equal display text for semantic equivalence. It also cannot erase an +unavailable view, producer truncation, unsupported entity, checker error, or +literal-union edge. + +## Decision + +The internal OXC consumer emits a versioned Rust-owned primitive/literal +candidate from each attached TypeFacts record as part of graph inspection. The +candidate consumes the existing schema-v1 Go-produced graph; it does not add a +producer capability or protocol record. + +Each candidate contains: + +- the portable occurrence and fact completeness/recovery/truncation status; +- actual, contextual, widened, apparent, and declared roots in fixed view + order, retaining their view state and effective response-local TypeID; +- one deterministic record per reachable response-local TypeID; +- structured primitive, literal, null-like, and union semantics, including the + literal kind/value and ordered union member TypeIDs; and +- the source entity state/issues plus an explicit Rust candidate state and + machine-readable reasons for truncated, unsupported, or error cases. + +Candidate type records are ordered by response-local TypeID and repeated roots +share one record. Display text is not part of the Rust semantic record. The +first candidate covers boolean, string, number, and bigint primitives; +boolean, string, number, and bigint literal values; null, undefined, and void; +and unions over those records. + +The candidate version is internal to the reference consumer. TypeScript 7's Go +checker remains authoritative for type construction and all TypeFacts roots. +This decision neither changes the TS7 producer protocol nor satisfies +ADR-0016's replacement gate. + +## Consequences + +- Future differential work can compare structured kinds, literal values, + member identity, roots, and states rather than display strings. +- OXC attachment remains the ownership seam: no Rust or OXC type enters the Go + producer protocol. +- Shared canonical fixtures can be decoded and round-tripped by Go while the + Rust consumer verifies its candidate projection through the same graph. +- Unsupported and truncated observations stay useful evidence instead of + being coerced into a supported primitive/literal record. +- Replacement readiness still requires an independent producer and the exact + structured differential threshold in ADR-0016. + +## References + +- [RFC 0001](../../rfcs/0001-semantic-facts-kernel.md) +- [ADR-0015](0015-attach-semantic-facts-without-expanding-graph-identity.md) +- [ADR-0016](0016-port-occurrence-attachment-before-semantic-categories.md) +- [Issue #40](https://github.com/swernerx/typescript-semantic-kernel/issues/40) diff --git a/docs/adr/README.md b/docs/adr/README.md index 797be6a4c..c72cc9d0d 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -24,6 +24,7 @@ decision is recorded in a successor ADR that links to the record it replaces. | [0014](0014-isolate-the-oxc-reference-consumer.md) | Accepted | Isolate the OXC reference consumer | | [0015](0015-attach-semantic-facts-without-expanding-graph-identity.md) | Accepted | Attach semantic facts without expanding graph identity | | [0016](0016-port-occurrence-attachment-before-semantic-categories.md) | Accepted | Port occurrence attachment before semantic categories | +| [0017](0017-project-primitive-literal-candidates-from-go-graph-identity.md) | Accepted | Project primitive/literal candidates from Go graph identity | ## Current migration decision @@ -34,8 +35,9 @@ Rust/OXC consumer remains a reference and migration harness; the evidence does not establish compiler equivalence, production readiness, or a performance advantage. -[ADR-0016](0016-port-occurrence-attachment-before-semantic-categories.md) -identifies primitive/literal Rust type-record construction as the next semantic -candidate. It remains Go-authoritative until independent Rust output passes the -documented replacement gates; no semantic category is currently approved for -replacement. +[ADR-0017](0017-project-primitive-literal-candidates-from-go-graph-identity.md) +records the first Rust-owned primitive/literal candidate over the Go-produced +graph. It makes structured comparison possible but is not an independent +producer. Primitive/literal construction remains Go-authoritative until a +future Rust producer passes ADR-0016's replacement gates; no semantic category +is currently approved for replacement. diff --git a/docs/evidence/README.md b/docs/evidence/README.md index 755a6863b..71377f52b 100644 --- a/docs/evidence/README.md +++ b/docs/evidence/README.md @@ -26,6 +26,8 @@ count is zero. The checked record supports the migration decision in [ADR-0016](../adr/0016-port-occurrence-attachment-before-semantic-categories.md): occurrence identity and attachment plumbing may be ported behind the Go oracle. -Primitive/literal Rust type-record construction is the next semantic candidate, -not an approved replacement. The TypeScript 7 Go checker remains authoritative -until independent Rust output passes the differential compatibility gates. +[ADR-0017](../adr/0017-project-primitive-literal-candidates-from-go-graph-identity.md) +subsequently adds a structured primitive/literal Rust candidate over that +Go-produced graph. It is not an approved replacement or an independent +producer. The TypeScript 7 Go checker remains authoritative until independent +Rust output passes the differential compatibility gates. diff --git a/docs/porting/migration-contract.md b/docs/porting/migration-contract.md index 9b8205729..d42ae2fd7 100644 --- a/docs/porting/migration-contract.md +++ b/docs/porting/migration-contract.md @@ -88,12 +88,14 @@ source remains Go-only because the current OXC parse fails; its facts are reported as consumer differences rather than omitted. ADR-0016 selects occurrence identity and attachment plumbing as the first safe -mechanical port. Primitive/literal type construction is the first proposed -semantic implementation candidate, but no semantic category may replace Go -until independent Rust output matches the normalized structured oracle with no -completeness/state downgrade or new unsupported form. Project loading, -resolution, binding, symbols, inference, contextual/widened types, overloads, -generic instantiation, narrowing, and recovery remain Go-authoritative. +mechanical port. ADR-0017 adds the first Rust-owned primitive/literal candidate +record over the Go-produced graph so future comparisons can use structured +kinds, literal values, union edges, roots, and states. It is not an independent +producer, and no semantic category may replace Go until independent Rust output +matches the normalized structured oracle with no completeness/state downgrade +or new unsupported form. Project loading, resolution, binding, symbols, +inference, contextual/widened types, overloads, generic instantiation, +narrowing, and recovery remain Go-authoritative. ## Gate ladder diff --git a/internal/oxc_reference/README.md b/internal/oxc_reference/README.md index b92630374..d854cdb0c 100644 --- a/internal/oxc_reference/README.md +++ b/internal/oxc_reference/README.md @@ -77,6 +77,15 @@ diagnostics remain visible. Depth, node, and edge budgets are consumer-local guards and do not rewrite or reinterpret producer records. See [ADR-0015](../../docs/adr/0015-attach-semantic-facts-without-expanding-graph-identity.md). +Each inspection also contains the internal version-1 primitive/literal Rust +candidate documented by +[ADR-0017](../../docs/adr/0017-project-primitive-literal-candidates-from-go-graph-identity.md). +It records the occurrence, fact status, all five roots, structured primitive or +literal values, ordered union member TypeIDs, and explicit candidate/source +states. Candidate records intentionally omit display text. The shared +`primitive-literal-candidate` canonical fixture is round-tripped by Go and +attached, inspected, and asserted here through OXC. + ## Migration boundary Keep parser-specific traversal and allow-listed span/kind projection in this @@ -89,9 +98,11 @@ negotiation in Go. The intended migration sequence is: explicit and met. Occurrence identity and attachment plumbing is the first approved mechanical -port category. Primitive/literal record construction is only the next proposed -semantic candidate; all semantic answers remain Go-authoritative until an -independent Rust producer passes ADR-0016's differential gate. +port category. The primitive/literal projection is the first implemented +semantic candidate record, but it consumes Go-produced graph facts and does +not constitute an independent producer. All semantic answers remain +Go-authoritative until an independent Rust producer passes ADR-0016's +differential gate. Adding a projection does not transfer semantic authority. New parser-boundary normalizations require a shared fixture and the versioned portable contract. diff --git a/internal/oxc_reference/crates/occurrence-consumer/src/candidate.rs b/internal/oxc_reference/crates/occurrence-consumer/src/candidate.rs new file mode 100644 index 000000000..a3074711d --- /dev/null +++ b/internal/oxc_reference/crates/occurrence-consumer/src/candidate.rs @@ -0,0 +1,323 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde::Serialize; + +use crate::{ + contract::Occurrence, + facts::{ + EntityState, GraphIssue, OccurrenceTypeFacts, TypeGraph, TypeId, TypeKind, TypeView, + TypeViewState, + }, +}; + +pub const PRIMITIVE_LITERAL_CANDIDATE_VERSION: u32 = 1; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PrimitiveLiteralCandidate { + pub candidate_version: u32, + pub occurrence: Occurrence, + pub fact: CandidateFactStatus, + pub roots: Vec, + pub types: Vec, + pub summary: CandidateSummary, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CandidateFactStatus { + pub complete: bool, + pub recovered: bool, + pub truncated: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CandidateRoot { + pub view: TypeView, + pub state: TypeViewState, + #[serde(skip_serializing_if = "Option::is_none")] + pub type_id: Option, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum CandidateState { + Complete, + Truncated, + Unsupported, + Error, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum CandidateReason { + EmptyUnion, + MissingLiteralDetails, + SourceError, + SourceTruncated, + SourceUnsupported, + UnsupportedLiteralKind, + UnsupportedTypeKind, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CandidateTypeRecord { + pub id: TypeId, + pub source_kind: TypeKind, + pub source_state: EntityState, + pub candidate_state: CandidateState, + #[serde(skip_serializing_if = "Option::is_none")] + pub semantic: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub reasons: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub issues: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(tag = "kind", rename_all = "kebab-case")] +pub enum CandidateSemantic { + Primitive { + primitive: PrimitiveKind, + }, + Literal { + literal: LiteralKind, + value: String, + }, + NullLike { + #[serde(rename = "nullLike")] + null_like: NullLikeKind, + }, + Union { + members: Vec, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum PrimitiveKind { + Boolean, + String, + Number, + Bigint, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum LiteralKind { + Boolean, + String, + Number, + Bigint, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum NullLikeKind { + Null, + Undefined, + Void, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CandidateSummary { + pub complete: usize, + pub truncated: usize, + pub unsupported: usize, + pub error: usize, +} + +impl PrimitiveLiteralCandidate { + pub fn build(graph: &TypeGraph, facts: &OccurrenceTypeFacts) -> Self { + let roots = facts + .roots() + .into_iter() + .map(|root| CandidateRoot { + view: root.view, + state: root.state, + type_id: root.type_id.cloned(), + }) + .collect::>(); + let mut builder = CandidateBuilder::new(graph); + for root in &roots { + if let Some(type_id) = &root.type_id { + builder.visit(type_id); + } + } + let types = builder.records.into_values().collect::>(); + let summary = types + .iter() + .fold(CandidateSummary::default(), |mut summary, record| { + match record.candidate_state { + CandidateState::Complete => summary.complete += 1, + CandidateState::Truncated => summary.truncated += 1, + CandidateState::Unsupported => summary.unsupported += 1, + CandidateState::Error => summary.error += 1, + } + summary + }); + Self { + candidate_version: PRIMITIVE_LITERAL_CANDIDATE_VERSION, + occurrence: facts.occurrence(), + fact: CandidateFactStatus { + complete: facts.complete, + recovered: facts.recovered, + truncated: facts.truncated, + }, + roots, + types, + summary, + } + } +} + +struct CandidateBuilder<'a> { + graph: &'a TypeGraph, + records: BTreeMap, + visiting: BTreeSet, +} + +impl<'a> CandidateBuilder<'a> { + fn new(graph: &'a TypeGraph) -> Self { + Self { + graph, + records: BTreeMap::new(), + visiting: BTreeSet::new(), + } + } + + fn visit(&mut self, id: &TypeId) -> CandidateState { + if let Some(record) = self.records.get(id) { + return record.candidate_state; + } + if !self.visiting.insert(id.clone()) { + return CandidateState::Unsupported; + } + + let source = self + .graph + .type_record(id) + .expect("validated fact and type edges reference existing graph types"); + let mut reasons = BTreeSet::new(); + let mut child_state = CandidateState::Complete; + let semantic = match source.type_kind { + TypeKind::Boolean => Some(CandidateSemantic::Primitive { + primitive: PrimitiveKind::Boolean, + }), + TypeKind::String => Some(CandidateSemantic::Primitive { + primitive: PrimitiveKind::String, + }), + TypeKind::Number => Some(CandidateSemantic::Primitive { + primitive: PrimitiveKind::Number, + }), + TypeKind::Bigint => Some(CandidateSemantic::Primitive { + primitive: PrimitiveKind::Bigint, + }), + TypeKind::Null => Some(CandidateSemantic::NullLike { + null_like: NullLikeKind::Null, + }), + TypeKind::Undefined => Some(CandidateSemantic::NullLike { + null_like: NullLikeKind::Undefined, + }), + TypeKind::Void => Some(CandidateSemantic::NullLike { + null_like: NullLikeKind::Void, + }), + TypeKind::Literal => match &source.literal { + Some(literal) => match literal_kind(&literal.kind) { + Some(kind) => Some(CandidateSemantic::Literal { + literal: kind, + value: literal.value.clone(), + }), + None => { + reasons.insert(CandidateReason::UnsupportedLiteralKind); + None + } + }, + None => { + reasons.insert(CandidateReason::MissingLiteralDetails); + None + } + }, + TypeKind::Union => { + if source.members.is_empty() { + reasons.insert(CandidateReason::EmptyUnion); + } + for member in &source.members { + child_state = merge_state(child_state, self.visit(member)); + } + Some(CandidateSemantic::Union { + members: source.members.clone(), + }) + } + _ => { + reasons.insert(CandidateReason::UnsupportedTypeKind); + None + } + }; + + let source_state = match source.state { + EntityState::Complete if source.truncated => { + reasons.insert(CandidateReason::SourceTruncated); + CandidateState::Truncated + } + EntityState::Complete => CandidateState::Complete, + EntityState::Truncated => { + reasons.insert(CandidateReason::SourceTruncated); + CandidateState::Truncated + } + EntityState::Unsupported => { + reasons.insert(CandidateReason::SourceUnsupported); + CandidateState::Unsupported + } + EntityState::Error => { + reasons.insert(CandidateReason::SourceError); + CandidateState::Error + } + }; + let structural_state = if semantic.is_some() + && !reasons.contains(&CandidateReason::EmptyUnion) + && !reasons.contains(&CandidateReason::MissingLiteralDetails) + && !reasons.contains(&CandidateReason::UnsupportedLiteralKind) + { + child_state + } else { + CandidateState::Unsupported + }; + let candidate_state = merge_state(source_state, structural_state); + let record = CandidateTypeRecord { + id: id.clone(), + source_kind: source.type_kind, + source_state: source.state, + candidate_state, + semantic, + reasons: reasons.into_iter().collect(), + issues: source.issues.clone(), + }; + self.visiting.remove(id); + self.records.insert(id.clone(), record); + candidate_state + } +} + +fn literal_kind(kind: &str) -> Option { + match kind { + "boolean" => Some(LiteralKind::Boolean), + "string" => Some(LiteralKind::String), + "number" => Some(LiteralKind::Number), + "bigint" => Some(LiteralKind::Bigint), + _ => None, + } +} + +fn merge_state(left: CandidateState, right: CandidateState) -> CandidateState { + use CandidateState::{Complete, Error, Truncated, Unsupported}; + match (left, right) { + (Error, _) | (_, Error) => Error, + (Truncated, _) | (_, Truncated) => Truncated, + (Unsupported, _) | (_, Unsupported) => Unsupported, + (Complete, Complete) => Complete, + } +} diff --git a/internal/oxc_reference/crates/occurrence-consumer/src/facts.rs b/internal/oxc_reference/crates/occurrence-consumer/src/facts.rs index f143d0d34..b0dd2ec55 100644 --- a/internal/oxc_reference/crates/occurrence-consumer/src/facts.rs +++ b/internal/oxc_reference/crates/occurrence-consumer/src/facts.rs @@ -281,6 +281,13 @@ pub struct GraphIssue { pub limit: Option, } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LiteralValue { + pub kind: String, + pub value: String, +} + #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ConditionalTypeDetails { @@ -355,6 +362,8 @@ pub struct TypeRecord { #[serde(default)] pub index_signatures: Vec, #[serde(default)] + pub literal: Option, + #[serde(default)] pub conditional: Option, #[serde(default)] pub mapped: Option, diff --git a/internal/oxc_reference/crates/occurrence-consumer/src/inspector.rs b/internal/oxc_reference/crates/occurrence-consumer/src/inspector.rs index 77c611a44..8df09eb3c 100644 --- a/internal/oxc_reference/crates/occurrence-consumer/src/inspector.rs +++ b/internal/oxc_reference/crates/occurrence-consumer/src/inspector.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeSet, VecDeque}; use serde::Serialize; +use crate::candidate::PrimitiveLiteralCandidate; use crate::facts::{ EntityState, GraphIssue, GraphRef, OccurrenceTypeFacts, TypeGraph, TypeView, TypeViewState, }; @@ -29,6 +30,7 @@ impl Default for InspectorLimits { pub struct InspectionReport { pub fact: InspectedFactStatus, pub roots: Vec, + pub primitive_literal_candidate: PrimitiveLiteralCandidate, pub nodes: Vec, pub edges: Vec, pub diagnostics: Vec, @@ -252,6 +254,7 @@ impl<'a> GraphInspector<'a> { truncated: facts.truncated, }, roots, + primitive_literal_candidate: PrimitiveLiteralCandidate::build(self.graph, facts), summary: InspectionSummary { nodes: nodes.len(), edges: edges.len(), diff --git a/internal/oxc_reference/crates/occurrence-consumer/src/lib.rs b/internal/oxc_reference/crates/occurrence-consumer/src/lib.rs index cb2b21fb8..2b9a70331 100644 --- a/internal/oxc_reference/crates/occurrence-consumer/src/lib.rs +++ b/internal/oxc_reference/crates/occurrence-consumer/src/lib.rs @@ -1,3 +1,4 @@ +pub mod candidate; pub mod contract; pub mod evidence; pub mod facts; @@ -18,6 +19,10 @@ mod tests { use oxc_allocator::Allocator; use crate::{ + candidate::{ + CandidateReason, CandidateSemantic, CandidateState, LiteralKind, NullLikeKind, + PrimitiveKind, + }, contract::{DiagnosticCode, correlate}, facts::{GraphRef, SemanticSnapshot, TypeViewState}, fixture::load_fixtures, @@ -138,6 +143,152 @@ mod tests { assert!(std::ptr::eq(first.graph(), second.graph())); } + #[test] + fn primitive_literal_candidate_is_structured_deterministic_and_state_preserving() { + let snapshot = load_canonical_snapshot("primitive-literal-candidate.jsonl"); + let source = fs::read_to_string(canonical_fixture_path("primitive-literal-candidate.ts")) + .expect("read primitive/literal candidate source"); + let allocator = Allocator::default(); + let mut consumer = + OxcConsumer::parse(&allocator, "src/primitive-literal-candidate.ts", &source) + .expect("parse primitive/literal candidate source"); + let report = consumer + .attach(snapshot) + .expect("attach primitive/literal candidate fixture"); + assert_eq!(report.summary.facts, 4); + assert_eq!(report.summary.mapped, 4); + + let inspections = report + .mappings + .iter() + .map(|mapping| { + let node = consumer + .node_for_fact(mapping.fact_index) + .expect("mapped candidate node"); + let attached = consumer + .type_facts_for_node(node) + .find(|facts| facts.fact_index == mapping.fact_index) + .expect("attached candidate facts"); + let first = attached.inspect(InspectorLimits::default()); + let second = attached.inspect(InspectorLimits::default()); + assert_eq!( + serde_json::to_string(&first.primitive_literal_candidate) + .expect("serialize first candidate"), + serde_json::to_string(&second.primitive_literal_candidate) + .expect("serialize second candidate") + ); + first + }) + .collect::>(); + + let primitives = &inspections[0].primitive_literal_candidate; + assert_eq!(primitives.roots.len(), 5); + assert!(matches!( + &candidate_type(primitives, "type:1").semantic, + Some(CandidateSemantic::Primitive { + primitive: PrimitiveKind::Boolean + }) + )); + assert!(matches!( + &candidate_type(primitives, "type:2").semantic, + Some(CandidateSemantic::Primitive { + primitive: PrimitiveKind::String + }) + )); + assert!(matches!( + &candidate_type(primitives, "type:3").semantic, + Some(CandidateSemantic::Primitive { + primitive: PrimitiveKind::Number + }) + )); + assert!(matches!( + &candidate_type(primitives, "type:4").semantic, + Some(CandidateSemantic::Primitive { + primitive: PrimitiveKind::Bigint + }) + )); + assert!(matches!( + &candidate_type(primitives, "type:5").semantic, + Some(CandidateSemantic::NullLike { + null_like: NullLikeKind::Null + }) + )); + + let literals = &inspections[1].primitive_literal_candidate; + for (id, kind, value) in [ + ("type:6", LiteralKind::String, "ready"), + ("type:7", LiteralKind::Boolean, "true"), + ("type:8", LiteralKind::Number, "42"), + ("type:9", LiteralKind::Bigint, "42"), + ] { + assert!(matches!( + &candidate_type(literals, id).semantic, + Some(CandidateSemantic::Literal { + literal, + value: actual + }) if *literal == kind && actual == value + )); + } + assert!(matches!( + &candidate_type(literals, "type:10").semantic, + Some(CandidateSemantic::NullLike { + null_like: NullLikeKind::Undefined + }) + )); + + let union = &inspections[2].primitive_literal_candidate; + assert!(matches!( + &candidate_type(union, "type:11").semantic, + Some(CandidateSemantic::Union { members }) + if members.iter().map(|id| id.as_str()).collect::>() + == vec!["type:6", "type:7", "type:8", "type:9", "type:5", "type:10"] + )); + assert_eq!(union.summary.complete, 7); + + let incomplete = &inspections[3].primitive_literal_candidate; + assert_eq!(incomplete.roots.len(), 5); + assert_eq!( + incomplete.roots[3].type_id.as_ref().map(|id| id.as_str()), + Some("type:12") + ); + assert_eq!(incomplete.roots[4].state, TypeViewState::Unavailable); + assert!(incomplete.roots[4].type_id.is_none()); + assert_eq!( + candidate_type(incomplete, "type:12").candidate_state, + CandidateState::Unsupported + ); + assert_eq!( + candidate_type(incomplete, "type:13").candidate_state, + CandidateState::Truncated + ); + assert_eq!( + candidate_type(incomplete, "type:14").candidate_state, + CandidateState::Truncated + ); + assert!( + candidate_type(incomplete, "type:12") + .reasons + .contains(&CandidateReason::SourceUnsupported) + ); + assert!( + candidate_type(incomplete, "type:14") + .reasons + .contains(&CandidateReason::SourceTruncated) + ); + assert_eq!(incomplete.summary.complete, 1); + assert_eq!(incomplete.summary.truncated, 2); + assert_eq!(incomplete.summary.unsupported, 1); + + let serialized = serde_json::to_string( + &inspections + .iter() + .map(|inspection| &inspection.primitive_literal_candidate) + .collect::>(), + ) + .expect("serialize structured primitive/literal candidates"); + assert!(!serialized.contains("\"display\"")); + } + #[test] fn inspector_is_deterministic_identity_preserving_and_complete_about_states() { let snapshot = load_canonical_snapshot("graph-inspector.jsonl"); @@ -359,6 +510,17 @@ mod tests { ) } + fn candidate_type<'a>( + candidate: &'a crate::candidate::PrimitiveLiteralCandidate, + id: &str, + ) -> &'a crate::candidate::CandidateTypeRecord { + candidate + .types + .iter() + .find(|record| record.id.as_str() == id) + .unwrap_or_else(|| panic!("candidate omitted {id}")) + } + fn minimal_snapshot(syntax_kinds: &[&str]) -> String { let mut output = String::from( "{\"record\":\"header\",\"schemaVersion\":1,\"offsetEncoding\":\"utf8-bytes\",\"capabilities\":[]}\n\ diff --git a/internal/tsfacts/canonical_test.go b/internal/tsfacts/canonical_test.go index f9d6827a3..296e30391 100644 --- a/internal/tsfacts/canonical_test.go +++ b/internal/tsfacts/canonical_test.go @@ -15,7 +15,7 @@ func TestCanonicalJSONLinesFixturesRoundTrip(t *testing.T) { t.Parallel() fixtures, err := filepath.Glob("testdata/canonical/v0/*.jsonl") assert.NilError(t, err) - assert.Equal(t, len(fixtures), 6) + assert.Equal(t, len(fixtures), 7) for _, fixture := range fixtures { t.Run(filepath.Base(fixture), func(t *testing.T) { t.Parallel() diff --git a/internal/tsfacts/testdata/canonical/v0/primitive-literal-candidate.jsonl b/internal/tsfacts/testdata/canonical/v0/primitive-literal-candidate.jsonl new file mode 100644 index 000000000..3f43e5a7f --- /dev/null +++ b/internal/tsfacts/testdata/canonical/v0/primitive-literal-candidate.jsonl @@ -0,0 +1,20 @@ +{"record":"header","schemaVersion":1,"typescriptVersion":"7.0.0-dev","typescriptRevision":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","offsetEncoding":"utf8-bytes","capabilities":["graph.references","graph.signatures","limits.type-graph","occurrence.file-wide","occurrence.type-views","protocol.explicit-states","protocol.fixtures.v0","types.advanced","types.core-composite"],"budgets":{"limits":{"maxTypeNodes":4096,"maxTypeDepth":32},"typeNodesUsed":13,"maxTypeDepthObserved":1,"truncated":true},"project":"tsconfig.json","compilerOptions":{},"diagnosticCount":0} +{"record":"file","id":"src/primitive-literal-candidate.ts","origin":"project","selected":true,"diagnosticCount":0} +{"record":"type","id":"type:1","typeKind":"boolean","display":"boolean","flags":["Boolean","Union"],"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:2","typeKind":"string","display":"string","flags":["String"],"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:3","typeKind":"number","display":"number","flags":["Number"],"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:4","typeKind":"bigint","display":"bigint","flags":["BigInt"],"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:5","typeKind":"null","display":"null","flags":["Null"],"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:6","typeKind":"literal","display":"\"ready\"","flags":["StringLiteral"],"literal":{"kind":"string","value":"ready"},"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:7","typeKind":"literal","display":"true","flags":["BooleanLiteral"],"literal":{"kind":"boolean","value":"true"},"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:8","typeKind":"literal","display":"42","flags":["NumberLiteral"],"literal":{"kind":"number","value":"42"},"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:9","typeKind":"literal","display":"42n","flags":["BigIntLiteral"],"literal":{"kind":"bigint","value":"42"},"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:10","typeKind":"undefined","display":"undefined","flags":["Undefined"],"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:11","typeKind":"union","display":"\"ready\" | true | 42 | 42n | null | undefined","flags":["Union"],"members":["type:6","type:7","type:8","type:9","type:5","type:10"],"state":"complete","complete":true,"truncated":false} +{"record":"type","id":"type:12","typeKind":"opaque","display":"intrinsic","flags":["NonPrimitive"],"state":"unsupported","issues":[{"code":"unsupported-type-form"}],"complete":false,"truncated":false} +{"record":"type","id":"type:13","typeKind":"union","display":"\"ready\" | intrinsic","flags":["Union"],"members":["type:6","type:12"],"state":"truncated","issues":[{"code":"referenced-incomplete-type"}],"complete":false,"truncated":true} +{"record":"type","id":"type:14","typeKind":"truncated","display":"","flags":[],"state":"truncated","issues":[{"code":"max-type-depth","limit":32}],"complete":false,"truncated":true} +{"record":"fact","file":"src/primitive-literal-candidate.ts","span":{"start":0,"end":9},"syntaxKind":"KindIdentifier","actualType":"type:1","typeAtLocation":"type:1","contextualType":"type:2","widenedType":"type:3","apparentType":"type:4","declaredType":"type:5","typeViewStates":{"actual":"available","contextual":"available","widened":"available","apparent":"available","declared":"available"},"complete":true,"recovered":false,"truncated":false} +{"record":"fact","file":"src/primitive-literal-candidate.ts","span":{"start":11,"end":19},"syntaxKind":"KindIdentifier","actualType":"type:6","typeAtLocation":"type:6","contextualType":"type:7","widenedType":"type:8","apparentType":"type:9","declaredType":"type:10","typeViewStates":{"actual":"available","contextual":"available","widened":"available","apparent":"available","declared":"available"},"complete":true,"recovered":false,"truncated":false} +{"record":"fact","file":"src/primitive-literal-candidate.ts","span":{"start":21,"end":33},"syntaxKind":"KindIdentifier","actualType":"type:11","typeAtLocation":"type:11","typeViewStates":{"actual":"available","contextual":"same-as-actual","widened":"same-as-actual","apparent":"same-as-actual","declared":"same-as-actual"},"complete":true,"recovered":false,"truncated":false} +{"record":"fact","file":"src/primitive-literal-candidate.ts","span":{"start":35,"end":46},"syntaxKind":"KindIdentifier","actualType":"type:12","typeAtLocation":"type:12","contextualType":"type:13","widenedType":"type:14","typeViewStates":{"actual":"available","contextual":"available","widened":"available","apparent":"same-as-actual","declared":"unavailable"},"complete":false,"recovered":false,"truncated":true} diff --git a/internal/tsfacts/testdata/canonical/v0/primitive-literal-candidate.ts b/internal/tsfacts/testdata/canonical/v0/primitive-literal-candidate.ts new file mode 100644 index 000000000..303f2f582 --- /dev/null +++ b/internal/tsfacts/testdata/canonical/v0/primitive-literal-candidate.ts @@ -0,0 +1,4 @@ +primitive; +literals; +literalUnion; +unsupported; diff --git a/rfcs/0001-semantic-facts-kernel.md b/rfcs/0001-semantic-facts-kernel.md index 281af1ec8..caab6ca29 100644 --- a/rfcs/0001-semantic-facts-kernel.md +++ b/rfcs/0001-semantic-facts-kernel.md @@ -264,11 +264,11 @@ general performance claim. This evidence approves mechanical porting of occurrence identity, correlation, response-global fact indexing, and side-table attachment behind the Go oracle. -It approves no semantic checker category for replacement. Primitive/literal -record construction is the first proposed independent Rust semantic candidate, -but decoding Go-produced roots and correlating syntax is not semantic -equivalence. ADR-0016 defines the compatibility gates and lists the semantic -areas that remain Go-authoritative. +It approves no semantic checker category for replacement. ADR-0017 now records +the first Rust-owned primitive/literal candidate over the Go-produced graph, +but decoding and projecting Go-produced roots is not semantic equivalence or an +independent producer. ADR-0016 defines the compatibility gates and lists the +semantic areas that remain Go-authoritative. ## Considered alternatives @@ -338,9 +338,10 @@ spike demonstrate: Acceptance establishes the versioned boundary, not compiler equivalence, production readiness, or a Rust performance advantage. The TypeScript 7 Go -checker remains the semantic oracle. The next candidate is independent -primitive/literal Rust type-record construction under ADR-0016's differential -gates; no semantic category is approved for replacement yet. +checker remains the semantic oracle. The first primitive/literal Rust candidate +is now a structured projection of the Go graph under ADR-0017; an independent +producer and ADR-0016's differential gates are still required before any +semantic category can be approved for replacement. ## Open questions @@ -362,3 +363,4 @@ gates; no semantic category is approved for replacement yet. - [Oxlint type-aware architecture](https://oxc.rs/docs/guide/usage/linter/type-aware.html) - [TS7-to-OXC/Rust spike evidence](../docs/evidence/ts7-oxc-spike-2026-08-17.json) - [ADR-0016: Port occurrence attachment before semantic categories](../docs/adr/0016-port-occurrence-attachment-before-semantic-categories.md) +- [ADR-0017: Project primitive/literal candidates from Go graph identity](../docs/adr/0017-project-primitive-literal-candidates-from-go-graph-identity.md)