# SURFACES — concrete syntax decisions (phase 2)

Every concrete-syntax choice the ADRs / ARCHITECTURE left open, resolved here for the
typed object model (`src/lang/ast.ts`) and canonical printer (`src/lang/print.ts`). One
line each, with rationale. This is the review artifact: the ADRs are binding and are not
re-litigated; this file is where the *unspecified* got decided. The examples teach
language features only, on generic public tickers (SPX/SPY/QQQ) per ARCHITECTURE §7.

## Canonical form, globally

- **2-space indentation; one clause per indented line under a statement header.** The
  ADR examples sometimes group clauses on one line (`PRIORITY 2  COALESCE hod-action`);
  canonical splits them, in a fixed per-statement clause order, so round-trip is
  deterministic. Grouping is illustrative, not canonical.
- **No trailing newline from `print()`.** File serialization appends one (`.editorconfig`
  owns final-newline); a bare statement and a module both print without it so tests
  compare exact bytes.
- **Blocks (module header + each statement) are separated by one blank line.**
- **Numbers render via minimal JS form** (`String(v)`): `1R`, not `1.0R`; `0.2R`, `0.5R`.
  The AST value is a `number`, so `1.0` vs `1` is unrecoverable — canonical picks the
  minimal form. `-0` normalizes to `0`. Non-finite numbers **throw** (fail-closed).
- **Strings are double-quoted**, with `\` and `"` escaped. Used for thesis, provenance
  author/origin/replay, and import paths.
