Skip to content

fix(safety): read a doNotPerform the XML reader kept as lexical text (FHIR-XML-BOOLEAN-NEGATION-LOST) - #79

Merged
NSchatz merged 2 commits into
mainfrom
fix/xml-lexical-boolean-negation
Aug 9, 2026
Merged

fix(safety): read a doNotPerform the XML reader kept as lexical text (FHIR-XML-BOOLEAN-NEGATION-LOST)#79
NSchatz merged 2 commits into
mainfrom
fix/xml-lexical-boolean-negation

Conversation

@NSchatz

@NSchatz NSchatz commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What this fixes

FHIR-XML-BOOLEAN-NEGATION-LOST, filed as a STOP-THE-LINE.

FHIR XML carries every primitive as the text of its value attribute, and the reader is schema-free
by design, so <doNotPerform value="true"/> lands as the string "true" where the JSON reader
builds true. That is a declared limit and it is not what changed.

What changed is the read. Measured at the base commit 4eb444f, a MedicationRequest carrying
"doNotPerform":true went through this package's own serializeResourceXml (which emits
<doNotPerform value="true"/>, correctly) and back through parseResourceXml, and readSafety
returned:

JSON            -> negations: ["do-not-perform"]   doNotPerform: true
XML round trip  -> negations: []                   doNotPerform: undefined
                   issues: []   safeToSummarize: true   assertSafeToSummarize PASSES CLEAN

The read matched only a JS boolean, and a failed match reads as absence, so an explicit
instruction not to give a medication disappeared while the safety spine affirmed the result was safe
to summarize. A lost doNotPerform does not degrade a value; it inverts an instruction.

The change

One read is widened, and only one: primitiveBooleans, the safety-layer read, whose sole caller
is readDoNotPerform. A new booleanOf recognises exactly true and false, the whole of the
R4 boolean lexical space.

It is deliberately not widened in the XML reader. The model still holds the lexical string,
nodesEquivalent still accounts for the difference, and nothing about re-emission changes: a
schema-free reader cannot know the datatype, and coercing there would author a value the sender did
not spell and launder it across a format change.

The census, and what it did NOT license

The item asked for a census of every boolean the safety spine reads. It found three sites.
Only one was changed, and the conformance gate is why.

Site Via Read from XML at base Disposition
MedicationRequest.doNotPerform primitiveBooleans LOST FIXED
ElementDefinition.mustSupport primitiveBoolean LOST left standing, pinned
ElementDefinition.slicing.ordered primitiveBoolean LOST left standing, pinned

The first draft widened both helpers. conformance-refuter pass 1 REFUTED it with a measured
behavioural counterexample: snapshot.ts merges a differential flag only when it is not undefined,
so an XML <mustSupport value="false"/> that base read as undefined began overwriting an inherited
true and retiring a MUST_SUPPORT_ABSENT the base emitted. That is the retirement class #78
measured at validatePrimitiveValue and declined, reopened one layer over.

The rule this bought: a safety read and a convenience read cannot share a widening. Additivity is
a property of the consumers, not of the helper. primitiveBooleans feeds negations, where a
newly-read value can only add. primitiveBoolean feeds callers that treat undefined as inherit,
where the same value removes a finding.

Censused and deliberately unchanged: validatePrimitiveValue's boolean branch, FHIRPath
convertToBoolean / toTrit / systemTypeOf, parseMin and FHIRPath numberOf (the same root
class one datatype over), and the writers.

Measurements

  • No diagnostic moves, measured base-vs-head in a real 4eb444f worktree rather than argued:
    collectProfileIssues over an XML-sourced profile and validateResource over the round-tripped
    MedicationRequest are identical on both trees. The only thing that moves is readSafety's
    doNotPerform and negations.
  • Red-at-base 6-of-22, 22-of-22 at head. The other 16 are green in both states on purpose and are
    named in the notes: seven lexical-space negative controls, the type gate, the decimal-form control,
    the JSON-reader control, three profile-census pins and three residual pins. Non-vacuity by
    mutation: a case-insensitive booleanOf reds 3, dropping the false arm reds 1, accepting "1"
    reds 1.
  • verify.sh fhir green: 11 step headers, 11 ran: entries, zero (FAIL), no unladdered-script
    warning. Suite 64 files / 1,316 tests, no existing test moved.

