unimsg provisional

Where it is wrong

docs/16-format-weaknesses.umsg, rendered unedited.

Rendered from docs/16-format-weaknesses.umsg raw%unimsg 0 findings/format-weaknesses/v0. The document itself is one click away.

Read the source instead, with every construct explained →

Sections appear in the order the document was written in. That order is not in the encoding: canonical ordering sorts map keys, so it travels beside the value and survives a text round trip only.

title

Sixteen weaknesses, found by writing documents, by two blind implementations, by rendering them all, and by running the checks on the reference

status

:closed

closed-on

2026-08-26

closing-note

All sixteen, and the count and the date both moved after this note was first written: fifteen were closed on 2026-08-23, and F31 arrived on 2026-08-26 already closed, from a 1.9 MB document the corpus had not held before. THE HEADER SAID FIFTEEN FOR A WEEK AFTER THERE WERE SIXTEEN, which is the same failure implementations/README.md records against its own status table: nothing checks a count written by hand.

Ten of them were closed in one session, and the pattern in what was found is worth more than any single fix: EVERY defect of the last five lived where no check could see it. The authored order, the escapes, the comment binding, the key types and the vectors that had never been run against the reference were all invisible to a harness that compared bytes between implementations, because none of what they govern reaches the bytes.

Three checks now cover that ground — tools/order, tools/binding and the reference's own row in check.sh — and the repository-wide habit of formatting every document twice and comparing found two of the five on its own.

found-by

authoring ten documents in one session

continues

docs/13-implementation-findings.md, F1 to F15

summary

idweaknesskindcostsfix-is
:F16canonical ordering destroys column order:closed:highauthored order kept out of band, and a decoded document now says its order is not the author's
:F17references have no defined target:closed:highrooted at the sole block; external marks the rest
:F18the bad-header diagnosis survived its own fix:closed:mediumdone
:F19no unit binding on a number:closed:lowthe shape is normative; the set is a vocabulary
:F20hand alignment is decorative:closed:nonerecorded; no action
:F21no way to mark what is binding:closed:higha vocabulary of qualifiers; tools/binding reads it
:F22a time of day has no representation:closed:mediuma literal, sharing the timestamp tag
:F23the reference rejects two of the spec vectors:closed:highthe grammar was right; the vectors were wrong
:F24the reference rejects a byte-order mark:closed:lowdone; one lexer case
:F25the authored order was specified, not kept:closed:highdone; a third harness check
:F26the reference emitted an escape it refuses:closed:highdone; five escapes and no others
:F27a trailing comment on a table row was refused:closed:highdone; the corpus denominator is whole
:F28what a key may be had three different answers:closed:highdecided by layer; four open corpus cases closed
:F29the table was the one container needing a trailing separator:closed:mediumthe production was the defect; rust and dart follow
:F30the vectors had never been run against the reference:closed:highone lexer rule; the harness now measures the reference
:F31the timestamp lookahead is quadratic in three languages:closed:higha bounded window in go and dart; js had already found it; five were immune
:F32a table of records encodes larger than its text:closed:mediuma typed array per column; measured at 1.47x in docs/52 P3, 0.30x after docs/54
:F33a fixed-arena implementation has a document-size ceiling the profile does not state:closed:lowdocs/52 P6: honest, positioned, and now a note the guide's third constant covers
:F34the canonical text form was never written down, and no formatter matched the reference:closed:highformatting.layout in the specification, 2026-09-03; tools/fmtcompare is the gate
:F35the reference emitted keys and table headers it could not read back:closed:higha key is bare only when it LEXES as one word; tools/roundtrip re-reads all 335 documents
:F36one-text-per-float did not say which of two equally short digit strings wins:closed:mediumthe nearer, and an exact tie to the even last digit; both neighbours are tried at each length

F16

title

Canonical key ordering destroys authored column order

kind

:tension

what

Table columns are map keys, and D2 sorts map keys, so a table authored as id, what, bytes renders as bytes, id, what.

evidence

In the PostgreSQL protocol document, a vector table written identifier-first renders with an eighty-one character hexadecimal string in the first column, pushing the identifier and the description off the width of the page. Verified by rendering it.

why-it-matters

In a specification, column order carries meaning: identifier, then what was given, then what is expected. That is the reading order of a vector, and losing it makes the table materially harder to use — which is the one thing a vector table exists for.

the-tension

Sorting is not a mistake. It exists so that two documents which decode identically encode identically, which is what makes an address stable and a diff meaningful. The ordering decision was taken deliberately and should stand.

proposed

Separate the two concerns. The *encoding* must sort — that is D2 and it is right. The *presentation* need not. A table can carry its authored column order as data, and a renderer can honour it while the canonical bytes stay sorted.

This costs one field in the table's representation and a change in the renderer. It changes no encoding and breaks no implementation.

resolved

Neither of the two options in the open question below. The format already had a third place for authored metadata that must not affect identity — where comments live — and the column order belongs there. A document now carries Columns, keyed by path, outside the value. The encoding still sorts; the presentation no longer has to.