- **Identifiers are case-sensitive and printed verbatim** (`spot`, `hod`, `vwap`). There
  is no case-normalization; the registry's registered spelling is the canonical spelling.
  The platform market series are **registered lowercase** and matched case-sensitively
  (`spot`, `vwap`, `hod`, `lod`, `prior_close`, `or_high`, `or_low`), so a case-variant like
  `HOD`/`VWAP` is NOT a market fact — it routes to the org side, reads UNKNOWN, and is
  refused at arm (the kestrel-mte unknown-series guard: "did you mean the market series
  `hod`? … case-sensitive and the platform market facts are lowercase"). Examples MUST use
  the lowercase registered spelling; the uppercase acronym belongs only in English prose
  (e.g. "the high of day (HOD)"), never in Kestrel code an author would copy.

## Lexical units

- **Windows/durations render tight**: `<number><unit>` — `1m`, `30s`, `5m`, `+30m`. Units
  are `s m h d w mo q y`.
- **`velocity(1m)`**: the window is printed in parentheses immediately after the series
  path. A series may carry a window **or** a segment selector (`plan(x)`), never both on
  the same token, so the paren form is unambiguous.
- **Baselines**: percentile prints prefix `p99`; sigma prints suffix `3sigma` — matching
  the two idioms in ARCHITECTURE §Wake. Only `p` and `sigma` are modeled in v1; other
  baseline stats are ABSENT (add as needed, not stubbed).
- **Quantities** carry an optional unit suffix: `R` (risk fraction), `c` (cents), `%`,
  `bp`. No unit → bare number. Signs are part of the number (`-0.5R`).
- **Time of day**: zero-padded `HH:MM` (`16:00`, `09:30`).

## Series references

- **Dotted path**, segments joined by `.`: `regime.intraday`, `alpha.pnl`,
  `fills.avg_px`.
- **Selectors on a segment** print as `name(sel)`: an org quantifier
  `children(any).drawdown`, or a named target `plan(chase-urgent).state`.
- **The market/org distinction is registry metadata, NOT a syntactic property.** Both
  print identically as a dotted path; the registry (later phase) resolves a path to its
  space. It is therefore **not a field on `SeriesRef`** — phase 3 removed the earlier
  `space?` field because it is unrecoverable from canonical text, and ADR-0004 forbids
  un-round-trippable state on a syntactic node. A bare symbolic value (a plan-state
  literal like `armed`) is likewise just a single-segment series — text cannot distinguish
  it, so there is no separate identifier-operand node.
- **Plan-state and org-aggregate reads are ordinary series comparisons**, not dedicated
  trigger nodes: `plan(x).state == armed`, `children(any).drawdown > 0.3R`. This keeps
  the trigger algebra small and the registry the single source of names.

## Trigger algebra

- **Comparison operators are symbolic**: `> >= < <= == !=`. Rationale: 1-token, unambiguous,
  and `spot > hod` reads cleanly. English relational sugar (`under`/`over`/`above`/`below`
  with an implicit subject) is a **parse-time alias** (phase 3) that canonicalizes to a
  symbolic comparison with an explicit subject — e.g. `EXIT under vwap` → canonical
  `EXIT spot < vwap`. Canonical has no implicit subject.
- **`crosses above` / `crosses below`** stay word-form: a cross is an *edge* event, not a
  level, and has no symbolic operator. This is the one place words beat symbols.
  - **At-or-touch predicate**: `touches above` / `touches below` fire on *reaching or
    touching* the level (`>=`/`<=`) where `crosses` needs a *strict* transition (`>`/`<`).
    Same node (`cross`), a `touch` flag; omitted = strict.
  - **Re-arm band**: `crosses above hod band 5c` (or `touches …`). After the cross fires the
    series must clear back past `right ∓ band` and **re-arm** before it can fire again, so a
    lone anomalous print cannot phantom-fire (the xrearm-1 lesson: a bare cross tripped 10/12
    phantom fires on one spurious tick). The band is a **positive** width (`c`/`%`/`bp`/bare);
    a zero/negative band can never re-arm and is refused (fail-closed). Omitted = a bare,
    single-shot cross.
  - **A cross may not be `held`** (fail-closed on all three surfaces — parser rejects the text,
    builder refuses to construct it, printer throws on it): sustaining an edge for a duration is
    exactly the phantom-fire anti-pattern. Reach for a re-arm `band` (reject the spurious tick)
    or hold a *level comparison* (`spot > hod held 30s`) instead. `within` over a cross stays
    legal — "the cross occurred inside this window" is a well-formed question.
- **Break-and-hold is a postfix modifier**: `<condition> held <dur>`. **`within` likewise**:
  `<event> within <dur>`. Both are their own nodes (`break-hold`, `within`) so they can
  wrap any inner trigger; the printer parenthesizes a compound inner (AND/OR) but not an
  atom, so `spot < vwap held 60s` needs no parens.
- **The thesis temporal envelope `until` / `at`** (kestrel-rtf, OSS-ADR-0049) are two more
  postfix siblings of `within`, each over a wall-clock `HH:MM`: `<event> until 15:45` (the
  expectation is in force up to a time) and `<event> at 15:45` (evaluated at a time). Own
  nodes (`until`, `at`) reusing the SAME trigger surface — the pre-registration envelope adds
  no second predicate language. Runtime firing of `until` / `at` is deferred: a session that
  cannot evaluate them reads UNKNOWN and de-arms with a logged reason (fail-closed).
- **Nth-occurrence** prints an ordinal *word* for 1–12 (`first`…`twelfth`), numeric
  `<n>th` beyond: `second failed-break of hod`. Word form is chosen for readability; the
  fixed table keeps it deterministic.
- **Structural events**: `<name> of <level>` (`failed-break of hod`) or bare `<name>`.
  Event names are open-vocabulary identifiers (kebab-case by convention).
- **Phase events**: `phase <name>` (`phase open`). Phase names are open-vocabulary.
- **Time windows**: `time <from>..<to>`, or one-sided `time after <t>` / `time before <t>`.
  At least one bound is required or the printer throws (fail-closed).
- **Fill events**: `filled`, `partial-fill`, `unfilled`, `rejected`, `cancelled`;
  optional `<event> leg <n>`. A fixed enum (not open-vocabulary) — the fill lifecycle is
  engine-defined.
- **Boolean precedence** (low→high): `OR` < `AND` < `NOT` < postfix(`held`/`within`) <
  atom. The printer emits **minimal parentheses**: `a > 1 OR c < 3 AND e > 5` (no parens),
  `(a > 1 OR b > 2) AND c < 3`, `NOT (a AND b)`. `AND`/`OR` are variadic and print
  flattened.
- **`AND`/`OR`/`NOT` are uppercase**; series/operators are not. Keywords that introduce a
  clause or a boolean are uppercase throughout.

## Price expressions

- **Anchors are bare keywords**: `fair intrinsic basis bid ask mid last join improve stub`.
  `fair` = ExecutionFair; `mid` is a legal authoring resting-price even though it is never
  a fair-value *source* (ARCHITECTURE §4) — the two roles do not collide in text.
- **Offsets render tight, no spaces**: `fair-3c`, `mid+2c`, `fair-5%`. Matches the ADR's
  `fair-3c`. Units are `c` (cents) or `%`.
- **`lean(a, b, x)`, `min(...)`, `max(...)`** render as calls with `, `-separated args.
- **Absolute prices** are bare numbers (`4.20`); distinguished from anchors by being
  numeric.

## Order policy (resting-order execution)

- **Price vs policy are separate**: `@ <price>` is *which* price; the trailing policy is
  *how* it rests. Policy clause order is fixed: `pricing` (`peg`|`fix`) → `esc` ladder →
  `cap` list → `floor` list → `cancel-if` → `gtc`.
- **`esc <price> <dur>`** per rung; multiple rungs allowed, printed in array order.
- **`cap <price>` / `floor <price>`** are lists, one keyword each (`cap fair cap 5.00`).
- **`cancel-if <trigger>`** is a per-order guard; the plan-level `CANCEL-IF` is the same
  trigger at a wider scope. Both reuse the trigger algebra.

## Legs & tickets

- **Leg**: `<side> <qty> <strike> <right>` — `buy 2 +1 C`. Sides `buy`/`sell`; rights
  `C`/`P`. Equity/underlying legs are ABSENT in v1 (options-only), not stubbed.
- **Strikes**: relative `+1`/`-1` (signed, `+` shown for ≥0), absolute `450`, `atm`, or
  delta `25d`. `atm` is its own kind rather than `+0` to read cleanly.
- **Multi-leg tickets** print legs `, `-separated. A valid v1 ticket is single-leg;
  multi-leg only appears under the reserved `atomic` flag.
- **`atomic`** prints as a leading keyword on the ticket (`DO atomic buy 1 +1 C, sell 1 +2 C @ fair`).
  It round-trips through the AST (`atomic?: boolean`) but phase-3 parsing REJECTS it
  loudly (ADR-0005) — reserved, never faked.

## Plan header & clauses

- **Header order (fixed)**: `PLAN <name> [budget] [ttl] [regime] [priority] [standing]
  [prov] [atomic]`, then indented `USING`, `WHEN`, then clauses in authored array order.
- **Clause order is preserved from the array**, not normalized — clause sequence is
  author-meaningful (reads top-to-bottom as the plan's logic).
- **`ttl`**: relative `+30m` or absolute clock `16:00` (hold-to-close is `ttl 16:00`).
- **`regime {intraday: trend}`**: brace-map, `scope: value` pairs, `, `-separated,
  array order preserved. Open-vocabulary values (ADR-0005).
- **`standing <s>`** prints the lifecycle enum (`authored`/`armed`/`versioned`/`superseded`).
- **`prov {...}`** on a plan header; module-level provenance prints as a `PROVENANCE {...}`
  directive line. Brace fields in fixed order `tier, author, origin, replay`; `tier` is a
  bareword, the rest are quoted strings. Only present fields print.
- **`TP`**: target is `+100%` (pct, sign shown), `2x` (mult), or a price; optional
  `frac <f>` (fraction of position — the documented TP-fraction control), optional held
  quantifier, optional `@ <price>` and policy. At runtime the target is the tier
  threshold, evaluated on `fair` (kestrel-lic3): the `@`-form arms its sell once fair
  crosses the threshold (resting at the `@` exec anchor); the no-`@` form rests at the
  target-derived price immediately and fills on market crossing.
- **`RELOAD [WHEN <trig>] <legs> @ <price> <policy>`** — the on-adverse trigger is inline
  `WHEN`.
- **`ARM [WHEN <trig>] [basis <price>] [<held-quant>]`** — the on-arm trigger plus the two
  inventory bindings (basis anchor, held-leg quantifier) from ADR-0005. Bare `ARM` = arm
  immediately.
- **Held-leg quantifiers** print `foreach held leg` / `any held leg`, attached to `TP` /
  `EXIT` / `RELOAD` / `ARM` after the head.

## Wake

- **Clause order (fixed)**: `WHEN`, `DELIVER`, `PRIORITY`, `COALESCE`, `BUDGET`,
  `UNIVERSE`.
- **`DELIVER <view> [MANDATORY] [KEYFRAME]`** — delta frame is the default (unmarked);
  `KEYFRAME` opts into a full keyframe (CONTEXT: Wake delivery).
- **`BUDGET`** prints present parts: `12 wakes/day`, `5000 tokens/day`, or both
  space-separated. A Wake with neither part omits the line.
- **A Scan is a Wake with a `UNIVERSE <name>` clause** — no separate `SCAN` keyword
  (CONTEXT: Scan is the Wake *kind*, not a new statement).

## Citation (the `because` pre-registration clause, OSS-ADR-0049)

- **`BECAUSE sha256:<64 hex>`** — an optional clause on **Plan and Wake only**, carrying the
  content hash of a platform-side Thesis (kestrel-rtf). It is an optional AST field
  (`because?: Citation`), NOT a fifth statement kind — the algebra stays View/Wake/Plan/Grade.
- **Content hash only**: the digest reuses the repo's ubiquitous 64-lowercase-hex sha256 shape
  (as `bus_sha256` / `plans_sha256` / `ConfigId`); the thesis prose lives platform-side, never
  inline. Printing a malformed digest is refused symmetrically (the printer would otherwise
  emit text that cannot re-parse).
- **Fixed placement**: in a **Plan** the `BECAUSE` line prints after `WHEN`, before the
  clauses; in a **Wake** it prints after `WHEN`, before `DELIVER`. At most one `because` per
  statement. Because it prints byte-stably like any canonical line, it binds into the
  armed-document hash `sha256(print(module))` with no special casing — pre-registration is
  tamper-evident once armed (round-trip byte-stable, golden contract).

## Grade

- **Header**: `GRADE <what> <name> [OVER <from>..<to>] [FILL <model>]`. `<what>` ∈
  `plan wake view pod tag`.
- **Corpus range**: `<from>..<to>` with opaque date tokens (`2025-01..2026-06`); the AST
  does not parse the dates in v1.
- **`VS`** prints counterfactuals space-separated in a **fixed canonical order**
  (`ungated null bracket`), de-duplicated — `VS` is a set, so canonical order is stable
  regardless of authoring order.
- **`BY`** prints dimensions `, `-separated in **authored array order** (dimensions are an
  ordered stratification): a series path, or the keywords `vehicle` / `name` / `lineage`.

## Pod / Book

- **Recursive indentation**: a child Pod prints at parent-indent + 1; a Book prints its
  header inline and appends any `RISK` lines at +1.
- **`RISK <metric> <threshold> -> <action>`** — `RISK day-loss 2R -> halt`. Metric and
  action are open-vocabulary identifiers; threshold is a quantity.
- **Book header (fixed order)**: `BOOK <name> [budget] [coverage <instr…> thesis "…"]
  [arbitration <policy> [concurrency <n>]]`. Coverage is instruments then thesis, matching
  ADR-0002's "instruments alone are not coverage" (thesis is mandatory when coverage is
  present).
- **Arbitration** is an open-vocabulary policy word plus optional `concurrency <n>` — how
  a book's concurrent plans compete for its envelope.

## Module

- **Imports**: `IMPORT { a, b } FROM "path"` — ESM-like, names in braces, path quoted
  (ADR-0003).
- **Module header order**: imports, then `PROVENANCE`, then `USING`, then a blank line,
  then statements.
- **`USING signal <instr> exec <instr> <expiry>`** — signal and/or exec, each optional.
  Expiry selectors: `<n>dte` (0dte/1dte), an absolute date token, or a named tag.

## USING elision (the round-trip subtlety, ADR-0004 / ARCHITECTURE §2)

- **The AST is always fully qualified**; the printer elides what matches the ambient
  `USING`. A plan whose `USING` equals the module's prints **no** `USING` line; a plan that
  differs on only one side prints **only that side** (`USING exec QQQ 0dte`). Instruments
  compare by symbol + expiry. This is why `print(parse(text))` can be shorter than a
  fully-qualified AST would suggest, yet still round-trips: the parser re-applies the
  ambient defaults.

## View addressing (which surface can say which View a frame renders)

How a rendered frame's pane set is *addressed* — distinct from the View **statement**
(the authored artifact above) and from the Wake `DELIVER <view>` clause (the authored
delivery binding). Addressing is the runtime question: given a frame to render, which
View applies?

- **One resolution point, three legs.** `resolveView(view, frameKind, seat)`
  (`src/frame/pane-catalog.ts`) owns the precedence: **explicit View > seat founder
  View > phase default**. An explicitly passed View always wins; a seat is consulted
  ONLY when no View was passed; with neither, the frame kind's DEFAULT panes render,
  byte-identical to before the catalog existed. Unknown pane id or over-budget View ⇒
  refused loudly (fail-closed), never silently degraded.
- **The in-process carrier is `RenderOptions {view?, seat?}`** (`src/frame/render.ts`):
  `view` is a `ViewSelection`, `seat` a `SeatId` from the closed pod roster
  (`src/frame/seat.ts` — `pm`/`strategist`/`watcher`; an unknown role is refused, never
  coerced). `renderBriefing`/`renderWakeDelta` stay pure functions of (input, options).
- **The config axis is `AgentConfig.seatViews`** (`src/session/agent.ts`), a
  `SeatViewsPolicy` (`"founder" | "phase-default"`, ADR-0041 §2 / A1). `"founder"` ⇒ the
  live harness passes the acting tier's seat into the render (strategist at OPEN,
  watcher at wakes — `src/session/harness/live-agent.ts` →
  `renderBriefingPrompt`/`renderWakePrompt`, `src/session/harness/prompt.ts`), so a seat
  with a graded founder seed reads it when no View was authored. Absent or
  `"phase-default"` ⇒ NO seat is passed ⇒ byte-identical output. A **ConfigId axis**
  (folds into `deriveConfigId` automatically), never an envelope field — a founder-View
  run mints a distinct grid column. Guard fixture through the real driver:
  `tests/simulate.founder-seat-views.e2e.test.ts`.