Residuals, pinned in test/xml-lexical-boolean.test.ts

Closing any of them must red its test in the same change.

  1. The two profile booleans, above.
  2. ElementDefinition.min unread from XML, silently, so an XML-sourced profile declares required
    elements this library enforces none of.
  3. FHIRPath convertToBoolean reads an XML-sourced false as true.
  4. safeToSummarize is unmoved in both directions, and the second direction is a real gap: for a
    value that does not read ("1", ordinary v2/C-CDA converter output) the element is present, its
    value is unread, nothing records it, and the readout still affirms. SafetyReadout has location
    channels for content the codec could not read and none for "value written, not readable". Base did
    this for true too, so it is strictly improved, but it wants its own item.

The lexical space itself is silent: "TRUE", "1", "yes", " true" read as no boolean with
nothing on any diagnostic channel.

The gate

conformance-refuter, REFUTED -> REFUTED -> NOT REFUTED, cumulative 3 (4b67787, 1d9953d,
27cb42c; the first two shas were amended away).

  • Pass 1 REFUTED with a behavioural finding and I took its prescribed cut rather than qualifying
    the claim: the profile widening is reverted, primitiveBoolean is byte-identical to base, and the
    census is reported instead.
  • Pass 2 REFUTED on a claim defect only and said the code was right and must not be cut back: the
    sentence "can only fill in a value that was undefined" was falsified by this slice's own fixture,
    the third generation of one universal. Replaced with the pass's own wording.
  • Pass 3 NOT REFUTED, narrow, against the remedy diff only. No findings at blocker, major or
    minor severity, nothing behavioural. It independently re-derived red-at-base 6-of-22, re-ran the
    suite green, and verified every claim it was asked to grade.

One commit is UNGRADED and disclosed: 713e718 applies pass 3's two advisories, which it
excluded from its verdict and said it would not hold the slice for. Documentation only,
git diff 27cb42c 713e718 -- src/ test/ is empty. It deletes an over-specified later / earlier
ordering from the doNotPerform transition sentence at three carriers and corrects
"single-consumer" to name the call sites. It is applied rather than deferred because one carrier is
CHANGELOG.md, frozen while #73 is open, where a false clause would be permanent under ADR 0001.

ADR 0027 is NOT invoked and is not available here: its route requires that no pass refuted the
code, and pass 1's finding was behavioural. The gate converged on its own.

Corpus caveat

Hand-authored XML fixtures plus mutations and hand-built probes, not the FHIR R4
published-examples corpus. Nothing here is corpus-wide.

Not touched

PR #73 (Version Packages) is untouched and stays open. No umbrella pointer bump in this PR.

NSchatz added 2 commits August 9, 2026 05:58
…(FHIR-XML-BOOLEAN-NEGATION-LOST)

FHIR XML carries every primitive as the text of its `value` attribute, and the reader is schema-free
by design, so `<doNotPerform value="true"/>` lands as the string "true" where the JSON reader builds
`true`. That is a declared limit and is not what changed.

What changed is the read. Measured at 4eb444f, a MedicationRequest carrying "doNotPerform":true went
through this package's own serializeResourceXml (which emits `<doNotPerform value="true"/>`,
correctly) and back through parseResourceXml, and readSafety returned doNotPerform: undefined,
negations: [], issues: [] and safeToSummarize: true, with assertSafeToSummarize passing clean, where
the JSON original reads negations: ["do-not-perform"]. The read matched only a JS boolean, and a
failed match reads as absence, so an explicit instruction not to give a medication disappeared while
the safety spine affirmed the result was safe to summarize.

One read is widened, and only one: primitiveBooleans, the safety-layer read, whose sole caller is
readDoNotPerform. It is deliberately NOT widened in the XML reader: the model still holds the lexical
string, nodesEquivalent still accounts for the difference, and no re-emission changes, because a
schema-free reader cannot know the datatype and coercing there would author a value the sender did
not spell.

