Skip to content

Commit 64f402d

Browse files
reid-spencerclaude
andcommitted
Decline [2.9]: the opaque TypeScript AST handles are the boundary working
Raised off "low priority" earlier today so it could be CONSIDERED rather than deferred again. Considered; the answer is no, on the merits. The gap is real but has no consumer. index.d.ts is 1,169 hand-written lines and already rich for everything the facade returns; only the 19 branded node handles are structureless. riddl-vscode -- the TypeScript consumer -- calls parseToTokens 14x, parseString 11x, getTree, validateString, inspectRoot, getMessageFlow and getHandlerCompleteness, and touches a raw AST handle ZERO times. An editor extension wants highlighting, diagnostics and an outline, and the facade serves all three. The consumer that genuinely walks the AST is Synapify, which is Scala.js and has the real objects with their methods. What settled it was measuring the proposed implementation rather than arguing about it. Generating the .d.ts from JsonModel sounds economical -- JSON is already a complete, gated, zero-divergence surface -- but JSON serializes STATE and this AST's value is substantially BEHAVIOUR. AST.scala carries ~540 def/lazy val members that do not serialize: 182 `format`, 67 `kind`, the 34 WithX accessor traits, and derived answers like effectiveShape, Connector.isPersistent (which accepts both spellings), Statement.canFail, NumericLiteral.asLong and Function.input/output. JsonModel has zero references to refMap, symTab or usedBy, so no resolution output crosses at all. The disqualifying detail is smaller and worse: JSON preserves Include and BASTImport as CONTENT ENTRIES, so a TypeScript consumer walking `contents` sees the wrapper rather than through it. That is precisely the defect that had riddl-generator emit 582 files for reactive-bbq with no entity class among them, at exit 0. A JSON-derived TS AST would not be "the AST minus methods" -- it would be an AST that invites every consumer to reimplement include-transparency and alias resolution, which is the pair two separate consumers have already got wrong. It would also add a fifth reflective surface to keep in lockstep with parse, prettify, BAST and JSON, for a consumer population of zero, and nothing would fail when it drifted. Recorded in CLAUDE.md so the next session does not re-propose it. If it ever returns the trigger is DEMAND, not opacity: a TS consumer hits a wall the facade cannot answer, and the facade gains one accessor inside the conversion layer. BACKLOG is now down to a single open item, [0.2], which is blocked by design until 2.0.0 exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent f8fc465 commit 64f402d

2 files changed

Lines changed: 55 additions & 8 deletions

File tree

BACKLOG.md

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,14 +1722,40 @@ that needs a ruling before either can be fixed.
17221722
from A23's `isEffectStatement``set` is an effect but cannot fail — and value-level
17231723
failure points are deliberately counted separately rather than folded into the predicate.
17241724