- **Who can construct the axis today: programmatic harness drivers only.** The only
  constructors of an `AgentConfig` are the bench/tournament scripts and tests. Every
  other surface is a stated **non-goal today**, fail-closed rather than half-wired:
  - **CLI** — the `frame`/`percept` verb accepts no `--view`/`--seat` flag
    (`src/cli/commands/frame.ts`); `LocalBackend.getPercept` calls the renderers with
    no `RenderOptions` (`src/cli/backend/local.ts`), so the CLI renders phase-default
    panes only. `view`/`seat` are `RenderOptions` fields, not `BriefingInput`/
    `WakeDeltaInput` fields, so no input JSON can smuggle them in.
  - **Remote CLI** — `RemoteBackend.getPercept` REFUSES (`REMOTE_UNSUPPORTED`): the
    contract has no View route, and fabricating one is an owner decision
    (kestrel-5rb owner-note, `src/cli/backend/remote.ts`).
  - **Agent protocol** — no op or argument shape carries a View or seat
    (`src/sdk/op-schema.ts`); `session/frame` delivers the TYPED `SessionFrame`
    struct, so `resolveView` never runs on the wire path — rendering is a client
    concern. The AUTHORED leg still travels here as document text (a Wake's
    `DELIVER <view>` inside `advance`/`revise`); the role-keyed legs have no carrier.
  - **MCP** — no Frame-render tool exists; tool shapes are single-sourced from
    the same op schemas, which carry none (`src/mcp/server.ts`).
  - **SDK facade** — `openSession(subject, document?)` takes no `AgentConfig`; no SDK
    shape carries `view`/`seat` (`src/sdk/types.ts`). Importing `src/frame` directly
    and passing `RenderOptions` is the library, not the SDK contract.

  These absences are deliberate scope, not omissions: CLI exposure and any remote
  View projection are tracked separately (kestrel-wa0j.73.2 / kestrel-wa0j.73.3), and
  each new exposure ships with its own red-on-inert fixture through the real driver.

  **Decided (2026-07-17, kestrel-wa0j.73.3).** The client renders. The wire keeps the
  typed `SessionFrame` as canonical truth; there are no server-rendered Frame bytes
  on the wire, so recomputability rests on the struct, not on a rendering the server
  chose. `resolveView` is a client-side concern: the SDK ships `resolveView` semantics
  (role-keyed View addressing) when a real remote consumer materializes, not before —
  `openSession` gains no render-config argument and no remote Frame-render op is
  minted speculatively. This ratifies the remote leg's current refusal (`RemoteBackend`
  `REMOTE_UNSUPPORTED`) as the intended contract, not a gap awaiting a server-render fix.

