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
| id | weakness | kind | costs | fix-is |
|---|---|---|---|---|
| :F16 | canonical ordering destroys column order | :closed | :high | authored order kept out of band, and a decoded document now says its order is not the author's |
| :F17 | references have no defined target | :closed | :high | rooted at the sole block; external marks the rest |
| :F18 | the bad-header diagnosis survived its own fix | :closed | :medium | done |
| :F19 | no unit binding on a number | :closed | :low | the shape is normative; the set is a vocabulary |
| :F20 | hand alignment is decorative | :closed | :none | recorded; no action |
| :F21 | no way to mark what is binding | :closed | :high | a vocabulary of qualifiers; tools/binding reads it |
| :F22 | a time of day has no representation | :closed | :medium | a literal, sharing the timestamp tag |
| :F23 | the reference rejects two of the spec vectors | :closed | :high | the grammar was right; the vectors were wrong |
| :F24 | the reference rejects a byte-order mark | :closed | :low | done; one lexer case |
| :F25 | the authored order was specified, not kept | :closed | :high | done; a third harness check |
| :F26 | the reference emitted an escape it refuses | :closed | :high | done; five escapes and no others |
| :F27 | a trailing comment on a table row was refused | :closed | :high | done; the corpus denominator is whole |
| :F28 | what a key may be had three different answers | :closed | :high | decided by layer; four open corpus cases closed |
| :F29 | the table was the one container needing a trailing separator | :closed | :medium | the production was the defect; rust and dart follow |
| :F30 | the vectors had never been run against the reference | :closed | :high | one lexer rule; the harness now measures the reference |
| :F31 | the timestamp lookahead is quadratic in three languages | :closed | :high | a bounded window in go and dart; js had already found it; five were immune |
| :F32 | a table of records encodes larger than its text | :closed | :medium | a typed array per column; measured at 1.47x in docs/52 P3, 0.30x after docs/54 |
| :F33 | a fixed-arena implementation has a document-size ceiling the profile does not state | :closed | :low | docs/52 P6: honest, positioned, and now a note the guide's third constant covers |
| :F34 | the canonical text form was never written down, and no formatter matched the reference | :closed | :high | formatting.layout in the specification, 2026-09-03; tools/fmtcompare is the gate |
| :F35 | the reference emitted keys and table headers it could not read back | :closed | :high | a key is bare only when it LEXES as one word; tools/roundtrip re-reads all 335 documents |
| :F36 | one-text-per-float did not say which of two equally short digit strings wins | :closed | :medium | the 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, bytesrenders asbytes, 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 decsays 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].cform 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/v0is 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ābmeaning 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
-
question why-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.xinsidelanguage { … }, 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, happensis 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
unitsblock, 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-kgthat 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 inis 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, sinceminis time andarcminis 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
kgmeans. - 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 :normativeon one block,must-reproduceandfree-to-replacelists in another, prose sayingan implementation conforms ifin 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 implementeris a thing documents need to say, and saying it the same way twice has value that saying it eleven ways does not. - options
-
option gives costs a reserved key, such as normative truegreppable, checkable, minimal convention only; nothing enforces it an extension, !spec/normativetyped, and degradable like any extension verbose on every block a profile defining conformance vocabulary composable, and the format's own answer a profile to write and agree nothing, and rely on section names no work what 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
bindingis 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 truefor 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 satisfytook 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
-
option gives costs nothing; quote it no work what happens today, and it is silently lossy a time production, 03:52 and 03:52:07 symmetric with the date form a new lexical shape, and it must be tried before numbers as dates already are a duration form as well answers the sibling absence more surface, and ISO 8601 durations are their own argument a profile defining a time convention the format's own answer, as with F19 a 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:52cannot 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 as2026-08is 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:1and1:1are 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 fmtscrambled 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 reportedexpected 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 offafter 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 02is 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
-
implementation behaviour :reference refused at the decoder: map keys are text :rust refused at the decoder: map keys must be text strings :js refused at the decoder: expected text key in map :dart refused at the decoder: map keys must be text strings :kotlin ClassCastException, escaping to the top of the process :dotnet decoded it, then refused to render it as text :esp32 decoded it, then refused to render it as text :swift decoded 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. Askingwhat may a key bewithout 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" 2re-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}as1 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 ]andx { 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 wassource.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
-
implementation how-it-reads-the-rest outcome go string(src[pos:]) — a rune slice, copied had it. Fixed: a 64-rune window, re-tried unbounded when a match fills it. 132 s became 192 ms dart source.substring(offset), twice had it. Fixed the same way. 110 ms became 20 ms, and the rate is flat from 7 KB to 2.6 MB js input.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 borrow immune. The same expression that copies in go is a view in rust, at no cost kotlin Regex.matchAt(s, i) immune. The regex API takes a start index, so nothing is sliced dotnet TryTimestamp(_i, out int end) immune. Index-based by construction swift a hand-written scalar matcher over an index immune. No regex, so no string to match against esp32 pointer and length immune 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 numbersbeside an anchored pattern, which maps ontomatch 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 0fis 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
0fand2fare floats exactly as2.5fis. 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,voiceandbarcolumns 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.