The open question was badly framed, and framing it as document-or-renderer is what hid the answer for a day: both options assumed the order had to live somewhere that already existed in the value or the tool, and the precedent was in neither.

the-cost

The order survives a text round trip and not an encode and decode. A document read back from CBOR has canonical columns and no way to know what its author intended — exactly as it has no comments. Stated in the specification rather than discovered.

AND IT NOW SAYS SO. issues/001 reproduced the loss and reported that nothing distinguished a recovered order from an invented one: three orders exist for a document — authored, canonical-binary, and the presentation fallback — and a reader of decoded text saw the third with no marker. A decoded document now carries Authored false, and umsg dec says in a comment that the column order below it is the renderer’s rather than the author’s.

THE REPORT ALSO CLAIMED THIS ENTRY SAID :closed AND STOPPED, WHICH IS WRONG — this field has named the residual since the commit that resolved F16, and is quoted in the issue’s own related table. What was missing was not the sentence but the ability of a decoded document to say the same thing to somebody who never reads this file.

found-while-implementing

Sequence elements had no indexed path, so two tables in one sequence both recorded their order under the same key and the second overwrote the first. Elements now carry the a.b[2].c form that paths already take elsewhere.

superseded-open-question

Whether the authored order belongs in the document, which makes it data and therefore hashable, or in the renderer as a heuristic. Data is more honest and more work.

F17

title

Cross-document references are decorative

kind

:gap

what

A reference such as continues -> @systems/postgresql-types/v0 is parsed, stored and rendered, and resolved by nothing. No tool checks the target exists.

evidence

Three such references were written today between the PostgreSQL documents. A reference to a document that had never been written would have been accepted identically, and nothing anywhere would have said so.

why-it-matters

The examples directory is now a suite of linked specifications rather than a set of samples. A suite whose links are unchecked drifts silently, which is the failure this project has spent a session learning to distrust.

what-building-it-found

The checker was written, and it changed the finding. Over eighteen documents it saw nineteen references, and they fall into three groups rather than two:

2 resolve to an @identifier declared somewhere 7 match no identifier, but match a map key 10 match nothing in the set at all

The seven are the interesting ones. A document writes kitāb { … } — a pair whose key is a word — and elsewhere writes -> @kitāb meaning that entry. Nothing is wrong with either line, and nothing in the format says whether a reference denotes an identifier, a key, or something else.

so-it-is-a-decision-first

The tool is not the finding. The finding is that a reference has no defined target, and it went unnoticed because nothing ever tried to follow one. A checker written before that is settled would be encoding a guess about semantics into a tool, which is how a convention gets established by accident.

the-questions
questionwhy-it-matters
does a reference denote an @identifier, a key, or either?the seven by-key references are correct or incorrect depending on the answer
may a reference point outside the document set?ten do; @cust-4471 is illustrative and resolving it is not the point
if so, how is that declared, so a checker can tell it from a mistake?otherwise every check is noise and gets ignored
is a reference scoped to its document, or global across a set?two documents may both declare @v1
decided

A reference with no dot denotes an @identifier. A reference with a dot denotes a path from the root of its own document, each segment a map key. Both already parsed; nothing had said what they meant. Written into spec/unimsg-v0.umsg, and tools/links now checks the two forms separately rather than guessing between them.

Cross-document references therefore use an identifier, since a path is rooted at its own document. That answers the fourth question — scope — as a consequence rather than as a separate ruling.

the-consequence-worth-knowing

A table row cannot be referenced. A path segment is a map key and a row is a sequence element, and an identifier written in a row annotates that row's first value rather than naming the row. So a collection whose members will be referenced must be a map keyed by their names, not a table. Recorded in the specification, because it decides document shape and is invisible until the first reference is needed.

what-the-check-then-found

Converting examples/morphology.umsg to paths left five references still unresolved, and they are not conversion errors: the document references the patterns faʿal, faʿala, fāʿil, fuʿūl and fuʿʿāl and defines none of them. It declares itself a fragment, so this may be intentional — but nothing said so, and nothing could have noticed until a checker existed.

That is the finding the tool was built for, and it arrived on the first run.

was-still-open

Whether a reference may point outside the document set, and how a document declares that it does. Ten references currently do not resolve: five are the patterns above, one is a table row that cannot be addressed, and four are illustrative — the @cust-4471 in reference.umsg and sigils.umsg is a customer id in an example and resolving it was never the point. Until a document can say which references are external, a checker cannot tell those four from the five.

resolved

TWO ANSWERS, AND THE FIRST ONE WAS NOT THE QUESTION ANYBODY WAS ASKING.

WHERE A PATH IS ROOTED. The rule said a path from the root of the document, and every author wrote paths from inside the document's sole top-level block instead — @sources.x inside language { … }, never @language.sources.x. Measured across fifty-seven documents: fourteen references resolved by the rule as written, one hundred and forty-two only from inside the block, and five neither way. So the rule now roots a path at the document AND at its sole top-level block when there is exactly one, which keeps resolution deterministic and stops the checker reporting a hundred and forty-two false failures.

