kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
101 lines (84 loc) • 7.04 kB
Markdown
# Byte-stable inline comments — the authored "why" travels WITH the action
**Status:** Accepted (2026-07-13). Extends ADR-0004 (the typed object model
IS the language; `print(parse(x))` is a byte-stable projection). This is the **byte-stable
inline-comments ADR** that ADR-0013 (b) forecast as "forthcoming" and that ADR-0031 named as
its one honest dependency. Both dependencies are now **satisfied** (see Consequences).
## Context
Kestrel's canonical representation is the typed object model; text is a byte-stable projection
of it (ADR-0004). A `#` comment is not decoration — it is **authored reasoning**: the human- or
agent-readable "why" that ADR-0013 (b) requires to ride _with_ the action, welded into the
armed-document hash (`sha256(print(module))`) so pre-registration is tamper-evident. ADR-0031's
journal-as-`#`-comment channel rides the same lexical surface.
But the language core violated the round-trip invariant for any comment-bearing document. The
lexer **dropped** comments (`src/lang/lex.ts` broke on `#` and skipped comment-only lines), the
AST had no place to hold them, and the printer emitted none — so `print(parse(text)) !== text`
whenever a `#` appeared, and the author's reasoning was **silently lost**. That is a
contradiction with ADR-0004's byte-stable guarantee, not a feature: the catalog and status page
rationalized it as "the printer discards them (ADR-0004)," which read the invariant exactly
backwards. This ADR reconciles the two: **the byte-stable requirement wins — comments are
preserved, not discarded.**
Comments carry **no executable behavior** (they are not a fifth statement kind, not a runtime
signal). The tension is purely representational: how to preserve them byte-identically through
`parse → print` without polluting the semantic object model (triggers, prices, legs) or
perturbing the thousands of existing deep-equality checks against builder output.
## Decision
**(a) Comments round-trip byte-identically. This is part of the definition of done.** For a
**canonical** comment-bearing document, `print(parse(text)) === text` byte-for-byte; for a
non-canonical one, `print ∘ parse` is idempotent (comments **canonicalize in position but are
never dropped**). The `check=roundtrip` doc-fence checker and `tests/lang.comments.test.ts`
pin both halves. Comment text is captured **verbatim**: everything after the opening `#` to end
of line, the `#` excluded — so `# why` stores `" why"` and reprints as `# why`, unchanged.
**(b) Two kinds, by position, captured as lexer trivia.** The lexer (still line-oriented,
still fail-closed) now distinguishes:
- an **own-line** comment — a line whose first non-space char is `#` — captured with its
indentation and held as the `leading` trivia of the next code line, or, when it closes a block
(more deeply indented than the line that follows, or it ends the document), as the block's
`tail`;
- a **trailing-inline** comment — a `#…` after the statement/clause tokens on the same line —
captured as that line's `trailing` trivia.
A `#` inside a string stays a string character (already handled — the string scanner runs
first).
**(c) The trivia model is a cross-cutting SIDECAR on block nodes, never a field on semantic
nodes.** A `CommentLayer` (`{ lines?: LineComment[]; tail?: string[] }`) is an OPTIONAL field on
each block node (the six statements + the Module). A `LineComment` binds to a printed line by
its **ordinal in canonical print order**: `0` = the block's own header line; `1..N` = the
block's direct interior lines (USING/WHEN/BECAUSE, clauses, panes, RISK lines, module
directives); child blocks own their own trivia and consume no ordinal. This is the standard
leading-trivia + trailing-trivia attachment, expressed positionally so it covers **field-lines**
(WHEN/USING/BECAUSE, which are AST fields, not nodes) uniformly with clause-lines — without
adding a comment field to `Trigger`, `PriceExpr`, `Leg`, or any clause type. The semantic object
model is therefore **unchanged**, and a comment-free document carries **no sidecar at all**, so
every existing builder round-trip and deep-equality test is untouched.
The parser and printer walk the interior ordinals in the **same** order, so a canonical document
maps parse-ordinal to print-ordinal identically and reproduces every comment in place.
**(d) Blank lines are NOT semantic and are NOT preserved — the canonical printer owns vertical
spacing.** This follows the canonical-printer doctrine (ADR-0004): the printer emits exactly one
blank line between top-level statements and none between clause lines; stray author blank lines
are normalized away. Comments are preserved; blank lines are not an independent channel. A
canonical document (blanks only where the printer puts them, comments in canonical position)
round-trips byte-for-byte; a document with extra blanks or aligned comment padding is
non-canonical and canonicalizes idempotently — **comments move to canonical position, never
vanish**. Canonical comment position is: an own-line comment on its own line at the following
line's indentation; a trailing comment after the line content with a **single** space before `#`.
**(e) A comment-only document is a valid module.** A reply that parses to nothing but `#` lines
is an **empty module carrying its tail comments** — the authored "why" survives even when there
is nothing to execute. This is exactly the ADR-0031 (b) "pass with reasoning" shape.
## Consequences
- **ADR-0004 is honored, not contradicted.** The byte-stable round-trip now holds for _every_
document, comment-bearing or not. The prior "printer discards them" rationalization is retired
from the capability catalog (the `comments` capability moves runtime `none → implemented`,
"preserved byte-stably") and the public grammar-fences page (a comment-bearing
`check=roundtrip` fence now pins it against source bytes).
- **ADR-0013 (b)'s dependency is satisfied.** A `#` comment now rides _with_ the armed Plan
document byte-stably and folds into `sha256(print(module))` automatically — the WHY is welded
to the action and is tamper-evident once armed. ADR-0013's "forthcoming" forecast is fulfilled.
- **ADR-0031's honest dependency is closed.** The comment-rides-with-the-armed-document promise
that ADR-0031 deferred to "the byte-stable-comments workstream" is now delivered; the
journal-as-`#`-comment channel is no longer a documented gap.
- **The semantic model stays clean.** Because comments are a sidecar keyed by print-order
ordinal, no trigger/price/leg/clause type grew a comment field, and comment-free ASTs are
byte-identical to before — the 200-seed property test and every builder deep-equality check are
unperturbed.
- **Reasoning is preserved, not executed.** Comments remain non-behavioral by design; this ADR
changes only their _durability_ through the projection, which is the whole point — a strategy's
reasoning must survive capture and replay, or it was never really recorded.