# Held-out validation — oracle (incomplete-mediation / self-deal) generalization [#13] (2026-06-17)

audience: AI coding agents first. MEASURED log; tags [MEASURED]/[INFERRED]. Do not prettify.

## BLUF
The deterministic oracle (`domains/security/detectors/oracle/oracle2.ts`) was validated on ONE donor domain only —
G2 = C02/C09 buyer≡vendorOwner self-deal in multideal payments. **Held-out test on a SYNTHETIC 3rd domain
(security bug-bounty grant disbursement, neither commerce nor referral). The headline is ONE axis, not two:**
- **The oracle is DOMAIN-BLIND by construction.** It reads field-name tokens (`canonPrincipal`, `AMOUNT_RE`/
  `BENEF_RE`/`TYPE_RE`) + AST structure and NEVER perceives a "domain" (it even hardcodes a `buyer→referee` alias).
  So it has a SINGLE generalization boundary: the field-name lexicon + the explicit `key:value` sink form. "Domain"
  is a label on the cell, not an input the detector sees. (Do NOT report a "domain-generalization" win and a
  "lexicon-dependence" loss as two findings — they are the two ENDS of this one boundary.)
- **Inside the boundary — integration + discrimination [MEASURED].** A self-deal in an unseen domain, written
  lexicon-compliant, FIRES (`[INTER-SINK GAP]`) and its safe twin is SILENT → the FULL asymmetry pipeline
  (principal extraction → guard dominance → sink shape → inter-sink family) engages end-to-end on freshly-authored
  input AND discriminates vuln from safe. NON-trivial: the first authoring attempt MISSED (shorthand below), so the
  FIRE is a real end-to-end confirmation, not a tautology. This is NOT "domain generalization" — the only variable
  that ever moves catch→miss is the lexicon.
- **The boundary itself — lexicon + property-form [MEASURED].** The SAME self-deal with natural off-payments field
  names (`approverId`/`recipientId`/`award`/`kind`) is a SILENT MISS, and an ES6 **shorthand** beneficiary property
  (`{ amount, beneficiaryUserId, entryType }`, which `extractSinks` skips — it ignores `ShorthandPropertyAssignment`)
  is ALSO a SILENT MISS. The catch is gated on the `*UserId`/`amount…`/`type` field lexicon + explicit `key:value`
  form, NOT on self-deal STRUCTURE. Shorthand is ubiquitous in modern TS → a live recall hole, not a corner case.

**Correction to an oracle overclaim:** the header says "SINK … (shape, not function name)" and "NO hardcoded
predicate/sink NAMES." True for the CALLEE and the PREDICATE (the rename-GATE proves predicate-name independence).
FALSE for the FIELD names — they are a hardcoded regex lexicon, and this test falsifies their generalization. The
oracle's robustness guarantee covers predicate/callee names; field-name independence was never gated and does not hold.

## Cells (SYNTHETIC — held-out, NOT git-pinned; kept OUT of `domains/security/corpus/` so bench.py never ingests them)
`cells/` — self-contained (no imports → zero unresolved-imports, so the only variable under test is domain + naming):

| cell | domain | naming | structure | PREDICT | MEASURED |
|---|---|---|---|---|---|
| `h1_vuln.ts` | bug-bounty grant | oracle lexicon (`*UserId`, `amount`, `entryType`) | INTER-SINK self-deal (auto-settle skips reviewer check) | FIRE | **FIRE** `[INTER-SINK GAP] autoSettleBounty :: bounty_payout @L50` |
| `h1_safe.ts` | bug-bounty grant | oracle lexicon | both paths mediate (fixed) | SILENT | **SILENT** (discriminates — no false-positive) |
| `h2_vuln.ts` | bug-bounty grant | natural (`approverId`,`recipientId`,`award`,`kind`) | identical to h1_vuln | MISS | **SILENT = MISS** (`value-positive sinks: 0`) |
| `h1_vuln_shorthand.ts` | bug-bounty grant | oracle lexicon but **shorthand** beneficiary | identical to h1_vuln | (discovered) | **SILENT = MISS** (`value-positive sinks: 0`) |

Re-run: `for c in h1_vuln h1_safe h2_vuln h1_vuln_shorthand; do bun domains/security/detectors/oracle/oracle2.ts \
docs/validation/heldout-oracle-generalization/cells/$c.ts; done`