## Deliberate ABSENCES (documented, not stubbed — Task rule)

- **Equity/underlying legs** (options-only in v1).
- **Template parameterization / plans-with-holes** (ADR-0003 defers; `USING` is the only
  customization point).
- **Size-modulation and time-parameterized schedules** in plan text (ADR-0005 defers;
  conviction stays out of plan text by design).
- **Multi-instrument N-node graph** beyond one signal + one exec pair (ARCHITECTURE §9;
  the types do not preclude it — `SeriesRef` paths and per-`USING` roles generalize — but
  no graph syntax ships).
- **Baseline stats beyond `p`/`sigma`**, and **fill-event leg semantics beyond an index**.

## Parse-time fail-closed doctrine checks (golden-corpus phase)

Four invariants are enforced at parse time and exercised by `tests/golden/rejects/*` —
each fails closed with a `KestrelParseError` (line/col + the doctrine named), never a
silent degrade:

- **`atomic` multi-leg is refused, never faked** (ADR-0005) — reserved keyword → loud
  rejection, in both the plan header and any ticket.
- **EXIT may not condition on a mark** (`mid`/`bid`/`ask`/`last`) — the "marks lie"
  doctrine (ADR-0005; ARCHITECTURE §6.3: the observed mid is a health signal, never a
  value). The guard inspects the EXIT **trigger** only; resting an order *at* a mark
  price stays legal authoring. (Registry-class refinement — knowing which *named* series
  is an option mark — still belongs to the later registry phase; this is the syntactic
  slice available today.)
