Skip to content

Latest commit

 

History

History
130 lines (97 loc) · 7.75 KB

File metadata and controls

130 lines (97 loc) · 7.75 KB

Conformance Contract (FROZEN)

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).


1. Oracle pinning (recorded in every golden header)

Goldens are generated by the TypeScript implementation, which is the specification.

{
  "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)
}
  • typescript is 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).

2. Position unit: UTF-16 code units (NON-NEGOTIABLE)

  • All start/end offsets 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: oxc spans 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.
  • end is exclusive (half-open [start, end)), matching TS node.end.
  • Mandatory property test (both engines): for every emitted range r, the UTF-16 substring source.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.

3. Canonical analyzer output schema

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
}

3.1 tagName canonical form

  • 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 AZ, code 65–90) are components; lowercase tags are never emitted.

3.2 ranges

  • 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).

3.3 Determinism: dedup + total order

  • 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:
    1. ranges[0].start (ascending)
    2. ranges[0].end (ascending)
    3. kind (client < server, lexicographic)
    4. tagName (lexicographic, UTF-16 code-unit order)
    5. sourceFilePath (lexicographic, UTF-16 code-unit order)
    6. remaining ranges compared pairwise by (start, end), then by length.
  • This order MUST yield a strict total order (the dedup guarantees no fully-equal entries).

3.4 Serialization

  • Canonical bytes = compact JSON (no insignificant whitespace) with object fields in fixed order kind, tagName, sourceFilePath, ranges; each range object start, 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.

4. Canonical path identity (sourceFilePath)

  • Canonical = the TypeScript oracle's resulting absolute path, normalized:
    • separators → forward slash /
    • on Windows, drive letter lowercased (C:/…c:/…)
  • 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 sourceFilePath is 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), so null does not appear in analyzer output. null is reserved for resolver-trace records where resolution fails.

5. Directive detection spec ("use client") — reimplement verbatim

Mirror hasUseClientDirective EXACTLY (do NOT use oxc program.directives, whose AST-directive semantics differ). Scan the raw source from offset 0:

  1. Skip any char with code <= 32 (whitespace/control), or ; (59), or BOM (0xFEFF).
  2. Line comment: // → skip to next \n (code 10), continue.
  3. Block comment: /* → skip until */, continue.
  4. String literal (" code 34 or ' code 39):
    • Client iff the following bytes are EXACTLY use client (codes: u117 s115 e101 space32 c99 l108 i105 e101 n110 t116) at offsets +1..+10 AND the char at +11 equals 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";).
  5. Any other character → stop; file is not client (i.e. server).
  6. 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.


6. Resolver trace (parity unit for the resolver)

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/.jsx and 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.json then jsconfig.json.

7. What this contract does NOT cover

  • 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.