## What each cell isolates [MEASURED]
- **h1_vuln vs h1_safe** — the INTEGRATION + DISCRIMINATION pair (corpus RED-on-vuln / GREEN-on-safe discipline,
  applied to novel lexicon-compliant input). Both share the lexicon and structure; they differ ONLY in whether the
  auto-settle path mediates the self-deal. FIRE on vuln + SILENT on safe = the full pipeline engages end-to-end AND
  is specific (does not fire on every value sink). NOT a "domain generalization" claim — lexicon is held constant
  here; the lexicon experiment is h1_vuln vs h2_vuln below.
- **h1_vuln vs h2_vuln** — the controlled LEXICON experiment. h2 is h1 with every lexicon token swapped to a natural
  domain synonym and NOTHING else changed. h2's MISS is **over-determined** (both the principal gate — `approverId`/
  `recipientId` lack the `*UserId` suffix — AND the sink gate — `award`/`kind` are outside the amount/type lexicon —
  fail simultaneously); a real off-lexicon codebase trips multiple gates at once. The point is the aggregate:
  natural naming in this domain is INVISIBLE to the oracle.
- **h1_vuln vs h1_vuln_shorthand** — isolates the `PropertyAssignment`-only sink recognizer. Identical to h1_vuln
  except the beneficiary is an ES6 shorthand. SILENT → confirms the gap is the property FORM, not the field name.

## Coverage contract consequences (feed `docs/taxonomy/security.md`)
The oracle is the ONLY leg that catches the founding C02 self-deal, and it is deterministic — so its boundaries ARE
the gate's deterministic self-deal coverage. State honestly, do not imply general coverage:
1. **In scope (MEASURED catch):** identity-collision / incomplete-mediation on value sinks, **across domains**, when
   the code uses the `*UserId` principal convention + an `amount|agorot|cents|value|sum|total|price|fee` field + a
   `*UserId|accountId|walletId` beneficiary as an explicit `key: value` property + a string `type|kind|entryType`.
2. **Out of scope (MEASURED miss):** any self-deal whose principals or value-sink fields use names outside that
   lexicon (h2), and any sink whose beneficiary is an ES6 shorthand property (shorthand cell). These are SILENT —
   and a SILENT oracle on such input is a **possible FALSE CLEAN**, the same failure direction the unresolved-imports
   guard already protects against, but here triggered by NAMING rather than missing files (no guard exists for it).

## NOT established (honest)
- **The asymmetry logic's REACH is tested only on the INTER-SINK shape.** H1 exercises ONE structure (inter-sink
  family gap). A SECOND lexicon-compliant domain would NOT firm anything — it just re-confirms the same single
  axis one level down (piling). To extend the REAL surface, vary STRUCTURE, not narrative: an INTRA-SINK self-deal
  (one sink gated on pair A but not pair B in the same fn), the ternary-amount mediation idiom
  (`amount = self ? 0 : compute()`, the C09 production form), or a ≥3-principal case. Those measure whether the
  asymmetry logic's reach generalizes — the thing actually worth measuring. The LEXICON+form boundary itself is
  structural (regex + `PropertyAssignment`-only loop read directly from `oracle2.ts`), not a sampling estimate — it
  needs no more cells.
- **Synthetic, not git-pinned.** These cells are constructed to probe generalization (per task #13 "synthetic
  3rd-domain self-deal"), so they carry no fix-commit ground truth. They validate the DETECTOR's reach, not a real
  defect; kept separate from the git-pinned bench corpus on purpose.
- **No fix proposed here.** Whether to broaden the lexicon (more field synonyms), add `ShorthandPropertyAssignment`
  handling, or instead emit a NON-SILENT "value-sink-shaped call with off-lexicon fields — UNRELIABLE" notice (the
  no-false-clean pattern) is a design call. Recommend the non-silent-notice direction (it cannot overclaim) over a
  bigger regex (whack-a-mole, never complete), but that is a separate task, brainstorm→plan, not a blind patch.

## Pointers
- Detector: `domains/security/detectors/oracle/oracle2.ts` (lexicon: `AMOUNT_RE`, `BENEF_RE`, `TYPE_RE`,
  `canonPrincipal`; sink extractor: `extractSinks`, the `PropertyAssignment`-only loop ~L211).
- Donor validation: `docs/VALIDATION.md` (G2 = C02/C09, the only domain the oracle was validated on).
- Rename-GATE (predicate-name independence, the guarantee this test COMPLEMENTS): `oracle2.ts` header GATE note.