The text recognised is exactly `true` and `false`, the whole of the R4 boolean lexical space. "TRUE",
"True", "1", "yes", "Y", " true" and "" still read as no boolean, silently, which is a residual
rather than a guarantee.

The census the item asked for is reported rather than acted on, and that is a finding. Two more
boolean reads have the same defect and are left standing, pinned: ElementDefinition.mustSupport and
ElementDefinition.slicing.ordered, both through primitiveBoolean, the convenience read. Widening
them was drafted and the conformance gate measured it retiring a diagnostic: snapshot.ts merges a
differential flag only when it is not undefined, so an XML <mustSupport value="false"/> that base
read as undefined began overwriting an inherited true and removing a MUST_SUPPORT_ABSENT the base
emitted. A safety read and a convenience read cannot share a widening, because additivity is a
property of the consumers and not of the helper. ElementDefinition.min and FHIRPath's
convertToBoolean / toTrit / systemTypeOf are the same root class, censused and unchanged, and
validatePrimitiveValue is untouched.

No diagnostic moves, measured base-vs-head: collectProfileIssues over an XML-sourced profile and
validateResource over the round-tripped MedicationRequest are identical on both trees. The only thing
that moves is readSafety's doNotPerform and negations. negations is monotone across the change, so
this read can only add the do-not-perform negation and never retire it; doNotPerform itself moves
further than undefined-to-a-value, also moving false to true where a later value spells the negation
an earlier JS boolean contradicts, which is the documented "a true anywhere wins" rule and is pinned.

safeToSummarize is unmoved in both directions and the second direction is a declared gap: for a value
that does not read ("1", ordinary converter output) the element is present, its value is unread,
nothing records that, and the readout still affirms. Pinned rather than absorbed.

Red-at-base 6-of-22 against a real 4eb444f worktree, 22-of-22 at head; the other 16 are green in both
states on purpose and are named in the notes. Suite 64 files / 1316 tests, no existing test moved.
Corpus: hand-authored XML fixtures plus mutations and hand-built probes, not the FHIR R4
published-examples corpus.
Documentation only. No source, no test, no behaviour: `git diff 27cb42c HEAD -- src/ test/` is empty.

THIS COMMIT IS UNGRADED. The conformance gate converged at 27cb42c (REFUTED -> REFUTED -> NOT
REFUTED, cumulative 3, the ADR 0016 cap), and these two changes were prescribed by pass 3 as
advisories it explicitly excluded from its verdict and said it would not hold the slice for. They are
applied anyway because one of them lands in CHANGELOG.md, which is frozen while the Version Packages
PR is open, so a false clause there would be permanent under ADR 0001.

ADR 0027 is NOT invoked and is NOT available here: its route to landing an unconverged gate requires
that no pass refuted the code, and pass 1's finding was behavioural (widening the convenience read
retired a MUST_SUPPORT_ABSENT through the snapshot merge). The gate converged on its own instead.

A1. The doNotPerform transition sentence said the value moves false to true "where a later value
spells the negation an earlier JS boolean contradicts". Pass 3 measured that the ordering adverb is
not general: {"doNotPerform":["true",false]} puts the lexical true earlier and still moves false to
true. The words "later" and "earlier" are deleted at all three carriers (CHANGELOG.md, the changeset,
the notes) in favour of "any value ... elsewhere in the element". The load-bearing half, that
negations is monotone and this read can only add the negation and never retire it, was verified true
by pass 3 and is unchanged.

A2. The notes said both helpers are "package-private, single-consumer". primitiveBoolean has two call
sites in one module, so that now names the call sites instead of asserting a count.

The ungraded status is disclosed here, in documentation/agent-notes/xml-lexical-booleans.md and in
the pull request.
@NSchatz
NSchatz merged commit 05ecc5a into main Aug 9, 2026
10 checks passed
@NSchatz
NSchatz deleted the fix/xml-lexical-boolean-negation branch August 9, 2026 06:10
NSchatz added a commit that referenced this pull request Aug 9, 2026
…reach (conformance-refuter pass 2)

