--- name: signature-kernel description: "Verter semantic signature kernel — signature records/descriptors, epoch-safe interned storage and retirement, request-pinned borrowed reads, positional matching, call substitution, ordered union/intersection reduction, VerterStableV1 deterministic ordering, the observation corpus and the determinism matrix" --- # Semantic Signature Kernel The one authority for **what a callable is**, **how two types are compared**, and **what order semantic composites are published in**. Every signature set, every intersection/union reduction, and every deterministic ordering decision in the session crate comes from here; there is no second signature producer and no per-consumer ordering rule. Normative contract: [`docs/arch/signature-kernel.md`](../../../docs/arch/signature-kernel.md) (revision 4.1, byte-locked by `docs/evidence/signature-kernel/manifest.json`). Where this skill and the contract disagree, the contract wins. --- ## 1. Module map `crates/verter_type_engine/src/signature_kernel/` (crate-private; its single consumer is `project_semantic_dispatch/signature_discovery.rs`): | Module | Owns | |---|---| | `records.rs` | The record vocabulary and its handles: `SignatureDescriptor`, `SignatureCandidate`, `SignatureTemplate`, `SignatureInputShape`, `ParameterLayout`/`ParameterSlot`/`RestSlot`, `BinderSpace`/`BinderDeclaration`, `SignatureResultRecipe`, `PredicateEffect` (the effect half of a result read: a declared result's type predicate / assertion, the predicate the checker infers from a body, or a union signature's composite predicate), `AppliedResult`, `TypeToken`, `GraphEpoch`. Every id is epoch-qualified. | | `storage.rs` | `AppendInterner` — the private append-only interner over `boxcar::Vec` with `DEDUP_SHARDS` (16) hash shards. Hash is computed outside the shard lock; equality decides collisions. A record is fully initialised before its handle is published, so `boxcar`'s `count()` is never a published-handle range. | | `lifetime.rs` | `SignatureStore`: interning entry points, `replace_epoch()`, `retain_result`/`drain_retained`/`retained_len`, `live_reader_count()`, `StoreError`. | | `read_view.rs` | `SemanticReadView::pin(&SignatureStore)` — the request-pinned borrowed read. `BorrowedSet::{Empty, One, Many}`, `ReadError`, plus the `descriptor_chain_walks` / `shard_lock_acquires` probes the performance gates assert on. | | `positional.rs` | The shared positional model: `PositionalShape`, `PositionalMode`, `TypeAt`, `MinArityFlags`, `ProjectedTuple`/`ProjectedElement`, `SlotTypeFacts`. Parameter matching, rest/receiver layout and arity are computed **once** here for every consumer. | | `provenance.rs` | `SignatureProvenance`, `OverloadOrder`, `ArmIdentity`, `ConstituentSequence`, `DeclarationGroupId`, `MappedConstituent`, `OriginRelation`, `SourceLocatorId` — where a candidate came from and in what authored order. | | `substitution.rs` | `CallSubstitution`, `SubstTerm`, `compose_canonical`, `MAX_SUBSTITUTION_CHAIN_DEPTH`. The two substitution stages (declared/outer map and frozen call-site map) compose canonically; a hand-built chain past the bound flattens rather than growing. | | `discovery.rs` | `publish_signature`, `set_from_candidates`, `append_signatures`, `union_signatures`, `intersection_signatures`, `heritage_signatures`, `merged_declaration_signatures`, `resolution_order`, `signatures_identical`, `DiscoveryError`. | | `result.rs` | Demand-driven results: `ResultDemand`, `ReadSignatureResultKey`, `SignatureSetValue`, `SignatureResultValue`, `SignatureCandidateNodes`. | Ordering and composite reduction live beside the kernel, in the dispatch crate: | Path | Owns | |---|---| | `semantic_query/stable_key.rs` | `VerterStableV1` encoding and `StableKey::cmp`. | | `project_semantic_dispatch/canonical_algebra.rs` | `intern_ordered_union` / `intern_ordered_intersection` — the two crate-private canonical builders, plus `compare_structural` and `CanonicalEvidence`. | | `project_semantic_dispatch/build.rs` | `build_reduce_union` / `build_reduce_intersection` — the query builders behind `SemanticQueryKey::ReduceUnion` / `ReduceIntersection`. | | `project_semantic_dispatch/signature_discovery.rs` | The cutover consumer: it drives discovery, positional matching and instantiation through the kernel records. | --- ## 2. Ordered reduction is ONE pair of queries Union and intersection construction is closed over **exactly two** query keys and **exactly two** crate-private builders: ```text SemanticQueryKey::ReduceUnion { members, nullability } → build_reduce_union SemanticQueryKey::ReduceIntersection { input, purpose, ctx } → build_reduce_intersection ↓ ↓ canonical_algebra::intern_ordered_union canonical_algebra::intern_ordered_intersection ``` Both builders perform recursive same-kind flattening, the lattice absorption laws, literal subsumption on unions, structural `T | T = T` / `T & T = T` through `compare_structural`, and proven-disjoint scalar collapse to `never` — an undecided relation is never guessed. Both thread `CanonicalEvidence` to `deposit_canonical_evidence`; an incomplete comparison sets `cache_suppress` (`ReturnOnly`, never a warm canonical result). A union is built under an explicit `NullabilityPolicy` — `strictNullChecks` as construction input. `Erased` (the option off) drops `null` / `undefined` beside any other member, as the checker's `getUnionType` does, and a nullable-only list becomes `null` if it names `null`, else `undefined`. The policy is family identity on `ReduceUnion` and is recorded on the canonical stamp (`CompositeOriginCategory::Canonical(NullabilityPolicy)`), so the two settings never share a memo entry, a node, or the pre-seal skip. Intersections run the strict algebra. `ProjectSemanticDispatch::intern_normalized_union_or_intersection` is the dispatch-level funnel every flow/meta-resolve/locator producer reaches these through; its unions run the strict algebra. The flow-return evaluator, which answers under its function's own project policy, constructs through `intern_normalized_union` with the frame's policy instead. Two composite constructions are deliberately outside the funnel, both `CompositeList::ordered_carrier` mints, and both for the same reason — an ordered carrier is an authored sequence, not a commutative intersection, and routing it through the reducer would change the origin category and therefore node identity: 1. same-name method **overload groups** (`walk.rs`, `build.rs`); 2. a **possibly-callable member-value intersection** (`walk.rs` `merge_value_nodes_recursive`). Call resolution over an intersection tries arms in declaration order, so a commutative sort would break overload precedence. The classification (`value_may_contribute_call_signatures`) fails CLOSED on anything undecidable from the graph alone, and callable merging itself belongs to `SignaturesOfType` → `signature_kernel::discovery::intersection_signatures`, never to a local concatenation of signature nodes (§15). An interface/class declaration body with heritage is a third construction outside the funnel, the `CompositeList::heritage` mint (§6, rule 8): it is a declaration, not a commutative intersection, and is never re-decided. **Retired spellings.** `NormalizeUnion`, `NormalizeIntersection`, `SemanticMeet`, `canonical_intersection`, `build_normalize_union` and `UnionSelected` are retired names. Each is an entry in `RETIRED_SYMBOLS` (`crates/verter_session/tests/cases/g_misc0/no_legacy_walker.rs`), enforced by `retired_symbols_absent_from_production_source`. Re-introducing one resurrects a second composite-construction authority beside the ordered reduction pair, which is exactly how two producers start ordering arms differently. --- ## 3. Determinism: `VerterStableV1` `StableKey::cmp` compares `(fingerprint, exact)` — the FNV-1a hash of the exact key bytes FIRST, the exact bytes only on collision. Consequences worth keeping in mind: * Canonical union member order is **fingerprint order**, not authored order and not lexicographic order. `Extract<'a'|'b'|'c', 'a'|'b'>` renders `"b" | "a"`. * An authored-order display pin in an older test is an arena-id-sort coincidence (arena id == lowering order), not a contract. * `intern_ordered_union` interns with ORDER-SENSITIVE identity (`CompositeMembers::eq` compares the member slice), so `[a,b]` and `[b,a]` are distinct nodes — there is no set-collision first-wins there. Every `SemanticNodeData` variant has a `VerterStableV1` encoding. The registration table `STABLE_KEY_TABLE` in `crates/verter_session/tests/cases/g_block/semantic_determinism_matrix.rs` enumerates the live variant set exactly (guard: `stable_key_table_enumerates_every_semantic_node_data_category`) and carries a `residual` column naming, per variant, any identity input the encoder currently **approximates** rather than consumes. --- ## 4. Epoch-safe storage and the lifetime contract * Handles are epoch-qualified. A handle minted in a retired epoch is rejected, not silently re-read (`stale_epoch_handle_is_rejected`). * `replace_epoch()` installs a new graph epoch. A reader pinned before the replacement finishes against **its** epoch (`old_pinned_reader_finishes_against_its_epoch`, `pinned_view_reads_substitution_after_epoch_replacement`). * Live readers are roots until drop (`live_readers_are_roots_until_drop`, `live_reader_count_includes_pinned_retired_epoch`). * Retained results outlive an epoch replacement until drained (`retained_results_outlive_epoch_replacement_until_drained`). * Interning rejects stale embedded handles across a replacement (`intern_rejects_stale_embedded_handles_across_epoch_replacement`), and a result lookup MISS never publishes (`lookup_result_does_not_publish_on_miss`). The kernel store does **not** own the process byte budget: aggregate retention is `SemanticRetentionAccount` (see `/type-cache-architecture` → Aggregate retention account). A retained parse snapshot is a `Pinned` charge — charged unconditionally, never refused. ### Semantic identity records are owned by their handles Intersection recipes, semantic contexts, order domains and the large family-key payloads (`RelateMemoKey`, `ResolveCallKey`) are NOT ordinals into process-owned tables. `semantic_query_memo/intern_table.rs` owns the substrate: * `Interned` is one owning `Arc` pointer. `IntersectionInputId`, `SemanticContextId` and `OrderDomainId` wrap one; they are `Clone`, not `Copy`, and expose the value (`recipe()`, `context()`) — there is no `as_u32`, no `lookup_*`, and no raw slot number another index could also mint. * Each kind declares itself with `intern_domain!`, which gives it a private `WeakInternTable` reached as `InternDomain::index()`. The index only DEDUPLICATES (digest → `Weak`); it owns no record. A record's destructor forgets its entry, and a surviving collision bucket and the index both shrink their backing capacity once drained. * Identity is exact value equality behind the digest: equal handles share a record, or match digest AND value. A digest collision never aliases. * **Retained children:** a record owns exactly what its value owns. An `OrderedSteps` recipe's `EvaluateSubgroup` recipes are its only same-kind children; a held parent keeps them valid. A kind with same-kind children overrides `InternDomain::take_children`, so a released chain is reclaimed iteratively — nesting depth never becomes destructor stack depth, including when a step slice is still watched by a `Weak` (the children are handed to the worklist by clone before the slice is released). * `IntersectionInputId`'s `Debug` prints the recipe digest and top-level shape only; a nested subgroup is never expanded, so formatting a key is O(1) in nesting depth and sharing. * `WeakInternTable::occupancy` / `IdentityIndexSnapshot::capture` are the always-compiled lifetime counts (records, digests, digest-map and spilled collision capacity), surfaced as `HostRetentionSnapshot::identity_indexes`. * Walk recipe operands only through `IntersectionInputRef::for_each_operand`: iterative, and each distinct subgroup recipe is walked once however many parents share it (the family release sweep runs it under the memo lock). * Lock discipline: never drop something that can destroy a same-kind record while holding that kind's index lock. * `SemanticContextId::production()` is the one permanent record. `SemanticPolicySetId` is the policy set's own value (`SemanticPolicySet::id()`), so it needs no table. --- ## 5. Evidence `docs/evidence/signature-kernel/`: | File | Role | |---|---| | `manifest.json` | Contract byte-lock, pinned oracle identity (TypeScript 7.0.2 + per-platform toolchain digests), corpus identity and observation digest, generator versions. | | `semantic-difference-ledger.md` | The four-class release authority of §5.8. Every recorded difference is classified: exact agreement, presentation-only, `VerterStableV1` order-induced (causal proof required), or independent semantic difference / incompleteness. A row never silently disappears. | | `determinism-matrix.md` | Per-row status of the §5.9 perturbation matrix and what keeps a row undrivable. | | `performance-gates.md` | The §12 structural gates, the executable guard for each, and the 5% regression investigation policy. | Executable homes: | Home | Role | |---|---| | `crates/verter_session/src/signature_corpus_rows_tests.rs` | THE 26-row observation corpus (`verter-signature-corpus-v0@typescript-7.0.2`). Append-only: adding a row is one `Row` literal. Each row records the checker print, the `--declaration --emitDeclarationOnly` bytes, and the implementation's `Verdict`. | | `crates/verter_session/src/signature_corpus_tests.rs` | The corpus driver and the **flip law**: a `MatchesChecker` row fails when the live answer stops matching, and an owed/degraded row fails when the live answer STARTS matching. Both directions are proven by `signature_corpus_flip_law_fires_in_both_directions`. A verdict can only move by a deliberate re-pin. | | `crates/verter_session/tests/cases/g_block/semantic_determinism_matrix.rs` | The §5.9 perturbation matrix and the §5.4 stable-key table, each enumerated against its authority and consumed by replay drivers. Every comparison runs on TWO bases: the stable-text completed observation AND the generated-bytes digest. | | `crates/verter_session/tests/allocator_canaries.rs` | `signature_kernel_warm_positional::warm_positional_read_does_not_allocate_or_lock` — the §12 Empty/One gate, in a separate test binary because it installs a counting `#[global_allocator]`. | | `crates/verter_type_engine/src/signature_kernel/*_tests.rs` | Per-module unit coverage (lifetime, storage, substitution, positional, provenance, discovery, read view). | **Re-locking the contract digest.** `docs/arch/signature-kernel.md` is byte-locked by `manifest.json` → `contract.sha256`, checked by `typeinfo::oracle_core::identity::tests::evidence_manifest_digests_reproduce_from_checked_in_inputs`. If the contract bytes change intentionally, re-lock the digest in the same change; never weaken the test. --- ## 6. Working rules 1. **One signature producer.** A new consumer that needs callable shape goes through `signature_discovery`, never through its own walker over `SemanticNodeData::Signature`. 2. **One ordering rule.** Rendering, display, and comparison all read the same `VerterStableV1` order. A consumer that sorts arms itself is a defect. 3. **Two builders, both private.** Raw interning stays crate-private behind `ReduceUnion` / `ReduceIntersection`. A thin adapter may remain only when it makes no semantic decision. 4. **Never game a determinism test.** Serialising node allocation to make a replay agree is explicitly forbidden by §12. Fix the ordering input instead. 5. **A typed gap is not a fast success.** Do not compare a partial Verter query to a complete TypeScript project check and label the ratio a speedup. 6. **A corpus verdict moves by re-pin only.** Change the row literal in the same change that changes the answer, and move the matching ledger row with it. 7. **Semantic decisions read `SignaturesOfType`; representation may read the surface.** A reader whose answer depends on which signatures a type HAS — callability, callable anchoring, runtime classification, overload choice, applicability — asks discovery (`shared_signature_nodes` / `shared_signature_buckets`), as the apparent-type anchor and the broad runtime classifier do. Rendering, serialization, hashing, traversal and surface carriage read an object's `call_signatures` / `construct_signatures` directly. That split is sound only because every list on an interned object is either the object's own authored list or discovery's answer for the composite it was merged from: the shallow intersection merge keeps arm members, but takes its call/construct entries from `SignaturesOfType` for the intersection (`with_discovered_signatures`), never from its own identity-deduplicated concatenation. A new merge that interns an object carrying signatures must do the same, or it creates a second signature authority. 8. **A declaration body with heritage is not an intersection type.** An interface/class body with `extends` is an intersection NODE (bases in clause order, own body LAST) minted `CompositeList::heritage` (`CompositeOriginCategory::Heritage`; the single-declaration projection and the merged-declaration reducer mint it, and every order-preserving rebuild keeps it through `CompositeList::rebuilt_from`). `SignaturesOfType` reads the category and answers it with `signature_kernel::discovery::heritage_signatures` — own signatures first, then each base's in clause order, no identical-signature dedup, no mixin composition (TypeScript's `resolveObjectTypeMembers`) — so call resolution, the signature utilities, the relation engine and the shallow walker's heritage flush all agree, through an alias of the declaration and after instantiation too. Because the category is part of node identity, the body never shares a node with the authored `Base & { … }` over the same arms. 9. **Merged declarations list in order and resolve later-first.** One declaration's own overloads are minted `CompositeList::overload_group` and a merged declaration's groups `CompositeList::merged_overload_group` (one arm per declaration; `CompositeOriginCategory::{OverloadGroup, MergedOverloadGroup}`, kept by every order-preserving rebuild). `SignaturesOfType` answers them with `signature_kernel::discovery::merged_declaration_signatures` — every declaration's signatures in declaration order, identical ones included — which the signature utilities and conditional inference read (the LAST signature). Call resolution alone reads `shared_signature_nodes_in_resolution_order`, the kernel's `resolution_order` (TypeScript's `reorderCandidates`): a later declaration's group before an earlier one's, and a signature whose parameter is WRITTEN as a literal type (`FunctionParam::declared_literal`, carried through instantiation) before the rest. No call site orders candidates itself. A function VALUE merged from several declarations is minted the same way by `build_typeof` (`prepared_signature_groups`, `merged_declaration_signatures_node`): a namespace member declared in several blocks, a global function declared in several `declare global` blocks, and a global function declared in several files (one arm per file, in declaration precedence order). A file's own top-level overloads are ONE declaration. ## Related skills `/type-resolution` (query modes, macro traversal, the five-mode dispatch), `/type-cache-architecture` (key composition, candidate substrate, retention account), `/component-meta` (publication surface), `/audit-infrastructure` (`ReduceUnion` / `ReduceIntersection` work sites and origin-graph kinds).