- **Provenance may only narrow** (ARCHITECTURE §6.6) — a statement inside a module whose
  declared `prov` tier out-ranks the module's `PROVENANCE` tier is elevation → refused
  (`unvetted < candidate < vetted`). The channel-authority slice of the ceiling is a
  harness concern; this is the intra-document slice.
- **A risk budget must be positive** (ARCHITECTURE §6.1: `size × max_loss ≤ budget`) — a
  zero/negative `budget` can never admit a real position, so it is refused rather than
  armed as a dead plan.
- **A `because` citation is content-hash-only, Plan/Wake-only** (kestrel-rtf, OSS-ADR-0049) —
  three symmetric refusals: (a) a digest not of the fixed shape `sha256:<64 lowercase hex>`
  (`because sha256:deadbeef`); (b) an inline thesis body — any token trailing the digest
  (the body lives platform-side); (c) `because` on a View or a Grade — a citation
  pre-registers an *executable expectation*, which only a Plan or Wake authors.

### Doctrine note: marks in triggers

The mark ban is **EXIT-only, and deliberately so** (ADR-0005; ARCHITECTURE §6 invariant 3).
The `findMarkInTrigger` guard fires only in the EXIT clause because that is the failure mode
the doctrine names: EXIT keys a *position's fate* on a possibly-fictional book, so acting on
a lying mark is where money is lost. Everywhere else a trigger may legitimately **observe**
`mid`/`bid`/`ask`/`last` as *book-state signal* — spread-regime and dark-book detection read
exactly these. The invariant governs **prices** (mid is never a silent price anchor, never
worth), not book-state observation; it is not a blanket ban on the mark series. Widening the
guard past EXIT would refuse real, legitimate authoring — the EXIT-only scope is the resolution,
not an oversight.