HOW A DOCUMENT SAYS A REFERENCE IS EXTERNAL. external, as a qualifier on the reference:

accession external -> @genbank/OQ441238

Fifteen references are marked so far — a customer id, a ship's IMO number, a GenBank accession, two dictionaries, the IPA, a DOI — and each is a real identifier in a system this repository does not contain. It is the same mechanism F19 and F21 arrived at, which is becoming the answer to most questions of this shape.

what-the-checker-says-now

239 resolve, 15 external, 37 do not — against 97 resolve and 194 not, before either change. The thirty-seven are now QUESTIONS rather than noise: the five morphology patterns the document references and does not define, references to documents that have not been written, and a handful of paths that name nothing. That is a list somebody can work through, which the previous one was not.

status

:closed

F18

title

The bad-header diagnosis survived its own fix

kind

:defect

what

A table header cell containing a space silently becomes several columns. The error then arrives on the *following* row as a cell-count mismatch, naming neither the header nor the cause.

evidence

Encountered four times in one day, in four different documents, by the same author. Quoted header cells were then added to the format so the construct is expressible — and the misleading error for the unquoted case was left exactly as it was.

why-it-matters

The fix gave the format a way to say the thing. It did nothing for the person who does not yet know that, which is everyone the first time. Four occurrences in one day by someone who had already diagnosed it once is a property of the diagnosis, not of the author.

proposed

Detect the shape at the header rather than at the first row. When a header is followed by rows that consistently supply fewer cells than the header declares, say so at the header, name the probable cause, and show the quoting escape.

smallest-version

Even without detection, the existing error could name the header's cells. header declares 8: hazard, what, happens is enough for anyone to see it.

done

The error now names every column and, when a header declares more columns than any row supplies and none is quoted, guesses the cause and shows the escape. The guess is suppressed when a header already contains a quoted phrase, because a short row there is a different mistake and the hint would send the reader wrong. Both behaviours are pinned by a test.

status

:closed

F19

title

A number carries no unit

kind

:absence

what

Documents state quantities and describe their units in prose or in a separate block. Nothing binds the two, and nothing checks them.

evidence

The prayer times document carries angles in degrees and minutes of interval; the solar system document carries astronomical units, days, kilograms and degrees; the tones primitive carries milliseconds and hertz. Each invented its own convention — a units block, a suffixed column name, or prose — and no two agree.

why-it-matters

Less than it first appears. A wrong unit is a document error, and the format's job is to carry what a document says rather than to adjudicate it. But three documents inventing three conventions in one day suggests the absence is felt.

the-argument-against

A unit system in the format would be policy, and the format holds that policy belongs in documents. The consistent answer is a *profile* that defines a unit convention, which documents may adopt and tools may check — mechanism in the format, policy in a composable extension.

proposed

Not a format change. A profile, and a suffix convention such as mass-kg that a checker could read. Low priority, and worth recording so the next document does not invent a fourth convention.

resolved

NOT A SUFFIX AND NOT A PROFILE. mass kg 5.97e24 — the unit is a qualifier on the value, which is the mechanism the format already had. It parsed, encoded and round-tripped before anything was written down; what was missing was anybody saying it was THE way.

THE SHAPE IS NORMATIVE AND THE SET IS NOT, which is the division that took the longest to see. D12 fixes the shape in the specification, so what unit is this quantity in is answerable for any document by anything. vocab/qualifiers/units-v0.umsg carries a set — SI base units, the common derived ones, and the two that collide, since min is time and arcmin is angle and one document here uses both.

A LIST OF UNITS IN THE KERNEL WOULD BE A REGISTRY, and a registry is governance, which this project deleted on purpose when it made tags hash-derived. A document may use a unit the set does not name; what adopting the vocabulary buys is that two documents which both declare it agree about what kg means.

status

:closed

F20

title

Hand alignment is decorative

kind

:accepted

what

Columns aligned by hand are reflowed by canonical formatting. The source written and the source returned differ in whitespace.

why-this-is-fine

It follows from there being exactly one canonical rendering, which is what makes diffs stable and stops anyone arguing about style. The alternative is worse.

proposed

Nothing. Record it so that effort spent aligning a table is spent knowingly.

status

:closed

F21

title

There is no way to mark what is binding

kind

:design-gap

what

A specification written in unimsg has no convention distinguishing a normative statement from commentary. Tables and paragraphs are equally easy to write and equally weightless.

evidence

Every specification written in this project invented its own marker, independently and inconsistently: status :normative on one block, must-reproduce and free-to-replace lists in another, prose saying an implementation conforms if in a third. No two used the same mechanism, and nothing reads any of them.

why-it-matters-most

This is the one item on the list that is a decision rather than a task, and it is the one with the largest consequence. The store specification's twenty-four prose vectors were all satisfied by two implementations that could not read each other's descriptors. The vectors read exactly like the commentary around them, and there was no way — for a reader or a tool — to tell which sentences an implementation was obliged to satisfy.

what-it-is-not

