UNPKG

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