### Doctrine note: `atomic` is refused symmetrically

`atomic` is refused on **every** surface, not just the parser (ADR-0005, ADR-0004 round-trip):
the parser rejects the keyword, the **builders refuse to construct** an atomic clause/plan, and
the **printer throws** if it ever encounters `atomic: true`. The AST keeps the field so the
parser has the vocabulary to reject loudly and the type documents the future surface — but
because no surface will emit or construct it, `parse(print(x))` round-trips. Atomicity is
reserved until a whole-structure preflight + atomic execution adapter exist; it is never faked.

### Doctrine note: the never-naked SELL boundary (runtime, ARCHITECTURE §6.1)

Legs are independent single-leg children (atomic multi-leg is refused), so an independent short
leg carries unbounded risk unless it merely closes a long we already hold. The runtime therefore
admits an **entry / ALSO / RELOAD** leg with `side: sell` **only as a COVERED sell**: at fire time
`uncovered_qty = sellQty − (net held of that exact leg − qty already committed to resting sells)`;
`uncovered_qty > 0` ⇒ the leg is **refused loudly** (a `PLAN` event, reason `"uncovered sell
refused: never naked"`) and never submitted, while **other legs in the same `DO` still proceed**.
A covered sell **consumes $0 budget** (it reduces risk, not adds it). TP/EXIT sells are a separate
path — they are already clamped to held inventory and intrinsic-floored, so they are exempt from
this gate.