Not a request for a schema language. The format's whole position is that it carries what documents say. But this sentence binds an implementer is a thing documents need to say, and saying it the same way twice has value that saying it eleven ways does not.

options
optiongivescosts
a reserved key, such as normative truegreppable, checkable, minimalconvention only; nothing enforces it
an extension, !spec/normativetyped, and degradable like any extensionverbose on every block
a profile defining conformance vocabularycomposable, and the format's own answera profile to write and agree
nothing, and rely on section namesno workwhat happened, eleven times
recommendation

The profile. It is the mechanism the format already has for exactly this — policy in a composable extension rather than in the kernel — and it would let a checker answer which statements in this document must an implementation satisfy, which is currently unanswerable.

resolved

A VOCABULARY RATHER THAN A PROFILE, because the repository grew a vocabulary directory while this finding sat open and that is where a shared meaning now lives: vocab/qualifiers/conformance-v0.umsg, identified @vocab/conformance/v0.

AND THE MECHANISM TURNED OUT TO ALREADY EXIST, which is why none of the four options in the table above was taken as written. A bare word in front of a value is an annotation and can be nothing else — the format calls it a QUALIFIER, and binding is exactly a qualifier. So a rule is marked by writing one word in front of it, with no new syntax, no restructuring of statements into blocks, and no wrapper on every paragraph:

duplicate-keys-are-an-error normative "In any map, at any depth …"

Four qualifiers — :normative, :vector, :unresolved, :commentary — and silence binds nobody. A qualifier on a block covers every statement inside it and an inner one narrows it, which is what makes a normative section cost one word rather than one per sentence.

UNRESOLVED EARNED ITS PLACE by being missing. An open question recorded among the rules reads exactly like a rule, which is how two settled findings in this very document sat marked blocks-v1 true for a fortnight after both were fixed.

what-it-answers

tools/binding, over spec/unimsg-v0.umsg: 71 binding statements — 70 normative and one vector table — one unresolved, and fifteen commentary. It names the unresolved one, which is whether a table's final row needs a separator.

The question which statements must an implementation satisfy took a day to become answerable and about a second to answer. It refuses three things as well: two qualifiers on one statement, a :vector that is not a table, and a :normative statement that says nothing. The middle one has the expensive history — twenty-four vectors written as prose, satisfied by two implementations that could not read each other.

the-adoption-is-incremental-and-that-is-safe

Because silence binds nobody, a document may be marked a section at a time without any intermediate state being a lie. What is unmarked claims nothing rather than claiming to be optional — which is the property that made it possible to adopt this in the specification in one pass instead of arguing sentence by sentence.

status

:closed

F22

title

A time of day has no representation

kind

:absence

what

The timestamp production requires at least YYYY-MM, so a bare clock time is not a temporal value. It is not a number either: 03:52 begins with a digit, so it lexes as a number and fails on the colon. The only way to write one is as text.

evidence

Found while writing examples/karst-way-sensorium.umsg. A soundscape table recorded acoustic indices against the time of day they were measured, and 03:52 was refused with "03:52" is not a valid number or word. The same document already quoted "25:1", "1:1" and "1/640" for the same lexical reason, which is exactly why the bare one was easy to miss: the file looked consistent with itself.

why-it-matters

A recurring daily time is ordinary data — opening hours, watch changes, prayer times, a departure board, a sampling schedule. examples/prayer-times.umsg is entirely about times of day and carries them as minutes and as text, inventing its own convention exactly as F19 describes.

Quoting works, and loses the thing that makes a timestamp worth having. A decoder cannot tell an instant from a label, so nothing can order, compare or validate them.

why-the-rule-exists

The minimum of YYYY-MM is deliberate and correct. It is what keeps a bare year an integer, so 1889 is a number and not a date. The gap is at the other end of the production, not in that rule.

options
optiongivescosts
nothing; quote itno workwhat happens today, and it is silently lossy
a time production, 03:52 and 03:52:07symmetric with the date forma new lexical shape, and it must be tried before numbers as dates already are
a duration form as wellanswers the sibling absencemore surface, and ISO 8601 durations are their own argument
a profile defining a time conventionthe format's own answer, as with F19a profile to write, and it stays text in the kernel
recommendation

Decide it with F19 rather than separately. Both are the same shape — a quantity the format can carry but cannot name — and answering one without the other will leave the project with two conventions where it already has too many.

resolved

DECIDED WITH F19 AND ANSWERED DIFFERENTLY, because they turned out not to be the same shape at all. A unit is SEMANTIC: the number can be written and nothing says what it means, which a vocabulary fixes. A time of day is LEXICAL: 03:52 cannot be written, and no vocabulary can fill a hole in the lexer.

So a time of day is now a literal — HH:MM, with optional seconds and a fraction — sharing the timestamp tag, because a time of day is a temporal value at a precision rather than a new kind of thing. It is stored verbatim exactly as 2026-08 is a month and never a widened instant.

IT COST LESS THAN IT LOOKED. No new type, no new tag, no encoder change: one lexer rule and one grammar production in each of nine implementations. And it is a PURE EXTENSION — every colon form was a syntax error beforehand, so no document that parsed can change meaning.

Two details, both settled by precedent rather than by taste. NO OFFSET, because a time carrying a zone but no date is a confusing object and what opening hours and prayer times need is the wall clock. RANGES ARE NOT CHECKED, matching the date form, which accepts 2026-13-45 today: both are shapes rather than assertions about the calendar. Two digits for the minute is what keeps ratios out — 25:1 and 1:1 are quoted strings in karst-way-sensorium and stay that way.

status

:closed

F23

title

The reference rejects two of the specification's own vectors

kind

:defect

what

spec/unimsg-v0.umsg lists x [ | a b || 1 2 ] and its quoted variant among the conformance vectors, each with a canonical encoding. The reference implementation refuses both.

evidence

unimsg encode reports "table headers must be bare or quoted keys" at the second pipe. The parser's table() reads header cells until a separator, and a second pipe on the same line is not a separator. Found by running all twenty-eight vectors through the reference while assessing the first blind implementation, which failed exactly these two and no others.

why-it-matters-most

This is the only item on this list that makes the specification untrue about itself. The vectors are described in that document as the part of the conformance criterion that travels with it — the thing an implementer with no repository checks against. Two of them cannot be produced by the reference. So an implementer who satisfies all twenty-eight disagrees with the reference, and one who agrees with the reference fails the specification.

what-has-to-be-decided

Whether a row may begin on the same line as the header. The grammar says row is '|' , { cell } , separator, which reads as requiring a separator and therefore agrees with the reference. The vectors say otherwise. One of the two is wrong and nothing in the document says which.

note

Both blind implementations reproduced the reference's behaviour rather than the vectors', working from example documents alone. That is evidence about which reading is natural, not about which was intended.

resolved

The grammar was right and the vectors were wrong. Both were rewritten with an explicit separator — x [ | a b, | 1 2, ] — and every maintained implementation now passes all twenty-nine. Verified 2026-08-23 by running the table: 29/29 on all eight.

WHAT THIS DID NOT SETTLE is the sibling question, which is still open and is recorded in the specification rather than here: whether the FINAL row needs a separator before the closing bracket. Rust and Dart refuse x [ | a b, | 1 2 ]; the reference and six others accept it. Rewriting the vectors moved this document out of the way of that decision without taking it.

status

:closed

F24

title

The reference rejects a byte-order mark

kind

:defect

what

The specification's byte-order-mark block says permitted true, ignored true. The reference reports 1:1: unexpected character '\u{feff}'. The lexer has no case for U+FEFF.

evidence

Found while assessing the first blind implementation, which also rejected a byte-order mark — and was marked down for it against a specification its own reference does not implement either.

why-it-matters

Small, and cheap. It is listed because it is a second instance of F23's class: the specification and its reference disagree, and the disagreement was found by an outsider rather than by either. A specification that cannot be trusted where it is explicit is worse than one that is silent, because silence prompts a question and a false statement does not.

fix

One case in the lexer, skipping a leading U+FEFF before the first token. The wording already says what the behaviour should be, so nothing needs deciding.

resolved

Done. The reference accepts a leading byte-order mark and ignores it, as the specification always said it should. Verified 2026-08-23.

status

:closed

F25

title

The authored order was specified and then not kept

kind

:defect

what

F16 was closed by a decision — a document records the order its table columns were written in, out of band, and the encoding still sorts. The specification says so. The reference formatter never read the record: it sorted every block and every table it rendered, so umsg fmt scrambled the column order of any document it touched, including this one and the specification itself.

evidence

A four-line document with columns written id, given, expect came back expect, given, id, and a block written zebra, alpha came back alpha, zebra. The record was in the parse the whole time — Columns and Order, keyed by path — and only the website renderer and the readme generator ever read it.

why-it-matters

THE ORDER SURVIVES A TEXT ROUND TRIP AND NOTHING ELSE, and the formatter IS the text round trip. A formatter that sorts regardless is not one holding a different opinion about presentation; it is the thing that destroys what the record exists to keep. Three implementations — dotnet, kotlin, swift — restored a table's columns and were right against the reference, which is the wrong way round for a reference.

It is F23's class again, and worse: not a specification that disagrees with its reference on a lexical corner, but a decision taken deliberately, written down in full, and then absent from the thing that was supposed to carry it.

what-the-fix-found

The same loss one level up, which nothing had noticed. A table's columns are map keys and lose their order to D2 — and so does every other block, because a block's pairs are map keys too. Only a table displays its keys side by side where the loss is obvious. A specification rendered in canonical key order reads conformance, grammar, kernel, scope: four sections in an order nobody chose, leaving the reader to reassemble the argument. The rule now covers both, and the reference records both.

why-nobody-caught-it

Nothing in the harness had ever run a formatter. compare and vectors both go through the ENCODER, and the authored order deliberately never reaches the bytes — so four implementations whose formatters scrambled every document they touched passed every check the repository had, perfectly. The gap was exactly the shape of the rule.

tools/order closes it: it formats one fixture whose every order sorts differently from the way it was written, re-parses the output with the reference parser, and compares the recorded orders rather than the text — so an implementation that pads its columns differently still passes, and one that sorts cannot.

resolved

The rule is generalised and normative in spec/unimsg-v0.umsg (authored-order-is-kept-beside-the-value, and formatting.ordering), the reference honours it, and all eight maintained implementations keep it: 3/3 orderings on every one. The record may be partial — a document edited between the parse and the format keeps the order of the keys it recorded and lets the rest follow — because the strict alternative throws the whole authored order away on the first pair anything adds.

the-cost

Unchanged from F16, and now stated for blocks as well as columns: the order survives a text round trip and does not survive an encode and decode. A document read back from CBOR has canonical order throughout and no way to know what its author intended, exactly as it has no comments.

F26

title

The reference emitted an escape its own parser refuses

kind

:defect

what

The lexical section defines five escapes — \n, \t, \r, \" and \\ — and says any other escape is an error. The reference formatter quoted strings with Go's strconv.Quote, which escapes anything Go considers unprintable, so a RIGHT-TO-LEFT MARK came back as a \u escape. The reference then refused to read its own output.

evidence

Formatting examples/language-msa.umsg and formatting the result again: line 225 col 29, unknown escape "u". Found by formatting all fifty-four documents in the repository twice and comparing, which is a check nothing had run.

why-it-matters

It breaks the property the format is FOR. Text to CBOR to text is claimed to be lossless, and here text to text did not survive one pass. An Arabic document is not an exotic case in this repository — there are three, and directional marks are ordinary content in them, invisible in an editor and fatal on the way back in.

It is also the F25 shape again: the specification was explicit, the reference did something else, and the fleet was right against it. Every other implementation emitted the character raw. The reference was alone, and it is the one the others are measured against.

the-fix

A quoter that emits the five escapes and passes everything else through as written, control characters included — which is what the escape list says by omission, and what the other five implementations do. Whether a document SHOULD carry a bare control character is a different question, open in the corpus as txt-005, and not one a formatter answers by mangling the value it was given.

guarded-by

Two tests in syntax: one that no invented escape is ever emitted and the output re-parses to the same value, one that the five that must be escaped still are. The repository-wide check — format every document twice, compare, and re-parse — is worth keeping as a habit; it found this and it found nothing else.

status

:closed

F27

title

A trailing comment on a table row was refused

kind

:defect

what

The specification says a comment following a value on the same line binds to the pair just read and TERMINATES THAT PAIR AS A SEPARATOR WOULD, and that trailing comments are legal. Three implementations applied that to pairs and not to table rows: the reference read on past the comment looking for cells, and the Go and JavaScript implementations left it sitting where the next row should start.

evidence

The reference reported row has more cells than the 3 header columns; Go and JavaScript reported expected closing ] for table. All three name a token and none names the cause.

examples/self-describing-2.umsg has one such row — a retest table with -- registrar signed off after the cells — and has therefore never parsed. tools/compare skips a document the reference cannot encode, so the harness reported 39 of 40 and read as a pass. The README explained the missing document as a property of the corpus rather than as a defect.

why-it-matters

A construct the specification calls legal, refused by the reference, found by nothing, and papered over by a denominator. The fleet split six to three, and the six were right — which is the F25 and F26 shape for the third time in one day: the specification says one thing, the reference does another, and the harness cannot see the difference because it only ever compares bytes.

what-the-fix-found

Comments bound to a sequence ELEMENT were recorded by nothing and rendered by nothing, so they vanished on the first format. D6 says comments survive text to text and they did not. The whole repository turned out to contain exactly one such comment — the one in the document that would not parse — which is why the loss had never been seen.

So a comment now binds to the element it precedes or the row it follows, and a sequence carrying comments renders as neither a table nor a single line, because neither has anywhere to put them. That is the rule blocks have always obeyed, arriving at sequences.

the-denominator-is-whole

All eight maintained implementations now agree on 40 of 40 documents. It was 39 of 40 for as long as the defect existed, and nothing said which document was missing or why.

status

:closed

F28

title

What a map key may be had three different answers

kind

:defect

what

a1 01 02 is three bytes of ordinary CBOR — a map with the integer key 1, the shape COSE and CWT are built from. profiles/v0 said in writing that integer keys were allowed. Six implementations disagreed about it in four different ways, and one of them crashed.

evidence
implementationbehaviour
:referencerefused at the decoder: map keys are text
:rustrefused at the decoder: map keys must be text strings
:jsrefused at the decoder: expected text key in map
:dartrefused at the decoder: map keys must be text strings
:kotlinClassCastException, escaping to the top of the process
:dotnetdecoded it, then refused to render it as text
:esp32decoded it, then refused to render it as text
:swiftdecoded it, then refused to render it as text
why-it-matters

Four documents in this repository answered the question and no two agreed: the grammar said map, text keys, D10 said float keys were legal in the kernel, profiles/v0 allowed text, bytes and integers, and the corpus expected a float key to round trip exactly. Asking what may a key be without saying WHERE produced four answers that all sounded like the same answer.

And a crash is not a decode error. The specification requires decoding to be total; an unhandled cast escaping the process is the one outcome no rule permits.

resolved

THREE LAYERS, THREE ANSWERS, EACH WRITTEN DOWN. The kernel admits any canonical value as a key, and D1 settles equality by canonical bytes. The v0 profile admits text and integers. The text syntax spells a key as a word or a quoted string and has a spelling for nothing else — so a document with an integer key is legal, encodes, decodes, hashes, and HAS NO TEXT FORM, exactly as a document read back from CBOR has no comments.

A formatter refuses what it cannot spell rather than inventing a spelling, because "1" 2 re-reads as a TEXT key and encodes to different bytes — a round trip that silently changes the document. The reference had been doing something worse than either: rendering {1: 2} as 1 null.

All eight implementations now decode an integer key and refuse it at the text form, with a message that says which layer refused and why.

what-a-value-model-costs

AND THE LIMIT IS RECORDED RATHER THAN HIDDEN. The reference holds values in a Go map, so it cannot represent a float key at all: -0.0 == 0.0 there, so f90000 and f98000 arrive as one entry and a pair is lost in silence. A byte-string key is unhashable outright and panics.

So the reference refuses both AT THE DECODER, and that is a representation limit rather than a profile check — the specification now distinguishes the two, because pretending it was the profile talking would hide a limit behind a rule. Reaching the full kernel needs an ordered list of entries instead of a Go map, which is the model every other implementation already has, and is a larger change than this one.

what-it-closed

Four of the six open corpus cases the runner exercised — key-001, key-002, key-007 and key-008 — were all a non-text key, and one ruling answered all four. The runner reports 39 pass, 0 fail, 2 open, against 34 pass and 6 open before. A new case, key-009, pins the integer key that started it.

status

:closed

F29

title

The table was the one container needing a trailing separator

kind

:tension

what

x [ | a b, | 1 2 ] — a closing bracket directly after the final cell. Seven implementations accepted it; rust and dart refused it, on the grammar, which wrote a row as '|' , { cell } , separator.

what-decided-it

Asking the two strict implementations the same question about the other two containers. Rust accepts x [ 1, 2 ] and x { a 1, b 2 } and refused only the table — so the strict reading was not a principle either of them held. It was one EBNF line leaking into one code path.

The map rule carries a { separator } tail and so does the sequence rule. The row rule was written without one, which made the table the single place in the format where a closing bracket could not follow the last item. That is an oversight in a production, not a decision.

resolved

Lenient, and the grammar corrected: a separator goes BETWEEN rows and the bracket ends the last one. Rust and dart follow; the other seven were already right. x [ | a b | 1 2 ], with nothing between the two rows, is still malformed — the precedent a-separator-between-items-is-required is about BETWEEN, and always was.

A conformance vector now carries the form, so it travels with the specification rather than living in a note about what nobody had decided. The specification's UNRESOLVED count is zero for the first time: tools/binding reports 74 binding, 0 unresolved.

status

:closed

F31

title

The timestamp lookahead is quadratic wherever the rest of the input is a copy

kind

:defect

what

The lexer tries a timestamp before a number, and both patterns are anchored. The obvious implementation matches them against THE REST OF THE DOCUMENT — and does so at every token that could begin a number.

IN GO THAT WAS string(l.src[l.pos:]), a rune slice converted to a string, which copies. In dart it was source.substring(offset), twice per candidate. Both are O(n) per token and therefore O(n squared) per document.

IT IS INVISIBLE UNTIL A DOCUMENT IS BIG. Every document in this repository was small until a 1.9 MB vector graphic arrived: the go reference took over ten minutes and did not finish; a 675 KB slice of it took 132 seconds. Dart parsed an 80 KB scene in 110 ms while parsing 1.8 KB in 0.22 — which is not the same rate, and that discrepancy is what exposed it.

the-tally-is-the-finding
implementationhow-it-reads-the-restoutcome
gostring(src[pos:]) — a rune slice, copiedhad it. Fixed: a 64-rune window, re-tried unbounded when a match fills it. 132 s became 192 ms
dartsource.substring(offset), twicehad it. Fixed the same way. 110 ms became 20 ms, and the rate is flat from 7 KB to 2.6 MB
jsinput.slice(pos).join()had it AND had already fixed it, independently, with a growing window. Its comment records the symptom: a 3 MB document spending thirteen seconds rebuilding its own tail
rust&source[at..] — a borrowimmune. The same expression that copies in go is a view in rust, at no cost
kotlinRegex.matchAt(s, i)immune. The regex API takes a start index, so nothing is sliced
dotnetTryTimestamp(_i, out int end)immune. Index-based by construction
swifta hand-written scalar matcher over an indeximmune. No regex, so no string to match against
esp32pointer and lengthimmune by the nature of C. Not separately measured
what-it-says-about-the-specification

