Repository navigation
feat(@mdxld/frame): the unstyled contract for Field, Frame, View and Rendering - #4
Conversation
…Rendering One typed model, many faces, and an assertion that they can never disagree. Field — value + attribution (OBS|CALC|MODEL) + source watermark + asOf, with four presence states: present, absent (`—`), withheld (licence-gated, carrying its class) and unconfirmed (a local optimistic value the server has not acknowledged). absent and withheld are different facts and collapsing them is a lie about the record. Honesty guards refuse a malformed MODEL at construction AND again at Frame assembly, because the dishonest Fields that actually arrive came over the wire as JSON. Frame — a typed bundle of Fields materialised at one moment, carrying its snapshot token. Invents nothing. Every Field has a canonical path. View — the standing definition: which Roles, at what token budget, addressed by id. Unknown ids fail closed with the known-id list. An over-budget View THROWS; there is no truncation, elision or summarisation tier, and a test asserts the surface exposes none. Rendering — one serialization. One walk, sink adapters over it; the walk computes the canonical glyph and the sink only places it, so a face is structurally never given the chance to invent a value. markdown + data faces ship; markdown is DERIVED from Fields, with no HTML renderer and no turndown-style laundering anywhere. Face parity — every Field in the Frame appears in both Renderings and neither invents one. This is what lets a designer restructure markup without breaking the agent face, at a fraction of a pixel-snapshot's cost. Its limit (it compares declared emissions) is documented rather than papered over. asOf is carried from day one and NO staleness policy is decided; a test asserts the surface exposes nothing matching /stale|fresh|expire|age/. Ported from kestrel's shipped and measured Frame vocabulary (makeField, isHonestConfidence, the walkKernel sink seam, "an over-budget View is a defect the author must fix"). New here: withheld, unconfirmed, and a wall-clock asOf alongside kestrel's date-blind ordinal. Zero appearance, zero dependencies, no react import. 68 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review: @mdxld/frame - the unstyled Field/Frame/View/Rendering contractThis is a well-conceived and unusually well-documented package - the honesty-at-construction model, the single-walk/sink-adapter seam, and assertFaceParity are a genuinely elegant way to keep an agent face and a human face from drifting. Test coverage (68 tests across 5 files) is thorough and the "no dependencies, zero appearance" claims are backed by executable assertions rather than just prose. Package placement under @mdxld/* (parsing/transformation) is defensible per CLAUDE.md, given it has no rendering appearance and is consumed by @mdxui-style faces as a customer. That said, per the PR description this is explicitly requesting adversarial review before merge, so here is the adversarial pass. Bug: assertFieldHonest crashes with an uncaught TypeError instead of FieldHonestyError for a malformed withheld FieldThis is the one I would block on. isField (src/field.ts, around line 150) only checks structural shape common to every presence state (presence, attribution, watermark.source, asOf) - it never checks that a withheld Field actually carries a licenceClass property. Then assertFieldHonest, around line 210, calls field.licenceClass.trim() to check for an empty licence class, with no undefined guard beforehand - unlike every other optional-field check in the same function, where e.g. the MODEL/modelVersion check a few lines above explicitly tests for undefined before calling trim on it. So constructing a withheld Field the way it would arrive over a JSON boundary - an object with presence set to withheld, an attribution, a watermark with only a source, and an asOf, but with the licenceClass property missing entirely rather than empty - and passing it to assertFieldHonest throws a raw TypeError (Cannot read properties of undefined, reading trim), not the FieldHonestyError the whole module is built around. The doc comment on assertFieldHonest specifically calls out this exact scenario as the reason the function exists: it should be called on any Field that did not come from makeField, notably one parsed from JSON, because the Fields that actually arrive dishonest are the ones that came over the wire as JSON and never met a constructor. A withheld field missing its licence class is a dishonest field by the package's own definition (see the existing test that constructs withheld with an all-whitespace licence class), but the wire-boundary path for a missing property isn't handled the way the wire-boundary path for a malformed MODEL field is. Worse, makeFrame in frame.ts calls assertFieldHonest on every leaf specifically to catch dishonest wire values, so a Frame assembled from an untrusted or legacy payload containing this shape would crash with an unhandled TypeError rather than the documented, catchable FieldHonestyError or FrameError. This is not covered by the existing test suite - field.test.ts's test for refusing a withheld value with no licence class goes through the withheld constructor with an all-whitespace string (not a missing property), and the "over the wire" describe block only exercises the missing-modelVersion case for MODEL, not the missing-licenceClass case for withheld. Suggested fix: either have isField require the licenceClass property to be a non-empty string when presence is withheld, or add an undefined check before calling trim in assertFieldHonest, mirroring the modelVersion check just above it. Design gap: a present Field holding a null value silently renders identically to absentcanonicalText in glyph.ts treats a null or undefined value as the ABSENT_GLYPH constant (the em dash). But nothing stops a caller from constructing a present Field whose value is null - PresentField's generic value type places no runtime constraint preventing that. The result is a present Field, server-confirmed and carrying provenance, rendering as an em dash - visually indistinguishable from a genuinely absent Field. That is precisely the "quiet lie" the README argues absent and withheld must never commit: a real, confirmed value silently rendered as unknown. Given the package's stated goal that there are no naked numbers and never a default, it seems worth also disallowing a null or undefined value on a present or unconfirmed Field at construction time, the same way a MODEL field without a receipt is refused. Minor: UnknownIdError used for a validation failure, not a lookup failureIn view.ts, withRoles throws an UnknownIdError when a Role's id is empty after trimming. UnknownIdError is documented and used everywhere else - the role and view resolver methods, and the roles check in withViews - for "an id was asked for and not found." This call site is instead a registration-time validation failure (an empty id), and the two adjacent checks in the same method, for a missing markdown face and for an unknown markdown face kind, both correctly use TypeError for that kind of input error. Worth aligning for consistency, since callers may reasonably pattern-match on UnknownIdError specifically to mean "id not found, here are the valid ones," which isn't true for an empty-string id. Minor: sinks are single-use but nothing enforces or documents itmarkdownSink and dataSink close over mutable state and are only ever used for one renderFrame call in this codebase, but RenderSink is a public, documented extension point described as the way a new face gets built. Nothing stops a caller from holding a sink instance and passing it to renderFrame twice, which would silently accumulate or corrupt output rather than fail closed - a little at odds with how carefully everything else in this package fails loudly, for unknown ids, over-budget views, and dishonest fields alike. Consider either guarding against reuse, for instance by throwing if the frame-begin hook is called a second time, or documenting sinks as explicitly single-use. Nits
Overall: strong foundation, and the parity-assertion mechanism is the right idea. The withheld and licenceClass gap is the one I would actually fix before merge, since it is a correctness bug in the exact code path, JSON-boundary validation, that this package exists to make trustworthy. |
The headline: assertFaceParity could not detect a face whose bytes disagreed
with the Frame. renderFrame built `emitted` from the walk and took `body` from
the sink separately, and nothing related them — so a sink that discarded every
value and returned "Status: DELIVERED. Unit price: $4.20. Temperature: 0degC."
carried a perfect emissions list asserting the withheld price and the absent
temperature, and parity passed. The list was the answer key, not the answer.
It was not hypothetical: the shipped markdown legend mode already emitted bytes
that differed from its own declaration.
RenderSink.endFrame() now returns a SinkReport { body, emitted } — the face
declares what it emitted and the literal fragments it wrote — and renderFrame
reconciles that claim against the walk and against the bytes, throwing
FaceParityError on divergence. The walk is the oracle.
Also closed:
- isField now checks the presence-specific payload, so a `present` Field with
no value (which rendered as a confirmed UNKNOWN) and an `absent` Field
carrying one are refused at the JSON boundary; a withheld Field with no
licenceClass throws FieldHonestyError rather than TypeError.
- A NaN budget override no longer disables budgeting (spent > NaN is false).
- The budget is PER FACE. Measured: markdown 136, data 644 on the fixture.
One number could never cover both; three tests were passing budget: 5000 to
work around it. Those workarounds are gone and the README example runs, as a
test.
- The four presence states are distinguishable in markdown: a present value
that would imitate a marker is quoted. present(null) renders null, not the
absent glyph.
- The markdown sink escapes | and newlines, so a supplier string can no longer
forge a section or break a table.
- Deleted the two legislative tests that banned R4.2's ratified vocabulary
(and page/pageSize/usage/coverage) from the export surface. The behaviour
they were reaching for is still tested, as behaviour. R4.2 itself stays
unimplemented pending dot-do/vis#361.
- Field.confidence is a branded ModelConfidence (MDX-17).
- Fields and Frames are deep-frozen; ViewRegistry's constructor is private and
copies its Maps.
Should-fix, same pass: path injectivity, parity checks the Rendering is of this
Frame, textMustMatch/facesMustAgree separated, empty Frames refused, duplicate
Role ids and keys rejected, single-use sinks, declareRendering no longer
fabricates a token account, the data face is versioned, RoleFrame carries
ok/empty/blocked/loading state with the sentence as a Field, markdown.keys is
required and exhaustive, two false doc claims corrected (kestrel's Field has no
absence state; column subsetting was unreachable), plus the nits — U+200B,
-0 confidence, non-integer asOf seq, empty ids, and the tokenizer's CJK
fail-open documented.
68 tests -> 145. tsc --noEmit clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review:
|
…pecs, unblock typed-array Fields
Three pre-land fixes from the MERGE-WITH-FOLLOWUPS verification review.
F1 — the stated guarantee was false twice, and is now an enumerated limit.
`render.ts` claimed "every Field's honest glyph is IN the bytes, in the count the
face declared". Neither half holds:
- a sink supplies BOTH sides of the byte comparison. `wrote: [' ']` against a
body of pure fiction reconciles clean — verified. What is checked is that
fragments the face declares occur in the body the same face returned; nothing
relates a fragment to the glyph;
- for any escaped value the glyph is literally not in the bytes.
`present('x | y')` has glyph `x | y [OBS epcis-spine]` and the markdown body
holds `x \| y [OBS epcis-spine]`, so `body.includes(glyph)` is false.
Undisclosed and also confirmed passing: row transposition. Every glyph present
exactly once, each at the wrong address — `lines[0]`'s GTIN beside `lines[1]`'s
quantity — because occurrences are counted over the whole body and nothing
relates a fragment to a position. That is the failure the path system exists to
prevent, and the old doc disclosed only "bytes around the fragments".
The module doc in `render.ts`, the `EmittedValue.wrote` docstring, `reconcile`'s
docstring, `parity.ts` and the README now state exactly what is checked
(path-set equality, presence/attribution/text against the walk; declared-fragment
occurrence against the body) and exactly what is not (no glyph relation, no
position check, not a parse, empty body skips the byte half entirely). Three
tests execute the attacks so the doc cannot drift from the behaviour.
An overstated guarantee is worse than none, because consumers trust it.
F2 — the registry stored specs by reference, so the NaN door was still open.
`ViewRegistry` copied the Maps but held the caller's `View` and `RoleSpec`
objects. `view.budgets.markdown = NaN` after registration made a 2000-char
render return `{ budget: NaN, spent: 520 }` with no throw — every comparison
against NaN is false — reaching the exact defect the previous pass closed at the
`budget` override door. The class docstring's "cannot be mutated out from under
a Rendering in flight" was false.
`withRoles`/`withViews` now copy each spec and deep-freeze the copy. The
registry resolves objects the caller holds no mutable reference to, and the
caller's own literal is left unfrozen — freezing someone else's object is a side
effect this class does not own.
F3 — a Field carrying bytes was unconstructible. Blocker for the first consumer.
`present(new Uint8Array([1,2,3]))`, `present({sig: new Uint8Array(32)})` and
`present(Buffer.from('x'))` all threw `TypeError: Cannot freeze array buffer
views with elements` from inside `makeField`, and the error named none of that.
In the consuming product `complete` takes an event hash, so a hash, a signature
or a symbol image in a Field is the central mechanic of the worklists door.
`deepFreeze` now skips values that cannot be honestly frozen — every
`ArrayBuffer.isView`, plus `ArrayBuffer`, `Date`, `Map`, `Set`, `WeakMap`,
`WeakSet` — behind the exported `isUnfreezable` predicate. This also closes the
adjacent lie: `Object.freeze` on a Date/Map/Set SUCCEEDED, `Object.isFrozen`
answered true, and `d.setTime(0)` / `m.set()` / `s.add()` all kept working,
because their state is in internal slots. Skipped values now report
`Object.isFrozen === false`, truthfully. They are documented as mutable rather
than reported as frozen.
Also documented, not changed: `deepFreeze` freezes the CALLER'S object, so
`present(sharedAppState, obs)` freezes live application state in place. Named on
the docstring and pinned by a test.
157 tests green (was 145). The eight F2/F3 tests each fail before their fix; the
three F1 tests are receipts for a documentation change and pass either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both predate the review rounds and were carried in with the original contract commit. Fixed rather than merged around, since this package becomes the contract every agent-legible view derives from and a lint failure on it is inherited by every consumer. - render.test.ts: hoist NaN to a variable so use-isnan does not read a deliberate assertion (every comparison against NaN is false, which is the bug the test pins) as an accidental comparison. - render.ts: markdownSink's beginRole does not use its RoleFrame argument; prefix with _ per the configured allowed-unused pattern. eslint clean, tsc --noEmit clean, 157/157. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review:
|
Review:
|
The types-only contract every agent-legible view derives from. Ported from kestrel's shipped and measured vocabulary; consumed by vis as a customer.
Field — every rendered value carries its provenance:
OBS | CALC | MODELattribution, a source watermark,asOf, and confidence for model values. Four presence states: present, absent (genuinely unknown, renders as an em dash), withheld (licence-gated, carries its class — absent and withheld are different things and conflating them is a lie about the record), and unconfirmed (a local optimistic value the server has not acknowledged, so offline/store-and-forward stays open rather than foreclosed). Construct-time honesty guards refuse a MODEL field missing its receipt and an OBS/CALC field smuggling one.Frame — a typed bundle of Fields at one moment, carrying its snapshot token. Invents nothing.
View — the standing definition: which Roles, at what token budget, addressed by id. Unknown ids fail closed; an over-budget View throws. No truncation, no elision, no summarisation tier.
Rendering — one serialization, parameterised by format and tokenizer. Never invents or changes a value.
Plus the two things that make it real: a declared markdown face per Role, so the markdown Rendering derives from Fields rather than being laundered out of HTML; and
assertFaceParity, a CI assertion that every Field appears in both faces and neither invents one — which is what lets a designer restructure any markup without breaking the agent face.Zero appearance. Does not import react — runtime-agnostic, consistent with
@mdxui/jsx.Gate: per
vis/CLAUDE.md, the unstyled layer in this repo lands on adversarial review, not design review — an unstyled stateful abstraction has no look and feel. Not merging; requesting that review.Context: vis ADR-0007.
Review round 1 — 12 blockers closed (
90bb4c0)Three adversarial reviews landed on this branch. All twelve blockers and every should-fix are
addressed in one commit. 68 tests → 145;
tsc --noEmitclean; the markdown fixture snapshot isbyte-identical, so nothing about the shipped output changed except where it was wrong.
Every fix was verified to fail before it. The reviews' own demonstrations were replayed against
HEAD~1's source in a scratch harness: 10 of 10 reproduced, including the measured facedivergence (markdown 136 tokens, data 644 — 4.74×).
B1 — face parity could not detect a face whose bytes disagreed with the Frame
The important one.
renderFramebuiltemittedfrom the walk and tookbodyfromsink.endFrame()separately; nothing related them, andfaceParitynever readbody. A sink thatdiscarded every value and returned
"Status: DELIVERED. Unit price: $4.20. Temperature: 0°C."carried a perfect emissions list — asserting the withheld price and the absent temperature — and
assertFaceParitypassed it. The declaration was the answer key, not the answer. Not hypothetical:the shipped
markdownSinkinprovenance: 'legend'mode already wrote cells viavalueText/provenanceMarkwhileemittedcarried the fullrenderGlyph, so the shipped faceemitted bytes that differed from its own declaration and parity was blind to it.
RenderSink.endFrame()now returnsSinkReport { body, emitted }— the sink reports what itactually emitted, including
EmittedValue.wrote, the literal fragments it put in the body — andrenderFramereconciles that claim against the walk and against the bytes, throwingFaceParityErroron divergence. The walk is the oracle. It refuses a sink that: never emitted aField it was handed; emitted a path the walk never produced; disagrees about presence, attribution
or canonical glyph; produced a body but named no bytes for a Field; or declared a fragment its body
does not contain as often as claimed.
The residual limit is stated in the module doc rather than papered over: fragment occurrence is not
a parse, so a sink can still write bytes around its declared fragments. What is now guaranteed is
that every Field's honest glyph is in the bytes, in the count the face declared.
parity.ts's docstring, which claimed the opposite of what the code did, is corrected.The rest
isFieldnever checked the presence payload —{"presence":"present"}with novaluepassed every guard,hasValuesaid true, and it rendered—: a confirmed UNKNOWN. AnabsentField carryingvalue: 42rode inside the Frame.isFieldrequires avaluekey for present/unconfirmed, its ABSENCE for absent/withheld, and alicenceClassfor withheld.field.test.ts› a presence state without its payload is not a Field (7 tests)licenceClassthrewTypeErrorfrom.trim(); callers catchingFieldHonestyErrormissed it.isField, soassertFieldHonestthrowsFieldHonestyError.NaNbudget override silently disabled budgeting (spent > NaNis false).Number.isFinite(b) && b > 0.render.test.ts› REFUSES a NaN budget overrideView.budgetfor every face. Measured: markdown 136, data 644 — 4.7× on eleven Fields. Three tests passedbudget: 5000to work around it; the README example threw.View.budgetsis a map keyed by face format,markdownrequired, a face with no entry refused rather than defaulted. Workarounds deleted.readme.test.tsruns the README example and snapshots its outputpresent('—')vsabsent(),present('withheld(x)')vswithheld('x'),present('12 ~')vsunconfirmed(12)were byte-identical. The docstring claimed distinguishability "by construction".COLLIDES_WITH_A_PRESENCE_MARKERis exported so a consumer face can reuse the rule. Docstring is now true and carries the encoding table.glyph.test.ts› a present value can never imitate a presence marker (7 tests)present(null)rendered as the absent glyph, which the module doc two lines above forbade.null. The string'null'renders"null", so both directions stay distinct.glyph.test.ts› present(null) is not promoted to UNKNOWN (4 tests)|broke table columns; one containing\n## Shipment\n- **Status**: DELIVEREDforged a section — and parity returned clean, because the value was exactly what the Frame held.escapeMarkdownat the sink boundary: newlines flatten,|escapes, leading block markers escape.render.test.ts› a supplier string cannot forge a section or break a table (3 tests)/truncat|elid|elision|summari|degrad/iand/stale|fresh|expire|age/i— banning ratified R4.2's own vocabulary (andpage,pageSize,totalPages,usage,coverage), making the ruling's implementation fail CI.asOfis on every rendered Field, nothing decides staleness. R4.2 is not implemented — it stays escalated asdot-do/vis#361and is named in the README's Deferred section.confidence: extractResult.confidence(amatchedSlots / totalSlotsparse-coverage ratio) typechecked into a Field. MDX-17 asked for this in advance.ModelConfidence, constructed only viamodelConfidence(n).field.test.ts› ModelConfidence is a different type… — the assignment is a@ts-expect-error, verified bytsc --noEmitoversrc/f.attribution = 'MODEL'; f.watermark.modelVersion = 'fabricated-9.9'after construction, and the glyph reported the fabrication.makeFramereturned the caller's own object.deepFreezeout ofmakeFieldandmakeFrame;makeFramereturns a copy.ViewRegistry's public constructor bypassed every registration guard and held caller Maps by reference — a registry mutated under a Rendering in flight, which its docstring said was impossible.private; Maps are copied;createRegistry()goes throughViewRegistry.empty().view.test.ts› has no public constructor (@ts-expect-error)Should-fix, same pass
.,[,]refused in Role ids and Field keys (rolex/keyy.zand rolex.y/keyzboth addressedx.y.z, silently merging two Fields and then failing parity on a good Frame).asOf, as a newsnapshotbreach kind.textMustMatch(face vs the Frame) andfacesMustAgree(face vs face). One flag used to turn off both, under which every value could readDELIVEREDand parity was clean.markdownSink()/dataSink()are explicitly single-use — a secondbeginFramethrows instead of silently concatenating two Frames.declareRenderingno longer fabricates{ budget: Infinity, spent: 0 }—Rendering.tokensis absent unless measured.mdxld.frame.data/1(R1.3).RoleFrame.state—ok | empty | blocked | loading, with the sentence as aFieldwalked and parity-checked atrole.$state(R1.5)._(no rows)_— renderer-authored English inside a generic package — is gone.markdown.keysis required and exhaustive — absent-not-hidden is structural, not opt-in.Fieldhasvalue: Trequired and no absence state at all (absence lives inPaneRefusal's four-state cell paradigm) — the "proven port" warrant now cites accurately; and column subsetting was unreachable, so the doc says so.isBlankcatches U+200B and friends (.trim()did not);-0confidence normalised;FrameErrorrather thanUnknownIdErrorfor an invalid id; empty View id refused;isAsOfrequires an integerseq; the chars/4 tokenizer's CJK fail-open (3–5× under-count) documented intokenize.tsand the README.vitest.config.tsadded (every sibling has one);pnpm-lock.yamlgains thepackages/@mdxld/frameimporter.Deliberately NOT done
dot-do/vis#361. B9 only stops foreclosing it.ParamSlot/bindArgs) — a real gap with no expression for R4.1–R4.4; a follow-up, not a fix.Test output
tsc --noEmit: clean.tsup: builds ESM + CJS + d.ts.Still not merging — re-requesting adversarial review on the changed surface.
Review round 2 — three pre-land fixes (
e2a399c); everything else filedA verification review returned MERGE-WITH-FOLLOWUPS. Three items land here because each is
either a false claim in shipped documentation or a hard blocker for the first real consumer.
Everything else is filed: #6, #7, #8, #9, #10, #11, #12, #13.
145 tests → 157.
tsc --noEmitclean.tsupbuilds. No shipped output changed — the markdownand data fixture snapshots are byte-identical; F1 is documentation, F2 and F3 only close doors.
F1 — the reconciliation guarantee was false twice, and is now an enumerated limit
Round 1 above claims: "What is now guaranteed is that every Field's honest glyph is in the bytes,
in the count the face declared." Both halves are false.
A sink supplies both sides of the byte comparison. A sink declaring
wrote: [' ']for everyField, returning
"Status: DELIVERED. Unit price: $4.20."as its body, reconciles clean — verifiedand now shipped as a test. The body contains a space, so the count check is satisfied, and no other
clause looks at
wroteat all. What is actually checked is that fragments the face declares occurin the body the same face returned. Nothing relates a fragment to the glyph.
For any escaped value the glyph is literally not in the bytes.
present('x | y')has thecanonical glyph
x | y [OBS epcis-spine]; the markdown body holdsx \| y [OBS epcis-spine]. Sobody.includes(glyph) === false. This is correct behaviour — it is precisely whyEmittedValue.wroteexists — which makes "the honest glyph is in the bytes" a sentence this package cannot say.
And one failure mode round 1 did not disclose at all: row transposition. Every glyph present in
the body exactly once, each at the wrong address —
lines[0]'s GTIN besidelines[1]'s quantity— renders clean, because occurrences are counted over the whole body and nothing relates a fragment
to a position. Round 1 disclosed only "bytes around the fragments". Right glyphs, wrong addresses is
the failure the path system exists to prevent, and it was undisclosed.
The module doc in
render.ts, theEmittedValue.wrotedocstring,reconcile's docstring,parity.tsand the README now state exactly what is checked — path-set equality, andpresence/attribution/text against the walk, which are genuinely independent because the walk is an
oracle the sink cannot influence; plus declared-fragment occurrence against the body, which is not —
and exactly what is not: no glyph relation, no position check, not a parse, no protection
against a face that declares fragments it did not meaningfully write, and an empty body skips the
byte half entirely.
parity.tsadditionally now says the thing it had never said:faceParitynever reads a body.It compares declared emissions and does not look at
rendering.bodyat all.Three tests execute the attacks, so the documentation cannot drift from the behaviour. When #6
lands, those tests invert to assert the throw.
A precise limit is useful. An overstated guarantee is worse than no guarantee, because consumers
will trust it.
F2 — the registry stored specs by reference, so the
NaNdoor was still openViewRegistrycopied its Maps but held the caller'sViewandRoleSpecobjects. So B4's fixcovered one door and left the other standing:
Every comparison against
NaNis false, sospent > budgetis false and budgeting is off — theexact defect B4 closed at the override door, still reachable through the registry door. The class
docstring's "a registry cannot be mutated out from under a Rendering in flight" was false.
withRoles/withViewsnow copy each spec and deep-freeze the copy. The registry resolvesobjects the caller holds no mutable reference to; the caller's own literal is left unfrozen, because
freezing someone else's object is a side effect this class does not own. Four tests, including the
mutate-then-render case and the caller's-literal-untouched case.
F3 — a Field carrying bytes was unconstructible. Blocker for the first consumer.
Thrown from inside
makeField, and the error names none of it. This is pre-land rather than afollow-up because in the consuming product
completetakes an event hash — the central mechanicof the worklists door. A Field carrying a hash, a signature or a symbol image was unrepresentable.
deepFreezenow skips values that cannot be honestly frozen — everyArrayBuffer.isView(so everyTypedArray, DataView and Buffer), plus
ArrayBuffer,Date,Map,Set,WeakMap,WeakSet—behind an exported
isUnfreezablepredicate.That also closes the adjacent false guarantee.
Object.freezeon aDate,MaporSetsucceeded,
Object.isFrozenthen answeredtrue, andd.setTime(0)/m.set()/s.add()all kept working through the "frozen" Field, because their state lives in internal slots rather than
own properties. Skipped values now report
Object.isFrozen === false— truthfully. They aredocumented as mutable rather than reported as frozen, and the docstring says what to hold instead
(a hex string for a hash, an ISO instant for a date).
Documented, not changed, and pinned by a test:
deepFreezefreezes the caller's object, not acopy, so
present(sharedAppState, obs)freezes live application state in place for the rest ofthe process. Named on the
deepFreezedocstring, in thefield.tsmodule note, and in the README.Filed, not fixed
wrotetotext, and check emission position. Declare the escape and assertunescape(wrote.join(''))coversvalueText; add a monotonic position check over emission order. Closes both the one-space attack and row transposition. Highest-value follow-up.body: ''silently skips byte reconciliation and re-fabricates the token account round 1 removed — a React sink gets{ budget: 100, spent: 0 }, a measurement never taken wearing the shape of one.declareRenderingis now the more honest door, which inverts the intended incentive.blockedcollapses intoempty.RoleState.kindnever entersemitted, is never reconciled and never parity-checked. A face implementing noroleState, on a blocked Role with no sentence, produces a body identical to the empty case with zero breaches.roleStateappears in zero shipped tests.blocked/loadingFrame — both{state:{kind:'blocked'}}and{..., fields:{}}throwFrameError: carries no Fields. Table Roles getrows: []free. The exhaustive-keys rule and the no-empty-Frame rule collide exactly on R1.5's headline case.countOccurrencesrescans the full body per fragment. 250→4000 rows: markdown 3.0→425.1 ms, data 10.3→2266.0 ms; at 5000 rowsrenderData+ parity is 4310 ms. Fine at the 20–200 row target, one order of magnitude from unusable as a CI gate.escapeMarkdownis not injective.#fooand\#fooboth →\#foo; a literal newline and the two characters\ncollide;\r\n/\r/\ncollapse. The same "two facts, one byte string" defect B6 fixed, reintroduced one layer out, in a package advertising the escape as "unambiguous and reversible". Also unescaped: backticks,<script>,**bold**,[text](url).EmittedValueliteral, andwrote: [glyph]is a lie for any face that escapes, with nothing telling the author.roleStatebranches, the empty-body bypass,escapeMarkdowninjectivity. (deepFreezedirectly, registry-spec mutation andwroteself-certification are covered by this commit.)Test output
Each of the eight F2/F3 tests was verified to fail against
90bb4c0's source (stash the threechanged source files, run the suite:
Tests 8 failed | 98 passed). The three F1 tests pass eitherway by design — they are receipts for a documentation change, and they assert the hole rather than
its absence.
tsc --noEmit: clean.tsup: builds ESM + CJS + d.ts.eslint src: two errors, both pre-existingat
90bb4c0and untouched here (render.test.ts:349use-isnan,render.tsbeginRole'sunused
rolearg).Still not merging.