`conformance-refuter` pass 2 returned **REFUTED** on `9760562`, cumulative 2. **It refuted no code
again**: it verified the remedy diff was docblock text only, `booleanOf` unwidened base-to-head,
`hasUnreadableBoolean` and `checkUnreadableBooleans` untouched, the complement exact,
`safeToSummarize` monotone toward refusal, the window a superset of the read, and the seventh
mutation consistent with the code. Every finding was a claim defect, and this commit is the
**deletion** the pass prescribed. No `src/` behaviour changes.

## Finding 1, major: the corrected claim was a new claim, and it was also too wide

Pass 1's replacement said the validator "is no substitute, because it draws `TYPE_MISMATCH` on the
lexical `"1"` and on a conformant `<doNotPerform value="true"/>` alike, so it does not separate
readable from unreadable". **The three rows behind that were all XML**, and the conclusion was drawn
unscoped over a channel whose own docblock says "in either wire format". On the **JSON** wire it is
plainly false: `validatePrimitiveValue` shape-checks `typeof value === "boolean"`, so with a schema
supplied a conformant `{"doNotPerform":true}` validates **clean** while `{"doNotPerform":"Y"}` draws
`TYPE_MISMATCH`. The package's own pending `olive-comets-listen` changeset already recorded that, so
two changesets in one release body contradicted each other.

**Deleted, not qualified**, at all six carriers: `README.md`, `CHANGELOG.md` (frozen under ADR 0001
while PR #73 is open), `.changeset/olive-herons-report.md`, the notes, and both docblocks in
`src/safety/status.ts`. The pin's `conformant` row went with it, since it existed only to support the
deleted sentence and the same false error is already pinned with the `Quantity` residuals.

**What survives unscoped is availability, not discrimination**: `MedicationRequest` has no built-in
schema (`BUILTIN_SCHEMAS` is `[Patient]`), so the validator is silent about this element unless a
caller supplies one, and a readout that has to hold on every document cannot rest on a diagnostic
that only exists when a caller opts in. The pin now asserts exactly that and nothing more: silent by
default, available on request.

**This was the second generation of one over-general sentence in one paragraph, one per pass.** The
notes record both refuted versions verbatim beside the survivor, because the trap is that a corrected
claim inherits nothing.

## Finding 2, minor: the public docblock named the wrong read, and it ships in `dist/index.d.ts`

`unreadableBooleans`' docblock (and its README twin) said the window is "wider than `readSafety`'s
own read, which only visits the resource it is handed". `readSafety` does walk the whole tree, and
`arrayWrappedScalars`, named two clauses earlier, is itself computed at every nested resource root.
The subject is the **`doNotPerform` read**, which is what the private docblock already said. Corrected
in both.

## Finding 3, minor: the mirrored correction pointed the wrong way, in the frozen file

`CHANGELOG.md` twice said the channel is added by "the entry below". The `unreadableBooleans` entry is
under `### Added` at the top of `[Unreleased]`; the `#79` entry referencing it is under `### Fixed`
below. Both now say "the entry above". Not present at base.

## Still `PRE-EXISTING`, re-verified by pass 2, and the coordinator owes an item

**`ServiceRequest.doNotPerform` and `CommunicationRequest.doNotPerform` are `?!` boolean modifiers the
safety spine never reads.** Neither type is in `SAFETY_RESOURCE_TYPES` and `readSafety` gates on
`isType("MedicationRequest")`, so a **conformant** `{"resourceType":"ServiceRequest",
"doNotPerform":true}` reads `negations: []`, `safeToSummarize: true`, `assertSafeToSummarize` clean.
Identical at `05ecc5a`, so not a refutation. It is a sharper shape than the item this slice closes:
the document is conformant and the instruction is simply not looked for. Recorded in the notes;
**it is not yet in the backlog**, which is the coordinator's to file. Also still standing: "the six
safety resource types" at `README.md` and six `src/` sites while the set holds seven.

## Re-measured after the remedy

- Suite **65 files / 1,346 tests**, all green. Red-at-base still **18-of-30**, 30-of-30 at head, same
  12 both-states pins.
- `verify.sh fhir` green: **11 `==>` headers, 11 `ran:` entries, zero `(FAIL)`**, no
  unladdered-script warning.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant