The single home for open work: tasks, features, bugs, and design questions not
yet decided. If it is not here, it is not tracked. Items carry the
verification already done, with file:line where a claim was checked against
the code, so the next session does not re-derive it.
Completed items leave this file: what they taught goes to NOTEBOOK.md, what
is durably true goes to CLAUDE.md. Incoming task/*.md files are INPUTS —
triage them with /ossuminc-skills:check-tasks and file what survives here.
Lines migrated from NOTEBOOK.md predate the 80-column rule for this file; clean them as items are touched rather than in one sweep.
Large items get their own plan (~/.claude/plans/) before implementation; the
plan is discarded once built.
ITEM IDs — STABLE, never reassigned (corrected 2026-08-17). Every open item
carries a [section.n] identifier so it can be named in conversation without
quoting its title.
An ID belongs to an item for life. When an item closes its number RETIRES with it and is never reused, so gaps in the sequence are expected and correct.
They were positional and renumbered on every close for one day, which was a mistake: it silently moved items under the person reading them, and it did — the lookup-value item was 2.5 in one message and read as 2.6 in the next, in a conversation where both of us were naming items by number. A handle that changes is worse than no handle, because it fails exactly when it is being relied on.
Things deliberately deferred to the release itself, not to be done piecemeal.
-
[0.1] Run one— DONE 2026-08-18 (scalafmtpass.a6e740204), deferred since 2026-08-04 and done once as planned. 171 files. It is overwhelmingly COMMENT REFLOW: of 165 reformatted.scalafiles, 164 differ only in whitespace and docstring rewrapping. It required a scalafmt version bump, and that is the durable finding (07b6e9c9e): the pinned 3.8.3 emits Scala 3 that does not compile — it indentedend checkInvariantScopeto align with theend matchabove it, givingduplicate end markeratValidationPass.scala:2069, and then rejected the hand-fixed (compiling) form, so no state satisfied both formatter and compiler. 3.9.4 handles Scala 3endmarkers correctly. Noteversion =in.scalafmt.confis SCALAFMT's version, not Scala's — the digits coincide confusingly; Scala remains 3.9.0-RC6. Known artifact, do not "fix" it: scalafmt escapes a|landing at the start of a wrapped scaladoc line, so two comments read\|. Removing the backslash makesscalafmtCheckred because the formatter re-adds it. -
[0.2] Upgrade riddl-vscode. Reid, 2026-08-06 — deferred here deliberately, not overlooked. It consumes
@ossuminc/riddl-libvia npm, which carries only PUBLISHED releases, so it cannot take a staged build at all and chasing it between RCs means cutting an RC for its benefit. It is on2.0.0-rc.9(package.json:128); bring it to 2.0.0 when 2.0.0 exists. -
[0.3] Regenerate every checked-in— HANDED OFF 2026-08-20 to.bast.../riddl-models/task/2026-08-20-regenerate-checked-in-bast-after-2.0.0.md, to be run shortly after 2.0.0 ships. It waited becauseFORMAT_REVISIONkept moving — 6 → 7 → 8 → … → 19 — and regenerating against a number about to change again buys nothing. The files live in riddl-models, so tracking it here as well was duplicate bookkeeping. riddl's OWN fixture is not part of that hand-off:language/input/import/ NotImplemented.bastis regenerated at every bump as a matter of course, orIncludeAndImportTestreddens. Regenerate it FROM ITS OWN DIRECTORY, or the.bastembeds a different source path and the diff stops being a one-field revision bump — done right it is 93 bytes andcmpdiffers at byte 12 and nowhere else. (sbt cleandeletes the stage, so regenerate BEFORE certifying, not after.) -
[0.4] Update the Computational Model with everything— DONE 2026-08-18 (release/2changed.ossuminc70d2cee+c7498d1). Every item the entry listed now has a home in its own section rather than a changelog: entity intentions as GRAMMAR and the four event-sourcing rules (§4), the unified processor model and the ruled definition of a streamlet (§8), the three integer ranges (§11),Riddl.Envelope(§16), A55/A56/A57 (§18). A38 and [2.6] were recorded when they landed. The sweep's most valuable find was a STALE CLAIM, not a missing one: §9 saidrequires/returns"should instead take a type reference... otherwise the saga cannot be initiated with a message". A9 had shipped. A generator author reading that would have built for a language that no longer exists — which is precisely the failure this item exists to prevent, and an argument for sweeping the CM for WRONG statements, not only absent ones.../RIDDL-Tools-To-Do-List.mdPart A checked too: 77 items, of which two were undecided — A13 (message and type versioning) and A58 (parser continues after errors). RULED 2026-08-18 by Reid: both DEFERRED past 2.0 (ossuminccff53d9), so Part A is now decided end to end with nothing ambiguous left in it. Deferred rather than declined — neither was rejected on merit. Per the no-2.1-pile rule they stay out of this file until 2.0 ships. Superseded: -
[0.4] history: Update../RIDDL-Computational-Model.mdwith everythingrelease/2changed. Reid, 2026-08-06. That document is the authority for any lowering decision — what a conforming generator MUST preserve versus may freely choose — so a language change that does not reach it leaves generator authors working from a stale contract. This branch has changed a lot of what it describes: entity intentions and the four event-sourcing rules, the unified processor model, implicit invariant scope,requires/returnsin contents,Riddl.Envelope+option message_envelope, A56 (tell p) and A57 (on other as x). Work fromgit log 2.0.0-rc.1..HEADrather than memory. Also add (2026-08-15, numeric-literals plan): the three integer types' ranges —Integersigned,Wholenon-negative,Naturalpositive (Reid, 2026-08-14) — were undefined anywhere until this work, and aconstantliteral outside its declared type's range is now a validation Error (checkNumericLiteralConformance,passes/.../ValidationPass.scala). The "vocabulary of information shapes" passage that already lists the predefined types is the right place for the range table; see the task dropped in../ossum.tech/task/2026-08-15-integer-type-ranges.mdfor the worked examples and the two related grammar widenings (Constantaccepting a bare literal,Comparandaccepting one). Also add (2026-08-15, A20 typed-holes plan):prompt("...") as <type>ascribes a type to an AI-computed value — the type is known and checkable, the computation is prose an AI fills in at generation time. Legal in every position an ordinary value can occupy (let,constant, constructor argument,set,whencondition), with either a predefined type or a declared alias. The ascription RESTATES the position's already-known type, it never OVERRIDES it — a contradicting ascription is a validation Error, and aconstantwith apromptvalue needs no ascription at all since the constant already declares the type. See the task dropped in../ossum.tech/task/2026-08-15-a20-typed-holes.mdfor the full writeup and worked examples, and its caution thatCurrencycannot be used bare in an example (it requires acountryargument). Also add (2026-08-15, not-bang-synonymy plan):notand!are synonymous EVERYWHERE, as the inverse of a boolean expression — both spellings build the identicalNotExpressionAST node, so a generator lowering a boolean expression needs to know there is only ever one node to handle, never a spelling to branch on.!is not related to the!in!=(an ordinary comparison operator). See the task dropped in../ossum.tech/task/2026-08-15-not-bang-synonymy.mdfor the worked examples and the!=caution. -
[0.5] Update the ossum.tech documentation site.— DROPPED AS A TASK 2026-08-18, which is what this item always was: ossum.tech is a separate repo, so the work is not done here. File:../ossum.tech/task/2026-08-18-riddl-2.0-language-changes.md. It is organised BY CONSTRUCT, not by commit — 704 commits is not a usable index — and every syntax claim cites a line inebnf-grammar.ebnfso they check the grammar rather than trusting the task file. It carries Reid's 2026-08-06 instruction to lighten the treatment of implicit/optional forms, the deprecated spellings that should stop being taught, and the seven semantic rules readers get wrong (inlet/outlet direction and own-port delivery especially). Not diffed against ossum.tech's current content — with 704 commits that was the wrong shape of work — so it reads as "what 2.0 contains", for them to skip what they have covered. -
[0.4] Update the Computational Model with everything— DONE 2026-08-18 (release/2changed.ossuminc70d2cee+c7498d1). Every item the entry listed now has a home in its own section rather than a changelog: entity intentions as GRAMMAR and the four event-sourcing rules (§4), the unified processor model and the ruled definition of a streamlet (§8), the three integer ranges (§11),Riddl.Envelope(§16), A55/A56/A57 (§18). A38 and [2.6] were recorded when they landed. The sweep's most valuable find was a STALE CLAIM, not a missing one: §9 saidrequires/returns"should instead take a type reference... otherwise the saga cannot be initiated with a message". A9 had shipped. A generator author reading that would have built for a language that no longer exists — which is precisely the failure this item exists to prevent, and an argument for sweeping the CM for WRONG statements, not only absent ones.../RIDDL-Tools-To-Do-List.mdPart A checked too: 77 items, of which two were undecided — A13 (message and type versioning) and A58 (parser continues after errors). RULED 2026-08-18 by Reid: both DEFERRED past 2.0 (ossuminccff53d9), so Part A is now decided end to end with nothing ambiguous left in it. Deferred rather than declined — neither was rejected on merit. Per the no-2.1-pile rule they stay out of this file until 2.0 ships. Superseded: -
[0.4] history: Update../RIDDL-Computational-Model.mdwith everythingrelease/2changed. Reid, 2026-08-06. That document is the authority for any lowering decision — what a conforming generator MUST preserve versus may freely choose — so a language change that does not reach it leaves generator authors working from a stale contract. This branch has changed a lot of what it describes: entity intentions and the four event-sourcing rules, the unified processor model, implicit invariant scope,requires/returnsin contents,Riddl.Envelope+option message_envelope, A56 (tell p) and A57 (on other as x). Work fromgit log 2.0.0-rc.1..HEADrather than memory. Also add (2026-08-15, numeric-literals plan): the three integer types' ranges —Integersigned,Wholenon-negative,Naturalpositive (Reid, 2026-08-14) — were undefined anywhere until this work, and aconstantliteral outside its declared type's range is now a validation Error (checkNumericLiteralConformance,passes/.../ValidationPass.scala). The "vocabulary of information shapes" passage that already lists the predefined types is the right place for the range table; see the task dropped in../ossum.tech/task/2026-08-15-integer-type-ranges.mdfor the worked examples and the two related grammar widenings (Constantaccepting a bare literal,Comparandaccepting one). Also add (2026-08-15, A20 typed-holes plan):prompt("...") as <type>ascribes a type to an AI-computed value — the type is known and checkable, the computation is prose an AI fills in at generation time. Legal in every position an ordinary value can occupy (let,constant, constructor argument,set,whencondition), with either a predefined type or a declared alias. The ascription RESTATES the position's already-known type, it never OVERRIDES it — a contradicting ascription is a validation Error, and aconstantwith apromptvalue needs no ascription at all since the constant already declares the type. See the task dropped in../ossum.tech/task/2026-08-15-a20-typed-holes.mdfor the full writeup and worked examples, and its caution thatCurrencycannot be used bare in an example (it requires acountryargument). Also add (2026-08-15, not-bang-synonymy plan):notand!are synonymous EVERYWHERE, as the inverse of a boolean expression — both spellings build the identicalNotExpressionAST node, so a generator lowering a boolean expression needs to know there is only ever one node to handle, never a spelling to branch on.!is not related to the!in!=(an ordinary comparison operator). See the task dropped in../ossum.tech/task/2026-08-15-not-bang-synonymy.mdfor the worked examples and the!=caution. -
[0.5] Update the ossum.tech documentation site— DROPPED 2026-08-19. The task file is in../ossum.tech/task/2026-08-18-riddl-2.0-language-changes.mdand is that repo's to execute; nothing here is waiting on it. (NOTEBOOK briefly called this "closed" while the task sat undone — dropped is not done, and only the receiving repo can close it.) Original ask, for the record: the same syntax changes, plus a LIGHTER treatment of the implied syntax. Reid, 2026-08-06 — the reference currently spells out more of the implicit forms than a reader needs, and the balance should shift toward what someone actually writes. Same source of truth: the commits on this branch, not recollection. (ossum.tech is a separate repo; this is a task DROP, not work done here.) -
[0.6] Bump Scala 3.9.0-RC6 -> 3.9.0 when the final ships.— DONE 2026-08-27. 3.9.0 verified on Maven Central for all THREE toolchains before touching anything — the JVM compiler/library,scala3-library_sjs1_3underorg.scala-lang, and Scala Native'sscala3lib_native0.5_3at the COMPOUND version3.9.0+0.5.12. Include a control when probing Maven Central: my first JS/Native probes 404'd, and so did the RC6 controls, which proved the coordinates were wrong rather than the artifacts missing. 36 occurrences across 9 files, of which 17 are build-output path segments inscala.yml,release.yml,coverage.yml,.sonarcloud.propertiesandDockerfile. Compiles clean on all three platforms with zero source changes — RC4 had needed one (aSeq/Listimplicit), 3.9.0 final needed none. Original entry: deliberately: riddl 2.0 rides Scala Next ahead of LTS, and the RC line is a waypoint, not the destination. The bump is NOT one line -- the full Scala version is a path segment in the build outputs (target/out/<platform>/scala-<fullVersion>/...), so it is hardcoded in CI and packaging and every site must move together. Verified 2026-08-26 doing RC4 -> RC6: 32 sites across 8 files --project/Dependencies.scala:18(the pin,val scala),.github/workflows/scala.yml(7:RIDDLC_PATHtwice plus 5 artifact upload paths),release.yml(2: native cp and JVM stage zip),coverage.yml(1),.sonarcloud.properties(6 scoverage paths),Dockerfile(1 stage copy), plusCLAUDE.mdand one liveBACKLOG.mdline. The grep that finds them all isgrep -rn "3\.9\.0-RC6" . --exclude-dir=.git --exclude-dir=target-- and note a narrower grep restricted to.github/missed nothing, but one that omitted.github/missed 11 of the 32. Leave dated historical records (NOTEBOOK's adoption entry,docs/superpowers/plans/) at their original version; rewriting those falsifies history. Evidence RC6 itself is clean (2026-08-26): 0 compile errors and 0 compiler warnings on all three platforms, and tests 3068/1005/3026 -- byte-identical to the rc.26 certification on RC4, which is the expected result since no test source changed.
EXECUTION ORDER (set 2026-08-15, ordered by dependency, not by severity). All of § 1 is now the active to-do list. The detailed entries stay in their original positions below — they are cross-referenced from code comments and other repos' task files, so they were NOT physically reshuffled; this index carries the order.
| # | Item | Blocked by | Why here |
|---|---|---|---|
OnInit/OnTerm params |
— | DONE c530337d9 — defaulted IN PLACE; the prescribed "move it trailing" was unnecessary and would itself have broken all five positional call sites. |
|
| — | DONE d46646e10 — one of them was the sole holder of a live design rationale, graduated to CLAUDE.md before deletion. |
||
valueTypeExpr predefined types and PromptValue.typeEx |
— | DONE 141486ed4 — merged as planned; one function, one corpus A/B. |
|
checkPromptAscription at the remaining A20 positions |
— | DONE 0fd7bb54e — all seven wired; the "decide per position" premise dissolved once every position turned out to already hold its expected type. |
|
| 5 | Close the JVM/Native test gap | — | MOSTLY DONE 2026-08-15 (682e835bc, 54ff5fe73, 6cf60baf2, 0919f191e). Gap 560 → 275, Native 1840 → 2393. What remains is ~191 in commands, of which 189 are the corpus gate — blocked on a decision (isolating the shared corpus per platform), not on effort. |
| 6 | ! into Punctuation.tokenPunctuation |
— | Isolated, tooling-facing (idea-plugin, synapify). Fits any gap. |
| 7 | JSON strict-key rejection | — | Needs a design decision before code. Independent. |
| 8 | Three CM amendments owed by the identity design | — | Documents work already SHIPPED, so it is overdue debt under the definition of done. Fold into § 0's CM sweep — do that once, not twice. |
| 9 | Survey the CM/A items for future self fields |
— | Cheap, and it BOUNDS 10 by deciding what is in scope. |
| 10 | Clusterability: clustered, self.isClustered |
9 | Defines the vocabulary self.isClustered was deliberately kept out of the identity spec to avoid forward-referencing. Wants its own plan. |
| 11 | Cross-context tell isolation seam |
5 | Largest item and the biggest corpus-migration risk. Needs a counting mode built and run under real resolution before the Error ships. Wants its own plan. |
| — | Part 1 DONE faf7551c0 — and it was NOT false-positive-only as filed, it was a missed Error. Part 2 (emittedMessageTypes) split out and left last: genuinely advisory, genuinely a restructuring. |
| 13 | [1.10] Prefix truthfulness for fields/aliases | — | A DECISION, not effort. Shipped for TypeRef 2026-08-24; extending to AliasedTypeExpression roughly triples an already 1,032-site migration, so measure first. |
Three real dependency edges, not twelve: 4 ← 3, 10 ← 9, 11 ← 5. Everything else is independent and may be reordered by appetite. Items 10 and 11 each want an approved plan before implementation, per the standing rule.
-
— STALE, CLOSED and REMOVED 2026-08-15. Numeric literals shipped (Valuehas no NUMERIC LITERAL, soinitiate entity Order(1)does not parse.6cfeceb2f) and closed it. Verified against the staged riddlc at2.0.0-rc.14-121-fe768026: the entry's own example —initiate entity Order(1)againston init(total: Integer)— validates with zero errors. Its two supporting claims are dead too:count > 5parses (A28's ban was reversed) and a constant may hold a bare number. Body deleted; nothing cited it (the twoBACKLOG-referencing code comments point at theFinderentry and at § 2'sFORMAT_REVISIONreservation, neither of them this). -
NEW CHECK — the unused— BUILT, CLOSED and REMOVED 2026-08-15.initiateid Warning.UnusedInitiateIdTestis green with 7 cases covering the escape-route analysis the entry called "the real work" — terminated, kept in state, passed in a message, and used only inside a nestedwhenbody. Verified by running it, not by reading the file name. Its durable half was GRADUATED to CLAUDE.md (the processor-instance-identity section): why this is a Warning and not an Error, and why it is ungated. That reasoning was recorded "so it is not re-litigated", which is exactly the kind of thing that must not die with the backlog entry. -
andvalueTypeExprdoes not surface alet's declared PREDEFINED type— BOTH DONE 2026-08-15,checkTerminateis SILENT for an ascribedprompt(…)target141486ed4. Merged deliberately: one function, one corpus A/B.letDeclaredPredefinedTypeanswers the first from the samePredefTypes.typeExpressionForset ResolutionPass andcheckStatementScopesalready share, ordered BEFORE inference because a declared type outranks an inferred one; aPromptValuearm answers the second by returningtypeEx. The UNASCRIBED hole still yieldsNone, by design — A20's conservative rule is untouched, and the silence belongs to the form that says nothing rather than to everyprompt(...).TerminateTargetTestnow pins all four corners. Two things worth keeping. It made an existing comment honest:ResolutionPass:435claimed "ValidationPass.letType/checkStatementScopesspecial-case the same set directly" —checkStatementScopesdid,letTypedid not, which is the defer-to-something-that-does-not-do-it shape. And the corpus A/B came back identical (187/2, 188 clean) while containing zerolet x: <predefined>ascriptions and zero ascribed prompts — all 834 of itslet x:use named aliases — so that green is evidence the ALIAS path still works, and no evidence at all about the shapes actually changed. -
— DONE 2026-08-16,JsonModel's reader never rejects unknown/misspelled keys.927898a97, as a WARNING per Reid's ruling. Validated on the rawujsontree BEFORE any reader runs, which is the only place both reader layers are visible at once: this entry's proposed consumed-keys tracker would have covered the 6 hand-written readers and never the 59 macro-derived ones, where upickle drops unknown keys internally with no hook of ours. Two guards, and the second is not redundant.JsonUnknownKeyVocabularyTestre-derives the vocabulary fromJsonModel.scala's own source, so the list cannot drift from the readers.JsonKeyFalsePositiveTestruns every corpus model through the writer and back. It caught two defects the source-derived guard could not see, either of which would have fired on EVERY correct document: the writer emits sigil keys ($kind,$at) that no DTO field or reader lookup spells (188/188 models warned), and aSchema'sdata/linksare maps keyed by the MODELLER's identifiers (184/188). The second is the lesson. I had asserted in a comment that no object in the schema is keyed by data, having grepped the READERS for key iteration and found none — nothing appears there because upickle's derivedMap[String, _]reader does the iterating. Only running the writer's output back through the check exposed it. A diagnostic that fires on correct input is worse than none, so both checks are permanent tests rather than things verified once. Corpus: 0 unrecognized keys across 188 models. -
[1.1] STRATEGIC: should the JSON input surface exist at all?— RULED 2026-08-17 by Reid: option C — keep it for hosted models, point self-hosted users at GBNF/XGrammar, and DOCUMENT the split. Done in61d028e4e: the rationale now sits at the top ofJsonModel, where anyone extending 131 DTOs will read it — the deciding fact, the price, and what JSON does not buy. The documentation WAS the deliverable: the surface was drifting without a stated justification, which is what made the question live. Original analysis kept below. Not urgent — the warning above protects today's users either way — but it outlives that fix and should be decided rather than drift. The argument for retiring it. JSON guarantees SHAPE only. Our own path runs the identical validation passes afterwards, so "correct-by-construction" covers structure and never meaning, and an AI must learn RIDDL's semantics regardless. The price is a second serialization surface — 131 DTOs tracking the AST — which is what produced the unknown-key defect class in the first place. What the alternatives actually are (researched 2026-08-16):- Hosted frontier models (Claude, GPT, Gemini) expose JSON Schema and tool schemas only. There is no logit-level hook for an arbitrary CFG, so for them JSON is the ONLY constrained channel. This is the fact that decides it.
- Self-hosted: GBNF already exists here (
riddl-grammar.gbnf, generated from the EBNF, CI-checked against drift), and XGrammar is the modern retry if GBNF's collapse on our 263-rule grammar was a performance problem. Outlines / lm-format-enforcer are equivalents. - Generate RIDDL then repair, using riddlc's diagnostics and
provideTips. No constrained decoding, but far more viable than when JSON was chosen. Options: keep JSON; retire it in favour of repair loops; or keep it only for hosted models and point self-hosted users at GBNF/XGrammar. AMENDED 2026-08-20 — the second half of that ruling is void. The bundledriddl-grammar.gbnf, its generator and its validator are DELETED: llama.cpp could not run the full RIDDL grammar at a usable speed (an 8-token constrained generation did not finish in 7 minutes, against seconds unconstrained) and nothing consumed it. The DECISION to keep JSON for hosted models is unchanged and is why this entry stayed struck; what changed is where self-hosted users are pointed — llama.cpp derives a small GBNF from a JSON Schema on the fly, so that route needs no file from this repo.JsonModel.scala's docstring carries the correction. Left in place rather than rewritten, since the ruling itself was real.
-
— DONE 2026-08-15,Punctuation.tokenPunctuationdoes not include!.66388821c. The damage was worse than filed: not!isValidbut!isValid then do "no" end— the whole remainder of the input — became oneToken.Other, so an editor lost highlighting for everything after the!.!is guarded by a negative lookahead rather than listed in theStringIn, mirroring the parser's"!" ~~ !"=":!=is a comparison OPERATOR and this set holds no comparison operators at all. No EBNF change —not_expressionalready described it; the tokenizer is a highlighting surface, which is why the parser accepted!all along while the tokenizer did not. -
— DONE 2026-08-15,OnInitializationClause.parameters/OnTerminationClause.parametershave no default.c530337d9. Defaulted IN PLACE. This entry's prescribed fix would have caused a second break: it said to move the field aftercontents/metadatabecause@JSExportTopLevelwants defaulted params trailing, but that constraint bites only while the field has NO default — once it has one,locis the sole undefaulted parameter and the rule is already satisfied where it stands. Moving it would have broken all five positional construction sites (HandlerParserx2,BASTReader,JsonAstBuilderx2). Verified on cJS and cNative, which is the only place that hazard is visible. -
[1.2]— DONE 2026-08-17. Closed by looking the answer up inemittedMessageTypesis still narrow.ValidationOutput.deliverableTypes([4.3]) rather than re-resolving. The entry's size estimate was wrong: it judged the fix to be "walking the root container-by-container the waycheckStatementScopesalready does", andcheckStatementScopesalready did that walk — the obstacle had dissolved without anyone noticing. Second time today ([2.6] was the first). Superseded: The second of the two gaps, and the one that really is a restructuring rather than a fix round. It is a whole-rootFindersweep with no per-clause scope, feeding A70's correlation-fold advisory (ValidationPass.scala:158documents the flatness). Still false-POSITIVE-only, still zero corpus impact, so it stays deliberately last — but it is now filed on its own rather than bundled, because its sibling turned out to be a missed Error and bundling the two hid that. -
FLAKY CI GATE:— DONE 2026-08-14,PerformanceBenchmarkTest100x cache-speedup assertion32340312e. Replaced with a monotonic check (cached strictly faster) plus a 5x floor; the precise multiplier stays asinfooutput. Healthy runs measure 600x–1600x, so the floor has real headroom while a genuine regression collapses toward 1x and still fails hard. Proven still able to detect one by removingFinder.findByType's cache-hit branch and watching both assertions go red at 1.17x and 0.9x. A SECOND identically-shaped 10x assertion existed in the same file and was fixed with it. The lesson worth keeping:BASTPerformanceBenchmark.scalaALREADY carried this exact fix from an earlier round, comment and all — the defect class was diagnosed once and its sibling missed, so when fixing a test-shape defect, grep for the shape rather than fixing the instance. -
[1.3] Close the JVM/Native test gap.— CLOSED 2026-08-18 by Reid: "find the easy/obvious ones... then declare victory — close enough: we are in the thousands of common tests." Final: JVM 2747 / Native 2708, gap −39, from −729 originally.commands,riddlLibandriddlcare at parity. The last big win was a BUILD omission, not a platform constraint: riddlLib was the one cross-platform module with noscala-jvm-nativetest wiring, so every suite it had was JVM-only by accident. Two lines ofbuild.sbt. What remains is deliberately JVM-bound and should stay: benchmarks (timing),Tar/FigmaClient(no Native impl),LoaderTest(namesJVMPlatformContext),LoadingURLTests(real behavioural difference in URL error handling — it COMPILES on Native and fails, which is worth knowing), and two suites still on commons-io. Do not reopen this to chase the last 39. Superseded detail: -
[1.3] history: Close the JVM/Native test gap: 729 cases run on JVM that never run on Native. Reid, 2026-08-14, from the rc.14 certification. "Testing on the JVM does not guarantee correctness on Native, and I can't believe there are ~800 test cases that genuinely cannot run there."Measured, not estimated — rc.14 certification from clean under a throwaway
--sbt-cache, module order taken from thetJVM/tNativealiases (build.sbt:538,:549):module JVM Native gap commands 245 47 −198 language 668 512 −156 passes 1196 1040 −156 utils 146 108 −38 riddlLib 122 111 −11 testkit 2 1 −1 riddlc 21 21 0 total 2400 1840 −560 PROGRESS 2026-08-15: the gap is HALVED, −560 → −275, and what remains is essentially ONE suite that needs a decision. Commits
682e835bc(commands + wiring),54ff5fe73(language),6cf60baf2(passes),0919f191e(utils). Measured per module, JVM and Native, after:module JVM Native gap commands 243 (+2 red) 52 −191 passes 1397 1355 −42 language 726 709 −17 utils 148 134 −14 riddlLib 134 124 −10 testkit 2 1 −1 riddlc 18 18 0 total 2668 2393 −275 Native went 1840 → 2393 (+553). The old "expected floor 1840" note below is superseded; the floor may only be RAISED by a certified tri-platform run, so treat 2393 as the number to certify against, not as an already-raised floor.
RE-MEASURED 2026-08-17 at the
2.0.0-rc.15certification (clean, tri-platform,testOnly *), and the picture is UNCHANGED — which is the useful finding:module JVM Native gap commands 245 52 −193 passes 1446 1403 −43 language 731 714 −17 utils 148 134 −14 riddlLib 144 131 −13 testkit 2 1 −1 riddlc 21 21 0 total 2737 2456 −281 The gap moved −275 → −281 only because new tests landed JVM-side; no module regressed and none improved. So the 2026-08-15 conclusion holds exactly: subtract
commandsand the repo is within ~88 cases of parity, andcommandsis blocked on a decision rather than on effort. Re-measuring cost one grep of a certification log that had to be produced anyway — worth doing at every RC, so the entry can never drift as far as the ~800 estimate it opened with.THE
commandsBLOCKER IS NOW IDENTIFIED PRECISELY (2026-08-18), by experiment rather than by reading. Three suites there —RunCommandOnExamplesTestand its two subclasses — cannot run on Native, and the reason is NOT what the entry assumed. Moving them toscala-jvm-nativeand compiling reveals a chain:org.apache.commons.io(a JVM-only Java library) blocks COMPILATION. Removable: all three uses (iterateFiles,iterateFilesAndDirs,forceDeleteOnExit) map cleanly ontojava.nio.file, which Native's javalib does support. Verified — the rewrite compiles for Native and stays green on JVM at 245.java.net.URL.getFileis???in Native's javalib — compiles, then throwsscala.NotImplementedErrorat run time. Also removable: it is used only to derive a filename, recoverable fromtoExternalForm.java.net.URL.openStreamis???too — and this one is the wall. These suites DOWNLOAD the riddl-examples archive over HTTP inbeforeAll. That is network I/O; no string trick substitutes for it.
So the "shared-corpus decision" this entry gestured at is a concrete question: should these suites keep downloading a pinned archive, or read the sibling
../riddl-examplescheckout the wayRiddlModelsRoundTripTestalready reads../riddl-models? Downloading is hermetic and Native-impossible; reading a checkout is Native-possible and makes the tests depend on the working copy. That is Reid's call, and it is the whole of the remaining gap — roughly 193 of the 281 cases.RESOLVED 2026-08-18 for
commands— the gap there is ZERO (1c318b844). Reid ruled the design: read the already-checked-out../riddl-modelsand../riddl-examples, SKIP when absent rather than fail, and let CI clone them so the runner has them locally. That removes the download rather than repairing it, which is what three earlier attempts had been trying to do.module JVM Native gap commands 245 245 0 passes 1456 1413 −43 language 731 714 −17 utils 148 134 −14 riddlLib 144 131 −13 testkit 2 1 −1 riddlc 21 21 0 total 2745 2659 −86 −281 → −86, and
RiddlModelsRoundTripTest's 189 cases — the single largest block — now run on both platforms. What remains is the ~86 spread thinly across five modules, with no single blocker. The next RC certification should RAISE the Native floor substantially (2456 → ~2659); these numbers are fromtestOnly *, which ignores incremental state, but the floor may only be raised by a certified clean tri-platform run.The skip is LOUD by design. CLAUDE.md records that a cancelled corpus suite "reads as green in a summary scan"; the message names the absolute path searched and the branch expected. The CI step carries the same warning, because the failure mode of this design is SILENCE — a clone that fails leaves the suites skipping and the log green.
Still open and worth knowing, since
loadBytesis now public API: the Native fetch returns a SHORT body for a binary URL. Redirect-following did not fix it. Leading untested theory: sttp's Native backend truncates at the first NUL byte, treating the body as a C string — a ZIP's header contains NULs within the first few bytes, which fits "too short to be Zip" exactly and explains why the redirect fix changed nothing. Test it by fetching a known-length binary and asserting the byte count. Nothing in the build depends on it now.Superseded — steps 1-3 were fixed in
e4f91525c, on Reid's steer to use sttp (already a dependency) or write the code, and with his permission to extendPlatformContextprovided all three platforms implement it.PlatformContext.loadBytes(url): Future[Array[Byte]]is new — the binary counterpart ofload, which returns aStringand therefore corrupts a ZIP. JVM usesopenStream; Native uses sttp (the same stackloadalready uses for text); Scala.js usesdom.fetchwitharrayBuffer().PathUtils.copyURLToDirno longer touches either stub.- commons-io is gone from
RunCommandOnExamplesTest.
A FOURTH link is what remains, and it was invisible until the first three were cleared: the download arrives SHORT on Native, so unzip reports
java.util.zip.ZipException: too short to be Zip. Note what that error proves —java.util.zipWORKS on Native, or it could not produce a real ZipException. So this is one bug in the Native fetch, not another missing platform capability. Verified with curl that the archive URL 302-redirects (0 bytes without-L, 207,955 with), and explicit redirect-following was added to the Native fetch without resolving it — so the cause is elsewhere in that fetch, and that is exactly where the next attempt starts.The three test files stay in
scalajvmuntil item 4 is fixed: moving them leaves two suites ABORTING on Native, which is worse than not running them. Moving them back is a one-linegit mveach once the fetch is right.Nearly all of the remaining gap is
commands, and nearly all of THAT isRiddlModelsRoundTripTest's 189 cases — blocked on the shared-corpus decision recorded under item 1 below, not on effort. Subtract it and the whole repo is within ~86 cases of parity.The entry's per-module predictions were wrong in both directions, which is worth remembering before estimating this kind of work again.
commandswas called "probably the cheapest win" and delivered +5.passeswas to be "audited last" because its residue was "likely genuinely JVM-bound", and 26 of its 32 files moved with no source change at all. The shape that predicts portability is not the module's general hygiene; it is whether a file names a JVM-only TYPE, which no import scan reveals.PROGRESS 2026-08-14 (
546f2f834):languageclosed from −325 to −156. The 13 abstract parser suites now run on Native — their concrete runners moved fromsrc/test/scalajvmtosrc/test/scala-jvm-native(the root already wired byjvmNativeSrc("language")), renamedJVMNativeTestssince they serve both platforms. Native 343 → 512, exactly the predicted +169, nothing excluded: those suites build every input fromRiddlParserInputand string literals, with nojava.io/scala.io.Source/regex-.r, so no Native hazard was present. The Native floor is therefore expected to be 1840 at the next full certification — it is NOT raised here, because a floor may only be raised by a certified tri-platform run, and this number is arithmetic plus an isolatedlanguageNative/testOnly *.Remaining: 510 of the 560 sit in three modules. In this order:
commands(−198) — PARTLY DONE 2026-08-15,682e835bc, and its central question is ANSWERED: NO. The module gained thejvmNativeSrc("commands")wiring it lacked, and three suites now run on both platforms (RegressionTests,BastGenCommandTest,UnbastifyCommandTest). Native 47 → 52; JVM unchanged at 245, so the gap is now −193. The ~200 win hoped for here is NOT available. The corpus gate (RiddlModelsRoundTripTest, 189 cases) does run JVM-only, as suspected — but it importsorg.apache.commons.io.FileUtilsandscala.jdk.StreamConverters. The usage is shallow (ONEforceDeleteOnExiton the CI download path, TWO.toScala(Seq)conversions of aFiles.walk), so the rewrite is small. The rewrite is not the blocker. That suite WRITES a.bastbeside every model in../riddl-modelsand restores each in afinally; running it on two platforms means two runs mutating one shared external directory, and sbt may run those rows concurrently. DECIDED 2026-08-16 by Reid: it STAYS JVM-ONLY. This is a decision, not an omission. The suite's value is CORPUS coverage, which does not vary by platform, and BAST's platform behaviour is separately covered by the 26 BAST suites moved to Native in6cf60baf2. Copying the corpus per platform, or serialising the two rows, would buy a number rather than a fact. Do not "close" this by porting it. Excluding it, the whole repo sits about 86 cases from tri-platform parity, and that residue is the part worth chasing. Also still JVM-bound:RunCommandOnExamplesTest(commons-ioFileUtils+filefilter) and, through it,RunCommandsOnExamplesTestandNamespaceTest. Method note that cost a cycle: scanning imports OVERSTATES what is movable.NamespaceTestimports nothing JVM-only and is still unmovable, because it EXTENDSRunCommandOnExamplesTest. Check the base class too — moved it, watched it fail to compile, moved it back. Candidate scan for the next two modules (files undersrc/test/scalajvmimporting nocommons-io/scala.jdk/java.io/reflection), recorded so it is not re-derived — but treat it as an UPPER BOUND, per the base-class trap above, and note several are deliberate JVM runners for shared abstract suites (JVMASTTest,JVMValidationTest,JVMDiagramsPassTest) that may already have Native twins: language 19 files, passes 32 (dominated by one homogeneous block of ~20*BAST*suites, which is the obvious next batch), utils 6.language(−156, was −325) — the abstract-suite half is DONE (see PROGRESS above). What remains is its 22src/test/scalajvmfiles, which need the per-file triage below rather than a wiring fix.passes(−156) — 26 shared, 139scala-jvm-native, 29scalajvm. This module already does the right thing at scale, so the residue is likely genuinely JVM-bound; audit it last.
The
languagefix is the template for the other two: the suites were already compiled for all three platforms and only the CONCRETE RUNNER was JVM-only, so moving it tosrc/test/scala-jvm-nativecost nothing and excluded nothing. Check for that shape first in each module before assuming a test is genuinely JVM-bound.79 test files sit under
src/test/scalajvmacross the seven modules (utils 11, language 22, passes 29, commands 7, riddlLib 7, riddlc 1, testkit 2). Each needs the same triage: does it use a JVM-only facility, or wasscalajvmjust where it got written? Where a real JVM dependency exists (filesystem, reflection,java.*APIs without a Native equivalent,Awaiton JVM-only futures), ask whether it can be abstracted behindPlatformContext— which exists for exactly this — rather than accepted as unportable.Why this matters beyond coverage arithmetic: Native fails DIFFERENTLY. It rejects regex lookahead the other platforms accept, and a pattern compiled in a
valfails at class INITIALISATION, surfacing as a Severe message with EMPTY text that names nothing (.claude/skills/rc/SKILL.md§ Red flags). A JVM-green suite says nothing about that class of defect.Do not lower the Native floor to accommodate anything found here. If a count drops, the standard of proof is the one the rc skill records for the 2026-08-05 drop: per-row before/after with the unchanged rows shown unchanged.
-
GAP: 13 shared— DONE 2026-08-14,languageparser suites — 169 cases — never ran on Native546f2f834. Concrete runners moved fromsrc/test/scalajvmtosrc/test/scala-jvm-nativeand renamedJVMNativeTests. Nativelanguage343 → 512, exactly the predicted +169, nothing excluded and nothing weakened — the suites build every input fromRiddlParserInputand string literals, so no Native hazard was present. Rolled into the JVM/Native gap item above. -
[1.4] How many corpus— RETIRED 2026-08-18 by Reid (option B). Not answered; retired. The measurement existed to size the blast radius before the cross-context isolation seam became an Error, and that check shipped anyway (tells cross a context boundary?3059a43f8), so the decision it was meant to inform is already made. Reid's original framing was "I don't really care", and nothing has since made it matter. The one durable fact is kept below, because it is a trap, not a task: a grep CANNOT answer this. A dotted path means the author QUALIFIED the target, not that it crosses a boundary, and comparing the first segment to the nearest enclosingcontextis unsound because sagas and adaptors sit at DOMAIN level. That approach reported 7,393 of 7,396 as crossing, which is not credible. Anyone tempted to re-measure by text should read that and stop. Original: Reid: "I don't really care, but go ahead and count how many models do this, probably not many." Recorded so the next person does not repeat the attempt. What is solid: 7,561 lines carry atell; of 8,254to <kind> <path>targets in the corpus, 7,396 are DOTTED (89.6%) and only 175 are bare. What is NOT solid: a dotted path means the author QUALIFIED the target, not that it crosses a context boundary —to entity OrchestrationContext.Marketplace‑ Ordermay name an entity in the teller's own context. Comparing the path's first segment against the nearest enclosingcontextwas tried and is unsound: sagas and adaptors sit at DOMAIN level, outside any context, so the tracked context is stale for exactly the statements most likely to cross a boundary. It reported 7,393 of 7,396 as crossing, which is not credible. The real count needs RESOLUTION, not text — compare the telling processor's enclosing Context to the resolved target's, which is a throwaway pass over the corpus (or ariddlcrun), not a command. Given "I don't really care", the honest read is: qualified targets are near-universal, so if the seam rule bites it will bite widely — which is itself the argument for the warn-then-flip sequencing this repo now uses twice. -
Cross-context— DONE 2026-08-16,tellisolation seam.3059a43f8. Shipped straight as an Error, skipping this repo's warn-then-flip, because the census removed the reason for it: 18 crossings in 7,537 tells (0.24%), not the 5,301 (64%) a text heuristic had claimed — a dotted path means the author QUALIFIED the target, not that it crosses anything. The real migration is smaller still: 8 Errors, in ONE model. Ten of the 18 are already adaptor-mediated — all 4 inticket-salessit insideadaptor MarketingAdapter, several in reactive-bbq insideadaptor ToLoyalty/ToBar— verified by reading those sources rather than inferred from the drop. Migration task with the 8 file:line pairs and the modelling choice at each is at../riddl-models/task/2026-08-16-cross-context-tell-is-now-an-error.md. Scope, both settled on evidence:sendis NOT covered (its target is aPortletRef, so it cannot name a foreign processor at all; a message crossing bysendgoes through a CONNECTOR, the streaming counterpart of an adaptor), and an Adaptor is exempt, since A4 makes it the sanctioned place to name another context's messages. The bug worth remembering: the exemption must test the IMMEDIATE parent.parentsOfreturns every ancestor, so a type declared inside the target's own context still lists the shared domain among them — as does everything in the tree — and the exemption swallowed the whole rule until it askedparentOf. Recorded in CM §3.6.RiddlModelsRoundTripTestgoes 187/2 → 186/3 until riddl-models migrates; under the 100%-corpus gate that blocks a release. -
Clusterability:— DONE 2026-08-16. Reid chose the keyword and DECLINED theclustered, andself.isClustered.selffield, resolving the contradiction this item was filed with: writingclusteredin the model is exactly what makes clustering statically knowable, which is what theselfadmission test excludes.SelfValue.fieldNamesstays closed atid/version. Shipped as an advisoryoption clustered, not a grammar intention, on the test every intention has been judged by — may a generator decline to honour it? Here it may: deploying one instance is a legitimate realization. Contrastevent-sourced, where declining changes what the model MEANS. Scoped to the SINGLETON processors (Context, Projector, Repository, Adaptor and the seven streamlet shapes, spelled out because a Streamlet's parent kind is its shape's simple name). Not an Entity — already distributed by identity, so it would state nothing; the misplacement is a StyleWarning rather than an Error because it asserts nothing false, unlikepersistenton a stateless definition. Verified end to end against the staged riddlc: silent on a Context and a Projector, and on an Entity it reports "Option 'clustered' is not typically used on Entity definitions (expected: Context, Projector, …)". The one HARD rule nearby deliberately needs no keyword: a correlating projector must distribute by key rather than round-robin, which follows from declaring correlations. Recorded in CM §39.1. -
Survey the CM and every A item for future— DONE 2026-08-15. Reid's admission test applied: is it runtime-only? Anything a generator can know statically it should inline, which is whyselffields.versionis in andisClusteredis not. Result — one genuine candidate, one documentation debt, and everything else rejected with a reason.CANDIDATE:
self.state— the FSM state the instance currently occupies (§4.5: "the entity occupies exactly one named state at a time", changed bymorph). The interesting part is that it is only sometimes runtime-only. Inside a handler declared within aState, the current state is known STATICALLY by construction — the clause could not be running otherwise — so thereself.stateis exactly the kind of thing the admission test excludes. But a handler declared directly on the ENTITY handles its message whatever state the instance is in, and there the current state is genuinely unknowable until run time. So the candidate is real but narrow, and admitting it would put a field onselfthat is redundant in one position and essential in another. Queued for a ruling (NOTEBOOK § QUESTIONS, Q5) rather than decided: the admission test gives two different answers depending on where you stand, which is precisely the sort of thing this survey was meant to surface.DEBT:— WRONG, RETRACTED 2026-08-16. I filed it as semantically undefined and as the same defect class asself.version's meaning is undefinedInteger/Whole/Naturalshipping without ranges. It is neither. Reid: it is the fully-qualified version number from RIDDL'sversiondefinition — static but COMPUTED at generation time, components joined with.. That is A53, already implemented:AST.composedVersionString(VersionSeparator = ".") yields e.g."Jellyfish.Garibaldi.4.2"(pinned byCopyrightTest:234) and is exposed asAnalysisResult.composedVersionStringOf. Its purpose is developer convenience: a component names its own version accurately even when a PARENT component's version changes, because the coordinate is composed from versioned ancestors. What was actually missing was the LINK, and it has been written (2026-08-16):SelfValue.fieldNames' scaladoc now says which field qualifies for which reason, and the CM §4.5 now tells a generator to resolveself.versionat generation time rather than carry it at run time. The lesson is mine, not the language's: I searched for a DEFINITION ofself.versionand found none, and concluded there was none — without asking what already-implemented thing it might be naming. A53 was right there. An absent definition is evidence of an absent definition, not of an absent concept. It also falsified the admission principle as the code stated it. The scaladoc saidself"carries what cannot be known statically, which is whyversionis here" — butversionis static. The real test, now written down: a field belongs onselfwhen the author would otherwise restate something that can DRIFT, either unknowable until run time (id) or derived from context that changes without them touching this definition (version).REJECTED, with the reason, so they are not re-proposed:
isClustered, enclosing context/domain names, the processor's own kind, the handler name — all statically knowable; a generator inlines them.correlationId,messageId,source,time,replyTo— these belong to the Envelope (CloudEvents context attributes, reachable viaoption message_envelopeandon other as x). Duplicating a modelled concept ontoselfwould create two ways to say one thing, and they would be free to disagree.isActive/isPassivated/ shard / partition — the CM says these are invisible to the model. §4.5 makes activation and passivation "the runtime's business", and clustering treats instances as interchangeable. Exposing them would let a model depend on something the CM explicitly reserves to the runtime, which is worse than merely redundant.
-
Computational Model amendments owed by the identity design.— DONE 2026-08-16,18bdb8fin theossumincrepo. All three: §4.5 now reads "rehydrate an already-existing instance" with a paragraph on why activation is never creation; §3.6 gains the memory-space axiom in its POSITIVE form (only processors within one context are guaranteed to share memory, which is what licenses optimizing a same-contexttell); and §38.9 states, as a table, that a definition's ULID is notId(P)— the failure modes are symmetric and both silent. -
[1.6] Does an Adaptor satisfy the cross-context boundary rule?— RULED 2026-08-18 by Reid: NO EXEMPTION. The Context's own portlet is the boundary and an Adaptor sits BEHIND it, so a cross-context connector must still terminate on the context's port; the context then routes inward to the adaptor. No code change —checkBoundaryEncapsulationalready implements this strictly. Recorded so it is not re-opened: the case FOR an exemption was that CM § 7 calls an adaptor a process "at the context boundary … defending that context's model", which reads like the canonical anti-corruption seam. The ruling says being the translator does not make it the boundary — it is content of the context like anything else, and one rule with no exceptions keeps the context's message set the single public surface. The cost was never the issue: only 12 of the 491 corpus violations involve an Adaptor at all, despite 1,475 adaptors declared. -
[1.7] An inlet fed only by— RULED 2026-08-18 by Reid: KEEP REQUIRING A CONNECTOR.tellreports as "not connected".tellis sugar for a send on the outlet connected to the target's inlet (CM § 25.7 / A6), so the connector genuinely SHOULD exist andcheckUnattachedOutletsis correctly telling the author to model the channel. No code change. The consequence belongs to [3.6], not here: atelltarget now needs BOTH a declared inlet AND a connector into it, so each of the 24 no-inlet warnings costs two edits to clear, not one. There is no live corpus population — the corpus emits ZERO "is not connected" messages today, because its tell-target entities declare no inlets at all. The interaction appears only as models comply. -
[1.8] The Native CI leg died as a LOST RUNNER.— DONE 2026-08-19 (962e62638,59e5d7f5c,b4af3a3e6). The cause was memory, and it was measurable: Scala Native builds withgc = "none"(sbt-ossuminc's default, never overridden), a bump allocator that never reclaims. Sampling the liveriddl-commands-testprocess while it ran the corpus suite: 18.18 GB peak RSS withnone, 1.11 GB withimmix— 16x, identical results. A runner has 15,989 MB, so the Native corpus rows needed MORE MEMORY THAN THE MACHINE HAD and the host killed them. Fixed by scopingimmixtoTeston the two corpus-reading rows; the SHIPPED riddlc still builds withnone, deliberately. The leg is also split into seven per-module rows and instrumented. All rows green on the rc.19 tag. Original entry follows for the diagnosis trail:1c318b844putRiddlModelsRoundTripTest's 189 corpus cases onto Native — the coverage win item 5 asked for — and the single Native row went 18 min → 36 → 55–56, then failed 18 runs in a row.It was NOT the
timeout-minutes: 60cap, and an earlier version of this entry said it was. Recorded so nobody loops on it again: the two failure modes are DISTINGUISHABLE, and both occurred here.duration step state logs real timeout 60:23 cancelledpresent what actually kept happening 55:01 still in_progressnone — BlobNotFound, empty attempt archiveA job that hits the cap is cancelled by GitHub and keeps its logs. One that vanishes mid-step having written nothing has lost its agent. Raising the timeout would therefore have fixed nothing — it never reached it.
Leading cause: memory, and the prime suspect is
gc = "none". sbt-ossuminc'sWith.Nativedefaults the GC tononeandbuild.sbtnever overrides it (zero occurrences ofgc). Scala Native'snoneGC is a bump allocator that never reclaims — correct for a short-lived binary, wrong for a test binary that runs 189 models × 4 riddlc invocations in one process. Direct supporting evidence: the same seven-moduletNativechain run locally died withfatal signal 9(SIGKILL), while every module passed when re-run on its own.Done: the Native leg is now seven per-module matrix rows (
scala.yml), each a fresh runner with its own memory, running in parallel, and naming the module when it fails instead of vanishing. Exactly one row (Native-riddlc) carries the release artifacts. Every row is bracketed byfree -m/df -h, runs sbt under/usr/bin/time -vfor Maximum resident set size, and grepsdmesgfor kernel OOM lines.Still open — needs the owner's call: whether to change the Native GC from
nonetoimmixorcommix. It is not a test-only setting: it also governs the shippedriddlcbinary, so it trades a little throughput for bounded memory in a long-running compile. The per-module split may well be enough on its own; the diagnostics above are what will say. Do not change the GC without measuring first — that is what the RSS numbers are for. -
[1.9] CI's corpus-dependent greens are replayed from cache.— FIXED 2026-08-20, in BOTH halves. The corpus suites now assert what they actually cover, and CI can no longer serve a stale result for them.The mechanism, confirmed from a CI log rather than inferred:
sbt/setup-sbtrestores$HOME/.cache/sbtunder keyLinux-X64-sbt-runner-<sbtVersion>-<actionVersion>— keyed on VERSIONS, not content — andv2/acinside it maps task-input hashes to task RESULTS, test results included. The corpora are cloned by a workflow step and are not build inputs, so their content is in no key and a result computed before a rule landed stayed valid-looking. A new step deletesv2/acafter restore; dependency downloads, the launcher, the JDK andv2/casare all untouched, so only task-result reuse is given up.The second half matters more, and was found while trying to reproduce the first. Neither corpus suite asserted that the corpus was FULL.
Root2JsonCorpusTest's three assertions are RELATIVE (identical mustBe reparsed,reparsed mustBe parsed,parsed mustBe files.size), so they are equally satisfied by 190 models and by 3;RiddlModelsRoundTripTestgenerates one case per model found, so a truncated corpus produced fewer green cases and said nothing. That suite's own docstring already recorded the same shape biting once before — every read failing, every failure skipped, and the assertions reducing to0 mustBe 0for months.Both now carry an absolute
MinimumModelsfloor (189 and 190), FAIL rather than cancel when the corpus is present but partial, and keep Reid's [1.3] ruling that an ABSENT corpus is a skip. Both floors were canary-tested by raising them to 9999 and confirming the expected cases redden — a check that has only ever passed is not evidence.Raise the floors when the corpus grows; never lower one to make a run pass.
A note on the original evidence, since it was weaker than it read: this entry cited two CI cases logged 0.4 ms apart as proof of replay.
runMainForTestruns IN-PROCESS, so a small model completing sub-millisecond is not impossible, and the local runs could not be made to replay at all. The cache-restore path is real and is now closed, but the timing observation was not the proof it was presented as. -
[1.10] Does prefix truthfulness extend to a field's type and a type alias?— RULED 2026-08-26: check a prefix that is WRITTEN, never demand one. A field's type admits only a type, so a prefix there removes no ambiguity — unlike a portlet's type or a function'srequires, where several kinds are legal. Only a NON-DEFAULT keyword can be checked: an omitted one is parsed as"type", so the AST cannot tellCtx.Recfromtype Ctx.Rec. Zero corpus cost. Original entry follows.[1.10 — as filed] — a DECISION, not effort. The check shipped 2026-08-24 (
cb05e3748) coveringTypeRef: portlet types, invariantrequires, a function'srequires/returns. A field's type (two: Ctx.Cmd) and a type alias (type A is Ctx.Rec) areAliasedTypeExpression(AST.scala:1958) — a different node that carries its ownkeywordand does NOT route throughcheckRef, so nothing checks them. Verified against the staged binary:record Holder is { two: Ctx.Cmd }naming a command draws zero errors.The requesting task asked for it (
task/done/2026-08-24-reference-prefix-must- match-declared-kind.md): "the rule is abouttype_refgenerally — a field's type, alet's ascription, anupdates repository… applying it everywhere at once is the honest reading of the requirement." It was deliberately NOT done, and the reason is cost, measured rather than guessed: reactive-bbq alone holds 283 portlet references and 542 aliased field references, so extending the rule roughly TRIPLES a migration that already touches 1,032 sites in 188 of 188 models. That deserves its own estimate, not a ride-along.Pinned, so it cannot quietly become permanent:
ReferencePrefixAndRepository PortsTesthas a case named "NOT yet cover a field's type, which is a different AST node". If the scope widens, that test fails — which is the intended direction; update it rather than narrowing the check back.What to decide: whether a field typed by a record must read
two: record Ctx.Cmd. The argument for is Reid's own — the prefix exists to make a model readable without chasing the declaration, and a field is where a reader most often wants that. The argument against is that it is the single most common syntactic position in the language, so the cost is not proportional to the ambiguity being removed. Measure the corpus-wide count before choosing; the 542 above is one model.
Fifteen tasks arrived from riddl-models and riddl-generator, all pre-approved by Reid
with their authors. Thirteen landed between 5b00bea1b and e226f240e. These two are
the remainder, and NO code is written for either. They are ordered: the second is
blocked on the first.
-
[1.11] Give every diagnostic a stable rule id, and a— DONE 2026-08-25 (--jsonemitter forvalidate.9eb75de02,8d82d183d,3ea9b7b9d,1c7b7e34f). 303 rules over 307 sites;ruleIdis MANDATORY on theadd*helpers, so a new diagnostic does not compile without one. Non-reuse is enforced by an enum plus a committed append-only ledger, both canary-tested.--no-msg-idsrestores the previous output. Original entry follows, for the reasoning.[1.11 — as filed] Requested so riddl-models can filter, suppress and count diagnostics without matching on message TEXT, which reworks every time a message is reworded.
Reid's rulings, all made 2026-08-25:
- Threaded, not a registry — the id is a parameter at each diagnostic site. "The registry has multiple problems."
- kebab-case.
- Rendered rustc-style (
error[saga-no-timeout]: …). - More buckets than first proposed — "I think there aren't enough buckets. What about domains, epics, options, …?" Aim for ~23 subject prefixes, not 14.
- A CODE mechanism must guarantee a number/id is never reused — "and I mean code mechanism not documentation." Mechanism is my choice.
- Do it in isolation from other changes, to keep the diff readable.
The single most important finding, which changed the design:
Messages.DeprecationCode(language/.../Messages.scala:190) IS ALREADY THIS MECHANISM, in kebab-case. It holds 14 stable codes (state-is-record,send-to-inlet, …), threaded throughMessage.deprecationCode: Option[String]at 14 sites, and consumed atRiddlLib.scala:970to buildSourceEdits. So this work GENERALIZES an existing field rather than introducing a scheme. An earlier draft in that session proposedREF001-style ids; that would have been a SECOND competing id scheme alongside this one, and was dropped for exactly that reason. Do not reintroduce it.Use an enum, and the reason is a live bug in the current code.
DeprecationCode.allis a hand-maintainedSeqof the codes, andEntityOptionToIntentionwas defined but never added to it for months — so every "exhaustive" migration report silently omitted every entity option-to-intention deprecation since rc.10. The file's own comments record this happening twice. An enum'svaluesis exhaustive by construction, which is both the non-reuse mechanism Reid asked for and the fix for that whole bug class. A uniqueness test plus a committed snapshot of retired ids covers the rest.Measured, so the size is not a guess (2026-08-25, over
passes+languagesrc/main): 304 diagnostic sites, 154 distinct enclosing functions, 82 of which have exactly one site.File Sites Severity Sites ValidationPass.scala216 Error 176 StreamingValidation.scala16 Completeness 49 ResolutionPass.scala15 Warning 34 DefinitionValidation.scala13 Style 17 BASTReader.scala11 Missing 15 BasicValidation.scala9 Usage 5 Messages.scala8 Severe 5 7 other files 16 Deprecation 3 Settle SCOPE before threading anything. Two of those groups are not rules a user can act on:
BASTReader's 11 are internal corruption errors ("Unknown node type tag"), andMessages.scala's 8 are theadd*method DEFINITIONS, not call sites. A rule id is for a diagnostic about the user's MODEL.A mechanical draft is available and is worth generating again rather than naming 304 ids by hand: subject-prefix (from the enclosing
check*/validate*function) plus a slug from the message text produced 293 distinct ids for 304 sites, 11 collisions. That is a review artifact, not the answer — the function name alone is insufficient becausevalidateSchemacarries 10 distinct rules andvalidateEntity9. -
[1.12]— DONE 2026-08-25. The rule carries its fix; the fixer is generic; writing goes through the SAME gate asvalidate --fix/--fix-rule <id>: ship each rule with its own codemod.find -replace, now lifted toFindEditor.applyVerifiedrather than copied. Two rules carry a fix today — see [1.16] for the one that is genuinely mechanical and still excluded. Original entry follows.[1.12 — as filed] Blocked on [1.11] — a fix has to name the rule it fixes.
Smaller than it first appears, because both halves already exist.
DeprecationCode.mechanicalReplacementmaps a code to its replacement text andRiddlLib.scala:970already turns that into aSourceEdit, so the "rule carries its own fix" shape is built and working for deprecations; this generalizes it beyond them. AndFindEditor(commands/.../find/FindEditor.scala) already does span rewriting with overlap rejection, back-to-front application, and re-parse + re-validate + restore-if-worse — which is the dangerous half. Reuse it; do not write a second editor.
Each was noticed while doing something else, checked against the code at the
file:line cited, and deliberately not fixed in the same commit. The
verification is carried here so it is not repeated.
-
[1.13] Type-check— DONE 2026-08-26. Reid: type-check them, corpus cost accepted. Original entry follows.put,returnandrequire … with.[1.13 — as filed] Constructor arguments gained type checking on 2026-08-25 and these three did not. Verified:
checkAssignable(ValidationPass.scala:7473) has exactly two call sites —:1853(constructor arguments) and:9011— whileputandreturnshare an arm at:2024that performs no type check at all. It was never a missing policy:isAssignmentCompatiblealready answers these questions; nothing at these positions has ever asked it. That is the same shape as the constructor gap, which riddl-generator found by emitting Java that would not compile. Expect corpus cost, and measure it before ruling. -
[1.14]— DONE 2026-08-26: a typo is now a parameter error with close matches, exit 7. Original entry follows.find -type <unknown>silently matches nothing.[1.14 — as filed] A typo in a type name produces
0 matchedand exit 0, which is indistinguishable from a correct query with no hits — the exact "confident answer computed over nothing" failurefindwas built to end. Verified:FindPredicates.scala:227comparesProjectionPass.kindOf(n.value) == wantwith no check thatwantis in the vocabulary. Unknown tests ARE caught (:190,unknown test '<x>'); unknown type values are not. The fix is a vocabulary guard rejecting an unknown-typeargument, which needs the single source of truth for the-typevocabulary that thefindplan already flagged as a risk — so do that first, and put it beside the AST rather than in the command. -
[1.15] Decide whether— RULED 2026-08-26: addStatsPass.numPromptStatementsis renamed.numDoStatements, deprecate the old name. Done. Original entry follows.[1.15 — as filed] The
PromptStatement→DoStatementrename (e226f240e) deliberately stopped at this field. Verified: it survives at five sites inpasses/.../stats/StatsPass.scala(:67,:84,:229,:254,:268), with:67already carrying the comment "formerly spelledprompt \"...\"". It was left because it is published, JS-exported API and the backward compatibility policy in CLAUDE.md says add, don't change. This is a decision to ratify, not a bug: either accept the name as history (and the comment is already correct), or addnumDoStatementsalongside it and deprecate this one. Do NOT simply rename it. -
[1.16] A codemod whose replacement is COMPUTED from the matched text.— DONE 2026-08-26.Fixis a sum type (Constant|Computed), so the publishedMap[String, String]keeps only what it can express andvalidate --fixapplies both.quoted-constant-literalnow fixes. Original entry follows.[1.16 — as filed]
RuleId.mechanicalFixis anOption[String]-- a CONSTANT replacement -- which coversprompt-statement->doandabstract-type->Anythingand cannot express a fix that depends on what it matched. Verified: of the five deprecations markedautoFixable,type-first-aggregateis a REORDERING (type X is command {…}->command X is {…}) andshape-keywordinserts somewhere other than the reported span, so both are correctly excluded from mechanical fixing. Butquoted-constant-literalIS a pure span replacement —constant N: Integer = "5"->5— and is excluded only because the replacement is the matched text minus its quotes, which a constant cannot say. Do not just widen the type.mechanicalFixfeedsMessages.DeprecationCode.mechanicalReplacement: Map[String, String]andRiddlLib.deprecationEdits, both published and both expecting constants; a function type breaks them. Adding a SECOND field beside it would be the "two fields describing one thing" shape this repo keeps recording as a defect. Decide the shape first. -
[1.17] Multi-line— DONE 2026-08-25 (doandprompt.21e65251a). Braced likedoc_block;Seq[LiteralString]plus a derived.textfor riddlg; additive at every layer so no corpus model moves. FORMAT_REVISION 23. Riddlg was the requester. -
[1.18]— DONE 2026-08-26: the override is deleted. It also zeroed every message counter, which nobody had noticed. Original entry follows.withLoggeris a silent no-op on Scala.js.[1.18 — as filed] Verified:
DOMPlatformContext.scala:88overridesdef log: Logger = SysLogger(), returning a FRESH logger on every call, so the loggerPlatformContext.withLoggerswaps into theloggerfield is never consulted.pc.withLogger(CallBackLogger(...)) { ... }therefore captures nothing on JS while working on JVM and Native. Found 2026-08-25 byRuleIdTest, which passed on JVM and failed on JS in CI. The tell that the instrument was broken rather than the feature: the same CI run PRINTED[error] [field-duplicate-name] ..., so the rendering was correct and only the capture was blind. The two logger-dependent cases moved toRuleIdLogRenderingTestunderscala-jvm-native; the platform-independent half stayed shared. This is the same family as theConsole.outtrap in CLAUDE.md: a redirect that silently does not apply, whose symptom is "printed nothing" — which is exactly the defect several suites exist to detect, so it arrives disguised as a true positive. What to decide: whether the override is load-bearing (a browser build wanting console output regardless) or simply predateswithLogger. If the former,withLoggershould FAIL LOUDLY on a platform that cannot honour it rather than appearing to work. -
[1.19] Make a validator diagnostic's rule id non-optional at the type level.— DONE 2026-08-26, with the boundary recorded. Every entry point into anAccumulatornow REQUIRES aruleId: the eightaddXhelpers already did, and the five lowercase ones (style/info/warning/error/severe) do as of today — there was exactly ONE production call site, which gaineddoc-figma-unavailable.Message.ruleIdstaysOption[RuleId], and that is the right type rather than a concession.Accumulator.add(Message)is the deliberate remaining path, and it carries messages that legitimately have no rule: parse failures from fastparse, thrown exceptions, and command-level errors like "find: nothing written". None come from the validator and no rule id would describe them. Forcing a fake rule on those would make the id mean less, not more. What a compiler CAN enforce is that the validator always names a rule, and that is now enforced at the helpers. Original entry follows.[1.19 — as filed] riddl-examples' line, and it is correct: "
ruleIdbeing mandatory to PASS is not the same as mandatory to BE."Option[RuleId]lets a site answerNone, so the compiler cannot enforce what3ea9b7b9d's message claimed -- which is exactly how 68 un-ruled diagnostics shipped and were found downstream rather than here. Why it is not a simple type tightening:Message.ruleIdis legitimatelyNonefor parse failures and thrown exceptions, which do not come from the validator at all. The change has to separate "a diagnostic the validator raised" from "a message riddlc emitted", rather than just replacingOption[RuleId]withRuleIdeverywhere. The interim guard isEveryDiagnosticHasARuleIdTest, which asserts over OUTPUT rather than call sites -- a call-site census is what missed this twice, since the grep that findsaddError(does not findcheck(, and neither findsmessages.add(warning(...)). -
[1.20] Publish "resolve before you match" as guidance for AST consumers.— DONE 2026-08-27 (8134f3017), and WIDENED by Reid from "publish the guidance" to "make sure the AST definitions and the Pass classes carry sufficient scaladoc". The entry offered two candidate homes, ossum.tech or a scaladoc note; the scaladoc won because it is where a consumer is STANDING when they get this wrong, and it carries both incidents as evidence. Coverage audited first rather than guessed: AST.scala 301/370 and Pass.scala 16/21 documented, so 74 declarations had none — all 74 now do, andlanguage/docbuilds clean. Sufficiency is NOT claimed: 98 of 370 (26%) carry a single-line scaladoc, nearly all pre-existing. Right forcase class Mass, thin for a node with real semantics; a second pass if wanted. Original entry: The same defect has now been written independently in two codebases. riddl-models reported it against riddlc on 2026-08-14 (task/done/2026-08-14-instance-addressing-check- does-not-resolve-id-aliases.md): the instance-addressing check compared a field's written type expression rather than its resolved one, so it saw the inlineId(E)spelling and missedcampaignId: CampaignIdwheretype CampaignId is Id(...). riddl fixed it. riddlg then carried the identical bug in its own copy of the same logic for another twelve days, finding it on 2026-08-26 while re-measuring: 80 sites it had reported as "not derivable" were all its own defect, none the model's. Why it recurs: the alias IS the documented house style, so a model written idiomatically is exactly the one that breaks a consumer matching onfield.typeExshape. The wrong instinct -- match the AST node you were handed -- is also the natural one. What to write: guidance for anyone reading the riddl AST saying a type expression must be RESOLVED before it is matched on, namingrefMap.definitionOfas the way, and citing both incidents so the advice carries its evidence. Candidate homes: a section in ossum.tech's riddl docs, or a scaladoc note onAliasedTypeExpressionitself, which is where a consumer is most likely to be looking when they get it wrong. Not urgent, and not riddl code -- but countingresolvePath's missingClassTag, this is the third time the shape has cost someone a day. -
[1.21] The— BUILT 2026-08-27 (codestatement's portability warning has NEVER been built.391d647af),stmt-code-not-portable. Fires on every OCCURRENCE per A27's own wording, not once per language — a per-language summary would hide how many sites there are, and site-by-site visibility is what the hatch was sanctioned in exchange for. A StyleWarning, and the severity is load-bearing: under [1.22]'s generability bar anything higher would make every use of the hatch block the generation the hatch exists to serve. A27 updated. Original entry: Tools-To-Do-List A27 requires that "the validator warns about portability on every use" ofcode(language, body). Verified 2026-08-26 by repo-wide grep: no portability diagnostic exists anywhere -- only emptiness is checked (ValidationPass.scala:2084). This is not recent drift; the gap has been open since the item was written and two reconciliations missed it, because the item reads as fully delivered. Why it still matters: the whole point of a sanctioned escape hatch is that using it is VISIBLE. A hatch nobody is warned about is an untracked dependency on one target language. The check is small -- one diagnostic at theCodeStatementarm. Both A27 and Computational Model §20.4 now record that it does not exist, so the documents no longer assert it; this item is the work to make them able to. -
[1.22]— RESOLVED 2026-08-27 (Messages.isActionabledraws a LOOSER line than the generability bar.f280932fc), and the entry's framing was wrong: it read as one bar drawn in the wrong place. Reid's correction is that these are TWO questions that must keep disagreeing —isActionableasks is this worth attention, generability asks can a generator emit correct code — soisActionableis UNCHANGED andisGenerablewas added beside it (plusblockingGeneration). Missing and Usage are exactly where they part: unused definitions are cruft a generator would emit as dead code, and what is missing cannot be generated at all. CM §0.3 names the predicate now. Original entry: Reid ruled 2026-08-26 that a conforming model has no Errors while a generable model has "no errors or warnings other than Style" -- so Missing and Usage warnings are hard stops for riddlg. ButisActionableisseverity >= CompletenessWarning.severity(Messages.scala:47), i.e. >= 4, andMissingWarningis 2 andUsageWarningis 3 (Messages.scala:81,92), so both fall BELOW it. This is filed, not fixed, because the two predicates may legitimately answer different questions: "is this worth acting on" is not the same as "can code be generated from this". The ruling is recorded in Computational Model §0.3 ruling 5 along with this discrepancy. Decide which questionisActionableis for, then either rename it, or add a separateisGenerable, or move the threshold -- but do not move it silently, since consumers key off it.
RULINGS TAKEN 2026-08-14, before an unattended run. Reid answered four questions
up front. Three are built (exactly-once, A43+A46 verbatim, and — as of
2026-08-15 — A20). The remaining one (A38, below) is APPROVED BUT NOT BUILT.
THE FORMAT_REVISION BUMP (17 -> 18) IS SPENT — numeric literals landed and
consumed it (6cfeceb2f, 2026-08-14/15). DONE 2026-08-15 — A20 typed
holes shipped riding the same revision 18 (spelled prompt("…") as T,
Reid's choice over prompt T ("…") and over the document's un-RIDDL
Value[T]("prose"); reuses the shipped prompt and ascribes a type after
it, matching on foo: command Foo and let x: T = …, so nothing new
entered the lexer). A38 is now the LAST claimant of revision 18 — it
still adds/changes an AST node BAST must carry, and 18 has not shipped in a
release yet, so it rides too rather than bumping again. Decide differently
only if 18 ships before A38 lands — then A38 bumps to 19 and says so in its
commit, per the message-value plan's "the 16 -> 17 bump is SPENT" precedent.
Still unbuilt from A46: the compound-output noun/verb consistency warning (a sound, a window and a haptic inside one output). The VERBS shipped; this diagnostic did not, and it is the design-y half.
Surfaced 2026-08-14 by reconciling that document against this branch. Six
items carry an ACCEPTED ruling and had no backlog entry, so none was tracked.
Reid, 2026-08-14: "add the things not built yet to the backlog so they stand a
chance of being implemented in 2.0.0." Each was verified unbuilt by the grep
quoted with it — re-run the grep rather than re-deriving the finding, and
scope it to the whole repo: the first pass of this audit called A42(ii) unbuilt
on a grammar-only grep and was wrong, because the REST client lives in utils.
A seventh entry closes the section: a contradiction between the two documents
that needs a ruling before either can be fixed.
-
[2.1] A42 (iii) — Figma bidirectional scaffolding.— RULED 2026-08-17 by Reid, and the ruling is NEITHER option I offered. Scaffolding is DROPPED entirely: "Just allow the Figma metadata URL on RIDDL UI elements and don't worry about the mapping or framing or correctness. It is only to keep an association. Allow it in an application intended context too so an entire application design can be referenced at that level." NO CODE WAS NEEDED — the ruling was already implemented.DefinitionValidation.mayCarryFigmaRefalready admits exactlyInput,Output,Groupand aContextwhose intention isApplication, andFigmaRefTestalready pins both halves ("be accepted on input, output, group and an application-intended context" / "be rejected on a context that is not application-intended"). Verified rather than assumed. Drift validation (part ii) is KEPT. It is opt-in behind--check-figma-drift, off by default, so it does not contradict "only to keep an association" — the association is what you get by default and verification is there for whoever wants it. Removing shipped, working, opt-in functionality would have been over-reading the ruling; say so if it was meant. The (a)/(b) analysis below is retained ONLY because it records a platform fact worth not rediscovering: Figma's REST API cannot create frames. Superseded: Parts (i) and (ii) shipped: theshown by figma "fileKey" node "1:23"reference form with placement enforced bymayCarryFigmaRef, and drift validation throughFigmaClient, four-valued and off by default behind--check-figma-drift.⚠ THE FIRST QUESTION IS WHETHER THIS BELONGS IN THIS REPO. A42's own text says part (iii) "is generator work and pairs with Part B item 4" — and Part B is riddlg, whose item 4 is the UI application generator ("structure and wiring come from RIDDL, the visual skin from Figma design tokens, with bidirectional scaffolding between them"). Filing it in riddl's backlog was probably a filing error. Recommendation: move the bulk to riddlg and keep in riddl only what riddlg cannot do for itself.
The two halves are NOT symmetric in difficulty, and this is the substance.
(a) RIDDL → Figma (generate wireframe skeletons from the group tree). Blocked on a platform constraint, not on modelling. Figma's REST API cannot create frames — node creation lives in the Plugin API, which runs inside Figma, so
FigmaClient(one method,lookupNode, read-only) cannot be extended into this. The real options are: ship a small Figma plugin that consumes a RIDDL-derived spec; emit a format Figma imports; or use whatever write surface REST now exposes. Confirm against current Figma docs before planning further — this constraint is the whole shape of (a), and their API moves.(b) Figma → RIDDL (draft RIDDL from a Figma file). Tractable today with the READ surface that already exists: walk a file's frames and emit
group/input/outputskeletons, reusing part (ii)'s name-normalisation (bare word-characters, so "Login Screen" ↔LoginScreen). Do (b) first — it is useful alone, needs no new platform capability, and exercises the mapping in the direction where a wrong guess costs a draft rather than a design file.What riddl owes either way, and the only part that is clearly ours: a stable view of the UI structure for a generator to walk (the group/input/output tree is already reachable via the content accessors and
TreePass),FigmaRefin the AST (shipped), andFigmaClientif a write surface ever lands here rather than in riddlg.Carry part (ii)'s two rulings into anything built here, since they were learned the expensive way: a lookup result must distinguish not found from could not ask (
Unavailableis not drift), and a build must never fail because of the network — off by default, never fatal. -
[2.2] A38 — ADMIT an invariant reference as an ALTERNATIVE refusal operand.— DONE 2026-08-17,e4f6f33f3. Additive exactly as the corrected framing said: prose stays valid and unwarned. Full reflective surface (parser, EBNF, regenerated GBNF, prettify, BAST, JSON), rodeFORMAT_REVISION18 as reserved — 18 is now fully spent; the next BAST change bumps to 19. CM §29 records it (ossumincadfce7c), including that the taxonomy there had never listedrefusalat all. Three lessons worth keeping. (1) The corrected framing was right that no corpus survey was needed, and the reason generalizes: removing a form needs a survey, adding one does not. (2) Keeping the two forms DISTINCT is what drove every design choice — a BAST discriminator, two separate JSON keys, and a test that prose spelling a path stays prose. One key holding both would have left every reader guessing. (3) The fixture found a defect the feature had nothing to do with — see the next entry. -
JSON dropped EVERY interaction's metadata.— DONE 2026-08-17, ine4f6f33f3.JsonAstBuilder.buildInteractionhardcodedContents.empty[MetaData](), sostep … with { briefly "…" }came back without the brief, for all thirteen interaction kinds. 745 affected keys in the corpus, so this recovered real data. Undetected because no fixture in the repo had ever put metadata on an interaction step; A38's fixture is the first, andRoot2JsonFixturesTestcaught it the moment it existed. Fixed on theInteractionContentDtoWRAPPER, so one change covers all thirteen kinds. STILL OPEN, and named rather than papered over: metadata on a NESTED step (insidesequential/parallel/optional) is still dropped — those hold a bareSeq[InteractionDto]with no wrapper to carry it, so closing it is a schema change, not a wiring fix. Filed as [1.5] below. -
[1.5] JSON drops metadata on a NESTED interaction step.— DONE 2026-08-17,61d028e4e, unblocked by [1.1]'s ruling. Composites now holdInteractionContentDto, but the JSON stays FLAT — a nested step keeps its own object withbrief/metadataas optional keys besidekind, rather than gaining a{"interaction": {…}}wrapper. So the schema gains keys, not a nesting level, and an old-shape reader is unaffected. The fixture that measured it is back in permanently andRoot2JsonFixturesTestreports lossy=0. Original analysis: A step inside asequential/parallel/optionalcomposite is serialized as a bareInteractionDto, with noInteractionContentDtowrapper to carrybrief/metadata— sosequence { step … with { briefly "x" } }loses the brief on every round trip, while the same step at use-case top level now survives. Closing it means the composites'interactionsfield becoming aSeqof wrappers, which changes the document schema. Marked atJsonAstBuilder.nestedInteraction, which exists to make the gap visible at the code rather than only here.MEASURED 2026-08-17, not reasoned — the entry was first written from reading the code, and this repo's own rule is that a claim about behaviour needs a run. Add a
with { briefly "…" }to a step inside asequenceinlanguage/input/refusal-reason.riddland runriddlLib/testOnly *Root2JsonFixturesTest*: it reportsBriefDescription: 2 -> 1— the top-level step's brief survives, the nested one does not. That is a two-minute reproduction for whoever takes this. The fixture change was REVERTED rather than left in place, because it turns two suites red and a permanently-red gate stops being read.DELIBERATELY NOT FIXED in the same session that found it, and the reason is [1.1]: Reid asked on 2026-08-16 whether the JSON INPUT surface should exist at all. Closing this means changing that surface's document schema — nested steps going from
{"kind": …}to{"interaction": {"kind": …}, "brief": …}— which is a compatibility event for every consumer reading it. Settle [1.1] first; if the input surface goes away, this fix is free (write-side only), and if it stays, the schema change wants announcing rather than discovering. -
[2.2] superseded framing(kept only so the reasoning is not re-derived). CORRECTED 2026-08-16 by Reid, and the previous framing was wrong. This entry said the operand "should name an invariant, NOT prose", and offered as a fallback "admit both and warn on the prose form". Both are incorrect, for the same reason: RIDDL has two legitimate refusal mechanisms, and only one of them has an invariant to name. A handler refuses either withrequire invariant Xor witherror "<prose>"— the validator treats them as equivalent discharges in two independent places (ValidationPass.scala:678and:1729, both matchingcase _: ErrorStatement | _: RequireStatement). So narrowing the step's operand to aninvariant_refwould make an error-based refusal undocumentable, and warning on the prose form would fire on models that are correct — the false-positive failure mode this repo has now hit twice in two days. The work that remains is purely ADDITIVE: allowany_interaction_ref "refuses" user_ref (literal_string | invariant_ref). Prose stays valid, unwarned, and is the honest spelling when the refusal is anerror. A38's goal — closing the loop to therequireand theInvariantViolateda generated test asserts — is then met exactly where an invariant exists to close it to, and claims nothing where one does not. Touches parser + EBNF + GBNF + prettify + BAST + JSON, so it needs aFORMAT_REVISIONbump. No corpus survey is needed to REMOVE the string form, because the string form is not being removed — which is most of what made the original framing expensive. -
RULED 2026-08-14 —— REMOVED from the backlog 2026-08-16 (Reid: "if there's nothing to build, why is it on this list?"). Nothing to build; the ruling is already implemented and documented. A5's generalization is DECLINED and omission is correct; the check says so at its own site, and CLAUDE.md carries the reasoning. Struck rather than deleted only so the next reader does not re-derive the question — a backlog is for OPEN WORK, and a recorded decision with no task attached belongs in CLAUDE.md, which has it.on otheris necessary to the LANGUAGE, not required in every handler. -
[2.3] Audit the remaining catch-all matches.— CLOSED 2026-08-18 by Reid, who asked whether exhaustiveness carries significant correctness value. My answer: no, and the audit's own record is the evidence. Across three slices, the hit rate was 5 real defects in ~25 sites read, and every one of the five was found by REASONING ABOUT A SYMPTOM rather than by sweeping — the BASTReader direction default, the two JsonifierPass wrong answers,PrettifyVisitor.keyword, and [2.6]'s resolution seam. Meanwhile the two most expensive defects of the whole week (typeDepsempty forever, MessageFlowPass dropping edges) were notcase _ =>arms at all, so a complete sweep of this shape would have missed both. The rule stands and is documented in CLAUDE.md; what is retired is the ambition to enumerate every site. Fix the shape when a symptom points at it. Superseded detail: -
[2.3] history: Audit the remaining catch-all matches against Reid's no-silent-fallthrough rule. AddFinder.fieldChildrento the list (2026-08-15): it is 29 hand-written cases ending incase _ => Seq.empty, so a future node holding statements or values in a FIELD returns nothing rather than failing loudly. Landed deliberately — consolidating four scattered special cases into ONE extension point was a genuine improvement and was not held for this — but it is the same shape as everything else in this item, and the next field-held node will be silently invisible to everyFinderconsumer. All 11Valuearms and all 18Statementarms are covered today; nothing keeps arm 12 or 19 covered tomorrow. Reid ruled 2026-08-09: "There must be no non-sealed matches — it is okay to fall through to generate an error or exception but not okay to not select anything and then carry on as if nothing happened." Offered as a follow-up and never answered, so it is filed rather than lost.AUDIT PROGRESS 2026-08-16. Sized: 198
case _ =>sites acrosspasses/src/main+language/src/main, 94 of them inValidationPassalone. A classifier that flags "catch-all whose siblings name a growable union" marked 182 of the 198 — useless, and worth recording as a dead end: it counts typed arms (case _: Foo =>, which are not catch-alls at all) and any sibling type name. Classification has to be by what the arm MEANS, which is reading, not grepping — which is what this entry has said from the start. FIXED:PrettifyVisitor.keyword'scase _: Definition => "unknown", the one CLAUDE.md already flagged. It emittedunknown Foo is { … }— text that does not parse — silently, from the one pass whose whole contract is that its output re-parses. Now throws, naming the kind and the id. Unreachable today, demonstrated rather than assumed:passes1404 green and the corpus round trip unchanged at 186/3, with noIllegalStateException. EXAMINED AND LEGITIMATE — do not "fix" these:AST.scala:2898(Type.kind'scase _ => "Type"— a plaintype X is Stringgenuinely IS kind "Type") andAST.scala:4735(Declaration.ascription'scase _ => ""— a type with noyieldsgenuinely has no ascription). Both are the "nothing to do here" class, which the rule explicitly permits. AUDIT PROGRESS 2026-08-17,7296cfc27. The output-producing slice named below is now WORKED, and it was small:grep -rn --include='*.scala' 'case _ *=> *"'overpasses/language/riddlLib/commandsreturns 10 sites, not hundreds, so this slice cost an hour rather than the day the 198-site figure suggests. Sizing a slice by grep is cheap even though CLASSIFYING one is not. FIXED (3):BASTReader.readAdaptorNodedefaulted an unrecognized direction tag toInboundAdaptor— the worst answer available, since direction decides which side of a bridge produces and which consumes; now throws, matching the rest of that reader.JsonifierPass's adaptorcase _ => "outbound"andaggregateFlavour'scase _ => "aggregation"are enumerated, so a thirdAdaptorDirectionorAggregateTypeExpressionis a compile error (-Werror is live in riddlLib) rather than a silent reversal or a lost keyword. EXAMINED AND LEGITIMATE — do not "fix" these:TypeParser.literalKindFor'scase _ => "a numeric literal"(its only caller has already established the type is numeric-like; theBoolarm above it is the exception) andcardinality'sprefixStrcase _ => ""(unreachable: the three suffix-only combinations are matched by earlier arms and.!.?can only yield "many"/"optional"). Plus the two already cleared:AST.scala:2907,:4748. That is the whole output-producing slice.RiddlAPI.scala:156carries its own justification (Comment/Include have no identifier).AUDIT PROGRESS 2026-08-18 — the resolution-position slice, sized and started.
grep -rn -E "case _ *=> *(None|Seq\.empty|Nil)"overpasses/language/riddlLibreturns 80 sites, concentrated inValidationPass(35),UseCaseWitnessPass(9),JsonifierPass(7) andAST.scala(7). EXAMINED AND CLEARED, with a test rather than an opinion:ResolutionPass.valueScopeField'saggFields(:724) — it reads a Type's fields and answersSeq.emptyfor anything that is not anAggregateTypeExpression, which does NOT see through an alias. That made it a strong suspect, sinceisAddressFieldForhad exactly this defect and was taught to follow alias chains inccd278c00because aliasing is riddl-models' house style. It is not a defect: an aliased state record resolves exactly as a direct one, becausevalueScopeFieldis not the only route — the A55ValueRefwalk reaches the field anyway.AliasedValueScopeTestpins both forms, so a future change that madevalueScopeFieldthe sole route reddens here instead of reddening riddl-models. Method note worth keeping: the suspicion was well-founded and still wrong. Reasoning from "this shape was a defect over there" got the site onto the list; only running it settled the question. That is the same lesson as [2.6]'s unreachable arm, in the opposite direction.The remaining ~78 are unexamined. Continue with the same slice:
case _ => Noneandcase _ => Seq.emptyin RESOLUTION positions, where an empty answer is read downstream as "no such thing" — the shape that produced both of this week's found defects (DependencyAnalysisPass.typeDepsempty forever,MessageFlowPassdropping let-local edges). Neither of those was acase _ =>arm at all, which is the caution: this item's grep-shaped framing cannot find the defects its own examples are made of.What is already done (do not redo): the total dispatches were fixed at
286ef8157and around it —Pass.processValuenow throws on an unhandledValuerather than returning unit,BASTWriter/BASTReaderthrow instead of aprintln-and-drop and a placeholderPromptStatement,classifyHandlersenumerates all 17Statementkinds with no catch-all, andcountValueFailPointsenumerates the rest.What is NOT done: roughly 140 remaining
case _ => ()sites across the codebase, most of which are legitimately "not interested in this node" rather than "silently gave up". The work is to separate those two, not to delete the arm — a mechanical sweep would be wrong. Suggested order:passes/first (where a miss changes validation results), thenlanguage/, then the serialization surfaces, which are already done.Start with
grep -rn "case _ =>" --include=*.scala passes/ language/ | wc -lto size it before committing to a plan.Widen the sweep to no-op HOOKS, not just catch-all arms (added 2026-08-14, from the prettify emitter fix
2ebe24a6c).PrettifyVisitor.doMethodwasUnit = ()with the comment "Methods are handled by their type" — they were not, and everymethodwas dropped from prettified output. The arm was perfectly explicit and the dispatch perfectly total; the defect was in a CLAIM ABOUT CODE ELSEWHERE that nothing verified. Same shape inPass.processValue, whereShownBywas skipped because such values are "read by the definition that holds them" — a survey of the visitors that existed, not a property of the node, and false the moment prettify needed it. So the sweep's question is not only "does this arm mean 'I don't know what this is'?" but also "if this arm defers to something else, does that something else actually do it?" Both were found by reading, never by the compiler:-Werroris live inpassesbut a wildcard arm makes a match syntactically exhaustive, so the prescribed terminalthrowis itself what silences the warning. A cheap first pass:grep -n "Unit = ()" passes/…/prettify/PrettifyVisitor.scalaand check each justification against the code it names. The five there are all legitimate NOW —doMethodonly became so with this fix.Widen the sweep a third time: to ONE-LEVEL COLLECTORS (added 2026-08-15, from
DiagramsPass.captureUseCase,b8a6057fb, reported by riddl-generator). A third shape that this sweep's two questions both miss.captureUseCaseenumerated its cases explicitly — no catch-all — and deferred nothing to anywhere, so it passes both tests above and was still wrong: it mapped overuc.contents.toSeqand gaveInteractionContaineran arm returningSeq.empty, so nested steps were never collected. The arm was not a fall-through; it was a wrong answer, written deliberately. Consumers render from the same data by RECURSING through those containers, so capture and render disagreed about what a use case contains. The symptom is the one this whole item keeps circling: an empty result is indistinguishable from a model that does not use the construct. Note it was only sometimes empty — 4 of reactive-bbq's 12 use cases returned partial maps and 1 returned nothing, which is worse, because a partial answer is not even suspicious. So the third question is: "does this collector descend as far as the code that CONSUMES its output does?" Sizing grep:grep -rn "contents.toSeq" --include=*.scala passes/ | grep -v "flatMap\|Finder". Same family asFinder.recursiveFindByType(b55d1d5cc) — riddlg hit both within a day, from different directions, which is the reason to believe there are more. A fourth defect rode along and is worth its own line, because no sweep of matches or traversals would find it:.sortWith(actorsFirst).toMapsilently DISCARDS the sort. Scala'sMap1..Map4keep insertion order incidentally; the fifth entry becomes a hash-orderedHashMap. Any.sorted…toMappipeline in this codebase is a latent version of the same bug and is invisible below five elements —grep -rn "sortWith\|sortBy" --include=*.scala passes/ | grep -i "toMap"is worth running once. Fixed here withimmutable.VectorMap, which preserves the declaredMaptype. -
[2.4] Finish the— DONE 2026-08-17,Streamlet→Processormigration in the remaining passes.2c19d6d70. The public-API question is answered the additive way:AnalysisResult.streamletsandDataFlowDiagramData.streamletskeep their exact meaning and type, andprocessors/portBearingare added alongside. Every accessor in that family is named after a KIND, so widening the one would have made it the only one whose name does not say what it returns. See [4.1] — Reid has not confirmed this reading. Both behavioural defects were WRONG ANSWERS, not dropped work, which the entry did not anticipate: MessageFlowPass's grandparent fallback SUCCEEDED on the enclosing Context (a Context is a Processor too), so a flow from an entity's own outlet named the container as producer; DiagramsPass fell back to the PORT, drawing arrows from an outlet to an inlet with neither owner shown. Demonstrated by reverting each helper, not assumed. A third defect rode along that no Streamlet-narrowing sweep would find:makeProcessorRelationships's Streamlet arm calledmakeInletRelationshipsand thenmakeOutletRelationships, and a block's value is its LAST expression — the inlet relationships were computed and discarded. Same family as the.sortWith(…).toMapdiscard recorded above.StatsPasswas examined and is NOT part of this: all three of itsStreamletsites are arms of a total enumeration over the six kinds. Its "+1 for shape" counting only Streamlets is a genuine question — see [4.4]. -
A lookup value:— DONE 2026-08-17,<mapping|array> at <index>.9ec30c5b5(parser) +977813b58(the rest). Mapping by key, Sequence by ordinal, Table by one ordinal per dimension;Set/Graphrejected; literal indices type-checked against the collection's key type. CM §3.6 records it. The blocker was a parser CUT, not the syntax.Keywords.keywordends in./(Keywords.scala:39), so matching the wordatCOMMITS the parser. A lookup is reachable from bothcomparand(a comparison operand) andbooleanAtom(a bare value); with the cut, whichever route ran first poisoned the other, because only the bare case has to backtrack out ofcomparison's first alternative.when inv at "sku" > 0worked whilelet n = inv at "sku"failed, moving the rule between the two only traded one failure for the other, and removing my own~/afteratchanged nothing — the cut was never mine. Fixed withNoCutaround the optional clause, plus ONE rule that parses a ref and optionally extends it rather than two alternatives, since anything that makes the parser CHOOSE must backtrack across that keyword.-Werrorfound 3 of the ~13 sites; the rest were behind catch-alls. Two guards caught real omissions: BASTWriter's total dispatch threw the moment a lookup reached it, and the JSON vocabulary guard caughtcollectionmissing fromknownKeys— without which the unknown-key warning would have fired on every document containing a lookup. -
[2.6] BUILD: an imported definition must RESOLVE without an explicit flatten.— DONE 2026-08-17,f99bd3d27. Two arms offindMatchingCandidateswitched fromdirectDefinitionstodefinitions. The obstacle this entry recorded — thatfilterThroughWrapperscannot express "includes but not imports" — DISSOLVED with the ruling, since the two wrappers are now meant to be treated alike, which is why it was two words rather than a redesign. Both arms are instances of [2.3]'s named next slice (an empty answer in a resolution position); this is the third example and the first found by deliberately looking for the shape. The method mattered more than the fix. My first attempt patched a plausible-lookingcase _ => Seq.emptyincandidatesFromContents, and the suite stayed GREEN — the arm is unreachable for aBASTImport, because every caller passes itdirectDefinitions, which has already filtered wrappers out. Only printing the actual messages showed the error was still there. A green suite after a fix is not evidence the fix did anything; instrument. Zero corpus movement, and that is evidence about the corpus — riddl-models uses no.bastimports. The real evidence isBASTImportLoadingTest's pinned contract, INVERTED: it required an error namingApp.Moneyand now requires none. NEW, filed as [4.6]: a local and an imported definition may now share a name, and declaration order decides which wins. -
[4.6] import/local shadowing.— RULED C, DONE0e8441aca: the LOCAL declaration always wins, regardless of position, AND the ambiguity warns naming EVERY side (Reid: there may be more than two). Precedence is expressed as ORDERING (localsThenImported), so.headOptionIS the rule and there is no second place to keep in step. The warning stays silent when all matches are local — that ischeckUniqueContent's report. Both orderings are tested, and that pair IS the ruling. Superseded: Consequence of [2.6]:findMatchingCandidatetakes the FIRST match in contents order, so whethertype Moneydeclared in a domain beats aMoneyimported into that domain depends on which was written first. Nothing warns. Options, none of them obviously right: leave it (position is a defensible rule and matches how includes already behave); warn on the collision; or make local always win regardless of position. My recommendation is to WARN and not to change the winner, because the ambiguity is what the author needs told, and silently reordering precedence would make the same source mean different things before and after an upgrade. Not built — this one genuinely needs a ruling, and no corpus model exercises it.Superseded framing(kept so the reasoning is not re-derived): Today the two halves disagree: the content accessors report a.bast-imported definition (domain.typeslists it, since 2026-08-03), but a REFERENCE to it does not resolve untilFlattenPassruns, because the symbol table is built by TRAVERSAL andBASTImportis a wrapper the traversal does not descend. So a model can SEE an imported type and cannot NAME it, which is the kind of split that makes imports feel broken without ever producing a clear error. The known obstacle, recorded so it is not rediscovered:ResolutionPasskeeps its own manual walk that descendsIncludeand deliberately NOTBASTImport, readingdirectDefinitionsat 7 sites, precisely becauseContents.filterThroughWrapperscannot express "includes but not imports". That asymmetry is what has to change, and it is the reason this was filed as a question rather than a chore. Pinned today byBASTImportLoadingTestandIncludeAndImportTest, both of which assert the CURRENT behaviour and will need to move with it. -
BUILD: a query must REPLY or REFUSE on every path.— DONE 2026-08-16,07228a9a6. Now usesdischargesOnEveryPathwith the refusal exemption, so the query rule is exactly PARALLEL to the command rule rather than stricter. Shipped with NO migration, and that was measured rather than hoped: 943 on-query clauses in the corpus, only 5 files with a conditional inside one, and every one already replies or refuses on every path. Contrast the error-sink ruling, which needed 187. Measuring found an older, unrelated defect — the more valuable half. The check was producing 10 false warnings across 6 models on handlers that plainly do reply, because reply/yield operands were still resolved with the NARROWoperandMessageKind, which cannot see through aValueRef. The comment there said they "stay MessageRef | Constructor only until Task 2"; Task 2 landed long ago and nothing came back to widen it, so the canonical spelling riddl-models actually uses —let r: type X.Result = prompt(…)thenreply r— was invisible. Widened; corpus goes 10 → 0. That is the third "claim about code elsewhere that nothing verified" found in this branch, and the second where the stale claim was a promissory note about work that had since landed. -
[2.7]— ALREADY BUILT; struck 2026-08-27, the day after it was filed. A45 landed:put/getUI-boundary statements.PutStatementis atAST.scala:4250andputStatementatStatementParser.scala:906(put <value> to output <ref>, scope-gated to application-context handlers), whileget from inputhas existed all along —GetValue.sourceisInputRef | StateRef(AST.scala:3360). This was filed in error, and the error is instructive. It was graduated VERBATIM out of NOTEBOOK's "Deferred — blocked on prerequisites (do NOT start yet)" section during the 2026-08-27 prune, on the strength of what that section SAID, without checking whether its prerequisites had landed. They had. This repo already has a name for that failure — a plan cannot notice the work happening ([2.x] history; the 2026-08-06 "plan for work that was already done") — and the cheap test it prescribes would have caught it: take the entry's most specific factual claim and check that one first. "Revive theput/getkeywords" was falsifiable by a single grep. Rule for pruning, learned here: a deferred item is exactly the kind that goes stale invisibly, because nobody re-reads a section headed "do NOT start yet". Verify before graduating one, not after. -
[2.8] Single failure point per saga do-block.— ALREADY BUILT; struck 2026-08-27 with [2.7], same provenance and same mistake. The blocker was the can-fail census, andStatement.canFailexists (AST.scala:3704, defaulting tofalse, overriddentrueonSendStatementat:3818and its peers) with its own suite, "Statement.canFail (A12/A36)" inSagaValidatorTest. The A12 check itself is built atValidationPass.scala:5554, and it is MORE complete than the filed design: it counts statement-level failure points viacanFailplus every embeddedCall/GetValuein value expressions, recursing throughwhen/match/foreachbodies, and skips the count when the step contains anask(anaskis itself a failure point, and a saga may notaskat all — Reid, 2026-08-10). The scaladoc oncanFailrecords a distinction worth keeping: it is a DIFFERENT axis from A23'sisEffectStatement—setis an effect but cannot fail — and value-level failure points are deliberately counted separately rather than folded into the predicate. -
[2.9] TypeScript AST declarations for the JS side.— DECLINED 2026-08-27, on the merits, after being raised off "low priority" the same day so it could be considered rather than deferred again. Considered, and the answer is no. The opaque*ASThandles are a BOUNDARY DOING ITS JOB, not a gap.index.d.tsis 1,169 hand-written lines and already rich for everything the facade returns —RiddlResult,Token,ValidationResult,OutlineEntry,TreeNode,MessageFlowEdge, the wholeRiddlAPI. Only the 19 branded node handles (RootAST,EntityAST, …) are structureless. There is no consumer for the missing part. riddl-vscode — the TypeScript consumer — callsparseToTokens(14×),parseString(11×),getTree,validateString,inspectRoot,getMessageFlow,getHandlerCompleteness, and touches a raw AST handle zero times. An editor extension wants highlighting, diagnostics and an outline; the facade serves all three. The consumer that genuinely walks the AST is Synapify, which is Scala.js and has the real thing, methods included (Reid: "most users are going to use it from Scala.js where we have mechanisms in place"). The proposed implementation — generate the.d.tsfromJsonModel— was measured and is WORSE than it sounds, which is what settled it. JSON serializes STATE; the AST's value is substantially BEHAVIOUR.AST.scalacarries 540def/lazy valmembers, none of which serialize: 182format, 67kind, the 34WithXaccessor traits, and derived answers likeeffectiveShape,Connector.isPersistent(which accepts both spellings),Statement.canFail,NumericLiteral.asLong,DoStatement.text,Function.input/output.JsonModelalso contains zero references torefMap,symTaborusedBy, so no resolution output crosses at all. The disqualifying detail: JSON preservesInclude/BASTImportas CONTENT ENTRIES, so a TypeScript consumer walkingcontentssees the WRAPPER rather than through it. That is exactly 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, the two things two separate consumers have already got wrong (see [1.20]'s two incidents). 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. If this ever comes back, 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. Do not export the AST. -
[2.10] Repo housekeeping — three items.— ALL THREE DONE 2026-08-27, on Reid's explicit go-ahead, which was the only thing the entry had been waiting on.developmentdeleted, local and remote (wasfdf9a0ad4). Verified 0 commits ahead ofmainbefore deleting, so nothing was lost.old-developmentno longer existed..claude/skills/ship/SKILL.mdde-GitFlowed. Its pre-flight had told every release togit merge --ff-only developmentintomain, and step 14 to check outdevelopmentand merge the tag forward. Both were no-ops or contrary to policy from 1.30.0 on and were skipped by hand each time — a skill documenting a process nobody follows is a trap for the next session that trusts it. It now says: ship a FINAL release frommain; when the work lives on a release branch, merge that branch INTOmainand tagmain, never the branch; delete the branch afterwards. The RC exception is named and pointed at the/rcskill.- The stray
helptag deleted, local and remote (fd1fbf5c7, "Make a test case pass if its environment is not found (#137)" — an obvious typo'dgit tag help). It sorted to the top ofgit tag --sort=-v:refname, so it LED the tag list every time anyone worked out the latest release; that list now starts at2.0.0-rc.26.
CLAUDE.md§ Branch Strategy carried all three as outstanding and now records them as done, so the file no longer contradicts the tree.
-
[3.1] riddl-generator's— CLOSED by THEM 2026-08-16, verified 2026-08-18 in theirFinderincompleteness.task/done/: all four criteria met. They re-pinned torc.14-121, diffed against baselines (longer, never shorter — the direction predicted), audited all 57recursiveFindByTypesites, and found no field still empty. Their audit is worth keeping: riddlg sweeps NO value type at all — every sweep names a Statement or a Definition — so the whole value-composition half of the fix could not affect them. The real delta was exactly TWO containers,Correlation.timeoutStatementsandInvariantBlock.statements. Superseded: -
[3.1] history: DELIVERED 2026-08-15 to riddl-generator:Finderwas returning incomplete results across 27 node fields. Task dropped at../riddl-generator/task/2026-08-15-finder-was-missing-content-across-27-fields.md— verified written, not merely claimed (the 49-alias entry below is what that qualifier exists for). Fixed here atb55d1d5cc+a3c0aa345, both AFTER riddlg's pin and in no tag yet (latest is2.0.0-rc.14), so they cannot act until the next RC or a localpublishLocal. Awaiting their reply on whether any of the 27 fields still comes back empty, and on whether any generated output got SHORTER (the unexpected direction). -
BLOCKED ON riddl-models: reactive-bbq's 2— DONE by riddl-models 2026-08-15,terminatelines.2e619c44. Both migrated toterminate self.idin the same commit that took2.0.0-rc.14-121and cleared the bare-message-operand migration. reactive-bbq parses again; it is red now for a DIFFERENT and newer reason (the cross-context seam, § 3), which is worth not confusing with this. -
[3.2] The exact— CLOSED 2026-08-18. Reduced in scope by Reid and now moot: riddl-models'figmainput behind riddl-models' emitter report.task/is empty and nothing here was ever blocked on it. The behaviour was NOT reproduced — riddlc prints a specific Error and exits 7, which is correct — and [2.1]'s ruling has since settled whatfigmamay decorate (UI elements and an application-intent Context), which is the language question their report might have been circling. Superseded: -
[3.2] history: AWAITING riddl-models: the exactfigmainput from their emitter report. Their2026-08-14-prettify-emitter-drops-method-and-shown-by.mdclaimedfigmaon a domain or context "writes no file, exits 7, prints no error". Not reproduced — riddlc prints a specific Error ("A 'figma' reference is not allowed on Domain 'Dom'; it may only appear on an input, an output, a group, or an application-intended context") and exit 7 with no output is CORRECT for a validation Error. Their report says they saw[style]and[missing]messages, which rules out--quiet, so something differs between the two inputs and guessing would be worse than asking. Asked in the Results section of that file (now intask/done/). Nothing here is blocked on it; if the real complaint is that A42 forbidsfigmaon a domain at all, that is a language question and belongs in § 2, not a defect. -
[3.3] riddl-models' coverage model held out of their repo.— CLOSED 2026-08-18, verified rather than assumed:../riddl-models/language-coverage/exists (.riddl,.confand.bast), so CI's EBNF validation — which walks that repo — now exercisesmethod,shown by,table of … of […],attachment,replica ofandfigmaagainst the corpus. That was the gate whose absence let six emitter defects through. Superseded: -
[3.3] history: riddl-models' coverage model is being held out of their repo until this lands, so CI grammar validation is NOT currently exercisingmethod,shown by,table of … of […],attachment,replica oforfigmaagainst the corpus — precisely the gate that would have caught all six emitter defects. Expect it to land after the next RC. Until then, the only coverage for those constructs is this repo's own round-trip tests (AggregateContentsRoundTripTest,ShownByRoundTripTest,TypeExpressionSpacingRoundTripTest,AttachmentRoundTripTest). -
[3.4] riddl-models corpus migration.— COMPLETE 2026-08-17. Their99fc29d1("Upgrade to riddl 2.0.0-rc.15; the corpus validates 188/188 with zero errors") took validation-parity to 190/190 andRiddlModelsRoundTripTestto green. Reid's release gate — "corpus at 100%" — is MET. The last two red cases anywhere in this repo's suite belong to../riddl-examples, which now has a migration task (task/2026-08-17-migrate-to-2.0-syntax.md). History: -
[3.4] supersededNEARLY UNBLOCKED — riddl-models landed the migration mid-session (2026-08-15 19:19). Their2e619c44, "Upgrade to riddl 2.0.0-rc.14-121-fe768026 and validate the corpus cleanly", took the binary staged an hour earlier and did theterminateAND bare-message-operand migrations in one pass. Re-measured immediately after, at that commit:measurement earlier same day now corpus models validating clean 59/190 188/190 RiddlModelsRoundTripTest115 ok / 130 failed 187 ok / 2 failed Root2JsonCorpusTestjson-identity189/190 190/190 (100%) Root2JsonCorpusTestvalidation-parity59 188 (98.9%) All that remains is TWO models, and they are the same two by every instrument:
patterns/entity/aggregate-root/example(6 errors) andpatterns/entity/event-sourced/example(14 errors), both still carrying bare-message operands (18 message-type + 2 record-type). Note both files are namedexample.riddl, so a failure list shows "example.riddl, example.riddl" — do not read that as one model reported twice.RE-MEASURED 2026-08-17 and it is now THREE, not two. validation-parity is 187/190, and the third model is
reactive-bbq.riddl, failing on two errors of a kind not in the list above: "crosses the context isolation seam from Context 'X' to Context 'X': receiveDrinkOrder is not declared in a domain ancestral to both" (and the same forsendPushNotification). Measured againstorigin/release/2with my own changes STASHED, so this is corpus drift or a pre-existing seam defect, not anything landed this session. Whose side it belongs on is unresolved — see [4.5]. The 188 figure above is what was true on 2026-08-15; it is left in place because the delta is the interesting part.— RESOLVED 2026-08-16 by Reid: "Corpus at 100% should be the release gate, > 95% is contrived." So the EQUALITY assertion was right all along (its own comment said "NO allowance") and the ADVERTISING was wrong. The case is renamed to "(EVERY model)" and now reports a count rather than a percentage — a figure like 98.9% reads as a score against a threshold, and there is no threshold. §0 and this entry had both been repeating the ≥95% bar as though the code implemented it. Consequence, and it is the point of a gate: this suite stays RED until riddl-models migrates its last two models (Root2JsonCorpusTest's name and assertion disagreepatterns/entity/aggregate-root,patterns/entity/event-sourced). That is not a defect here.The stale-number lesson, third instance in one day. Every figure in the old version of this entry — 173/189, then 59/190, then 115/130 — was accurate when written and wrong within hours, because the corpus is a LIVE checkout that another session edits in parallel. Twice during this session
git statusin../riddl-modelschanged between two consecutive commands. Re-measure before quoting any corpus number; never carry one forward. Cause, CORRECTED 2026-08-15 — it is no longer the alias fix. The original cause wasccd278c00, which taught the tell-addressing check to resolveIdaliases, turning it on for the spelling riddl-models uses and surfacing 49 ambiguity Errors it had been hiding. Those 49 are gone (see below), and what keeps these suites red now is a DIFFERENT and larger thing: Migration 2, the bare-message-operand tightening, which accounts for 130 of the 131 failing models. The 131st isreactive-bbq's two unmigratedterminatelines. Keep the history because the two are easy to confuse — the alias fix is closed, the corpus is still red, and those facts are unrelated. All 49 are corpus-side, in three classes, checked against riddl-models' sources rather than inferred from the messages: genuine two-id ambiguity (CartsMerged {targetCartId, sourceCartId}) needingby; actor fields legitimately of the same entity (identityId+suspendedBy) also needingby; and wrong-entity aliases —nursing-workflow/types.riddl:18 type TaskId is Id(NursingContext.NurseShift),radiology-workflow/types.riddl:27 type ReportId is Id(ImagingExam),member-enrollment/types.riddl:2 type MemberId is Id(Enrollment),policy-lifecycle/types.riddl:8,14 BeneficiaryId/RiderId is Id(LifePolicy). ✅ THE 49 ARE CLEARED, AND THE LIST NEVER NEEDED TO BE DELIVERED (verified 2026-08-15). riddl-models fixed all of them ITSELF in29598ad1, "Clear all 49 addressing errors, and correct 18 Id aliases", dated 2026-08-14 — the day before this entry was written demanding the list. They derived it from their ownriddlcrun and classified all three classes; 16 aliases became plain identifiers (a task is not a shift, a report is not an exam), two models whose only alias was the wrong one were repointed atMachineId/PartId, and 27 genuine two-id sites gainedby <field>. Everything this entry previously said was owed is moot: the phantom filename, the ~44 unwritten sites, the "regenerate before expecting them to act", and the RC-certification blocker. The task file in riddl-models has been corrected in place so it stops asking them to redo finished work. Verified, not assumed — a sweep of all 190 corpus entry points at2.0.0-rc.14-120-7cb40f45reports zero ambiguity Errors, and the instrument was PROVEN able to fire first (a positive-control model, in both the inlineId(entity X)and thetype OrderId is Id(entity X)alias spellings — the alias one being whatccd278c00turned on and what this corpus uses). The five wrong-entity aliases named above are nowUUIDs. The lesson worth keeping: this entry was the stale artifact. It described work as outstanding for a day after it was finished, in confident detail, and nothing about reading it suggested otherwise — the same failure mode this file documents for test counts. A backlog item asserting another repo owes us something should be re-verified against that repo before being acted on.A second, UNEXPLAINED regression— EXPLAINED, then RESOLVED, both 2026-08-15. The 59/190 was never a mystery: a sweep of all 190 entry points found 131 models carrying an Error and 130 of them carrying exactly ONE error class — 343 × "names a message type, not a value" plus 19 for record types, i.e. the bare-message-operand tightening. The 131st wasreactive-bbq, which did not parse at all for two unmigratedterminate entity Xlines. Nothing else appeared anywhere in the corpus. riddl-models then landed the migration the same evening and the figure went to 188/190, exactly as the diagnosis predicted — which is the corroboration, since the prediction was made before their commit existed. Method note, because the first attempt got it wrong: key the sweep's output files on each model's RELATIVE PATH, not its.confbasename — ten corpus models share a basename, so a basename-keyed run silently overwrites ten results and reports 180 files as though they were 190. Caught only by reconciling the file count against the exit-code count. -
Tell consumers about two BREAKING changes landed 2026-08-10.CLOSED 2026-08-11 by Reid: no announcement needed, for either half. Recorded so it is not re-raised: (1) BASTFORMAT_REVISION, now 11, so every.bastfrom an earlier build is rejected with a message telling the reader to regenerate; (2) a mapping's VALUE type is now resolved (b307909b5), somapping from K to Nonexistentused to validate clean and now errors. The second needed no ruling on BEHAVIOUR at any point — it is a correct tightening — only on whether to notify, which is what is now closed. -
Restage— DONE 2026-08-12, at~/Code/ossuminc/bin/riddlc2.0.0-rc.12-4-092ec2be. Verified by BEHAVIOUR, not by version string: the twowhen invariant Xreproducers that threw on rc.12 now validate clean.bin/riddlcis the NATIVE binary, a REAL FILE (Reid, 2026-08-12) — the same shape asbin/riddlg. It is not a symlink and not the JVM launcher, andscripts/publish-and-stage.shnow installs and verifies that exact path. The arrangement this replaced is worth remembering, because it failed silently.bin/riddlcused to be a hand-made symlink into../riddlc-dist, created a day before the script existed and invisible to git (bin/is ignored). The script wrote and verifiedriddlc-dist/bin/riddlcwhile its own header promised something aboutbin/riddlc— so the rule was enforced nowhere, and any change to the symlink would have left it reporting success over a frozen binary.riddlc-dist/is deleted; nothing references it. Restaging is still not a standalone act: the script runspublishLocalandriddlcNative/nativeLinkin ONE sbt invocation, because the ivy artifacts and the CLI must never disagree about what the language accepts. Use the script; do not stage by hand. -
[3.5] synapify's— CLOSED 2026-08-18: already delivered. The task file is in theirflattenASTworkaround.task/as2026-08-15-flattenAST-workaround-can-be-dropped.md, so the guidance is with them and nothing is owed from here. They still callflattenAST(3 sites, all in one test) and their code stays correct either way — it is their call when to take it. Superseded: -
[3.5] history: synapify:flattenASTworkaround can be dropped.Contents.definitionsbecame include- and import-transparent on 2026-08-06 (their task file, now intask/done/), so their 33.definitionssites no longer need the tree physically flattened first. Nothing owed until they take a build containing it — worth folding into whatever upgrade task they get next rather than a task of its own, since the change is source-compatible and their current code stays correct. (The standing 'consumer sweep' item was removed 2026-08-12 — Reid: riddl-generator is on rc.12 and building its own rc.1, and the rest are not worth tracking here.) -
riddl-vscode: adoption task for
IncrementalValidator— hold until the 2.0 upgrade above, so they take one change rather than two. -
ossum.tech doc debts— DROPPED as a task 2026-08-12 into../ossum.tech/task/2026-08-12-riddl-2.0-doc-debts.md, so it is tracked there now, not here. It carries theForeverEmpty/BottomlessPiterror-sink idiom, the event-sourcedon initidiom, paths-into-Functions plus the new function-privacy StyleWarning, and the rc.12 language changes (yields command, theset/get from statescope rules, the rewordeddo-statements warning,FORMAT_REVISION12, and the two previously-silent breaking changes). One item was CANCELLED as stale, verified not recalled: the/riddl/2.0/licenses/404 no longer exists —riddlc infoprintsgithub.com/ossuminc/riddlandopensource.org/license/apache-2-0, neither of which is an ossum.tech URL. -
[3.6] Corpus migration for the cross-context boundary Error.— DONE 2026-08-19, both corpora. Verified rather than assumed: every riddl-examples ENTRY POINT validates at 0 errors, and riddl-models is clean. The one apparent exception,FooBarSameDomain, is a DELIBERATELY ambiguous fixture — duplicate content names and an ambiguousInfoare it doing its job, not migration debt. (Validate entry points, not include fragments: a fragment validated alone reports errors by construction.) Original entry follows for the measured cost: 2026-08-18 (c67cfdbfd) on Reid's ruling that reaching past a context boundary is an Error, not a warning. Measured cost on the corpus, with the check live: 250 inbound + 241 outbound violations across 184 of 198 entry points (riddl-models + riddl-examples). Reid ruled the cost acceptable and "remedied quickly" before it shipped. The fix per site is mechanical — declare the portlet on the CONTEXT and route the inner definition's port to it within the context — but it is 491 sites and belongs to those repos, not this one. This blocks nothing here; riddlc is correct. Task drops owed to../riddl-modelsand../riddl-examples. riddl-models is DONE as of 2026-08-18 — 190 entry points at 0 errors / 0 completeness / 0 usage, verified with2.0.0-rc.16-20-c075f1af. What remains is riddl-examples alone: 49 errors, 48 completeness, and it is the only thing keeping riddl's own corpus gate red. Task dropped in that repo. [1.6] was ruled with NO adaptor exemption, so the original count stood at 491 — the 12 adaptor-terminated sites migrate like the rest. Second, separate cost, from [1.7]'s ruling: atelltarget needs BOTH a declared inlet AND a connector into it, so clearing each of the 24 no-inlet warnings takes two edits. Those are additive to the 491.
Every ruling is implemented, tested and committed (5919d0234 [4.4],
2dc789ec4 [4.1], 0e8441aca [4.6], b1b78c389 [4.2], 61d028e4e [1.1]+[1.5]).
[2.1] needed no code — see below. [4.5] went moot on its own.
The corpus gate is now MET: validation-parity 190/190, after riddl-models
shipped 99fc29d1 the same day. Three of the four long-standing known-red suites
are green; the fourth is riddl-examples, another repo, and a task has been dropped
there.
All eight questions were answered in one pass. Three confirmed what was built
([4.3]), and five changed it — so this section is now a work list, not a
question list. Each item below records the RULING verbatim in substance, then
what it costs. Struck items are complete.
The rulings, in one place:
| # | Ruling |
|---|---|
[1.1] |
C — keep JSON for hosted models, point self-hosted at GBNF/XGrammar, and DOCUMENT the split |
[2.1] |
Neither A nor B. Drop scaffolding entirely; just allow the Figma URL as an ASSOCIATION on UI elements, and on an application-intent Context |
[4.1] |
B — redefine streamlets to mean all port-bearing processors; delete the accessors added as redundant |
[4.2] |
A, but far broader — typeDeps is a TYPE-DEPENDENCY graph, not a message one |
[4.3] |
A — keep as built. No work. |
[4.4] |
B — count the shape spec for any processor that can ascribe one |
[4.5] |
C — investigate enough to route it; may be moot once rc.15 reaches riddl-models. MOOT, exactly as predicted: riddl-models shipped 99fc29d1 the same day and the corpus is 190/190. |
[4.6] |
C — local always wins, AND warn, and the warning must name ALL sides (there may be more than two) |
-
[4.1]— RULED B, DONEstreamletsmeaning.2dc789ec4:streamletsnow means every PORT-BEARING processor, andprocessors/portBearingare deleted. Reid: "streamlets is now an older idea, but in the new model every processor is capable of having one or more portlets." The AST CONTAINMENT accessorWithStreamlets.streamletsis deliberately UNCHANGED — say so if the ruling was meant to reach it. Superseded reasoning: The [2.4] question, decided the additive way:processorsandportBearingwere ADDED rather than widening the existing accessors. Reasoning: every accessor in that family (domains,contexts,entities, …) is named after a KIND, so widening this one alone would make it the only one whose name does not say what it returns, and would change the answer under existing callers without their asking. The compatibility policy says add, don't change. If you want the other reading, the change is twocollectbodies plus a type on each field; the accessors added here would then be redundant and should be deleted rather than left as synonyms. Landed in2c19d6d70. -
[4.2]— RULED A-but-broader, DONEtypeDeps' meaning.b1b78c389: it is a TYPE-DEPENDENCY graph. Reid's example is the contract — record→set→named-integer — so a consumer can find loops and walk the hierarchy. Reuses the resolver's existing usage edges, folding FIELD-level uses up to the owning type (without that fold the record→set half is simply absent). Known caveat recorded on the field: thetelledges remain, so two processors telling each other's messages read as a cycle here; if a consumer needs purely structural edges the answer is to SPLIT the map, which is worth asking about. Superseded: The field was empty for every model ever analyzed (its guard could not succeed — see the note inDependencyAnalysisPassTest), so filling it required choosing what its source means. I chose "handling PlaceOrder leads to telling ShipOrder", because the handled message is the only Type in atell's surroundings and it matches the field's own documentation, "map from each type to types it references". This makes a public field go from always-empty to populated. No in-repo consumer reads it, so nothing here changes behaviour, but riddl-gen or riddlsim might. If the intended edge was something else, say so and it moves. Landed in1a3c1cf05. -
[4.3]— RULED A: keep as built. The only ruling that confirmed what was there. Superseded: This is a new public field on a pass output, keyed by the statement itself, and it is how MessageFlowPass and DependencyAnalysisPass resolve adeliverableTypeson the pass output.let-local operand without duplicating ValidationPass's scope-threading walk. The design question is whether a pass output is the right place for it, or whether the resolution belongs in a shared utility both passes call directly (which would mean re-walking the statement tree to rebuild theletscope, so I did not). Landed inb6b3dd03e; the shared read path isDeliverableTypes.of. -
[4.4] shape counting.— RULED B, DONE5919d0234: every processor's shape counts, numerator and denominator both, moved into the shared helpers so they cannot drift apart. A Streamlet still counts unconditionally (its shape is required and known even whenascribedShapeis None). Expect maturity percentages to DROP for processors that ascribe no shape — that is the ruling, not a side effect. Superseded: ALONE deliberately in [2.4], because unlike the other sites this is not a narrowing bug — it is a question about what a maturity metric should count. Every Processor may now carry anascribedShape, so a Context or Entity that ascribes one arguably has the same specification to complete. The counter argument is that a Streamlet is the only kind for which a shape is REQUIRED, and a maturity denominator should count required specifications, not available ones. I believe the current behaviour is right and did not change it. Sites:StatsPass.scala:354and:439. -
[4.5]— MOOT 2026-08-17, exactly as Reid predicted when ruling C. riddl-models shippedreactive-bbqnow fails the corpus validation-parity gate.99fc29d1("Upgrade to riddl 2.0.0-rc.15; the corpus validates 188/188 with zero errors") and validation-parity went 187/190 → 190/190. Neither investigation nor routing was needed; the answer was "theirs, and already fixed". The lesson is about sequencing, not about the defect: this was filed as a question at 15:00 and answered by another repo's commit before anyone looked at it. When a corpus number moves and the corpus is a live checkout, WAIT one beat before spending effort attributing it. Original analysis: Re-measured 2026-08-17: 187/190, not the 188 recorded in [3.4]. The two new errors are context-isolation-seam errors ("receiveDrinkOrder is not declared in a domain ancestral to both"), a different class from the bare-message-operand errors the other two models carry. Verified NOT caused by anything this session landed — measured with my changes stashed, giving the identical 187 and the identical three model names. So it is either drift in../riddl-models(a live checkout another session edits) or a pre-existing seam-check defect. Deciding that needs a look at the model, which is another repo's business — hence a question rather than a task. If it is ours, it is a real Error being reported on a correct model, which is the false-positive shape this repo has been bitten by repeatedly.