### Doctrine note: market-level vs premium-level triggers (fade-ladder fix)

A trigger comparing an **underlier-space** series to a **premium-space** anchor is a category
error: `RELOAD WHEN spot < basis` compares the underlier price (~200–300) to the position's option
cost basis (~$2), so it is either always-true or always-false — never the intended "reload as the
market falls further". The fade-ladder accept fixture originally carried exactly this bug; it now
ladders on genuine market levels — `spot < or_low` (breach the opening-range low), then
`spot < lod` (breach the day's low) — two distinct underlier-space levels the fade deepens through.
The rule: a trigger relates **like-space** series (underlier vs underlier level, or an org-$ series
vs an org-$ baseline); `basis`/`fair`/`intrinsic`/`mid` are **premium-space price anchors** for the
`@` price line, not trigger operands against `spot`.

### Doctrine note: `because` pre-registration is content-hash-only, by construction

The citation carries a digest and nothing else **because the digest is the whole point**
(kestrel-rtf, OSS-ADR-0049). A Kestrel document's armed identity is `sha256(print(module))`;
a byte-stable `BECAUSE sha256:<64 hex>` line binds into that identity with no special casing,
so pre-registration is tamper-evident the moment the plan is armed. An inline thesis body
would be un-hashable-into-identity noise and is refused; the prose lives platform-side,
referenced by the hash. The clause attaches to a **Plan or Wake only** — the two kinds that
author an *executable expectation* against which a thesis can later be graded — never to a
View or Grade, and never as a fifth statement kind (the algebra stays View/Wake/Plan/Grade,
OSS-ADR-0001). All three refusals fire symmetrically on parser, builder, and printer
(OSS-ADR-0004 round-trip): a hand-built citation throws exactly where the parser does.

### Provider hooks: wired vs absent-with-reason (RUNTIME §2/§3)

The session's canonical provider now serves two of the OPTIONAL `SeriesProvider` hooks and leaves
one absent-with-reason:

- **`timeOfDayMinutes` — WIRED** (ET wall-clock, `America/New_York`, off the injected event `ts`):
  enables `time HH:MM..HH:MM` windows and the absolute `ttl 16:00` deadline. Pure calendar
  arithmetic (tz database), no wall clock read — deterministic replay (`src/session/clock.ts`).
- **`fillEvent` — WIRED, session-scoped** (`src/session/sim.ts` `FillTelemetry`): `filled` /
  `rejected` / `cancelled` latch to a definite `true` once seen on the emitted ORDER stream. It is
  deliberately **session-scoped and all-or-nothing** — per-plan / per-leg targeting, `partial-fill`,
  and `unfilled` are ABSENT (single-leg fills are never partial; `leg <n>` needs multi-leg tickets,
  which are reserved). Those read UNKNOWN. A **leg qualifier is refused by the ENGINE**, upstream of
  the provider (`ComposedProvider.fillEvent`, `src/engine/plans.ts`), and refused **loudly**: UNKNOWN
  *plus* a logged de-arm reason, because the session-scoped telemetry would otherwise answer
  `filled leg 1` with "some fill, anywhere, by anyone" — a plan armed on its own leg firing on a
  stranger's fill (cross-plan bleed, kestrel-s3h8). The scoping capability itself is kestrel-qbwo.2;
  when a provider can honour the qualifier it is admitted at that seam, not by dropping the refusal.
  Pinned e2e by `tests/session.fill-leg-qualifier-dearm-e2e.test.ts`.
- **`structural` (detector events, `failed-break of hod`) — ABSENT** with reason: no detector
  reducers ship in the v1 session, so structural-event triggers read UNKNOWN and de-arm cleanly
  (fail-closed). They are a later milestone, not a stub.

### Doctrine note: CANCEL-IF is acquisition-only (deliberate scope)

`CANCEL-IF` (plan- and line-level) cancels **unfilled acquisition children (entry/reload) only**;
resting **TP/EXIT management orders persist**. This matches author intent — "stop trying to get
in" — while inventory management stays governed by TP / EXIT / INVALIDATE. It is a deliberate
scope, not an oversight (RUNTIME §5).

### Report note: input pinning + honest reprice tally (F6)

The graded report pins **both** ends of the computation: `session.determinism_hash` is the sha256
of the emitted PLAN/WAKE/ORDER stream (the OUTPUT), and `session.bus_sha256` is the sha256 of the
raw INPUT bus. `session.reprice_count` is the honest count of actual cancel/replace reprice
submissions (peg drift + esc-stage boundary); `orders[].esc_stages` is the ladder **rung** an
order occupied, not a count of reprices (the earlier `reprices` field conflated the two).

## Parser bug fixed this phase

- **Zero-padded expiry dates** (`SPY 2026-07-17`) previously round-tripped lossily —
  `parseExpiry` rebuilt the date via `Number()`, dropping the leading zero (`2026-7-17`).
  Fixed to preserve the raw digit-token text (as `readCorpusToken` already does for grade
  corpus ranges). Locked by the property generator (which emits padded + unpadded dates).