THREE OF EIGHT FELL IN, AND THEY ARE EXACTLY THE THREE WHOSE LANGUAGES MAKE THE REST OF THE INPUT A COPY. That is not a fact about three implementers' care. It is a fact about a rule written as tried before numbers beside an anchored pattern, which maps onto match against what is left — free in rust and C, free in kotlin and dotnet because their matchers take an index, and quadratic in go, dart and javascript because the idiomatic spelling allocates.

THE SPECIFICATION NEVER SAYS THE MATCH IS BOUNDED, though it obviously is: the pattern cannot consume more than a few dozen characters. Saying so costs one sentence and removes the trap for every future implementation, which is what the lexical section now does.

resolved

The two defective implementations bound the match to a window and re-try unbounded only when a match fills it, so what matches is exactly what matched before — a speed fix and not a language change. Go and dart both pass their conformance suites unchanged.

AND THE CORPUS NOW HAS A LARGE DOCUMENT, which is the part that matters more than either fix. Every case in it was small on purpose, and a defect that only appears at scale cannot be caught by a suite that never reaches it.

status

:closed

F30

title

The conformance vectors had never been run against the reference

kind

:defect

what

a 0f is a vector in the specification's own conformance table, with canonical bytes beside it. Every implementation passed it. The reference refused it: "0f" is not a valid number or word.

evidence

Found by adding a vector for F29 and running the table against the reference — which turned out to be the first time anyone had. 30/30 everywhere else, 29/30 there.

why-it-happened

The harness measures implementations AGAINST the reference, so running the reference against itself looked like it could only ever pass. That is true of compare and false of the vectors: the vectors are written in the SPECIFICATION, they travel with it, and they can therefore disagree with the reference. Nothing in the repository ran them there, so a divergence between the document and its own reference had no way to surface.

It is F25's shape once more. Every one of these — the authored order, the escape, the row comment, the key types — was invisible to a harness that only compared bytes between implementations.

resolved

One lexer rule: the float marker attaches to ANY numeric form, so 0f and 2f are floats exactly as 2.5f is. The specification already said so outright, in a paragraph written after two implementations made the same mistake — and the reference was the third, unnoticed, because the paragraph had a vector and the vector had no runner.

check.sh now measures the reference: vectors and order, no compare line, because comparing the reference with itself answers nothing.

status

:closed

what-held-up

doc

Recorded because a list of weaknesses read alone is misleading about the whole.

tables-transfer

A Flutter implementation built from a specification reproduced twenty-three of twenty-three colour values exactly, and reproduced nothing at all from prose. The table form is the format's strongest property and the measurement is external.

extensions-degrade

A tones block carries chord, voice and bar columns the renderer knows nothing about, ignores, and preserves. One primitive served three musical documents with three different vocabularies.

comments-travel

Binding a comment to a path rather than a line means it survives reordering, which matters in a format that reorders by design.

prose-is-comfortable

Unexpected. Thousands of words of reasoning across ten documents, in multi-line strings, without fighting the syntax.

ordering

done

:F16, :F17, :F18, :F19, :F20, :F21, :F22, :F23, :F24, :F25, :F26, :F27, :F28, :F29, :F32, :F33, :F34, :F35, :F36, :F30, :F31

blocks-v1

empty sequence

do-first

empty sequence

why

F21 was first, because it was a decision and everything written before it was decided would need revisiting. It is done, and it cost no new syntax: the format already had qualifiers.

F23 was first before that, and is done: the grammar was right and the vectors were wrong. F16 was cheap, changed no encoding, and is done.

F17 moved from a task to a decision when the checker was built. F22 and F19 are the same absence wearing two hats and should be answered together, not in sequence.

F25 is done, and is the reason F16 should not be read as finished business: a decision recorded and not implemented looked exactly like a decision implemented, for as long as nothing checked. What closed it was the check, not the fix.

then

empty sequence

eventually

empty sequence

never

:F20

toward-v1

F23 and F24 are closed, and F25, F26 and F27 closed three defects of the same class that were found by rendering documents rather than by encoding them.

F21 is done too, and with it the specification can now say which of its own sentences bind: 71 binding, one unresolved, fifteen commentary, answered by tools/binding rather than by reading English.

WHAT REMAINS IS DECISIONS. F17's remaining question — how a document declares an external reference — should close next. F19 and F22 may ship as vocabulary work rather than kernel work, and saying so in v1 is itself an answer; both are the same shape as the qualifier F21 turned out to need, which is a word in front of a number.

Outside this document, and it is now empty: the specification carries no UNRESOLVED note, tools/binding says so on every run, and the corpus runner is at 41 pass, 0 fail, 0 OPEN — the last twelve open cases were settled in one pass, four of them by the ruling on what a map key may be and two by the ruling that a temporal literal checks its shape and not the clock.

EVERY FINDING IN THIS DOCUMENT IS CLOSED. What remains before a release is not on this list: Q5, the bootstrap core, which docs/10 always scheduled for the spike, and the question of what the thing is called — docs/11 says in as many words never to label it v1, and overriding that should be a decision recorded beside that sentence rather than a thing that quietly stops being true.