contractVersion: 1
Status: Frozen. Changes require a reviewed PR that bumps contractVersion and regenerates all goldens.
This document defines the canonical, normalized output that the TypeScript oracle (legacy packages/core) and the Rust engine (packages/core) MUST produce identically for the same input. Parity is asserted by byte-equality of the serialized canonical JSON (analyzer) and of the resolution trace (resolver).
Goldens are generated by the TypeScript implementation, which is the specification.
typescriptis pinned to 5.9.3 and treated as vendored spec. It is NOT upgraded as routine maintenance. Upgrading it (e.g. to 6.x) is a deliberate, reviewed change that regenerates goldens.- Regenerating goldens requires an explicit review gate (PR checklist).
- All
start/endoffsets are UTF-16 code-unit indices into the source text — the native unit of the TypeScript compiler API (node.getStart(sourceFile)/node.end) and of the LSP default position encoding. - Rust side:
oxcspans are UTF-8 byte offsets. At the AST→model boundary, EVERY offset MUST be converted UTF-8 byte → UTF-16 code unit. ALL downstream Rust logic (including range-contains checks used by kind inference) operates in UTF-16 units, mirroring the TS code 1:1. Conversion is NOT deferred to serialization. endis exclusive (half-open[start, end)), matching TSnode.end.- Mandatory property test (both engines): for every emitted range
r, the UTF-16 substringsource.slice16(r.start, r.end)MUST equal the expected literal substring for that fixture. - Mandatory encoding fixtures: ASCII, emoji (surrogate pairs), CJK, combining marks, BOM, CRLF, and mixtures — placed in identifiers (where legal), JSX text, string literals, and comments preceding the directive.
The analyzer output is ComponentUsage[]. Canonical serialization:
// Array of usages, each:
{
"kind": "client" | "server", // never "unknown" (internal only)
"tagName": "<canonical tag name>", // §3.1
"sourceFilePath": "<canonical path>", // §4 ; always present in analyzer output
"ranges": [ { "start": <u32>, "end": <u32> }, ... ] // §3.2
}- Reconstructed from the AST, trivia-stripped, never a raw source slice that could contain comments/whitespace.
- Member-expression tag names join identifier segments with
.(e.g.Foo.Bar.Baz). Lookup name (root identifier) is an analyzer-internal detail and is NOT serialized. - Only capitalized identifiers (first char
A–Z, code 65–90) are components; lowercase tags are never emitted.
- A usage may have multiple disjoint segments (e.g. opening tag shell
<Comp+ delimiter>or/>). - Each segment satisfies
start < end. - Segments within a usage are sorted ascending by
(start, end).
- Dedup: remove usages that are structurally identical on the full tuple
(kind, tagName, sourceFilePath, ranges). - Total order over usages (independent of internal production order), compared field-by-field until a difference is found:
ranges[0].start(ascending)ranges[0].end(ascending)kind(client<server, lexicographic)tagName(lexicographic, UTF-16 code-unit order)sourceFilePath(lexicographic, UTF-16 code-unit order)- remaining
rangescompared pairwise by(start, end), then by length.
- This order MUST yield a strict total order (the dedup guarantees no fully-equal entries).
- Canonical bytes = compact JSON (no insignificant whitespace) with object fields in fixed order
kind,tagName,sourceFilePath,ranges; each range objectstart,end. Non-ASCII emitted as raw UTF-8 (not\u-escaped);/not escaped. Numbers are integers (no decimal point/exponent). This is what both engines must emit byte-identically.
- Canonical = the TypeScript oracle's resulting absolute path, normalized:
- separators → forward slash
/ - on Windows, drive letter lowercased (
C:/…→c:/…)
- separators → forward slash
- Symlink behavior = match TS default. TS resolves through the filesystem; the Rust resolver MUST reproduce TS's result (realpath behavior included). Verified via the resolver trace (§6), not assumed.
- In analyzer output
sourceFilePathis always present (local usages use the analyzed file path; imported usages use the resolved path). Unresolved imports emit no usage (the TS analyzer skips them), sonulldoes not appear in analyzer output.nullis reserved for resolver-trace records where resolution fails.
Mirror hasUseClientDirective EXACTLY (do NOT use oxc program.directives, whose AST-directive semantics differ). Scan the raw source from offset 0:
- Skip any char with code
<= 32(whitespace/control), or;(59), or BOM (0xFEFF). - Line comment:
//→ skip to next\n(code 10), continue. - Block comment:
/*→ skip until*/, continue. - String literal (
"code 34 or'code 39):- Client iff the following bytes are EXACTLY
use client(codes:u117s115e101space32c99l108i105e101n110t116) at offsets+1..+10AND the char at+11equals the opening quote. Raw-byte match — escapes are NOT interpreted ("use\u0020client"does NOT match). - If it matches → file is client.
- Otherwise → skip past this string's closing quote and continue (this permits a preceding directive run, e.g.
"use strict"; "use client";).
- Client iff the following bytes are EXACTLY
- Any other character → stop; file is not client (i.e. server).
- End of input reached → not client (server).
A file is client iff this scan returns client; otherwise server. "use server" inside function bodies and the other kind-inference heuristics are separate (analyzer logic), not part of this file-level directive check.
The resolver is conformed separately. For each (fromFilePath, specifier) the engines emit a trace record; parity requires trace-equality (or a narrowly whitelisted, reviewed carve-out proven not to change which file is inspected for the directive).
{
"fromFilePath": "<canonical>",
"specifier": "<string>",
"configPath": "<canonical>|null", // nearest tsconfig/jsconfig chosen
"candidatesTried": ["<canonical>", ...], // in order
"resolved": "<canonical>|null", // final accepted file (after .d.ts rejection)
"rejectedReason": "unresolved|dts|unsupported-ext|null"
}- Accepted files end in
.ts/.tsx/.js/.jsxand are NOT.d.ts(post-filter mirrors the TS wrapper). - Default compiler options when no config:
{ allowJs:true, jsx:preserve, module:ESNext, moduleResolution:Bundler, target:ES2022 }. - Config discovery walks up from the importing file's directory looking for
tsconfig.jsonthenjsconfig.json.
- LSP position encoding (byte/UTF-16 →
{line, character}) — a separate layer (packages/lsp) with its own tests. - Caching/signature behavior — disabled or trace-asserted during conformance/fuzzing to avoid nondeterminism.
- Performance — measured separately after correctness is proven.
{ "contractVersion": 1, "tsVersion": "5.9.3", // typescript package version used to generate goldens "generatorVersion": "1", // golden-generator schema version "platform": "neutral" // outputs MUST be platform-independent (see §4) }