1725-
- **[2.9]** TypeScript AST declarations for the JS side. **NO LONGER LOW PRIORITY**
1726-
(Reid, 2026-08-27) — the "low priority, file it, do not schedule it" framing came from
1727-
NOTEBOOK's Active Work Queue and no longer holds. The full AST hierarchy (Domain, Context,
1728-
Entity, …) is still OPAQUE to TypeScript; only the public `RiddlAPI` methods are declared,
1729-
so a JS consumer wanting the AST itself gets a branded `RootAST` and no structure. The old
1730-
justification was that consumers are *expected* to use the facade — which is an argument
1731-
about what we intended, not about what consumers need, and it is the same shape as
1732-
[1.20]: a surface we did not wire is not the same fact as a surface nobody wants.
1725+
- ~~**[2.9]** TypeScript AST declarations for the JS side.~~**DECLINED 2026-08-27**, on
1726+
the merits, after being raised off "low priority" the same day so it could be *considered*
1727+
rather than deferred again. Considered, and the answer is no.
1728+
**The opaque `*AST` handles are a BOUNDARY DOING ITS JOB, not a gap.** `index.d.ts` is 1,169
1729+
hand-written lines and already rich for everything the facade returns — `RiddlResult`,
1730+
`Token`, `ValidationResult`, `OutlineEntry`, `TreeNode`, `MessageFlowEdge`, the whole
1731+
`RiddlAPI`. Only the 19 branded node handles (`RootAST`, `EntityAST`, …) are structureless.
1732+
**There is no consumer for the missing part.** riddl-vscode — the TypeScript consumer —
1733+
calls `parseToTokens` (14×), `parseString` (11×), `getTree`, `validateString`, `inspectRoot`,
1734+
`getMessageFlow`, `getHandlerCompleteness`, and touches a raw AST handle **zero times**. An
1735+
editor extension wants highlighting, diagnostics and an outline; the facade serves all three.
1736+
The consumer that genuinely walks the AST is **Synapify, which is Scala.js and has the real
1737+
thing, methods included** (Reid: *"most users are going to use it from Scala.js where we have
1738+
mechanisms in place"*).
1739+
**The proposed implementation — generate the `.d.ts` from `JsonModel` — was measured and is
1740+
WORSE than it sounds, which is what settled it.** JSON serializes STATE; the AST's value is
1741+
substantially BEHAVIOUR. `AST.scala` carries **540 `def`/`lazy val` members**, none of which
1742+
serialize: 182 `format`, 67 `kind`, the **34 `WithX` accessor traits**, and derived answers
1743+
like `effectiveShape`, `Connector.isPersistent` (which accepts both spellings),
1744+
`Statement.canFail`, `NumericLiteral.asLong`, `DoStatement.text`, `Function.input`/`output`.
1745+
`JsonModel` also contains **zero** references to `refMap`, `symTab` or `usedBy`, so no
1746+
resolution output crosses at all.
1747+
**The disqualifying detail**: JSON preserves `Include`/`BASTImport` as CONTENT ENTRIES, so a
1748+
TypeScript consumer walking `contents` sees the WRAPPER rather than through it. That is
1749+
exactly the defect that had riddl-generator emit 582 files for reactive-bbq with no entity
1750+
class among them, at exit 0. A JSON-derived TS AST would not be "the AST minus methods" — it
1751+
would be an AST that INVITES every consumer to reimplement include-transparency and
1752+
alias-resolution, the two things two separate consumers have already got wrong (see
1753+
[1.20]'s two incidents).
1754+
**It would also add a FIFTH reflective surface** to keep in lockstep with parse, prettify,
1755+
BAST and JSON, for a consumer population of zero — and nothing would fail when it drifted.
1756+
**If this ever comes back, the trigger is DEMAND, not opacity**: a TS consumer hits a wall the
1757+
facade cannot answer, and the facade gains one accessor, inside the conversion layer. Do not
1758+
export the AST.
17331759

17341760
- ~~**[2.10]** Repo housekeeping — three items.~~**ALL THREE DONE 2026-08-27**, on
17351761
Reid's explicit go-ahead, which was the only thing the entry had been waiting on.

CLAUDE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2493,6 +2493,27 @@ validation — resolution and type-checking — in `checkStatementScopes`.
24932493
enforces it.
24942494
- **npm prerelease publishing** — sbt-dynver versions like
24952495
`1.2.3-1-hash` are prerelease per npm semver; pass `--tag dev`.
2496+
- **The opaque `*AST` handles in `index.d.ts` are DELIBERATE. Do not "fix" them by exporting
2497+
the AST to TypeScript** (considered and DECLINED 2026-08-27, BACKLOG [2.9]). `parseString`
2498+
hands JS a branded handle (`RootAST`, `EntityAST`, …) that can only be passed back in;
2499+
structure is served through flattened projections (`inspectRoot`, `getOutline`, `getTree`).
2500+
Three reasons, in order of weight:
2501+
1. **JSON serializes STATE; the AST's value is largely BEHAVIOUR.** `AST.scala` carries ~540
2502+
`def`/`lazy val` members that do not serialize — 182 `format`, 67 `kind`, the 34 `WithX`
2503+
accessor traits, and derived answers like `effectiveShape`, `Connector.isPersistent`,
2504+
`Statement.canFail`, `Function.input`/`output`. `JsonModel` has ZERO references to
2505+
`refMap`/`symTab`/`usedBy`, so no resolution output crosses either.
2506+
2. **JSON keeps `Include`/`BASTImport` as content entries**, so a consumer walking `contents`
2507+
sees the WRAPPER rather than through it — the exact defect that had riddl-generator emit
2508+
582 files with no entity class, at exit 0. A JSON-derived TS AST would invite every
2509+
consumer to reimplement include-transparency and alias-resolution.
2510+
3. **It would be a FIFTH reflective surface** to keep in lockstep with parse/prettify/BAST/
2511+
JSON, and nothing would fail when it drifted.
2512+
**The real consumers agree**: riddl-vscode touches a raw AST handle zero times (all facade —
2513+
`parseToTokens`, `parseString`, `getTree`, `validateString`, …), and the consumer that truly
2514+
walks the AST is Synapify, which is **Scala.js and has the real objects, methods included**.
2515+
If this returns, the trigger is a TS consumer hitting a wall the facade cannot answer — add
2516+
one accessor inside the conversion layer, never the AST.
24962517
- **NEVER `@JSExport` an overridden `toString`.** Interpolation compiles to JS
24972518
`+`, so `s"…$loc…"` throws `TypeError: Cannot convert object to primitive
24982519
value` and takes down the whole validation run on JS while the JVM passes.

0 commit comments

Comments
 (0)