Repository navigation
[WIP] rework GroveHealthKitFHIR - #118
Draft
lukaskollmer wants to merge 179 commits into
Draft
lukaskollmer wants to merge 179 commits into
lukaskollmer wants to merge 179 commits into
Conversation
Parse each Bundle's JSON once and share the parsed document across every validation pass instead of re-encoding resources per pass; drain Foundation temporaries per graph. Move the plain-decimal formatter into String+Utils with a Foundation-free `isBlank`, and nest `ApplicationDeviceError` as `ApplicationDevice.InitError`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add a test-only factory for stored HealthKit samples with chosen UUIDs, source revisions and devices, and pin the converter's output for 40 shapes (observations, writers, disclosures, documents, retractions) as checked-in goldens compared with lossless-token equality. Check in the conversion throughput benchmark, gated behind GROVE_FHIR_BENCH_RUN. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Expose each exchange graph's validated bytes (sorted keys, unescaped slashes) and a validating initializer for received bytes, nest the graph kind, and add the shared producer configuration, the Grove-owned event sequencer with app-supplied storage and keyed in-flight reservations, a one-call identity-scope initializer, and an ASCII millisecond instant formatter. The earlier spellings stay as deprecated forwards. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A long-lived `HealthKitFHIRExporter` replaces the per-sample context plumbing of `HealthKitConverter`: it is configured once from an `ExchangeProducer`, a repository scope and `Options`, mints every event through the producer's `ExchangeEventSequencer` (so an exact redelivery before `Receipt.release()` reproduces the same bytes), converts samples, companion records (ECG, heartbeat series, workout route) and deletions in input order, and reports each outcome as a validated graph, a refusal diagnostic or `nothingToRetract` without ending the call. Options express the deployment's policies once: `WriterPolicy.automatic` classifies Apple's per-device sources (`com.apple.health.<device>`) as the physical device that recorded the sample and every other source as an application; a `.device` writer without a stable `HKDevice` token now derives the recording Device from that source (name, `Apple Inc.`, product type) instead of omitting the author; `.omit` writers and recording devices state nothing and never warn; `legacyBundleID = .healthKitUUID` keeps the uppercase HealthKit UUID in `Bundle.id` for receivers that still key on it (deprecated at introduction). A native identifier under one of the deployment's own systems is refused at configuration. The exporter is a facade over the existing conversion code: every graph is byte-identical to the old entry point's under the same event (pinned by the goldens and the new exporter tests). The `HealthKitConverter` public entry points stay as a deprecated shim for the existing tests and consumers; their bodies moved into internal statics the exporter calls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A refused record reports its `HealthKitConversionError` rather than only the registered diagnostic, so a consumer can match the reason; a retraction carries the same legacy `Bundle.id` as the addition it retracts; a deletion lower bound that a backwards clock adjustment put after the detection is dropped instead of clamped; and an export or retraction with nothing to reserve never touches the sequencer's ledger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The graph envelope (output identities and links, study context, device snapshots, writer deduplication, conversion Provenance, Bundle framing) moves into a package-level `ExchangeGraphAssembler` in GroveFHIRContract. An adapter describes one event as an `ExchangeGraphDraft`: content-only outputs with the envelope links each takes, the resolved recording device and writer, and the event's role and repository ids. The assembler mints every identity once, computes each fullUrl once, and builds the graph in the guide's fixed entry order. On the HealthKit side, `HealthKitAssembly` replaces the static `HealthKitConverter` pipeline: `SourceFacts` resolves what a sample says about its origin (recording device, writer, native and writer-record identifiers, withheld metadata) in one place, and the content builders (Observation, workout segments, ECG and its average heart rate, recording documents) no longer reach into an envelope. The old envelope code (`GraphEnvelope`, `GraphChildOutput`, `HealthKitGraphContext`, the device and sync-identity helpers) is deleted. `HealthKitFHIRExporter` converts through the assembly directly; the deprecated `HealthKitConverter` entry points forward to it until they are removed. Output is unchanged: the 40 golden graphs match byte for byte. Tests that exercised the deleted internal seams reach the same behaviour through a test-only seam file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes from the verified step-6 specifications that live in the envelope, the contract and the exporter: - Workouts export at all (F1). Every HKWorkout conversion used to be refused: its segment children claimed only grove-mobile-workout-segment, which the HealthKit conversion Provenance does not govern. The pinned guide defines no HealthKit segment output, so a workout now exports its session alone, under output role workout and discriminator single, with the full graph context. Its events and activities are withheld (field dispositions say so), segment construction and the assembler's hasMember hook are deleted, and the retraction of a workout names exactly that session output. New goldens: workout-session, retraction-workout. - Document graphs under a distinct gateway application (F2) no longer carry a gateway Device that nothing references (mobile-support.connected); the guide defines the gateway link for Observations only. - Event instants (F7) — Bundle.timestamp, Provenance.recorded and occurred, DocumentReference.date and every retraction instant — are written by ExchangeInstant: UTC, millisecond precision, ASCII digits, no binary64 noise. A retraction start before 0001-01-01 is stated as that instant. Whole-second instants are byte-identical. - A recording document carries no writer-record identifier (F10, part 1): the version that must travel with it has no carrier on a DocumentReference. This undoes a regression the shared assembler introduced; the sync pair is still validated. - The validator applies the kit's whole adapter-provenance-graph rule (F11): every adapter output is covered by exactly one conversion Provenance of its adapter, checked last as the kit does. - RolePolicy.gatewayForOwnWrites (F3) states the converting application as gateway only for samples written by the build it runs (bundle identifier and HKSourceRevision.version equal to its build). Merging a self-written sample's writer Device into the converter's needs a guide change first: the pinned mapping requires the writer to state the revision version itself. - Retraction (F8, simple policy): a workout-route deletion is retracted only while Options.route is .authorized; an undisclosed route was never exported, and retracting it would disclose that it existed. Outcomes now arrive in input order. All 40 earlier goldens are byte-identical; SensorKit and Questionnaire suites pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Implements the Grove part of the exchange-ledger design (ledger-design/final-spec.md sections (a), (b), (d) and the Grove tests of (f), steps 1 to 3 and the ledger half of step 5), except the writer policy, which is the next step. Storage and sequencer (GroveFHIRContract): - ExchangeEventSequencer.Storage is a backend-independent, transactional string-to-bytes map (Storage.transaction, Transaction.read/write/remove/keys(prefixedBy:)) with the C1-C5 contract documented in the new ExchangeLedgerStorage article. InMemoryStorage is the public reference implementation; inMemory() uses it. - The ledger keeps three versioned JSON entry kinds: `producer`, `event/<key>` and `facts/<digest>`. Every read is validated and faults surface as typed LedgerError (unsupportedEntryVersion, corruptEntry), never a trap; reset() always recovers. - reserve(_:at:facts:) is one transaction over its own keys: a key whose stored fingerprint matches returns its instance, sequence, instant and frozen facts and writes nothing; any other request takes the next sequence (sorted request order) under the current facts. The producer instance is returned with every reservation, and a counter that would overflow mints a new instance. - Holds are tracked in process memory (HoldRegistry); finish(_:released:forgetting:) removes a reservation only when its last live holder finishes and some holder released it, and only while the key still holds that exact (key, instance, sequence). Nothing is pruned implicitly; forgetReservations(madeBefore:) is the explicit maintenance call. - Deleted: State, Retention, StateError, the producerInstance getter, releaseIgnoringErrors, the sequencer lock and the reservation's Codable conformance. - ExchangeEventFacts (application, host, studies) are stored once per distinct value, decoded through the validating initializers, and every graph is built from the decoded entry, first delivery and redelivery alike. ExchangeEnvelope carries them and swaps them per event (with(_:)); the HealthKit assembly builds its assembler per request facts. - OpaqueIdentityScope.ledgerFingerprint, ExchangeGraphAssembler.outputRevision and HealthKitAssembly.outputRevision (both 1) feed the exporter's context fingerprint. Exporter (GroveHealthKitFHIR): - One reserve transaction per export or retraction call. Requests carry an exhaustive context fingerprint (output revisions, identity scope, subject, repository scope, every option by exhaustive switch) plus record parts (an ECG's sorted symptom UUIDs, a retraction's clamped bounds), so a changed context never reuses an identifier. - ECG symptoms are paired with their reservations by request, not by position. - Nothing is released mid-call: refusals and policy omissions keep their reservations until the receipt is released. - Receipt is a final class that releases at most once (also across copies), in one transaction, none when empty; dropped unreleased it lapses. A retraction receipt also forgets each deleted record's active reservation, only on release. - The gateway-for-own-writes role compares against the frozen application build. Docs: a new ExchangeLedgerStorage article (C1-C5, the backend table, cost, holds); the contract and HealthKit recipes now build a producer over the app's ledger, store graph.json verbatim and release the receipt after the cursor commit; the HealthKit catalog lists the exporter and its receipt, and three cross-module symbol links that could not resolve (two pre-existing) are plain code voice. Both modules' DocC builds report no symbol-link warnings. Audit ledger findings addressed: section 2 findings 1 (unfrozen facts), 2 (instance read in a second transaction), 3 (key-only release), 4 (keys not scoped to subject or repository, via the fingerprint), 5 (stored ledger not validated), 6 (implicit retention and the false releaseIgnoringErrors doc), 7 (storage contract), and the recipe part of 11; section 3 findings 4 (two deletions of one record shared an event), 5 (empty receipt opened a transaction), 6 (positional ECG symptom pairing), 7 (export(records:) untested, now through the internal seam) and the Receipt part of 8. Deviations from the design: ledgerFingerprint frames every derived system, not only the event system; a stored producer instance must state an RFC 4122 version, as every event identifier requires; the deprecated converter keeps its positional symptom contexts (HealthKitAssembly.SymptomRequests.positional) so its refusal order is unchanged. Tests: the sequencer and storage-contract tests move to GroveFHIRTests (G1, G5-G10, G12-G15, G18 codec), with CountingStorage, FaultyStorage and a single-file FileStorage; exporter tests G2, G3, G3b, G4, G8-G11, G17, G18, E1 and E2 in GroveHealthKitFHIRTests. The existing exporter tests now read sequences from the graphs, as new keys take sequences in sorted request order, and refusalsDoNotEndTheExport asserts that a refusal holds its reservation. Output unchanged: every golden is byte-identical, verified by GoldenGraphTests and the new G17 digest table at output revision 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frozen facts no longer rest on convention. HealthKitAssembly stored a whole ExchangeEnvelope built from the producer's live application, host and studies, and each graph swapped its reservation's facts in through with(_:); a call site that forgot the swap would have stated live facts under a frozen event. - ExchangeEnvelope is now its Scope (adapter, identity scope, subject, repository scope) plus the event's facts. HealthKitAssembly keeps only the Scope and builds ExchangeEnvelope(scope:facts:) per graph from the request's frozen facts; with(_:) is gone, and the exporter no longer hands producer facts to the assembly at all. - ExchangeProducer prepares its facts once at init (encode, digest, decode back through the validating initializers). Facts that do not survive that round trip are a configuration fault, ExchangeProducer.ConfigurationError.unfreezableFacts, instead of a LedgerError.corruptEntry whose remedy (reset) could never fix them. Exporters reserve through ExchangeProducer.reserve(_:at:), which passes those prepared facts, so a call no longer re-encodes, hashes and decodes them. ExchangeEventSequencer.reserve(_:at:facts:) takes the PreparedFacts and is internal. - PreparedFacts decoding no longer throws corruptEntry as control flow from its canonical helper; invalid stored values map to corruptEntry in one place. - The ExchangeProducer init doc names the sequencer as the ledger of sequences and frozen facts, as its property doc already did. Audit ledger: section 2 finding 1 (unfrozen facts), now enforced by construction. G1 review items: design M2, minor 1, the protocolURL nit, minor 3(e). Output unchanged: GroveHealthKitFHIR macOS suite including GoldenGraphTests and the G17 digest table passes with every golden byte-identical; GroveFHIR suite passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cleanup from the G1 review of the exchange ledger; no behaviour changes. - One RFC 4122 check: a stored producer instance is validated with ExchangeEventIdentifier.statesRFC4122Version, the check every event identifier already passes, instead of a second bit-level copy in LedgerEntryCoding. - One digest-form check: ExchangeIdentity.isUnpaddedBase64URLDigest (43 characters of base64url) serves both the opaque-identity validator and the ledger's facts digests; LedgerEntryCoding.isDigest is gone. - Dead state removed: ExportContext.callParts (never read; only the framed bytes are used), ExchangeEventKey's Codable conformance (nothing encodes keys since the ledger stores them as entry keys), CountingStorage.snapshot (unused). The reservation's sequence is computed from its handle instead of stored twice. - Renames: ReserveTransaction is ReserveCall (it is one reserve call, not a Transaction conformer); EventEntry.facts is factsDigest (the stored JSON member keeps its name "facts", so the entry layout is unchanged). - The keyed ECG symptom lookup can no longer miss: validation admits only the seven symptom types, all registered source types, and the exporter plans a request for every registered symptom; the positional shape keys every symptom. The unreachable symptomContextCountMismatch throw is a preconditionFailure naming that invariant. - A bundled Patient that cannot be encoded fingerprints as the constant "unencodable" instead of a random UUID, so the fingerprint is deterministic; no JSON text equals it, and such a subject's events never carry a graph. - G6 sleeps with Task.sleep(nanoseconds:), which the iOS 15 / macOS 12 floor has. - CountingStorage's doc says it counts totals over every key, which is what G15 bounds. Not changed: LedgerCountingStorage (GroveHealthKitFHIRTests) and CountingStorage (GroveFHIRTests) stay separate, as SwiftPM test targets cannot share sources and a shared test-support target for one helper is disproportionate. G1 review items: design minor 4 (RFC 4122 and digest), minor 5 (callParts, Codable, reservation sequence, CountingStorage.snapshot), nits (ReserveTransaction, EventEntry.facts, unreachable keyed miss, subject fallback, Task.sleep). HoldRegistry's test-only count/isEmpty go with the hold-registry change that follows. Output unchanged: GroveHealthKitFHIR macOS suite (goldens and the G17 digest table) and GroveFHIR suite pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 1.4 has the ledger reader check that a stored reservation instant is one ExchangeInstant can state; the reader did not. A stored instant of Int64.max reached the conversion and surfaced as a dependency refusal (FHIRDateParserError.invalidYear) instead of the typed ledger fault. - EventEntry(decoding:) requires -62_135_596_800_000 <= instant <= 253_402_300_799_999 (0001-01-01T00:00:00Z through 9999-12-31T23:59:59.999Z, now ExchangeInstant.statableMilliseconds) and throws LedgerError.corruptEntry otherwise; reset() recovers, as for every other corrupt entry. - reserve refuses such an instant before it touches the ledger, with the existing ExchangeIdentityError.invalidInstant. Without this the ledger would write an entry its own reader rejects, and a redelivery would end the whole call with corruptEntry. Deviation, needed for consistency: an export or retraction at an unstatable instant (only reachable with a caller-supplied instant, never with .now) now ends the call with that error instead of refusing each record with a dependency failure. - The export and retract docs list what ends a call: an unstatable instant, a LedgerError (reset() recovers), and errors of the storage or of receive. Tests: G14 rows for an instant after year 9999, before year 1 and Int64.max; both bounds read back as valid reservations; reserve at unstatable instants (year 10000, before year 1, infinity) throws and opens no transaction; an export and a retraction at an unstatable instant throw and touch no ledger. Audit ledger: section 2 finding 5 (stored ledger not validated). G1 review items: design minor 2 and correctness minor 5 (PROBE-F), part of design minor 3(c). Output unchanged: GroveHealthKitFHIR macOS suite (goldens and the G17 digest table) and GroveFHIR suite pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A narrow window let a release remove a reservation another call had just reused: the last holder's finish ended its hold, then opened its removal transaction; a reserve of the same key that committed in between reused the stored reservation and took a hold, and the removal then deleted it under that holder. The holder's redelivery became a new event: a duplicate, never a reuse, but avoidable. - HoldRegistry also tracks the reserves in flight, per event key of one ledger. reserve registers its keys before its transaction and, after it, turns them into holds in one locked step (or just drops them when the transaction did not commit), so a reservation it may reuse is never seen unused in between. - finish checks, inside its removing transaction, that no live call holds the exact handle and no reserve of its key is in flight on the same ledger before removing it. A reserve that registered before the check keeps the reservation; one that registers later starts its transaction after the removal, as a storage runs one process's transactions one at a time (every backend in the article's table does). The registry lock is taken inside the transaction only briefly and never held across I/O. - Holds stay keyed by handle, which includes the producer instance and so is unique across ledgers. Keys in flight are scoped to the ledger: ExchangeEventSequencer identifies it by its storage object (so sequencers over one storage share it), or by itself for a storage that is not a class instance. Scoping by key alone would let any ledger in the process (another participant's, or a parallel test's) that reserves the same record keep this ledger's reservation. - The test-only HoldRegistry.count and isEmpty are replaced by mayBeReused(_:in:), which finish needs; the tests that read them now assert per handle, including that a failed reserve leaves no key in flight. - The storage article's holds section states the in-flight rule and its premise. Tests (G9f, GroveFHIRTests): the review's PROBE-G as a test, where a reserve commits between the last holder's end and its removal transaction; the in-flight case, where the release runs while the reserve waits for its transaction; and a reserve of the same key in flight on another storage, which keeps nothing. Verified by mutating and running the suite: without the check in finish the first two fail, registering keys only after the commit fails the in-flight one, and scoping keys without their ledger fails the other-storage one. G1 review items: correctness minor 7 (PROBE-G), design minor 5 (HoldRegistry count/isEmpty). Audit ledger: section 2 finding 3 (release ownership). Output unchanged: GroveHealthKitFHIR macOS suite (goldens and the G17 digest table) and GroveFHIR suite pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ExchangeLedgerStorage: the keychain's C2 cell again says that the commit surviving power loss is Apple's guarantee and unverified here, as the design states it ([unverified] in spec 1.3); the article had dropped the caveat. - ExchangeLedgerStorage and the Receipt doc no longer claim a release opens no transaction "when nothing was reserved": a retraction's receipt forgets each deleted record's active reservation, so its release runs one transaction whenever it names a deletion, even when every deletion was of a type with nothing to retract (PROBE-C; the behaviour is per spec 1.5.3 and is kept). An export's receipt runs none when it reserved nothing or another call still holds its events. - ExchangeLedgerStorage's "a release only ever removes the exact reservation its call made" now names its one exception, a retraction's forgetting. - The GroveFHIRContract article again tells adapters that convert under an ExchangeEventContext the caller builds (GroveSensorKitFHIR today) to persist the producer instance and durably advance the next sequence before emitting, in the event-identifier section and the what-to-persist table; the ledger recipe had dropped that guidance. The export and retract docs naming LedgerError and the reset() recovery landed with the instant validation, which rewrote the same sentences; the ExchangeProducer init doc landed with the prepared facts. Audit ledger: section 2 finding 11 (contract article), section 3 findings 5 and 8 (receipt cost and docs). G1 review items: design minor 3(a), 3(b), 3(d); correctness minor 6. Output unchanged: documentation and doc comments only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The G1 review re-applied 42 mutations to ffec7fb; 11 survived the suite. Each now fails it. - G3 computes every context's request fingerprint directly (exporter.context.request(for:).fingerprint) and requires the base context and its 30 perturbations to be pairwise distinct, naming any colliding pair. It shared one ledger without releases before, so each perturbation was only compared with the previous one and a dropped part that followed another perturbation passed. New perturbations: the identity scope's key id (same systems, so only ledgerFingerprint can tell it) and the bundled Patient's content (same pseudonym). A second test ties the direct fingerprints to the export path: the stored reservation carries the base context's fingerprint, an equal context reuses the event and a perturbed one does not. - G3b compares, for the defaults and every option perturbation, each stored property's own value parts (through Mirror) with the parts fingerprintParts pairs with its name, so a value paired with the wrong name fails, not only a missing name. - G2 for gatewayForOwnWrites: a self-written sample (revision 100) under build 100, redelivered before release by a producer rebuilt with build 110, is the same event and byte-identical; the comparison uses the frozen build. After release the new event compares with build 110 and states no gateway. - A three-deletion retraction takes one reserve transaction and its release one more. - A retraction with only nothing-to-retract deletions reserves nothing, and its release runs one transaction that forgets the active key (PROBE-C, documenting the behaviour the corrected docs describe). - A released receipt dropped while another call holds the same event leaves the event stored: deinit after release() ends no hold a second time. - Record paths through export(records:): .electrocardiogram (refused by the record path's voltage validation, as the fixture ECG reports no voltage count; its and its symptom's reservations are held until release), .heartbeatSeries and an authorized .workoutRoute (byte-equal to the assembly's graph under the same event), and a route the policy omits, which delivers nothing, keeps the export going and holds its event until release. - E1 on every OS: a duplicated symptom is refused as duplicateSymptomSource, so the keyed ECG path is covered where no unregistered category type exists (before OS 27). Mutation acceptance (each re-applied to the final tree of this series, full GroveHealthKitFHIRTests target on macOS, then reverted; M5b re-expressed for producer.reserve(_:at:), the others with the review's exact texts): | Mutation | Change | Result | Failing tests | | --- | --- | --- | --- | | M2b | gatewayApplication build dropped from the fingerprint | FAIL | G3 pairwise ("gateway build" = "role gatewayApplication") | | M2c | native-identifier type display dropped | FAIL | G3 pairwise ("type display" = "type") | | M2g | repository scope dropped | FAIL | G3 pairwise ("repository scope" = base) | | M2m | legacyBundleID .healthKitUUID fingerprints as none | FAIL | G3 pairwise ("legacyBundleID" = base) | | M2o | udi paired with a constant .omit | FAIL | G3 pairwise ("udi" = base), G3b value pairing | | M2j | bundled Patient JSON dropped | FAIL | G3 pairwise ("bundled Patient content" = "bundled subject") | | M2k | key id dropped from ledgerFingerprint | FAIL | G3 pairwise ("key id" = base) | | M1c | gatewayForOwnWrites compares with the live build | FAIL | G2 gatewayForOwnWrites | | M5b | one reserve transaction per retraction request | FAIL | three-deletion retraction (3 transactions) | | M4b | Receipt.deinit ignores isFinished | FAIL | released receipt dropped while another holds | | ME1a | keyed symptom requests count-checked | FAIL | E1 duplicate symptom, E1 unregistered symptom | G1 review items: design M1; correctness MAJOR 1 and 2, minors 3, 4, 6 (test), 8 and the E1-on-every-OS item. Output unchanged: tests only; GroveHealthKitFHIR macOS suite passes (266 tests, 2 skipped by availability) with every golden byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpaqueIdentityScope.ledgerFingerprint must separate scopes by key id, epoch,
key and systems, but only the HealthKit exporter's G3 pairwise test checked
it. Dropping the key id from the fingerprint (review mutation M2k) passed the
GroveFHIR suite, so a build or platform that runs only the contract's tests
would miss the regression.
- GroveFHIRTests gains LedgerFingerprintTests. Scopes that differ only in the
key id, the epoch, the key, the ten opaque systems, the event system or the
entry-node system fingerprint pairwise apart, and the test names any
colliding pair. Equal scopes fingerprint equal, as an unpadded base64url
SHA-256 digest that does not contain the key.
- The HealthKit suite's ledgerFingerprintIsKeyed is removed, because the new
tests cover everything it checked. G3 keeps its key-id perturbation.
Verified by mutation, each run against the new suite (GroveFHIRTests):
- M2k, key id dropped from the parts: fails ("key id shares its ledger
fingerprint with base").
- Epoch dropped: fails ("epoch shares ...").
- systems.all replaced by systems.opaque.all: fails ("event system shares ...").
G1 verification finding: gates-mutations nit (M2k caught only by the
HealthKit suite).
Output unchanged: test-only change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The G1 verification said ExchangeProducer.ConfigurationError.unfreezableFacts
was unreachable and asked for it to be removed in favour of a
preconditionFailure. It is reachable. URLComponents().url, and also
NSURL(string: ""), is a URL whose text is empty (checked on this macOS 27
toolchain). StudyEnrollment accepts it as its protocol canonical. The ledger
then stores "" as protocolURL, and Canonical parsing cannot read that back, so
PreparedFacts refuses the facts. A preconditionFailure there would crash the
app at producer construction over a configuration fault. The case therefore
stays, and this commit covers it:
- The case's doc names that example.
- ExchangeProducerTests gains unfreezableFactsAreRefused, which builds the
producer with that enrollment and expects .unfreezableFacts; a well-formed
enrollment still builds.
- The diagnostic test now includes the case.
Verified by mutation: with PreparedFacts.init using the caller's facts
instead of decoding its own bytes, the new test fails ("an error was expected
but none was thrown"). ExchangeProducerTests and OpaqueIdentityScopeRootTests
pass on the fixed tree.
G1 verification finding: deviations-concurrency minor (unfreezableFacts),
rejected with the reason above.
Output unchanged: no source behaviour changes, only a doc comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The in-flight protection from b53930c scoped a reserve's keys by the storage object, or by the sequencer for a value-type storage. Sequencers over two objects or values in front of one backing store therefore did not see each other's reserves. A release could remove a reservation that another call had just reused and committed but not yet held, and that call's redelivery became a new event. MHC as specified (spec 5.3 and 5.5) creates exactly this setup, because FHIRExchangeLedger.sequencer(accountDataGeneration:) runs per batch. The verification reproduced the race with two wrapper objects, two value wrappers, and two FileStorage objects on one directory. How the protection works now: - A reserve notes the exact handles its transaction returns. It does this from inside the transaction, before the commit, and turns them into holds in one locked step after it, or drops them when the transaction did not commit. The notes live in a per-call HoldRegistry.Notes and are deduplicated under the registry lock, so an attempt the storage runs again notes nothing twice. - finish removes a reservation only when, checked inside its own transaction, no live call holds the handle and no reserve has noted it. A storage runs one process's transactions one at a time. So a reserve whose transaction ran first has already noted the handle, and one that runs after the removal no longer finds the reservation. - Handles include the producer instance, which every ledger mints for itself (C4 forbids cloning a ledger). They are therefore unique across ledgers and the same through every object, value or sequencer in front of one ledger. LedgerIdentity, the AnyClass branch, the ledger property and the per-ledger key map are removed. HoldRegistry.mayBeReused(_:in:) is renamed isHeld(_:). - The verification's option (a), scoping keys alone process-wide, is not taken. It couples unrelated ledgers in one process: a parallel HealthKit test that reserves the same fixture record on the shared registry would keep another test's reservation. The test that a reserve of the same key on another ledger keeps nothing fails under key-only scoping. - One behaviour changes at the boundary. A reserve that has not started its transaction when the release's transaction runs is now serialized after the release. It finds the reservation removed, takes a new event and holds it. Before, its keys counted as in flight from the start of the call, so it reused the released event. The old G9f in-flight test now checks the serial outcome. In both versions, a reservation a call holds is never removed. - Bodies now note handles in process memory until the call returns. Storage.transaction and the article therefore promise no effect outside the transaction "that outlasts the call", which still lets a storage run an attempt again. Tests (GroveFHIRTests, ExchangeEventHoldRaceTests): - The verification's race, with the reuse gated after its commit and before its hold, through two storage objects, two storage values (parameterized), and two FileStorage objects on one directory. Each case checks that the reservation is kept and that the reusing call's redelivery is an exact retry. - A reservation that another ledger is about to hold under the same key keeps nothing in this ledger. - A release whose transaction runs before a waiting reserve's removes the reservation, and that reserve's redelivery retries its own new event. - G7 checks that a failed attempt leaves none of the handles it would have returned noted. Verified by mutation (GroveFHIRTests ledger suites): - b17f10d mechanism restored: the objects, values and file cases fail, and so does the waiting-reserve case. - Notes taken after the commit: the objects, values and file cases fail. - finish checking holds only: the objects, values and file cases fail. - Notes never ended: G7, G8, G9a, G9d and the G9f removal cases fail. - Key-only scoping, option (a): the other-ledger case fails. G1 verification finding: deviations-concurrency minor (sequencers over separate storage wrappers). The MHC spec needs no change, because FHIRExchangeLedger may build a sequencer and a storage object per batch. Audit ledger: section 2 finding 3 (release ownership). Output unchanged, verified by the GroveHealthKitFHIR macOS suite (goldens and the G17 digest table) and the GroveFHIR suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The article said the in-flight rule relies on the storage running one process's transactions one at a time, "as every backend above does". Its own SQL row, however, allowed bare serializable isolation. Serializable engines that let transactions overlap, such as snapshot isolation with validation or optimistic concurrency, do not run them one at a time. On such an engine a release can check for holders before a concurrent reuse commits, and then remove the reservation anyway, because the reuse writes nothing the release read. The verification reproduced this with an optimistic backend that meets C1 to C5. The fix is documentation, one of the two options the verification offered. Making the reuse write instead would add a write to every reuse-only call (spec 1.5.1 cost). - SQL row, C3: a transaction that takes the write lock when it begins, such as SQLite's BEGIN IMMEDIATE. Serializable isolation whose transactions overlap also meets C3, with the cost the Holds and receipts section describes. MHC's GRDB DatabaseQueue with .immediate is the first form. - Holds and receipts: the premise is now its own sentence and names how every backend meets it (its lock, or a write-locking transaction). A storage whose serializable transactions overlap still never reuses a sequence. A release there can remove a reservation that a concurrent call has just reused, and that call's redelivery becomes a duplicate. - The Storage protocol doc says the same, so the API reference states it where an implementer reads. Tests (GroveFHIRTests): - OptimisticStorage, a test storage whose serializable transactions overlap: snapshot reads, buffered writes, backward validation, and a re-run on conflict. - G6 (concurrent reserves, releases and resets, checked by the reuse oracle) now also runs over OptimisticStorage and finds no reuse. This supports "still never reuses a sequence". - A new G9f case pins the documented outcome. The release is gated between its body and its commit, a reuse commits in between, and the holder's redelivery takes a new, higher sequence under the same instance. The change itself is documentation, so no test can fail without it; the new tests check the claims the documentation makes. G6 and the new case also pass on b17f10d's mechanism, because the outcome does not depend on it. G1 verification finding: deviations-concurrency minor (a serializable backend whose transactions overlap). Deviation from spec 1.3: the SQL C3 cell is reworded as above. Output unchanged: documentation and tests only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes two nits from the G1 fix verification.
Test: a storage may discard an attempt and run a transaction's body again
(clause wording in ExchangeLedgerStorage.md). HoldRegistry.beginReserving
dedups the notes per reserve call, so an attempt that re-runs and returns
the same handle notes it once; nothing pinned that. The new test
ExchangeEventHoldTests.retriedReserveNotesOnce goes through a test-only
RetryingStorage that runs the next transaction's body once, discards that
attempt, then runs it again and commits. Both attempts reuse one
reservation; after releasing, the registry no longer holds the handle and
the reservation is gone. Ported from the G1 fix-verification probe
(g1fv-probe/G1FixVerifyNotesProbeTests.swift, rerunAttemptNotesOnce).
Mutation proof: beginReserving counting every attempt
(`for handle in handles { notes.handles.insert(handle);
reserving[handle, default: 0] += 1 }`) fails exactly this test, 7/8 of
ExchangeEventHoldTests passing, with
"Fixtures.isHeld(first[request]?.handle, by: sequencer) -> false: nothing
stays noted after the retried call" and "the release removes the
reservation". Reverted; the suite passes 8/8.
Docs: "its bodies have no effect outside the transaction that outlasts the
call" (ExchangeEventSequencer.Storage.transaction and
ExchangeLedgerStorage.md) read as if the transaction outlasted the call. It
now says the bodies have no effect outside the transaction "other than
process-memory notes that end when the call returns". The Holds section now
says "every backend above in its primary form", because the SQL row also
admits overlapping serializable transactions, which the next sentence
treats separately.
Output unchanged: no source behaviour changed (doc comments and a test
only); all 43 goldens and the G17 table pass unchanged.
Gates:
- GroveHealthKitFHIR macOS: Passed, 266 total, 264 passed, 2 skipped
(same 2 skips as at 35f6894), 0 failed.
- GroveFHIR macOS: Passed, 103/103.
- GroveSensorKitFHIR macOS: Passed, 83/83.
- GroveQuestionnaire macOS: Passed, 260/260.
- GROVE_LOWERED_DEPLOYMENT_TARGETS=1 xcrun swift build --target
GroveFHIRContract: Build complete.
- swiftlint lint --strict --quiet (GroveFHIRContract, GroveHealthKitFHIR,
GroveHealthKitFHIRTests, GroveFHIRTests): no findings.
- reuse lint: compliant (2873/2873).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix-round step G2 (final-spec (c) 3.1/3.2, (g) step 4, tests W1-W4 of (f),
fingerprint 2.4). HKSourceRevision does not say whether a source is an
application or a device, and the pinned HealthKit guide forbids classifying
it from the identifier's shape, the source name or productType
(ig-pin healthkit/input/pagecontent/mapping.md:142-143); only the caller's
classification states a writer, and the author's name, identifier and
version are copied from that sample's HKSourceRevision (:136-137, :144).
Closes (rework-inputs/audit/ledger.md):
- 3, confirmed finding 1 (major): the .automatic default classified sources
by hasPrefix("com.apple.health."). Deleted; the default is .omit.
- 4, confirmed finding 2 (minor, both directions): the recording Device now
exists only when RecordingDevicePolicy resolved one from HKDevice, so
recordingDeviceOmitted fires exactly when an HKDevice was declined (never
next to a resolved or source-derived Device, never lost for a declined
one).
- 3, confirmed finding 8, HealthKitWriter docs: the .device doc that
contradicted inference and the "is an application" / "This is the
default" sentences are gone.
API:
- WriterPolicy = .omit (default) | .applications(Set<String>) |
.classify(@sendable (HKSource) -> HealthKitWriter); it loses Hashable
(it holds a closure). HealthKitWriter = .application | .omit.
- HealthKitFHIRExporter.Options.writer and HealthKitConversionOptions.writer
default to .omit, and with them HealthKitAssembly's default request
options and the retraction request options.
- Deleted: WriterPolicy.automatic/.application/.device,
HealthKitWriter.device, ExchangeWriterDraft.recordingDevice and the
assembler branch that made the recording Device the author,
SourceFacts.recordingDevice(fromAppleSource:) and its call,
HealthKitConverter.appleDeviceSourcePrefix. What RecordingDevicePolicy
states from HKDevice is unchanged.
- Context fingerprint writer part: ["omit"] | ["applications", <count>,
<ids in UTF-8 byte order>] | ["classify"], by an exhaustive switch. The
count keeps the length-framed sequence unambiguous; byte order keeps it
independent of insertion order and collation.
- HealthKitAssembly.outputRevision 1 -> 2. ExchangeGraphAssembler's stays
1: the deleted draft case had no other producer, so the assembler emits
the same bytes for every draft that still exists.
- Docs: WriterPolicy, HealthKitWriter, HealthKitConversionOptions.writer,
TheConversionGraph.md, ConfiguringAConversion.md, and a short note in the
GroveHealthKitFHIR.md exporter recipe on classifying sources.
Output diff (goldens regenerated through TEST_RUNNER_GROVE_GOLDEN_OUTPUT_DIR
outside the worktree, compared token-losslessly, then copied). Every change
is in the allowed class: the writer application Device and its writer host
Device are removed, the Provenance author agent is removed, nothing else in
any graph changes (all other entries, entry order, references and warnings
identical; no dangling reference). Changed because the fixture relied on
the default writer (foreign writer org.example.writer, now unclassified):
- bundled-patient-subject:
$.entry[5] removed: Device urn:uuid:27bc90ae-3192-5159-990d-4764c5569192 (writer host, grove-host-device)
$.entry[6] removed: Device urn:uuid:dafff25f-7b2a-50f1-8062-35eaaa6d3c23 (writer application, healthkit-application-device)
$.entry[7->5].resource.entity[0].agent removed: [author -> urn:uuid:dafff25f-7b2a-50f1-8062-35eaaa6d3c23] (the removed writer application)
- electrocardiogram:
$.entry[5] removed: Device urn:uuid:659ba41a-b773-5177-9923-8a596c45de30 (writer host, grove-host-device)
$.entry[6] removed: Device urn:uuid:ac9e967e-1606-58e5-8a33-72e6f7e602af (writer application, healthkit-application-device)
$.entry[7->5].resource.entity[0].agent removed: [author -> urn:uuid:ac9e967e-1606-58e5-8a33-72e6f7e602af] (the removed writer application)
- electrocardiogram-symptom-companion:
$.entry[4] removed: Device urn:uuid:342152e5-0fa1-578f-ae92-0d3bc245ea35 (writer host, grove-host-device)
$.entry[5] removed: Device urn:uuid:7cdf03d0-e248-5b92-901d-299e17845605 (writer application, healthkit-application-device)
$.entry[6->4].resource.entity[0].agent removed: [author -> urn:uuid:7cdf03d0-e248-5b92-901d-299e17845605] (the removed writer application)
- electrocardiogram-with-symptom:
$.entry[5] removed: Device urn:uuid:08844702-36cb-57ff-b249-2b1b843fb79f (writer host, grove-host-device)
$.entry[6] removed: Device urn:uuid:965a804c-2f33-5fe0-ae1e-a4a328007269 (writer application, healthkit-application-device)
$.entry[7->5].resource.entity[0].agent removed: [author -> urn:uuid:965a804c-2f33-5fe0-ae1e-a4a328007269] (the removed writer application)
- gateway-application-role:
$.entry[5] removed: Device urn:uuid:48aeeb20-6643-5b41-b3a3-44093a548f15 (writer host, grove-host-device)
$.entry[6] removed: Device urn:uuid:480efa67-eb12-5f15-92ba-fbc30cf24d40 (writer application, healthkit-application-device)
$.entry[7->5].resource.entity[0].agent removed: [author -> urn:uuid:480efa67-eb12-5f15-92ba-fbc30cf24d40] (the removed writer application)
- gateway-role:
$.entry[4] removed: Device urn:uuid:1f896505-6996-55e4-8231-8412a4c864a8 (writer host, grove-host-device)
$.entry[5] removed: Device urn:uuid:1fb8e303-b58a-5107-ac2d-9ddced20bb00 (writer application, healthkit-application-device)
$.entry[6->4].resource.entity[0].agent removed: [author -> urn:uuid:1fb8e303-b58a-5107-ac2d-9ddced20bb00] (the removed writer application)
- heartbeat-series:
$.entry[4] removed: Device urn:uuid:35f19e6b-6e60-5b6c-b203-d4ac0dce18aa (writer host, grove-host-device)
$.entry[5] removed: Device urn:uuid:f72582bb-3a7c-5255-808c-cdba39fd1a84 (writer application, healthkit-application-device)
$.entry[6->4].resource.entity[0].agent removed: [author -> urn:uuid:f72582bb-3a7c-5255-808c-cdba39fd1a84] (the removed writer application)
- native-identifier-disclosure:
$.entry[4] removed: Device urn:uuid:51571f65-d367-5848-9557-f958f42357bc (writer host, grove-host-device)
$.entry[5] removed: Device urn:uuid:647ca0a1-1154-58d5-bb7c-a880387bbbcb (writer application, healthkit-application-device)
$.entry[6->4].resource.entity[0].agent removed: [author -> urn:uuid:647ca0a1-1154-58d5-bb7c-a880387bbbcb] (the removed writer application)
- two-study-enrollments:
$.entry[10] removed: Device urn:uuid:2a8208fd-d3b8-5a36-8e6c-fe64275ae3da (writer host, grove-host-device)
$.entry[11] removed: Device urn:uuid:e34dda89-a30e-5394-b8ee-b0595e1dd7c7 (writer application, healthkit-application-device)
$.entry[12->10].resource.entity[0].agent removed: [author -> urn:uuid:e34dda89-a30e-5394-b8ee-b0595e1dd7c7] (the removed writer application)
- udi-disclosure:
$.entry[4] removed: Device urn:uuid:590dd017-f9f1-5627-abdc-b1f47959e10e (writer host, grove-host-device)
$.entry[5] removed: Device urn:uuid:4f6c6282-7153-5005-8765-7a6ad1ac2428 (writer application, healthkit-application-device)
$.entry[6->4].resource.entity[0].agent removed: [author -> urn:uuid:4f6c6282-7153-5005-8765-7a6ad1ac2428] (the removed writer application)
- workout-route:
$.entry[4] removed: Device urn:uuid:18fba38a-2ee6-5140-80f2-d84f938266b4 (writer host, grove-host-device)
$.entry[5] removed: Device urn:uuid:d21d9031-3216-5431-b57f-1b2afa147c46 (writer application, healthkit-application-device)
$.entry[6->4].resource.entity[0].agent removed: [author -> urn:uuid:d21d9031-3216-5431-b57f-1b2afa147c46] (the removed writer application)
- writer-device-classification.json -> writer-omitted.json:
$.entry[4].resource.entity[0].agent removed: [author -> urn:uuid:4d25df34-97c0-5441-a4e4-74e7de79ed10] (the recording Device, which stays)
- writer-device-classification-without-recording-device.json ->
writer-omitted-without-recording-device.json: renamed only, byte-identical
(the .device classification it pinned no longer exists; under .omit it
states exactly what it stated before).
- outlines.json: per changed case above, the two removed Device entries
leave its outline (same fullUrls and identifiers as listed); the two
writer-device-classification keys become writer-omitted and
writer-omitted-without-recording-device with identical outlines; all
warnings unchanged.
Unchanged (byte-identical): the 14 observation shapes, clinical-document,
the 5 retractions, repository-ids-on-every-node and the writer cases
writer-foreign-application, writer-self-build-equals-revision,
writer-self-older-build, writer-self-token-identical,
writer-host-equals-converter-host, writer-blank-name-with-sync-identity,
writer-without-version, sync-identity. These cases pin how a classified
writer travels (repository-ids-on-every-node names the writer nodes), so
their fixtures now pass .application explicitly; no other fixture changed.
G17: the 13 changed files carry healthKit: 2 with their new digests; the
byte-identical rename keeps healthKit: 1 under its new name.
Tests:
- New HealthKitFHIRExporterWriterTests: W1 exporter default (an Apple
per-device source com.apple.health.<uuid>, "Lukas's Apple Watch", without
HKDevice / with a declined HKDevice / with a resolved one: no writer, no
author, no Device naming the source, the HKDevice's recording Device only
when resolved, the warning only when declined) and W1 through
HealthKitConversionOptions() and .default; W2 .applications (listed
source states name, bundle id and version from HKSourceRevision and is
the author, byte-equal to the entry point under .application; unlisted
and Apple sources state nothing); W3 .classify (closure asked per source,
.application states the revision's values, .omit nothing); W4 scans
Sources/GroveHealthKitFHIR for the literal "com.apple.health. (and
`grep -rn '"com.apple.health.' Sources/GroveHealthKitFHIR` finds nothing).
- Fingerprint: G3 perturbations are now writer applications, a second
member, an empty set and classify; a writer-policy change gives a
reserved record sequences 1, 2, 3 across omit -> applications -> omit;
equal application sets built in different insertion orders (iteration
orders checked to differ) fingerprint equally, a member less does not.
- Deleted automaticWriterClassifiesAppleDeviceSources (W1-W4 replace it).
deviceSourceReusesRecordingDevice becomes "an unclassified source keeps
the recording Device its HKDevice names, and the Provenance names no
author". Test-helper writer defaults (HealthKitConversionContext test
init, HealthKitDeviceIdentityTests) follow production to .omit; the tests
that need a writer (invalidWriterBundleIdentifierIsRefused,
converterReadsTheStoredFacts) pass .application.
Mutation proof (each applied alone, full GroveHealthKitFHIRTests run,
reverted; the pristine files compared equal afterwards):
- (a1) Options.writer default .classify { _ in .application }: fails W1
exporter default, G3 (classify equals base), graphsEqualReference,
heartbeatSeriesRecord, authorizedWorkoutRouteRecord (5 failed).
- (a2) HealthKitConversionOptions writer default .application: fails W1
conversion options, healthKitOptionDefaults, matchesCheckedInGolden,
documentGraphsOmitTheGatewayApplication, graphsEqualReference, the
heartbeat and route record tests (7 failed).
- (b) .applications ignoring membership: fails W2 (org.example.other
stated and named as author).
- (c) .classify result ignored (closure called, .application returned):
fails W3 (org.example.other stated and named as author).
- (d) recording Device derived from a com.apple.health. source again: fails
W1 (both) and W4 (3 failed).
- (e) recordingDeviceOmitted appended next to a resolved Device: fails W1
(both), "A conversion reports exactly what the graph lost",
matchesCheckedInGolden, graphsEqualReference (5 failed).
- (f) applications fingerprint parts left unsorted: fails the
insertion-order fingerprint test.
Gates:
- GroveHealthKitFHIR macOS: Passed, 272 total, 270 passed, 2 skipped (the
same 2 as before), 0 failed.
- GroveFHIR macOS: Passed, 103/103.
- GroveSensorKitFHIR macOS: Passed, 83/83.
- GroveQuestionnaire macOS: Passed, 260/260.
- GROVE_LOWERED_DEPLOYMENT_TARGETS=1 xcrun swift build --target
GroveFHIRContract: Build complete.
- swiftlint lint --strict --quiet (GroveFHIRContract, GroveHealthKitFHIR,
GroveHealthKitFHIRTests, GroveFHIRTests): no findings.
- reuse lint: compliant (2874/2874).
- Scripts/validate-fhir-conformance.sh healthkit: "OK - all healthkit
resources conform to their declared R4 profiles" (28 resources), contract
generator and semantic-vector checks clean. Guides: a scratchpad copy of
the read-only ig-pin extract with mobile, sensor and healthkit built from
it (0 unsuppressed errors or warnings); the extract has no git history,
so the copy was git-initialized and GROVE_FHIR_REF pointed at that commit.
- xcodebuild docbuild GroveHealthKitFHIR: succeeded; no new warning in
GroveHealthKitFHIR or GroveFHIRContract (the unused public HealthKit
import warning in the Options file is gone, as WriterPolicy now exposes
HKSource).
Size (A): code 11,143 (+1,438), public 1,006 (+133)
(vs baseline Grove 9b086e6; vs parent 77e8b20: code -29, public -4.
Level B: code 21,205 (+1,438), public 1,909 (+133).)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
G2 review fix round (final-spec 2.4, the context fingerprint; G3 of (f)). The fingerprint tests could not tell two .applications sets of the same size apart: G3 paired sets of sizes 1, 2 and 0, and the membership test compared 24 members with 23. A fingerprint that kept only the member count passed the whole suite (mutation M10, re-verified at 55b9fc5 together with M24: 272 total, 270 passed, 2 skipped, 0 failed), and would reuse a reserved event for different bytes when a deployment swaps one listed application for another. Tests (HealthKitFHIRExporterFingerprintTests): - G3 gains "writer applications other" ({org.example.other}), the size of "writer applications" ({org.example.writer}) with another member. - applicationSetsFingerprintByMembership also swaps a member for another (app0 -> app24) and expects a new fingerprint. - New known-answer test: a fixed context (explicit output revisions 1/2, the test identity scope and subject, writer .applications({org.example.caff, org.example.cafe + U+0301})) has writer parts [applications, 2, org.example.cafe + U+0301, org.example.caff], compared byte for byte, and the fingerprint Y9AmAuXc9kZTd8ZjijBhBS7QD_7A8w4YKQLk7RXFRig. A reservation keeps the fingerprint it was made under across launches and app updates, so a build that derives the same context's fingerprint differently re-sequences every pending reservation on redelivery; the known answer makes such a change deliberate. The decomposed member separates UTF-8 byte order from Swift's String order. Doc (WriterPolicy fingerprintParts): "equal sets fingerprint equally whatever their insertion order or the platform's collation" overstated the guarantee. Set equality is canonical equivalence while the fingerprint frames raw UTF-8 (probe: Set(["org.caf\u{e9}"]) == Set(["org.cafe\u{301}"]) is true, their sorted parts compare == as Strings but differ as UTF-8). It now says sets of the same strings, byte for byte, fingerprint equally, and that canonically equal spellings fingerprint apart (a new sequence, never a reuse). Mutation proof (each applied alone to this tree, full GroveHealthKitFHIRTests on macOS, then restored and compared byte-equal; unmutated: 274 total, 272 passed, 2 skipped): - M09 (count dropped: ["applications"] + sorted): 1 failed, the known-answer test (parts and fingerprint). - M10 (members dropped: ["applications", count]): 3 failed, the known-answer test, G3 ("writer applications other shares its fingerprint with writer applications") and the membership test ("another member in its place"). - M14 (bundleIdentifiers.sorted(), Swift String order): 1 failed, the known-answer test (parts in the other order, another fingerprint). Output unchanged (no source change beyond a doc comment; goldens untouched). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s Exceptions G2 review fix round (final-spec (c) 3.1; audit ledger 3, confirmed finding 8, the HealthKitWriter docs; IG ig-pin healthkit/input/pagecontent/mapping.md:44-47 for the writer-record identity). Writer record. G2 moved the sync-identity golden to an explicit .applicationWriter and every converter test of the sync pair uses an unattributed sample (empty bundle identifier), so nothing pinned that an attributed sample from a source nobody classified still carries its writer-record identity and version. A SourceFacts that derived the writer record only under .application (mutation M24) passed the whole suite (re-verified at 55b9fc5 together with M10: 272 total, 270 passed, 2 skipped, 0 failed). New test in HealthKitFHIRExporterWriterTests: a foreign-writer heart rate with the sync pair (sync-abc, version 3) exported under Options() and under .omit, .applications listing it, listing another, listing none, .classify { .omit } and .classify { .application } carries exactly one writer-record identifier, equal to the identity scope's writerRecord(org.example.writer under the Apple bundle-identifier system, sync-abc), and the version extension "3". The WriterPolicy doc now says the sync pair travels as the writer-record identity under every policy. Docs (WriterPolicy.applications, HealthKitWriter.application): a classified source with a blank name or bundle identifier states no writer, and one whose bundle identifier is not a valid Apple bundle identifier is refused with HealthKitConversionError.sourceApplicationInvalid (SourceFacts.writer; the host can never fail there). The goldens writer-blank-name-with-sync-identity and GoldenGraphTests.invalidWriterBundleIdentifierIsRefused pin both. ProducerSurfaceTests.writerNodes ended with a conversion that once pinned .device and, after G2, repeated the test init's default .omit on an unattributed sample, so its two expectations pinned nothing. It now pins the other half of the test's claim: an attributed sample whose source the caller classifies as an application carries both writer nodes, and repository ids name each. Mutation proof (applied alone, full GroveHealthKitFHIRTests on macOS, then restored and compared byte-equal; unmutated: 274 total, 272 passed, 2 skipped): M24 fails the new writer-record test under the default, omit, applications listing another, no applications and classified as omitted (no writer-record identifier, no version); the two policies that classify the source as an application still pass, as they should. Output unchanged (goldens untouched; source changes are doc comments only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
G2 review fix round (final-spec (c) 3.2, which deleted the recording-Device author branch). With that branch gone, ExchangeGraphAssembler.Devices.authorURL was always writer?.applicationURL: one producer (resolveDevices) and one reader (the Provenance call in assemble). The stored field is deleted; the Provenance call reads devices.writer?.applicationURL and keeps the comment that the writer application is the author and that without one the Provenance names none. The Devices doc no longer claims to say who the author is. Output unchanged, verified by the GroveHealthKitFHIR goldens (byte-equal, G17 rows untouched), the GroveSensorKitFHIR and GroveQuestionnaire suites and the healthkit conformance run below. ExchangeGraphAssembler.outputRevision stays 1. Gates for this round (at this tree; the two earlier commits of the round changed tests and doc comments only): - GroveHealthKitFHIR macOS: Passed, 274 total, 272 passed, 2 skipped (the same 2 as before), 0 failed. - GroveSensorKitFHIR macOS: Passed, 83/83. - GroveQuestionnaire macOS: Passed, 260/260. - GROVE_LOWERED_DEPLOYMENT_TARGETS=1 xcrun swift build --target GroveFHIRContract: Build complete. - swiftlint lint --strict --quiet (GroveFHIRContract, GroveHealthKitFHIR, GroveHealthKitFHIRTests): no findings. - reuse lint: compliant (2874/2874). - Scripts/validate-fhir-conformance.sh healthkit (guides: the scratchpad copy of the read-only ig-pin extract built for G2): 28 resources, "OK - all healthkit resources conform to their declared R4 profiles"; contract generator and semantic-vector checks clean. - xcodebuild docbuild GroveHealthKitFHIR (macOS): succeeded; no unresolved symbol link in GroveHealthKitFHIR or GroveFHIRContract (the new HealthKitConversionError/sourceApplicationInvalid links resolve). Size (A): code 11,142 (+1,437), public 1,006 (+133) (vs baseline Grove 9b086e6; vs 55b9fc5: code -1, public 0. Level B: code 21,204 (+1,437), public 1,909 (+133).) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix-round step G3, audit ledger section 1 confirmed finding 7 (the
comparator treats canonically equivalent strings as equal, every golden is
ASCII) and the LosslessJSONValue BOM item of the ledger decision list.
LosslessJSONValue decides ExchangeGraph.isSemanticallyEqual (the exact-retry
predicate) and every golden comparison. Its synthesized equality compared
strings and member names with Swift String ==, which is canonical
equivalence, so "Sant\u{E9}" equalled "Sante\u{301}"; the guide's reference
comparator (ig-pin Tests/test_receiver_lifecycle.py:50-60, json.loads with
string lexemes, then Python ==) compares code points. And the string parser
decoded each UTF-8 run with String(bytes:encoding:), which drops a leading
U+FEFF, so "\u{FEFF}x" parsed as "x" (also right after an escape).
- Equality is written out: strings and number lexemes compare by
unicodeScalars; an object compares each member's value and the stored
member name scalar by scalar (a dictionary lookup finds canonically
equivalent names alike). Parsing is unchanged otherwise; two canonically
equivalent member names in one object stay a duplicate, as
StrictJSONScanner refuses them.
- UTF-8 runs are decoded with the UTF8 codec, which keeps every scalar.
- Docs: LosslessJSONValue and isSemanticallyEqual state the scalar rule.
- Test harness: TokenDiff reports a member name that differs only in its
scalars, and prints non-ASCII scalars as \u{...} so a failure is legible.
Tests:
- GoldenGraphTests.tokensCompareUnicodeScalars: precomposed vs decomposed
values and member names differ (with the reported path); a leading U+FEFF,
raw or after an escape, is kept; "" escaped equals the raw scalar.
- New golden writer-non-ascii-name (writers group, sequence 30): the foreign
application writer named "Sant\u{E9} Journal" under .applicationWriter, so
a non-ASCII string is pinned in bytes. G17 row added at the current
revisions (assembler 1, healthKit 2), no bump: no existing output changed.
Mutation proof (each applied alone, full GroveHealthKitFHIRTests on macOS,
then reverted; unmutated: 275 total, 273 passed, 2 skipped):
- synthesized equality restored: fails tokensCompareUnicodeScalars
("precomposed != decomposed").
- runs drop a leading U+FEFF again: fails tokensCompareUnicodeScalars.
- SourceFacts states the writer name decomposed
(decomposedStringWithCanonicalMapping): fails matchesCheckedInGolden for
writer-non-ascii-name at $.entry[5].resource.deviceName[0].name (golden
"Sant\u{e9} Journal", actual "Sante\u{301} Journal"). Applied together
with the synthesized equality, that golden passes again: only the scalar
comparison catches the drift.
Output diff: none. All 42 existing goldens regenerate byte-identical
(TEST_RUNNER_GROVE_GOLDEN_OUTPUT_DIR outside the worktree); outlines.json
only gains the writer-non-ascii-name entry.
Gates: GroveHealthKitFHIR macOS 275 total, 273 passed, 2 skipped;
GroveSensorKitFHIR 83/83; GroveQuestionnaire 260/260;
GROVE_LOWERED_DEPLOYMENT_TARGETS=1 xcrun swift build --target
GroveFHIRContract: Build complete; swiftlint --strict: no findings; reuse
lint: compliant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntities Fix-round step G3, audit ledger section 1 confirmed findings 5 (companion graphs, conversion identifiers and the case list are unpinned), 8 (setting GROVE_GOLDEN_OUTPUT_DIR skips the comparison and still reports success) and the fixture half of 10 (the workout golden is built from the benchmark's SampleFactory). - Regeneration fails visibly: with GROVE_GOLDEN_OUTPUT_DIR set, everyCaseIsDeterministicAndPinnable writes every case as before and then records "Regenerated N goldens into <dir> and compared none", so a run that compared nothing can no longer end in TEST SUCCEEDED. - Companions: GoldenOutput(primaryOf:companions:) requires the exact companion count of the set it takes the primary from (0 everywhere, 1 for electrocardiogram-with-symptom, whose companion has its own case). It replaces the five direct `.primary` call sites (GoldenFixtures.convert and the electrocardiogram, electrocardiogram-with-symptom, heartbeat-series and workout-route cases), which dropped any companion silently. - Identities and source: GoldenOutput keeps the conversion's ExchangeGraphIdentifiers and HealthKitSourceRecord, and matchesCheckedInGolden reads them against the graph's own tokens (reportMismatches): every reported output and Device is the entry its fullUrl names and carries that identifier, the Provenance is the one entry at its entry-node fullUrl, the source-record and artifact identities sit on the primary output, every Observation, DocumentReference and Device entry is reported (a distinct gateway application aside), and the source re-mints the graph's source-record identity through the test identity scope. The graphs are pinned by the goldens, so this pins what the API reports beside them. - Case list: already pinned by the G17 table (GoldenOutputRevisionTests, ffec7fb), whose literal rows must equal the checked-in goldens. - The workout fixture is GoldenFixtures.workout(withEvents:), the same run written out in the fixtures, no longer read from the benchmark factory; workout-session regenerates byte-identical. GoldenCaseError.workoutNotBuilt is gone with the lookup it guarded. Mutation proof (each applied alone, full GroveHealthKitFHIRTests on macOS, then reverted; unmutated: 275 total, 273 passed, 2 skipped): - an ECG without symptoms reports itself as its own companion: fails matchesCheckedInGolden, everyCaseIsDeterministicAndPinnable and two ExchangeGraphBytesTests with unexpectedCompanions(1). The same mutation against the previous harness (tests of e2e5397) passes all 275. - the assembler reports no childOutputs: fails matchesCheckedInGolden (electrocardiogram: ["outputs"]). - HealthKitAssembly reports every source as heart rate: fails matchesCheckedInGolden (step-count-period: ["source"]). - a regeneration run (TEST_RUNNER_GROVE_GOLDEN_OUTPUT_DIR outside the worktree) fails with the regeneration issue; with the Issue.record turned into a print it passes, which is the finding. Output diff: none. A regeneration writes all 43 goldens and outlines.json byte-identical to the checked-in files. Gates: GroveHealthKitFHIR macOS 275 total, 273 passed, 2 skipped; swiftlint --strict: no findings; reuse lint: compliant (tests only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Options
Fix-round step G3, audit ledger section 1 confirmed findings 2 (no golden runs
through HealthKitFHIRExporter or MHC's options), 3 (HKClinicalRecord glue
unpinned), 4 (envelope options combined only with heart rate), the exporter
half of 5 (the exporter delivers every graph of a set) and survivor M5 of
finding 1 (effective time truncated to whole seconds); G2 leftover: no golden
had an Apple per-device (com.apple.health.<uuid>) source.
Every golden so far came from the deprecated converter, so the exporter's own
layer (frozen facts and studies in the envelope, the options, the legacy
Bundle.id, the gateway role for own writes, the ECG companion delivery, the
retraction path, the clinical-record glue) was pinned nowhere in bytes.
New golden group "exporter" (sequences 100-119, GoldenGraphCases+Exporter
.swift): each case exports through a fresh in-memory ledger whose producer
entry hands out the case's sequence next under the fixed producer instance of
every golden, at the golden conversion instant, so the events are as
reproducible as the converter's. GoldenOutput gains init(_ export:), which
records the export's source and its diagnostics as code@location.
- exporter-default-apple-watch-heart-rate: Options() on a heart rate whose
source is com.apple.health.<uuid> ("Lukas's Apple Watch") and whose HKDevice
names its unit: no writer, no author, the recording Device only (W1 in
bytes).
- The deployment lane uses options equal to MyHeartCounts'
Options.myHeartCounts (HealthKitGroveConversion.swift:45-51 at MHC 79d70dbf)
under G2's writer policy: nativeIdentifier .authorized under the
deployment's own system, legacyBundleID .healthKitUUID, role
.gatewayForOwnWrites, writer .applications([own bundle id]), every other
option at its default; the producer states an application with a build and
one study enrollment, as MHC's does:
- exporter-deployment-own-heart-rate: the deployment's own write in the
running build, starting at a sub-second instant: gateway extension,
classified writer and writer host, disclosed UUID, Bundle.id, study links.
- exporter-deployment-electrocardiogram and ...-symptom: one export call
of an ECG with a correlated symptom through the evidence seam; the call
must deliver exactly these two graphs, the ECG's average-heart-rate child
and the symptom companion each carrying the study link.
- exporter-deployment-blood-pressure, exporter-deployment-state-of-mind:
study links on a correlation and on state of mind.
- exporter-deployment-retraction: a heart-rate deletion with sub-second
bounds through retract: Bundle.id, the period at millisecond precision.
- exporter-clinical-record-r4 / -dstu2: an HKClinicalRecord per admitted
release through export(_:), built by the new
StoredSampleFixtures.clinicalRecord (KVC on HKClinicalRecord and
HKFHIRResource, read back or fail), pinning convertClinicalRecord's glue.
watchOS has no clinical records; both are in GoldenCase.unavailableHere
there.
GoldenCase.bloodPressure takes its ordinals (defaults unchanged).
G17 rows: the 9 new goldens at assembler 1, healthKit 2 (current), no bump;
outlines.json row updated (it only gains the 9 entries; all existing
outlines unchanged).
Mutation proof (each applied alone, full GroveHealthKitFHIRTests on macOS,
then reverted; unmutated: 275 total, 273 passed, 2 skipped):
- audit M5, effective start truncated to whole seconds: fails
matchesCheckedInGolden for exporter-deployment-own-heart-rate at
$.entry[0].resource.effectiveDateTime (golden ...:00.512-07:00). Survived
the whole suite before this commit.
- the ECG average-heart-rate child without the studies link: fails
exporter-deployment-electrocardiogram (outline).
- the exporter builds every graph with no studies: fails
exporter-deployment-own-heart-rate (outline).
- legacy Bundle.id lowercased: fails exporter-deployment-own-heart-rate at
$.id, plus policiesApply and retractionBoundsAndLedger.
- convertClinicalRecord ignores the resource's release (always R4): fails
exporter-clinical-record-dstu2 at ...attachment.contentType.
Output diff: none; the 43 existing goldens and their outlines regenerate
byte-identical.
Gates: GroveHealthKitFHIR macOS 275 total, 273 passed, 2 skipped;
swiftlint --strict: no findings; reuse lint: compliant (tests only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix-round step G3, audit ledger section 1 confirmed findings 1 (six realistic regressions passed the suite), 3 (the public ECG evidence path has no test), 6 (unit-converted values are untested anywhere) and 9 (the ECG test seam lets the evidence and the sample metadata disagree on the entry method). The audit's six mutations (SP/audit-vgoldcov-mine, sites relocated at 28f9311) all still passed the whole suite at 28f9311 (274 total, 272 passed, 2 skipped each). After this commit and the exporter goldens of the previous one, five fail; the sixth is equivalent on the wire: - M1 notification category mapping (values[value] -> the largest key's code): equivalent. The only notification table with more than one value is walking steadiness, which every code it can pick leaves refused (contract admits only low/very-low; bug present in both trees, audit differential). Track 3's FX-walking-steadiness makes it observable and re-runs it. - M2 atrial fibrillation coded inconclusiveOther: fails the new classificationsStateTheGuideCodes. - M3 assessment score + 1: fails the new golden gad7-assessment ($.entry[0].resource.valueQuantity.value: golden 9, actual 10). - M4 ECG symptomsStatus read as notSet: fails the new sourceEvidenceReadsTheSample. - M5 effective time truncated to whole seconds: fails exporter-deployment-own-heart-rate (9b2e2d5). - M6 percent rounded to one decimal: fails convertsFromAnotherUnit (oxygen saturation: "7" for "7.000000000000001"). Tests: - HealthKitECGEvidenceValidatorTests.sourceEvidenceReadsTheSample: the public record path's ecgSourceEvidence on a stored HKElectrocardiogram (new StoredSampleFixtures.electrocardiogram: KVC on _privateClassification, _symptomsStatus and _averageHeartRate, read back or fail; HealthKit keeps the classification under a private numbering, 4 reads back as atrialFibrillation): type, bounds, zone, classification, symptoms status, average rate, algorithm version, and no count or frequency for unset voltages. - classificationsStateTheGuideCodes: every HKElectrocardiogram. Classification against the guide's closed code system (ig-pin healthkit/input/fsh/terminology.fsh:87-94), an independent oracle. - Golden gad7-assessment (observations group, sequence 15): an HKGAD7Assessment HealthKit scores 9; a supported type no golden covered. - HealthKitUnitBindingTests.convertsFromAnotherUnit: degF body temperature, miles, a percent fraction and mmol/L glucose, each pinned to its exact wire lexeme and code (37.00000000000006 Cel, 1609.344 m, 7.000000000000001 %, 99.08573400002975 mg/dL). Track 3's FX-F4-percent changes the percent row as an enumerated delta. - userEnteredECGMarksEveryOutput: an ECG whose metadata says it was user entered states manual entry on the waveform and on its average heart rate. Source (finding 9): the ECG's average-heart-rate child took its entry method from HealthKitECGSourceEvidence.wasUserEntered while the primary took it from SourceFacts (the sample's metadata); both read HKMetadataKeyWasUserEntered in production, but the evidence seam could make them disagree. The field is deleted and HealthKitAssembly.graph states the sample's own entry method on every output it assembles (documents take no manual-entry link, so they are unaffected). Output unchanged: all 53 goldens regenerate byte-identical, and HealthKitAssembly.outputRevision stays 2. Mutation proof for the new test of the source change: stating the entry method on outputs[0] only fails userEnteredECGMarksEveryOutput ("8867-4 states no manual entry"). Unmutated: 279 total, 277 passed, 2 skipped. Output diff: none. New golden gad7-assessment with its G17 row (assembler 1, healthKit 2, no bump); outlines.json only gains its entry. Gates: GroveHealthKitFHIR macOS 279 total, 277 passed, 2 skipped; swiftlint --strict: no findings; reuse lint: compliant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…L7 Lane Fix-round step G3, audit ledger section 5 confirmed findings 1 (the specs' merge gates and regression tests were not added; F11 and F7 could be removed with the suite green; the workout Observation never met the HL7 validator) and 6 (gatewayForOwnWrites without a build is undocumented and untested), section 1 finding 10 (the workout golden has a bare context); G2 leftover: no conformance fixture carried a Provenance author. At 1c2f2a7 the audit's removals still passed everything: weakening the F11 reverse check (`guard governs else { continue }`) and reverting the F7 retraction instants, the clamp and DocumentReference.date (FHIRModels' DateTime(utc:)/Instant(utc:) again) each left 279/277 green. Tests: - F11 T1-T4 (new AdapterProvenanceGraphTests, ungated): a golden edited as a plain JSON object and re-validated through ExchangeGraph(validating:): HealthKit outputs under the mobile, SensorKit and providers Provenance report adapter-provenance-graph @ Bundle.entry; under the Health Connect Provenance data-origin-agent @ Provenance.entity[0].agent (the graph rule does not pre-empt it); a HealthKit Provenance also targeting a source-neutral output of its record reports @ Provenance.target, the same output claiming the adapter is accepted; a Patient claiming healthkit-observation reports mobile-exchange.unclassified @ Bundle. Every expectation was computed by running the pinned kit's exchange_bundle_diagnostics on the same edits (ig-pin, read-only, PYTHONDONTWRITEBYTECODE=1) and equals the F11 spec's. - F11 T5 (gated ExchangeGraphCorpusTests): ExchangeGraph.adapterConversionClaims equals catalog/profile-claims.json adapterConversionProvenanceClaims in order, profile and target set (healthkit 119, health-connect 6, providers 4, sensorkit 13). - F1: GoldenGraphTests.workoutRetractionIsExact (the catalog states workout|single, and the retraction's targets are exactly the outputs the addition emitted); HealthKitFHIRExporterTests.workoutExportAndRetraction (one Observation exported, the retraction targets exactly its session). New golden workout-session-context (observations group, sequence 16): the session under a study, a gateway application, manual entry and its recording watch, so every link a workout takes is pinned in bytes. - F7 (ExchangeEnvelopeFixTests): retractionInstantsAreMilliseconds (period start .0004 -> 22:30:00Z, end .9996 carries into 23:30:01Z, recorded and Bundle.timestamp .2514 -> .251Z, an instant .2516 -> .252Z, a start of .distantPast clamped to 0001-01-01T00:00:00Z) and documentDateIsMilliseconds. - F3 negatives and finding 6: sameBuildIsExact, HealthKitAssembly.isSameBuild over the exact build (true) and revision "41", nil, " ", "42 ", a foreign bundle and a converter without a build whose version equals the revision (all false). - F2: writerEqualToAnUnnamedGatewayIsItsOwnSnapshot: a heartbeat series under a gateway application whose token equals the classified writer's carries the writer as its own snapshot (name from HKSourceRevision, parent the writer host, repository id w-1) and as the Provenance author; on an Observation, which names the gateway, the same writer is the gateway's entry and the gateway extension references it. Finding 6 decision: documented, not refused. Under gatewayForOwnWrites an application without a build is merely the assembler, which the guide always admits (omission, mapping.md:132); the role compares the build the event froze, so a configuration-time check on the live producer could not guarantee the role either, and a refusal would add a public error case. ApplicationDevice(bundle:) always states CFBundleVersion. The RolePolicy doc now states the F3 A3 rule (no build, no revision version or another build: assembler; build reuse caveat). HL7 conformance lane (ConformanceFixtureTests): - `workout` from the guide's workout semantic vector (running, 17:00-17:45 -07:00, one lap withheld, the watch): the F1 merge gate. - `heart-rate-classified-writer`: a foreign application classified as one, with its writer Device, writer host and Provenance author. - The goldens no fixture covers: workout-session and every shape this step added (workout-session-context, writer-non-ascii-name, gad7-assessment, the exporter goldens), written as golden-<name>.json. Left out: exporter-deployment-state-of-mind: the HL7 validator rejects any negative valence against the pinned guide's own healthkit-state-of-mind-value-domain-1 (value >= -1 and <= 1): probe valences -0.25 and -1 fail, 0 and 0.25 pass, an IG defect. Running every golden through the lane also showed insulin-delivery-bolus fails on HKInsulinDeliveryReason, a code the guide's healthkit-metadata-key CodeSystem lacks (pre-existing; not added). - The exporter deployment goldens move their disclosed-UUID system from https://study.example.org/fhir/NamingSystem/healthkit-store to https://grovealliance.org/fhir/testing/identifiers/native-healthkit-record, because the HL7 validator refuses example URLs: the only change in those six files (one identifier system each; ECG child has none) and in their outlines. A fixture input change of goldens added in 9b2e2d5, no converter output change, so their G17 rows keep healthKit 2. Mutation proof (each applied alone, full GroveHealthKitFHIRTests on macOS, then reverted; unmutated: 287 total, 285 passed, 2 skipped): - F11 weakened (`guard governs else { continue }`): fails reportsWhatTheKitReports (the mobile Provenance vector is accepted). - F7, each reverted alone: the instant occurrence (.251600027Z), the period end (.999599933Z), the start (.000400066Z), recorded (.251399993Z) fail retractionInstantsAreMilliseconds; DocumentReference.date fails documentDateIsMilliseconds; the clamp alone removed refuses the .distantPast start (invalidInstant). - the catalog retracts a workout segment besides the session: fails workoutRetractionIsExact, workoutExportAndRetraction and the retraction-workout golden. - isSameBuild falls back to the version when no build is stated: fails sameBuildIsExact. - the writer never shares a converter snapshot: fails writerEqualToAnUnnamedGatewayIsItsOwnSnapshot (and four golden tests); documents naming the gateway again: fails it and documentGraphsOmitTheGatewayApplication. Output diff: none. New golden workout-session-context with its G17 row (assembler 1, healthKit 2); the exporter deployment goldens change only as listed; all other goldens byte-identical. Gates: GroveHealthKitFHIR macOS 287 total, 285 passed, 2 skipped; swiftlint --strict: no findings; reuse lint: compliant; Scripts/validate-fhir-conformance.sh healthkit against g3-ig, a clone of the G2 scratchpad copy of ig-pin with mobile, sensor and healthkit built (GROVE_FHIR_REF its commit): "Validated 42 producer resource(s) against FHIR R4", "OK - all healthkit resources conform to their declared R4 profiles", contract generator and semantic-vector checks clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…enances Fix-round step G3, audit ledger section 5 confirmed finding 7 (Swift and the IG kit report different diagnostics for two invalid shapes). Grove's validator was the one that differed, and it also accepted what the kit refuses, so it is aligned to the pinned kit (ig-pin Scripts/producer_validation/profiles.py). - health-connect-provenance.data-origin-agent: Swift counted only the agents typed enterer, required just some identifier, and reported every fault at Provenance.entity[0].agent. The kit (profiles.py:663-707) requires exactly one agent on the source entity (else .agent), typed enterer and no other participant type, an identifier-only Device reference (else .agent[0].who), and an identifier under the Android package-name system with a non-blank value (else .agent[0].who.identifier). Swift now decides and locates it the same way, so an enterer beside an author, an author alone, or a package name under another system or blank is refused where the kit refuses it. - A Provenance claiming an adapter's conversion profile beside another profile: the kit refuses it without a rule in validate_adapter_conversion_provenance (profiles.py, before the profile-count rule), so it reports mobile-exchange.unclassified @ Bundle; Swift reported mobile-exchange.provenance-profile. The new validateAdapterProvenanceClaim reads meta.profile as written and requires an adapter conversion profile to be the only claim, before validateActiveProvenanceProfile. directProfiles now reads through the same writtenProfiles helper. No Grove producer writes a Health Connect Provenance or a second Provenance profile (the assembler states one), so wire output is unchanged; only re-validation verdicts and locations change, toward the kit's. Tests (AdapterProvenanceGraphTests, kit-computed expectations, same edits run through exchange_bundle_diagnostics read-only): - Health Connect Provenance over writer-foreign-application (its author agent): data-origin-agent @ .agent[0].who; an enterer under another system and one with a blank package name: @ .agent[0].who.identifier; an enterer and an author: @ .agent; an enterer referencing a Device: @ .agent[0].who; a complete data origin: passes the rule and the HealthKit outputs then fail adapter-provenance-graph @ Bundle.entry. - heart-rate-minimal's Provenance claiming the HealthKit and the mobile conversion profiles: mobile-exchange.unclassified @ Bundle. The agents reference the converter's own entry, so a vector has one fault: an unresolved reference is refused earlier by Swift's reference rule. Mutation proof (each applied alone, full GroveHealthKitFHIRTests on macOS, then reverted; unmutated: 287 total, 285 passed, 2 skipped): - validateAdapterProvenanceClaim not called: fails reportsWhatTheKitReports (reported mobile-exchange.provenance-profile @ Provenance.meta.profile). - a who fault located at .agent: fails it (reported @ Provenance.entity[0].agent). - any identifier system accepted: fails it (the wrong-system vector passes the rule and reports adapter-provenance-graph @ Bundle.entry). Output diff: none (goldens byte-identical). Gates: GroveHealthKitFHIR macOS 287 total, 285 passed, 2 skipped; GroveSensorKitFHIR 83/83; GroveQuestionnaire 260/260; GroveFHIR 103/103; GROVE_LOWERED_DEPLOYMENT_TARGETS=1 xcrun swift build --target GroveFHIRContract: Build complete (the manifest's "2 unhandled files" warning is pre-existing); swiftlint --strict: no findings; reuse lint: compliant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix-round step G3, audit ledger section 2 confirmed finding 8:
ExchangeGraph(validating:kind:) accepted and kept members the FHIR model
never sees, although `json` is documented as the bytes to upload (the
audit injected members into all 252 golden inputs; every one was accepted
and kept). Every rule decides over the decoded model, so such a member
would travel unchecked.
- After the model checks, decodeValidated compares the given bytes with the
decoded Bundle's own encoding by shape (LosslessJSONValue.shape: every
scalar blanked, members and elements kept) and refuses a difference with
.invalidEntries("Serialized event carries members the model does not
keep") (mobile-exchange.unclassified). Values may differ in lexeme only,
because the model rewrites decimals such as 72.0; that is why the bytes
are kept at all. A JSON null member, which the model drops, is refused
the same way.
- The init(validating:kind:) doc states the refusal and why.
No production caller re-validates (audit), and no shared corpus vector
carries such a member: the gated corpus suite, the questionnaire and
SensorKit suites pass unchanged.
Test (ExchangeGraphBytesTests.revalidationRefusesMembersTheModelDrops): a
Bundle-level "note" member and an "unmodeled" member inside the first
resource are refused; the same graph with its decimal 72 rewritten as 72.0
is accepted and keeps its bytes.
Mutation proof (full GroveHealthKitFHIRTests on macOS, then reverted;
unmutated: 288 total, 286 passed, 2 skipped): without the
validateKeptMembers call the new test fails ("an error was expected but
none was thrown").
Output diff: none (producers build graphs from models; goldens unchanged).
Gates: GroveHealthKitFHIR macOS 288 total, 286 passed, 2 skipped;
GroveSensorKitFHIR 83/83; GroveQuestionnaire 260/260; GroveFHIR 103/103;
GROVE_LOWERED_DEPLOYMENT_TARGETS=1 xcrun swift build --target
GroveFHIRContract: Build complete; swiftlint --strict: no findings; reuse
lint: compliant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synthesis section 13 F7 (owner-approved; there is no spec file): HealthKit crosses two axes in one
HKCategoryValueAppleWalkingSteadinessEvent case (initialLow, initialVeryLow, repeatLow, repeatVeryLow).
The table stated each as one code (initial-low, ...) that the pinned contract does not admit, so every
walking-steadiness notification was refused with invalidValue(.missingNormativeCode) and the plan compiler
reported its four values as the only compile defects. The guide splits the case in two: "Value is the severity
classification and the notification-occurrence component is whether it is a first or repeat notification."
(mobile/input/data/terminology-reviews.json:4248). The profile requires the component ("component contains
notification-occurrence 1..1 MS", healthkit/input/fsh/generated-measurement-profiles.fsh:2479), and the guide's
HealthkitWalkingSteadinessNotificationExample states exactly that shape (:4445-4447): value
HealthkitWalkingSteadinessNotificationCS#low "Low", component code
healthkit-measurement#walking-steadiness-notification-occurrence, component value
HealthkitNotificationOccurrenceCS#initial "Initial".
What changed:
- CodedTable.Row gains `occurrence`, the notification-occurrence component's result code. The walking-steadiness
table maps initialLow -> low/initial, initialVeryLow -> very-low/initial, repeatLow -> low/repeat,
repeatVeryLow -> very-low/repeat.
- ValueRule.coded maps raw values to a new internal CodedValue (the value and the optional occurrence component)
instead of a bare CodeableConcept; apply() sets the value and, when the value states one, the component. Every
other coded table states no occurrence, so its Observations are unchanged (the component stays absent, and
menstrual flow's metadata component is still appended after it).
- The compiler builds the component once per value from the contract: code = the contract component's coding
(no display, as every component code), value = the contract's published result with its display. A contract
without a coded notification-occurrence component, or one that does not admit the row's occurrence, refuses
the type as not yet convertible, like any component a rule reads; a classification the contract does not
admit still leaves just that value unresolved (the value-level defect policy is kept).
- HealthKitContentPlan.compileDefects is now empty; the plan doc says CI keeps it empty.
- HealthKitAssembly.outputRevision goes from 8 to 9: four inputs that were refused now yield graphs.
- .missingNormativeCode is now unreachable with the generated contracts; it stays, per the owner decision to
remove unreachable public error cases only in the final cleanup. No public API change.
Tests:
- HealthKitContentPlanTests: compileDefectsAreEmpty replaces the known-set pin of the four walking-steadiness
defects.
- HealthKitCategoryConversionTests.walkingSteadinessNotification: all four cases end to end, value
code/display/system and the single component's code (no display) and value code/display/system, against the
generated contract.
- HealthKitContentCompilerTests: the contract without its notification-occurrence component, and the component
without the result "repeat", each refuse the type with exactly that defect; a contract without "very-low"
leaves values 2 and 4 unresolved and converts values 1 and 3 with their occurrence.
- New golden walking-steadiness-notification (observations group, sequence 19, uuid(19)): initialLow over one
day under America/Los_Angeles on the fixture watch; the Observation is the guide example's shape. It joins the
HL7 lane's validated goldens (ConformanceFixtureTests.validatedGoldens), since no fixture covers the shape.
- Mutation proof (each applied alone, full GroveHealthKitFHIRTests on macOS, reverted): dropping the component
assignment fails the end-to-end test, the golden and both corpus suites (assembly and exporter); mapping
initialLow to "repeat" fails the same four; restoring the old one-code table fails 12 tests including
compileDefectsAreEmpty, the three compiler tests, the golden, both corpus suites and the conformance fixture
writer.
Output diff (goldens and corpus regenerated outside the worktree through TEST_RUNNER_GROVE_GOLDEN_OUTPUT_DIR and
TEST_RUNNER_GROVE_CONTENT_CORPUS_OUTPUT_DIR, then copied in):
- Goldens: new walking-steadiness-notification.json; outlines.json only gains that case's entry. The other 57
golden files are byte-identical. G17: a new row and the outlines row's new digest, both under healthKit 9.
- Content corpus (content-corpus.changes.txt): 3,437 lines checked in; added 0, removed 0, duplicated 0,
restated inputs 0, changed outputs 4, exactly the convert vectors
category/HKCategoryTypeIdentifierAppleWalkingSteadinessEvent/{1,2,3,4}: refused
mobile-input.value-shape-invalid invalidValue(.appleWalkingSteadinessEvent, .missingNormativeCode) at
HKSample.value -> one Observation graph (Observation, Device, Device, Provenance; no warnings) whose value is
low "Low" / very-low "Very low" / low / very-low and whose one component is initial "Initial" / initial /
repeat "Repeat" / repeat. An independent Python oracle derived each expected graph from the checked-in
low-cardio-fitness vector of the same inputs (only code, profile, source type, value and component replaced;
identifiers and the envelope digest left out) and matched all four, with every other line byte-identical. The
emitted source-output identifier equals the one the unchanged retraction vector of the type already targets.
Unchanged: raw values -1, 0 and 5-8 (still mobile-input.unsupported-source-value), the type's catalog, output and
retraction vectors, and every other vector.
Gates: GroveHealthKitFHIR macOS suite passed (359 tests, 357 passed, 2 skipped, 0 failed); swiftlint --strict on
GroveFHIRContract, GroveHealthKitFHIR and GroveHealthKitFHIRTests clean; reuse lint compliant. GroveFHIRContract
and the generator are untouched, so gate (3) does not apply. The HL7 validator lane was not run here (the pinned
guides are not built in this environment).
Size (A): code 10,396 (+691), public 1,008 (+135) against baseline 9b086e6; this commit +22 code lines
(+10 doc lines), +0 public, +0 package declarations against ae381a2.
Integration (FX round onto lukas/healthkit-content-rewrite after FX-proleptic-reverse, FX-time-ig, FX-F4-percent,
FX-F5-interval-period, FX-F6-codes, FX-F9-bp-members and FX-F10-metadata-warnings): the bump is renumbered 3 -> 4 to
8 -> 9, so the new golden's row and the outlines row carry healthKit 9. Conflicts resolved keeping both sides:
ValueRule.apply keeps FX-F6-codes' untyped throws (a quantity may rethrow GroveFHIRDecimalError) and states the
occurrence component; the golden case list holds sequences 17, 18 and 19. Goldens and the corpus regenerated outside
the worktree equal the checked-in files (58 goldens; outlines.json differs from the integrated parent only by the new
case; corpus 3,437 lines, the 4 changes above, each equal to its branch recording). GroveHealthKitFHIR macOS on this
tree: 429 tests, 427 passed, 2 skipped, 0 failed.
Integration (onto the cleanup, lukas/grove-fhir-rework-integration after 5453c80): the revision keeps its number
(8 -> 9); the new golden's row and the outlines row carry healthKit 9. Conflicts: ValueRule.apply's coded case takes
this commit's body under the cleanup's spelling HealthKitConversionError.ValueFailure, and the golden case list keeps
blood-pressure-member-metadata (18) beside this case (19), built through GoldenFixtures.export. Ported with every
assertion kept: the category test exports through ExporterFixtures.export(_:_:) instead of the deleted
HealthKitConverter. Regenerated outside the worktree on this tree: walking-steadiness-notification.json is
byte-identical to this commit's on its branch, outlines.json only gains its case (57 golden files here), and the
corpus (3,434 lines) changes exactly the 4 vectors above and nothing else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Lookup by Id
Review fixes for 95130856 (synthesis section 13 F7; there is no spec file). Tests only; no Sources change.
Finding (claude, low, test-coverage), confirmed: nothing tested that a coded value the contract does not
admit is refused at runtime as invalidValue(.missingNormativeCode). Before F7 the four walking-steadiness corpus
vectors were its only coverage; they are graphs now, no Swift test named .missingNormativeCode, and
unadmittedClassificationIsUnresolved checked only the compiled `values` and `unresolved` sets, so its own claim
("converts values 1 and 3 with their occurrence") was not exercised. The mechanism stays (the value-level defect
policy is kept, and the public case stays until the final cleanup per the owner decision).
What changed (HealthKitContentCompilerTests):
- unadmittedClassificationIsUnresolved now applies the plan compiled from the contract without "very-low" to
stored walking-steadiness samples: initialVeryLow and repeatVeryLow throw .missingNormativeCode; raw 5, which the
table does not map, still throws .unsupportedValue(5) beside the unresolved values; initialLow and repeatLow convert
to value low with occurrence initial and repeat. The structural checks it replaces are implied by these.
- New occurrenceComponentIsFoundByIdentifier: a contract listing a decoy component (the occurrence component under
id and code "decoy") ahead of notification-occurrence compiles without defects, and a repeatLow notification states
exactly the notification-occurrence component's code. The generated contract has one component, so nothing pinned
the id filter before.
- A notification(_:in:) helper builds the stored sample (HealthKit refuses an undefined value at creation) and runs
the compiled ObservationPlan with the bridged metadata; ComponentContract.with gains a `code:` parameter.
Surviving mutants, each now killed (applied alone to Sources, HealthKitContentCompilerTests on macOS, reverted):
- r09-unresolved-as-unsupported (HealthKitObservationContent.swift:164, `throw .unsupportedValue(raw)`): fails
unadmittedClassificationIsUnresolved (".unsupportedValue(2)" thrown instead of ".missingNormativeCode").
- r05-drop-component-id-filter (HealthKitContentPlan+Compile.swift notificationOccurrence,
`contract.components.first`): fails occurrenceComponentIsFoundByIdentifier (component code "decoy").
- Added converse r09b (`throw .missingNormativeCode` for every unmapped value): fails
unadmittedClassificationIsUnresolved (".missingNormativeCode" thrown instead of ".unsupportedValue(5)").
Output diff: none. No golden, outlines.json or corpus file changes; Sources are byte-identical to 95130856.
Gates: GroveHealthKitFHIR macOS suite passed (360 tests: 358 passed, 2 skipped, 0 failed); swiftlint --strict on
GroveFHIRContract, GroveHealthKitFHIR and GroveHealthKitFHIRTests clean; reuse lint compliant. GroveFHIRContract and
the generator are untouched, so gate (3) does not apply.
Size (A): code 10,696 (+991), public 1,012 (+139) against baseline 9b086e6, on the integrated FX round; unchanged
by this commit (tests only). This item, FX-walking-steadiness, adds 22 code lines and no public declaration to the
10,674 / 1,012 it started from (alone on ae381a2 it read 10,396 / 1,008).
Integration (onto the cleanup, lukas/grove-fhir-rework-integration after 5453c80): applied without textual
conflicts; the new pins spell HealthKitValueFailure as the cleanup nests it, HealthKitConversionError.ValueFailure.
Tests only, so goldens, outlines.json and the corpus are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FX-ecg-frequency-ig, first of two commits: pin today's verdicts before the fix changes them. The pinned guide (catalog/healthkit-adapter.json, ECG admissionRule) compares the stated sampling frequency and 1000 / SampledData.period as shortest round-trip decimals; the waveform check instead requires Decimal(hertz) * period == 1000. The two differ for a frequency whose decimal reciprocal does not terminate, and wherever Foundation's Decimal(Double) does not state the binary64 exactly. The corpus had no vector where they differ: every sampling-frequency vector used the base reading's 2 ms period. What changed (tests only): ContentCorpusGrid gains reciprocalFrequencies, three ECG readings beside the existing sampling-frequency vectors. The base reading's offset rewrite moves from a local function of waveformEdges into ContentCorpusGrid.reading(offsets:), which both use; the offsets/* vectors state the same inputs. Output diff: none for any existing vector; no source change, so all goldens, outlines.json and every existing corpus line are unchanged. Content corpus, regenerated through GROVE_CONTENT_CORPUS_OUTPUT_DIR (3,437 -> 3,440 lines; changes report: added 3, removed 0, restated 0, changed outputs 0). Each new vector is today's refusal, healthkit-input.ecg-evidence, ecgEvidence(samplingFrequencyMismatch), at HKElectrocardiogram: - added electrocardiogram/sampling-frequency/reciprocal: offsets 0.25, 0.253, 0.256, 0.259 s (a 3 ms period) at 1000/3 Hz (333.3333333333333). Refused because no decimal times 3 is 1000. - added electrocardiogram/sampling-frequency/reciprocal-next: the same period at the binary64 one step above (333.33333333333337). - added electrocardiogram/sampling-frequency/exact-quotient: offsets 0.25, 0.25131072, 0.25262144, 0.25393216 s (1.31072 ms) at 762.939453125 Hz, the exact quotient. Refused because Decimal(762.939453125) is 762.9394531249998848, whose product with the period is not 1000. Gates: the GroveHealthKitFHIR macOS suite passed in generation mode (357 tests, 353 passed, 4 skipped: the three corpus verification tests, which generation disables, and the throughput benchmark); the checked-in corpus is the regenerated file byte for byte. swiftlint --strict clean. Integration (FX round): applied without conflicts; the corpus regenerated outside the worktree on this tree equals the checked-in file (3,437 -> 3,440 lines, the 3 added vectors equal to their branch recordings). Integration (onto the cleanup, lukas/grove-fhir-rework-integration after 5453c80): the corpus conflicted only because the cleanup re-renders it through the exporter. Regenerated outside the worktree on this tree (3,434 -> 3,437 lines here), it adds exactly the 3 vectors above and changes nothing else, each equal to this commit's recording carried through the cleanup's rendering. Goldens (57 files) and outlines.json unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…parison
FX-ecg-frequency-ig (Codex review of M4, finding 3; synthesis section 20
had listed it as unchanged on purpose). The pinned guide's ECG admissionRule
(catalog/healthkit-adapter.json:242) says: "Canonicalize samplingFrequency
and computed 1000 / SampledData.period to the producer's shortest
round-trip base-10 decimal representation and require exact decimal
equality (tolerance zero)." The waveform check required
Decimal(hertz) * period == 1000 instead. That refuses every frequency whose
decimal reciprocal does not terminate (1000/3 Hz at a 3 ms period), and it
depends on Foundation's Decimal(Double), which does not state every binary64
exactly: Decimal(762.939453125) is 762.9394531249998848, so the exact
quotient of a 1.31072 ms period was refused as well.
What changed:
- HealthKitECGContent.Waveform.requireFrequency compares hertz with
Double((1_000 / period).description). Equal shortest round-trip decimals
are equal binary64 values, so the guide's text comparison is binary64
equality with the quotient's nearest binary64. "Computed 1000 /
SampledData.period" is read as the quotient of the exact decimal period
(the wire SampledData.period), rounded once to binary64. It is not 1000
divided by the period's own binary64, which rounds twice: that would
refuse exact agreements such as 762.939453125 Hz at 1.31072 ms (1000 /
1.31072 in binary64 is 762.9394531249999), against mapping.md:66
("must agree exactly with the SampledData period"). Decimal divides to at
least 37 significant digits less the period's, and a binary64 rounding
boundary other than the quotient itself lies at least
1 / (significand * 2^54) away relative to it, so for every period of at
most ten significant digits the parse lands on the exact quotient's
nearest binary64. HealthKit itself writes ECGs only from Apple Watch at
512 Hz (1.953125 ms), where both rules agree. The check order (count,
offsets, period, frequency, voltages) and both error cases are unchanged.
requireFrequency becomes internal, like requireCount, as a test seam.
- HealthKitAssembly.outputRevision 9 -> 10 (this item): equal inputs now
yield graphs where they were refused.
- Tests: HealthKitECGContentTests admits 1000/3 Hz at 3 ms, 762.939453125 Hz
at 1.31072 ms and 512 Hz at 1.953125 ms through the waveform, and refuses
the binary64 one step above 1000/3 Hz. A sweep covers every period of at
most four significant digits from 0.0001 ms to 9999 ms (49,995) and 1,313
ten-digit ones: exactly the binary64 nearest 1000 / period is admitted and
both its binary64 neighbours are refused, measured against the division of
the exact power of ten by the exact significand (one rounding).
Output diff: no golden changed (every ECG golden states 500 Hz at 2 ms,
which both rules admit; G17 rows unchanged), outlines.json unchanged.
Content corpus, regenerated through GROVE_CONTENT_CORPUS_OUTPUT_DIR (3,440
lines; changes report: added 0, removed 0, restated 0, changed outputs 2):
- changed electrocardiogram/sampling-frequency/reciprocal: the refusal
healthkit-input.ecg-evidence, ecgEvidence(samplingFrequencyMismatch), at
HKElectrocardiogram becomes the ECG graph (waveform and average heart
rate), SampledData.period 3, effectivePeriod 2026-08-17T15:30:00.25-07:00
to 2026-08-17T15:30:00.259-07:00.
- changed electrocardiogram/sampling-frequency/exact-quotient: the same
refusal becomes the ECG graph, SampledData.period 1.31072, effectivePeriod
end 2026-08-17T15:30:00.25393216-07:00.
- Unchanged: sampling-frequency/reciprocal-next (333.33333333333337 Hz,
still samplingFrequencyMismatch), mismatch (250 Hz), near (499.99 Hz),
zero, nan, negative, none, both 250 Hz precedence pairs, and every other
vector.
An input the old rule admitted is refused now only if Foundation's
Decimal(hertz) is the exact quotient of a terminating period yet lies more
than half a binary64 step from hertz; no vector, golden or test states one.
Gates: GroveHealthKitFHIR macOS suite passed in verification mode (359
tests, 357 passed, 2 skipped: the throughput benchmark and the corpus
regeneration); the sweep takes 0.55 s. swiftlint --strict clean (Sources/
GroveFHIRContract, Sources/GroveHealthKitFHIR,
Tests/GroveHealthKitFHIRTests); reuse lint clean. GroveFHIRContract and the
generator are unchanged, so gate 3 does not apply.
Size (A): code 10,374 (+669), public 1,008 (+135), against baseline
9b086e6. FX-ecg-frequency-ig leaves both unchanged against ae381a2 (the
rule is one replaced line; GroveHealthKitFHIR doc lines 1,018 -> 1,025).
Integration (FX round onto lukas/healthkit-content-rewrite after FX-proleptic-reverse, FX-time-ig, FX-F4-percent,
FX-F5-interval-period, FX-F6-codes, FX-F9-bp-members, FX-F10-metadata-warnings and FX-walking-steadiness): the bump
is renumbered 3 -> 4 to 9 -> 10. The test helper's doc conflict kept both sides (FX-F6-codes' average heart rate
parameter, this item's 3 ms points). Goldens regenerated byte-identical (58); the corpus (3,440 lines) changes the
two vectors above, composed with FX-F10-metadata-warnings: each admitted ECG graph also states that item's warning
"mobile-omission.unmodeled-metadata(HKTimeZone)", as every ECG whose record names a zone does (the ECG states the
offset, not the zone's name). GroveHealthKitFHIR macOS on this tree: 432 tests, 430 passed, 2 skipped, 0 failed.
Integration (onto the cleanup, lukas/grove-fhir-rework-integration after 5453c80): the revision keeps its number
(9 -> 10). The corpus conflicted only because the cleanup re-renders it through the exporter; regenerated outside
the worktree on this tree (3,437 lines), it changes exactly the 2 vectors above and nothing else, checked per vector
against this commit's own change carried through the cleanup's rendering. Goldens (57 files) and outlines.json
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FX-ecg-frequency-ig review fix (finding R1, surviving mutant m11). The
waveform's frequency guard refuses a stated frequency that is not finite or
not positive as invalidSamplingFrequency before it compares it with
1000 / period. The corpus pinned zero, NaN and negative, but not +infinity.
NaN fails `hertz > 0` on its own, so dropping `hertz.isFinite` (mutant m11)
passed all 359 tests while +infinity reached the comparison and was refused
as samplingFrequencyMismatch instead. The guard is unchanged; this pins it.
What changed (tests only): ContentCorpusGrid.electrocardiograms gains
("infinity", .infinity) beside the existing sampling-frequency edges. The
list is wrapped onto its own lines to stay within the line-length limit.
Output diff: no source change, so all goldens and outlines.json are
unchanged. Content corpus, regenerated through
GROVE_CONTENT_CORPUS_OUTPUT_DIR (3,440 -> 3,441 lines; changes report: added
1, removed 0, duplicated 0, restated inputs 0, changed outputs 0):
- added electrocardiogram/sampling-frequency/infinity: the base reading (2 ms
period) stating samplingFrequency "Infinity", refused as
healthkit-input.ecg-evidence, ecgEvidence(invalidSamplingFrequency), at
HKElectrocardiogram.
Every existing line is byte-identical.
Mutation check: with m11 applied, ContentCorpusTests.everyLineIsReproduced
fails on exactly this vector (golden invalidSamplingFrequency, actual
samplingFrequencyMismatch).
Gates: the GroveHealthKitFHIR macOS suite passed in generation mode (359
tests, 355 passed, 4 skipped: the corpus verification tests, which generation
disables, and the throughput benchmark). It passed again in verification mode
on the tree that also holds the next commit (360 tests, 358 passed, 2
skipped). swiftlint --strict clean; reuse lint clean. GroveFHIRContract and
the generator are unchanged, so gate 3 does not apply.
Integration (FX round): the grid's edge lists conflicted with FX-F6-codes' added subnormal average heart rate;
both are kept. The corpus regenerated outside the worktree equals the checked-in file (3,440 -> 3,441 lines, the
added vector equal to its branch recording).
Integration (onto the cleanup, lukas/grove-fhir-rework-integration after 5453c80): the corpus conflicted only
because the cleanup re-renders it through the exporter; regenerated outside the worktree on this tree (3,437 ->
3,438 lines here), it adds exactly the 1 vector above, equal to this commit's recording carried through the
cleanup's rendering, and changes nothing else. Goldens (57 files) and outlines.json unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FX-ecg-frequency-ig review fix (surviving mutant m15). The context fingerprint carries HealthKitAssembly.outputRevision, so an output that changes for equal inputs must bump it. G17 ties every golden to the revision its bytes were last changed under, but nothing tied the content corpus to a revision. Revisions 3 (993efcc, proleptic Gregorian) and 4 (29fc3cd1, FX-ecg-frequency-ig) were bumped for corpus output changes alone: no golden changed, and every G17 row names revision 1 or 2. Lowering the revision from 4 to 3 (mutant m15) therefore passed every test. The reviewer judged that no test could pin the bump. One can: the corpus gets a G17 row of its own. What changed (tests only): - GoldenOutputRevisionTests.contentCorpus pins the SHA-256 of the checked-in content-corpus.jsonl with assembler revision 1 and HealthKit revision 10, the revision at which a corpus output last changed. The new test "G17: the content corpus's bytes match its row, whose revisions the code has reached" checks it with the same rule as the goldens. A changed corpus fails until the row is updated. The row must carry a bumped revision when an output changed (review checks this in the diff). A corpus that only gains vectors updates its digest without a bump, as a new golden adds its row without one. - The digest and revision checks move into one helper, shared by the golden rows and the corpus row. The failure message now reads "update its row, with the bumped output revision if an output changed". The suite's and GoldenRevision's doc comments state the corpus rule. Output diff: none. No source change, so all goldens, outlines.json and the content corpus (3,441 lines, as the previous commit left it) are unchanged. Mutation check: with m15 applied (outputRevision 4 -> 3), the new test fails (row.healthKit 4 > HealthKitAssembly.outputRevision 3). Restoring the source makes it pass again. Gates: the GroveHealthKitFHIR macOS suite passed in verification mode (360 tests, 358 passed, 2 skipped: the throughput benchmark and the corpus regeneration). swiftlint --strict clean (Sources/GroveFHIRContract, Sources/GroveHealthKitFHIR, Tests/GroveHealthKitFHIRTests); reuse lint clean. GroveFHIRContract and the generator are unchanged, so gate 3 does not apply. Size (A): code 10,696 (+991), public 1,012 (+139), against baseline 9b086e6, on the integrated FX round. Unchanged against 29fc3cd1 (both commits change tests only); this item, FX-ecg-frequency-ig, changes no code line or public declaration (alone on ae381a2 it read 10,374 / 1,008). Integration (FX round): applied without conflicts. The paragraph above describes the item's branch; on the integrated branch the revisions bumped for corpus output changes alone are 3 (proleptic Gregorian), 4 (FX-time-ig) and 10 (this item), and the golden rows reach revision 9. The row therefore pins the integrated corpus (3,441 lines) under healthKit 10, the revision at which a corpus output last changed (29fc3cd1's integrated commit); the mutation of this item becomes outputRevision 10 -> 9. GroveHealthKitFHIR macOS on this tree: 433 tests, 431 passed, 2 skipped, 0 failed. Integration (onto the cleanup, lukas/grove-fhir-rework-integration after 5453c80): the table's doc keeps the cleanup's rule beside this commit's (a golden whose case changed its inputs while the output for equal inputs stayed the same adds no bump), extended to the corpus: one that only gains vectors, or renders equal outputs another way (the cleanup re-rendered it through the exporter), updates its digest without a bump. The content-corpus row pins the integrated corpus's digest under healthKit 10, the revision at which a corpus output last changed here too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FX-route-invalid-accuracy (IG compliance; flagged by the M6 fixer as surviving mutant X4, pinned by c1e392d). CoreLocation marks a fix whose latitude and longitude are invalid with a negative horizontal accuracy. The location track wrote such a fix as a position with horizontalAccuracy "-1". What the pinned IG (eff5af8f) states for the route's recording format, location-track-samples (catalog/format-registry.json, sensor formats page): - the payload has "one row per source sample in source order", so a fix cannot be left out; - horizontalAccuracy is not nullable ("`no` requires a non-empty field in every data row"), unlike verticalAccuracy ("empty when altitude is invalid") and the speed and course columns ("empty when unavailable"), so it cannot be written empty; - horizontalAccuracy is the "Radius of uncertainty for the horizontal position in metres" and latitude and longitude are WGS 84 degrees, so "-1" states a negative radius for a coordinate the source calls invalid, in a source-neutral schema whose reader cannot know CoreLocation's marker. No row can state such a fix, so the route is refused: DocumentPlan.locationTrack throws HealthKitValueFailure.outsideDomain for the first fix whose horizontal accuracy is negative, in source order, after the empty-route check. The IG registers no route-specific diagnostic; mobile-input.value-outside-domain ("A numeric source value is ... outside the measurement's inclusive value domain") is the closest, at the generic value location HKSample.value. A nonfinite accuracy still reaches the CSV writer as before. A route under RouteDisclosurePolicy.omit is still dropped before its fixes are read, and the exporter's companion fingerprint of a refused route is "unserializable", as for any route the assembly refuses. Vertical accuracy needs no change: a negative one is already written empty, as the registry states. Output diff: HealthKitAssembly.outputRevision 10 -> 11. No golden changed (golden G17 rows unchanged; the goldens' route fixes all state a non-negative horizontal accuracy). Content corpus, regenerated through GROVE_CONTENT_CORPUS_OUTPUT_DIR (3,441 -> 3,443 lines; changes report: added 2, removed 0, duplicated 0, restated inputs 0, changed outputs 1): - changed workout-route/negative-horizontal-accuracy: the graph whose track carried "-1" in the second fix's horizontalAccuracy is now refused with mobile-input.value-outside-domain, invalidValue(workoutRoute, outsideDomain), at HKSample.value. - added workout-route/precedence/omission-before-invalid-coordinate: the same fixes under the omit default; the route is omitted, not refused. - added workout-route/precedence/invalid-coordinate-before-sync: the same fixes with a broken sync pair; the content refusal is reported, not the sync failure. Every other vector is unchanged. Tests: HealthKitRecordingDocumentTests pins the refusal under authorization and the omission under the default policy; the corpus grid's doc comment now states the refusal. Gates: GroveHealthKitFHIR macOS suite passed (358 tests, 356 passed, 2 skipped) on this tree in verification mode, after the regeneration run (354 passed, 4 skipped); swiftlint --strict clean; reuse lint clean. Size (A): code 10,377 (+672), public 1,008 (+135) against baseline 9b086e6; this commit +3 code lines, +0 public declarations (against ae381a2: code 10,374, public 1,008). Integration (FX round onto lukas/healthkit-content-rewrite after the nine items before it): the bump is renumbered 3 -> 4 to 10 -> 11, and FX-ecg-frequency-ig's content-corpus G17 row takes the regenerated corpus's digest under healthKit 11, since a corpus output changed. The corpus regenerated outside the worktree (3,441 -> 3,443 lines) changes exactly the vectors above, each equal to its branch recording; workout-route/negative-horizontal-accuracy also drops the "mobile-omission.unmodeled-metadata(HKTimeZone)" warning FX-F10-metadata-warnings gave its graph, as the graph is now refused. Goldens regenerated byte-identical (58). GroveHealthKitFHIR macOS on this tree: 434 tests, 432 passed, 2 skipped, 0 failed. Integration (onto the cleanup, lukas/grove-fhir-rework-integration after 5453c80): the revision keeps its number (10 -> 11), and the content-corpus row takes the integrated corpus's new digest under healthKit 11. The location track throws the cleanup's HealthKitConversionError.ValueFailure.outsideDomain. Ported with both assertions kept: the route test exports through ExporterFixtures, so the authorized route's refusal is the exporter's .invalidValue(.workoutRoute, .outsideDomain), which wraps the failure the deleted HealthKitAssembly.convert(_:context:) seam threw, and the omitted route delivers no export. Regenerated outside the worktree on this tree (3,438 -> 3,440 lines here; 57 golden files and outlines.json unchanged), the corpus adds the 2 vectors and changes the 1 above and nothing else, checked per vector against this commit's own change carried through the cleanup's rendering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FX-route-invalid-accuracy, review fixes (R1-R3 on 62491c9d). The route stays refused: the pinned IG's location-track-samples schema (catalog/format-registry.json) writes "one row per source sample in source order" and horizontalAccuracy, the "Radius of uncertainty for the horizontal position in metres", is not nullable, so no row can state a fix CoreLocation marks invalid. Only the registered code changes. R1 (code). 62491c9d reported the refusal as mobile-input.value-outside-domain. That rule names "A numeric source value is nonfinite, outside the measurement's inclusive value domain, or fractional where the contract admits only integers" (catalog/exchange-protocol.json). A workout route states no measurement (healthkit-adapter.json HKWorkoutRouteTypeIdentifier: "measurementIDs": []), and the registry declares no domain for the column beyond the CSV encoding's "numberValueDomain": "finite-ieee754-binary64", which -1 satisfies. The owner-reconciled spec F6 reserves value-outside-domain for nonfinite values and violations of a declared domain and sends every other finite refusal to the fallback (A4; it rejected an implied domain for ECG heart rate). Conformance/README.md: "`mobile-input.unclassified` as the fallback for a reason no more specific rule names". DocumentPlan.locationTrack now throws the internal WorkoutRouteFailure.invalidCoordinate, which the existing default arm of HealthKitConversionError(conversionFailure:) reports as .dependency, code mobile-input.unclassified at "Bundle", the path F6 accepted for its own residue. No public API is added; HealthKitValueFailure.outsideDomain has no thrower again until FX-F6-codes lands. R2/R3 (tests). Every invalid fix in the tests was the last fix and also had verticalAccuracy -1, no fixture had horizontalAccuracy 0, and no route value was nonfinite, so `<= 0`, last-fix-only, `&& verticalAccuracy < 0` and refusing NaN in the check all passed. New corpus vectors pin each edge; the unit test now uses a first fix with a valid altitude and pins the diagnostic. The NaN behaviour stays as it was (the writer's refusal). Output diff: graph bytes unchanged, so HealthKitAssembly.outputRevision stays 11 (62491c9d's integrated commit). No golden changed. Content corpus regenerated through GROVE_CONTENT_CORPUS_OUTPUT_DIR (3,443 -> 3,446 lines; changes report: added 3, removed 0, duplicated 0, restated inputs 0, changed outputs 2): - changed workout-route/negative-horizontal-accuracy and workout-route/precedence/invalid-coordinate-before-sync: code mobile-input.value-outside-domain -> mobile-input.unclassified, location HKSample.value -> Bundle, error invalidValue(workoutRoute, outsideDomain) -> dependency(HealthKitDependencyFailure(underlying: WorkoutRouteFailure.invalidCoordinate)). - added workout-route/negative-horizontal-accuracy-first: the first fix has horizontalAccuracy -1 and verticalAccuracy 3; refused unclassified at Bundle as above. - added workout-route/zero-horizontal-accuracy: a 0 m radius is a reading; the second row writes "1787005801,37.4276,-122.1698,0,0,,,,,". - added workout-route/nonfinite-horizontal-accuracy: NaN reaches the writer as before; refused unclassified at Bundle with dependency(RecordingCSVWriter.WriterError.nonFiniteNumber(column: "horizontalAccuracy")). Every other vector is unchanged. Mutations (ContentCorpusTests + HealthKitRecordingDocumentTests, macOS), each now fails: `< 0` -> `<= 0` (zero-horizontal-accuracy), last fix only and `&& verticalAccuracy < 0` (negative-horizontal-accuracy-first and the unit test), `!(>= 0)` (nonfinite-horizontal-accuracy), and reverting to outsideDomain (corpus code and the unit test). outputRevision 4 -> 3 still passes by design: the literal is not pinned, the fingerprint tests vary it relative to itself. Gates: GroveHealthKitFHIR macOS suite passed in verification mode (358 tests, 356 passed, 2 skipped) after the regeneration run (358 tests, 354 passed, 4 skipped); swiftlint --strict clean; reuse lint clean. Size (A): code 10,702 (+997), public 1,012 (+139) Against baseline 9b086e6, on the integrated FX round; this commit +3 code lines, +0 public declarations; this item, FX-route-accuracy, adds 6 code lines to the 10,696 / 1,012 it started from (alone on ae381a2 it read 10,380 / 1,008). The FX round adds 328 code lines and 4 public declarations (two HealthKitSampleProjectionError cases) to ae381a2's 10,374 / 1,008. Integration (FX round onto lukas/healthkit-content-rewrite; this is its last commit): only the corpus conflicted; outputRevision stays 11 (FX-route-accuracy's bump), and the content-corpus G17 row takes the regenerated corpus's digest under healthKit 11. FX-F6-codes has landed, so HealthKitValueFailure.outsideDomain keeps its throwers, and the corpus row now pins the bump: lowering outputRevision to 10 fails it. The corpus regenerated outside the worktree (3,443 -> 3,446 lines) changes exactly the vectors above, each equal to its branch recording, except the added workout-route/zero-horizontal-accuracy, whose graph also states FX-F10-metadata-warnings' "mobile-omission.unmodeled-metadata(HKTimeZone)" (a document has no element for the zone). The FX round's output diff against ae381a2 (checked by script against every item's branch head): goldens 56 -> 59 files, adding body-fat-percentage-fraction, blood-pressure-member-metadata and walking-steadiness-notification and changing heart-rate-interval, each byte-identical to its item's branch; outlines.json adds those three cases and changes the eight FX-F10-metadata-warnings outlines, each equal to its item's branch. Content corpus 3,339 -> 3,446 lines (added 107, changed 859, removed 0): no vector changed outside the items' own changes; 952 equal the one item that changed them; 8 compose one item's change with another item's rule (listed in the integration notes of FX-F6-codes, FX-F10-metadata-warnings, FX-ecg-frequency-ig, FX-route-accuracy and this commit); 6 were changed by two items (blood-pressure members by FX-F9 and FX-F10, the 1500 Los Angeles ECG by FX-time-ig and FX-F10, the negative-accuracy route by FX-F10 and FX-route-accuracy, each ending as the item that refuses or decides it; the blood-pressure catalog entry carries FX-F5's withheld endDate and FX-F9's member dispositions). G17: HealthKitAssembly.outputRevision 3 -> 11, one bump per item that changes output (FX-time-ig 4, FX-F4-percent 5, FX-F5-interval-period 6, FX-F9-bp-members 7, FX-F10-metadata-warnings 8, FX-walking-steadiness 9, FX-ecg-frequency-ig 10, FX-route-accuracy 11; FX-proleptic-reverse and FX-F6-codes change no emitted graph bytes). Gates on this head: GroveHealthKitFHIR macOS (Scripts/run-package-tests.sh) 434 tests, 432 passed, 2 skipped, 0 failed; swiftlint --strict clean on GroveFHIRContract, GroveHealthKitFHIR and its tests; GroveFHIRContract changed in this round (ExchangeInstant.days(fromYear:month:day:)), so GroveSensorKitFHIR macOS 85 passed, GroveQuestionnaire macOS 260 passed, GroveFHIRContract builds at the iOS 15 floor, and the generator --check against the pinned catalog passes; reuse lint compliant. Integration (onto the cleanup, lukas/grove-fhir-rework-integration after 5453c80): outputRevision stays 11, and the content-corpus row takes the integrated corpus's digest under healthKit 11. The location track throws this commit's WorkoutRouteFailure.invalidCoordinate, which the cleanup's narrowing reports as .dependency, so the vectors render their error as dependency("GroveHealthKitFHIR.WorkoutRouteFailure") (the cleanup names the type alone). Ported with every assertion kept: the route test exports through ExporterFixtures and pins the refusal as the narrowing of WorkoutRouteFailure.invalidCoordinate, unclassified at "Bundle"; the omitted route delivers no export. Regenerated outside the worktree on this tree (3,440 -> 3,443 lines here; 57 golden files and outlines.json unchanged), the corpus adds the 3 vectors and changes the 2 above and nothing else, checked per vector against this commit's own change carried through the cleanup's rendering. The FX round on the integration branch (ae381a2..19144c13 picked onto 5453c80; 31 commits, each compiled with build-for-testing of the GroveHealthKitFHIR test plan and swiftlint --strict clean). Output diff against 5453c80, checked by script (fxi/union_check.py) as the cleanup's state plus exactly the union of the items' changes, each carried through the cleanup's oracle rendering; nothing lies outside it: - Goldens: 54 -> 57 graph files: body-fat-percentage-fraction, blood-pressure-member-metadata and walking-steadiness-notification added and heart-rate-interval changed, each byte-identical to its item's branch. - outlines.json: the three new cases, the eight FX-F10 outlines gaining mobile-omission.unmodeled-metadata@HKSample.metadata, and blood-pressure-member-metadata's warning located there. - Content corpus: 3,336 -> 3,443 lines (added 107, changed 796, removed 0): the 966 vectors the items changed, of which 66 render unchanged here (56 catalog entries differ only in the deleted field dispositions; 10 retractions of types without outputs stay {"nothingToRetract": true}), plus 3 cleanup vectors whose existing unmodeled-metadata warning FX-F10 moved to HKSample.metadata. Against ae381a2: 3,339 -> 3,443 lines (added 107, removed 3, changed 1,595, every change the cleanup's or the round's). - G17: HealthKitAssembly.outputRevision 3 -> 11, the round's numbering (FX-time-ig 4, FX-F4-percent 5, FX-F5-interval-period 6, FX-F9-bp-members 7, FX-F10-metadata-warnings 8, FX-walking-steadiness 9, FX-ecg-frequency-ig 10, FX-route-accuracy 11); rows body-fat-percentage-fraction 5, heart-rate-interval 6, blood-pressure-member-metadata 7, walking-steadiness-notification and outlines 9, content-corpus 11; the cleanup's retraction rows keep healthKit 1. Gates on this head: GroveHealthKitFHIR macOS 433 tests (431 passed, 2 skipped, 0 failed); GroveFHIR 108/108; GroveSensorKitFHIR 97/97; GroveQuestionnaire 272/272; swiftlint --strict clean on GroveFHIRContract, GroveHealthKitFHIR and its tests; GroveFHIRContract builds at the lowered deployment targets; the generator --check against the pinned catalog passes; reuse lint compliant (2,924/2,924). Size (A): code 10,058 (+353), public 676 (-197) against baseline 9b086e6; the round adds 324 code lines and 2 public declarations to 5453c80's 9,734 / 674 (the two HealthKitSampleProjectionError cases, minus the deleted unsupportedSourceType case, each counted twice by the tool). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 7 milestone M7 (synthesis section 15 M7): measure the rewritten content layer and fix the documentation that still described removed internals or the old caller-managed flow. Documentation only; no code, golden or corpus change. Documentation fixes: - GroveDevices HealthKit-Integration.md: the tip said the device this module models "appears beside every measurement" for a sample with "a published conversion contract" (removed vocabulary). Its hkDevice states no localIdentifier, so under the exporter's default recording-device policy the graph omits the recording Device and warns mobile-omission.recording-device; the tip now says so and names RecordingDeviceResolver through the .custom policy. - GroveHealthKitBulkExport BulkHealthExporter.md: "Deduplicate HealthKit samples by participant ID and sample UUID" predates the exporter and contradicts the article's own examples; it now separates raw-sample processors from graph exporters, which need no UUID (byte-identical redelivery before the release, source-record identity after it). - GroveFHIRContract article: "Five inputs make a context" (old flow); the host/instant note told every caller to replay persisted values, which only the caller-managed ExchangeEventContext still needs, since the producer freezes both per event; the producer list now names the Questionnaire exporter too. - GroveSensorKitFHIR article: the input count named the producer although the list includes the exporter's repository scope; the note said the exporter freezes the event facts, which the producer does. - BusinessIdentifier.reference(to:) doc comment: "Grove conversion contexts" -> "Grove exchange graphs" (comment only). DocC: xcodebuild docbuild (iOS Simulator, all traits, catalogs included) for GroveFHIRContract, GroveQuestionnaireExtraction, GroveSensorKitFHIR, GroveHealthKitFHIR, GroveDevices and GroveHealthKitBulkExport, before and after these fixes: all exit 0, and Scripts/build-documentation.py's filter reports 0 documentation warnings from this repository (no unresolved symbols) in every one. Release benchmark (ConversionThroughputBenchmark, xcodebuild Release, ENABLE_TESTABILITY, N=5000, 3 runs; e4d990b and the M0 tree ddee04a each built once and run interleaved M0/M7 three times; the machine was shared, load 5-17, so each number is the best of 3 runs per tree). observation-content, us per sample: M0 commit / M0 tree today / M5 commit / M7 today: heartRate-minimal 7.0 / 7.0 / 5.1, 5.4 / 5.0 heartRate-deployment-watch 8.0 / 8.4 / 5.4, 6.2 / 6.3 heartRate-deployment-recordingDevice 9.1 / 8.8 / 6.6, 6.9 / 6.9 stepCount-minimal 11.0 / 11.5 / 8.8, 8.9 / 8.8 stepCount-deployment 11.1 / 11.7 / 8.7, 8.9 / 8.8 workout-noEvents-deployment 35.4 / 36.5 / 15.9, 15.5 / 14.9 workout12segments-deployment 39.7 / 38.4 / 15.0, 15.3 / 16.5 No regression against M0 (22-59% faster), equal to M5 within noise. Whole conversion, best us per sample, M0 tree / M7 (M7 times export() over the batch including the ledger transaction; M0 timed convert() with prebuilt contexts): 935.6/903.4, 1342.5/1515.4, 1756.6/1738.4, 939.0/876.9, 1557.2/1228.4, 1546.6/1214.1, 1555.2/1235.5; run-to-run spread was larger than every difference (heartRate-deployment-watch on M7: 1515, 1915, 2806). Concurrent conversions/s at 1/2/4/6/8 threads, M0 / M7: 709/839, 1349/1432, 2108/2280, 1941/2495, 2050/2265. One-time plan compilation (scratch test, not committed): 5.6-6.5 ms on first use in a fresh Release process, 2.7 ms warm recompile; 218 plans, 0 defects. Size (measure.py, hand-written code lines / public declarations, 9b086e6 -> 2569b5c -> head): GroveFHIRContract 4,576 -> 5,621 -> 6,197 / 579 -> 620 -> 415 GroveHealthKitFHIR 5,129 -> 4,829 -> 3,861 / 294 -> 384 -> 261 GroveSensorKitFHIR 4,841 -> 4,841 -> 4,386 / 747 -> 747 -> 724 GroveQuestionnaireExtraction 996 -> 996 -> 1,086 / 94 -> 94 -> 105 four modules 15,542 -> 16,287 -> 15,530 / 1,714 -> 1,845 -> 1,505 Top-level public types in the four modules: 153 -> 156 -> 93. Gates: GroveHealthKitFHIR macOS suite passed (433 tests, 431 passed, 2 skipped benchmarks, 0 failed); swiftlint --strict clean on GroveFHIRContract, GroveHealthKitFHIR and its tests; reuse lint compliant; GROVE_LOWERED_DEPLOYMENT_TARGETS=1 swift build --target GroveFHIRContract succeeds. The only Swift change is one doc-comment line, so the SensorKit and Questionnaire suites and the generator check were not rerun. Output unchanged: no Sources code, golden or corpus file changed. Size (A): code 10,058 (+353), public 676 (-197) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…device The validator checked only that Observation.device resolves to a Device entry, so an assembler bug that wired the application or host snapshot there passed validation, although the docs promise validation fails closed. The Mobile guide binds Observation.device to the Grove Recording Device, or to an external logical Device reference (mobile devices.md, the role table and "Recording device"). The reference context now records which Device entries claim the Grove Recording Device profile, and a literal Observation.device that resolves to any other Device is refused as mobile-exchange.reference-target-type. An identifier-only logical Device reference stays admitted. A new test rewires an exported heart rate's Observation.device to the converting application and to its host and expects the refusal, and revalidates the untouched graph. Step 12 finding G2. Output unchanged: no producer links Observation.device to anything but its recording Device; verified by the goldens, the content corpus and the SensorKit and Questionnaire suites at the series head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gateway application whose snapshot token equals the converting application's minted the gateway under the same application role and token as the converter, so the graph carried two entries under one key and the validator's distinct-entry-key check refused every gateway-linked Observation (mobile-exchange.distinct-entry-key). Such a gateway is the converter itself, so it now resolves as `.gateway` does: no second snapshot, and observation-gatewayDevice names the converter's application snapshot. The shared assembler maps it before minting a gateway snapshot, and the source-neutral SensorConverter (a removal candidate) skips its separate gateway entry the same way. A test per path pins the graph byte-identical to the one `.gateway` emits. Step 12 finding G13. Output diff: only this configuration changes, from a refusal of every gateway-linked Observation to the `.gateway` graph; every other graph is unchanged (goldens and content corpus at the series head). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SensorKitCatalog.sourceToken(for:) mapped 11 sensors and answered nil for rotation rate, keyboard metrics, messages and phone usage and sleep sessions, although SensorKitRecord has a case for each and the generated catalog lists their tokens; entry(for:) therefore missed them too, and a caller treating nil as "not admitted" (as MHC's batch publication does) would have refused streams Grove converts. The function now answers the catalog token for every sensor the catalog lists: the five above, and the raw-recording streams it also skipped (face metrics, media events, odometer, Siri and telephony speech metrics) plus the deferred acoustic settings. The entry the token names says whether Grove converts the stream, so the doc now points there instead of promising "supported". Sleep sessions and acoustic settings are iOS 26 constants and join when the system has them. A new iOS test resolves every catalogued sensor to a distinct catalog token and pins the five named ones. Step 12 finding G6. Output unchanged (no graph reads this lookup); MHC collects none of the newly resolved sensors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SensorKit generator emitted a public Grove canonical, SensorKitContract.visitLocationIdentifierSystem (https://grovealliance.org/fhir/sensorkit/NamingSystem/sensorkit-visit-location-id), that no NamingSystem or page of the pinned guide defines. The catalog requires the visit's location system to be "a deployment- or source-store-scoped absolute Identifier.system" (catalog/sensorkit-adapter.json), and the SensorKit implementation page says it is owned by the deployment or exact source-store scope, which is why SensorKitFHIRExporter already demands a deployment-owned visitLocationIdentifierSystem. Nothing in Grove or MHC read the constant; offering it invited a non-compliant system. The generator no longer emits it; SensorKitGenerated.swift is regenerated, the generator's --check passes against ig-pin, and its unit tests pass. Step 12 finding G7. Output unchanged: no graph used the constant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Rules Five output-neutral clarity fixes from the step-12 maintainer read: - HealthKitFHIRExporter.request(for:policies:reservation:) returned the assembly's HealthKitAssembly.Request but shared its name with ExchangeRequestContext.request(for:recordParts:), the ledger request that reservation answered. It is now assemblyRequest(for:policies: reservation:), and its doc says which request it is not. - ResolvedPolicies, where an HKDevice resolves to its unit and a source to its writer classification, lived in +Fingerprint.swift. It moves to its own HealthKitFHIRExporter+ResolvedPolicies.swift, beside the SourceFacts that consume it. - ExchangeOutputDraft.Links.recordingDevice now says that on a DocumentReference the author also names the converting application, always, as the assembler does. - Questionnaire spelled the host and application snapshot token formats inline, duplicating HostDevice.sourceDeviceToken and ApplicationDevice.sourceDeviceToken. Both types now expose one package helper over the facts, which their properties and the Questionnaire writer snapshots share. - RecordingDevicePolicy.custom documents the merge rule: name, manufacturer and model come from the resolver, each falling back to the HKDevice; versions and the UDI always come from the HKDevice. Step 12 finding G12. Output unchanged: the token helpers spell the same formats (the Questionnaire baselines and exporter tests, the HealthKit goldens and content corpus at the series head). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since aadb4c3 kept RetractionEvent in the package, only the HealthKit exporter could emit a retraction, while the questionnaire guide says a projected response's outputs "are withdrawn through the retraction path against their own source-output identifiers" (questionnaire responses.md, lifecycle and participation metadata), and ExtractingObservations.md told readers to "retract by them". QuestionnaireFHIRExporter.retract(_:at:receive:) takes Withdrawal values (the pair exactly as exported, and when it was withdrawn) and reports one Retraction per withdrawal, shaped like HealthKit's: one ledger transaction reserves every retraction event, keyed by the response identifier with the withdrawal's millisecond as its revision and fingerprinted with the pair, so an exact retry before release restates the event and another instant is another event; releasing the receipt also forgets the response's active reservation. The targets are recomputed by extracting the pair again: one primary-output target per Observation the export emitted, under the source-output identity its graph minted, with the response's source-record identity as the source entity and the withdrawal instant as Provenance.occurred. A pair the export refuses is refused alike, before any reservation. The questionnaire key and the Observation discriminator are now shared by export and retraction, and the refusal narrowing maps the retraction builder's errors. The pair is the input rather than the response identifier alone because which Observations a response emitted depends on what it answered; a retraction naming every marked item would name nodes never emitted. SensorKit keeps no retraction for now: its guide's retraction also names the device snapshots the record's event emitted (sensorkit implementation.md, retracting a source record), whose identities are minted from that prior event, which no input of the exporter states. The exporter's doc now says it cannot retract yet; choosing that input is left to the owner. New tests pin exact target coverage against the exported graph, replay before release and new events after another instant or a release, the forgotten active reservation, and refusals reserving nothing. Step 12 finding G1. Output diff: none for exports; retraction graphs are new output of the new entry point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The context fingerprint states each exporter's output revisions, so an exact redelivery stays byte-identical only if every change to the bytes for equal inputs bumps a revision. For HealthKit that is enforced (GoldenOutputRevisionTests, 0781a62), but SensorKit had no goldens at all, although it now assembles through the shared ExchangeGraphAssembler, and QuestionnaireGraphBaselines.json was tied to no revision, so an assembler or projection change could alter their bytes unnoticed. - SensorKit gets a small golden set, one per output shape: a structured rotation-rate Observation, the hybrid ECG Observation with its native recording, a visit with its location focus, and a raw-only heart-rate recording whose payload ships as a sidecar. Each graph's Bundle is checked in pretty-printed with sorted members and compared byte for byte, and a revision row pins each file's SHA-256 with the assembler and SensorKit revisions it last changed under. They regenerate outside the checkout through GROVE_SENSORKIT_GOLDEN_OUTPUT_DIR (TEST_RUNNER_ prefix under xcodebuild), as the HealthKit goldens do; the test target now declares its Resources, which REUSE.toml annotates. - Questionnaire gets one row per baseline case pinning the SHA-256 of the exporter's ExchangeGraph.json with the assembler and questionnaire revisions, so a change through the baselines or through the approved changes fails until its row is updated. Step 12 finding G11. Output unchanged: the goldens and digests were generated from the series' code, and the existing Questionnaire baseline comparison passed on the same run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ectly Documentation fixes for the HealthKit exporter (step 12 findings G3, G4, G8, and the HealthKit copy of G9's scope claim): - G3: Deletion.deletedAfter said "the start of the query that first reported the deletion", which is too late, as the deletion may precede that query. It is now the latest instant every reported deletion is known to follow, when the query that produced the starting anchor was issued, as GroveHealthKit's HealthKitConstraint.handleDeletedObjects( _:ofType:deletedAfter:) defines and passes it; the doc and the landing page name that handler and the raw HKAnchoredObjectQuery equivalent. Both bounds key the retraction event, so the Deletion doc and the "What to persist" table now say to keep them until the retraction's receipt is released. (Making Deletion Codable is left for an owner decision, as it is new public API.) - G4: TheExchangeGraph.md promised that the application running the exporter, as a writer, folds into the converter's snapshot. It never does for an application that states its build: the writer is minted from HKSourceRevision.version, which the HealthKit guide requires, while the converter states its marketing version and build. The page now says the writer is its own snapshot, the guide allows either form, and only identical tokens fold (the host when model and OS version match; the writer only when the converter states no build and its version equals the revision's). - G8: the "Assemble it" snippet used export(_:), which refuses an ECG after reserving its event and refuses heartbeat series and routes. It now uses export(records:) and says which kinds need records, that blood pressure exports only as the correlation (a member alone is refused as componentRequiresCorrelation), how often receive is called (once per graph or refusal: an ECG once plus once per symptom, a route under .omit never), that exporting again after release mints new events, so anchored queries rather than overlapping date windows feed it, and how an ECG's symptoms relate to category queries. - G9: the repository scope enters every source-record and output identity, not every opaque identity (Device identities leave it out). Output unchanged: documentation only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Path The GroveFHIRContract landing page still taught the caller-managed event path as an input every reader needs: an "event identifier" section on numbering events yourself, a persist row for ExchangeEventContext adapters, and the RepositoryID and ConverterRole paragraphs. It also claimed the repository scope enters every opaque identity, which the recording-device and device-snapshot preimages contradict, and its "re-validate before you trust bytes" read as a mandatory producer step. The page now leads with the producer, its ledger, the receipt and the exporter: "The ledger" replaces "The event identifier", and the persist table keeps the ledger and the key rows. The repository scope enters every source-record identity and every output identity derived from one, and the page says which Device identities leave it out. Re-validation is marked as the reader's step, under the kind the Bundle's profile claims. ExchangeEventContext, ConverterRole, ExchangeGraphNode and RepositoryID move into a "Caller-managed conversion (candidates for removal)" section and topic group, and the glossary no longer routes the exchange event or the writer through them. Step 12 finding G9. Output unchanged: documentation only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GroveQuestionnaireExtraction landing page said only that unmarked items never project. It did not say that a response answering no marked item refuses entirely with noExtractableMeasurements, that the writer context must be stamped when the response is authored (said only in ExtractingObservations.md), or that a response whose author or source is not its subject refuses (authorIsNotTheSubject, sourceIsNotTheSubject), which no page said. A new "What a response needs to export" section states the three prerequisites with their refusals, notes that caregiver-authored responses (which the guide states as the Observations' performer) are not supported yet, lists the remaining stated-field requirements, and points to the new retraction entry point. Step 12 finding G10. Output unchanged: documentation only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SensorKit landing page's anchored-batch example was pseudo-code: it called undefined helpers (fetchAnchored on an unnamed object, digestCanonicalSource, verifyRetryOrPersist, makeRecord, persistedTimeZone) and derived a per-sample ordinal for every stream, although the tabular and PPG preparations are batch-level. The topics omitted the preparation types and their retryEvidence, nothing said how a sidecar travels, and nothing said where sourceTimeZone and the recording device's stable token come from. Modeled on MHC's SensorKitBatchPublication and its upload strategies, the page now: - says which streams become one record per batch (tabular, PPG, rotation rate) and which one per sample (visit, on-wrist, device usage, ECG session, wrist-temperature session), and how the source record id derives from coordinate, catalog token, product type and ordinal; - names retryEvidence as the bytes to digest, and says an ECG session's start, sampling frequency, lead and guidance enter its graph without being in its evidence, so they are digested beside it; - explains the two guards: the app's digest keeps an id meaning one content across releases, the exporter's fingerprint keeps an event meaning one content before the release; - says what failing a batch means (throw before acknowledge: same coordinate and ids on redelivery, reservations kept) and how a drifted batch is abandoned (SensorKit.resetQueryAnchors(for:)); - shows a tabular batch end to end with real API, naming the two app-owned pieces it stands for (a digest store and a sidecar upload); - documents .sidecar(path:): the path is Attachment.url verbatim, with size and SHA-1, a relative reference the deployment resolves, so the bytes ship unchanged beside the graph; - says where sourceTimeZone (the device zone at first publication, persisted with the batch) and the recordingDevice token (only a deployment-governed per-unit token; SensorKit describes models, not units) come from, and adds both and the sidecar bytes to the persist table; - states that SensorKit records cannot be retracted yet, and why; - lists the preparation types in a "Preparing fetched batches" topic. It also corrects the repository-scope claim as in G9. Step 12 finding G5. Output unchanged: documentation only. Gates at this head (the end of the step-12 series): GroveHealthKitFHIR macOS 433 passed, 2 skipped (benchmark and corpus regeneration); GroveSensorKitFHIR macOS 100 and iOS 109 passed; GroveQuestionnaire macOS 277 passed; GroveFHIR macOS 108 passed; SwiftLint strict clean on every touched directory; GroveFHIRContract builds at the iOS 15 floor; both generators' --check and reuse lint pass. Size (A): code 10,090 (+32), public 676 (+0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
31f2973 added the SHA-256 revision rows to QuestionnaireGraphComparisonTests and imported CryptoKit unconditionally. GroveQuestionnaireExtractionTests is one of GroveQuestionnaire's linuxTargets (packages.toml), so the Linux leg's compile-check of the target fails: Linux has no CryptoKit. The file now imports CryptoKit where it exists and swift-crypto's Crypto otherwise, as LedgerFingerprintTests does, and the test target declares the Crypto product for Linux itself, as GroveQuestionnaireFHIR does, instead of reaching it only through GroveFHIRContract's edge. The test uses only SHA256.hash(data:), which both modules provide. Step 12 verifier finding (Linux CI break from 31f2973). Output unchanged: tests only, no Sources change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6c772b9 made QuestionnaireFHIRExporter.retract(_:at:receive:) emit retraction graphs, and 31f2973 tied every Questionnaire export graph to the output revisions through a SHA-256 row, but no row or golden held the retraction's bytes, so an assembler or projection change could alter them without a revision bump and break byte-identical redelivery unnoticed. QuestionnaireGraphComparisonTests gets a "retraction" row in the same revision table: the guide's pair withdrawn a day after the guide's instant, retracted at that instant under the baseline's pinned producer instance (event sequence 1), whose ExchangeGraph.json SHA-256 is pinned with the assembler and questionnaire revisions 1. The row test computes each row's bytes (a case's export graph or the retraction) and the name-set check admits the retraction beside the cases. The retraction has no pre-exporter baseline, so it takes part only in the digest check. Step 12 verifier follow-up. Output unchanged: tests only; the digest was recorded from the series' code, and the four export rows are untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three statements in the exporter docs did not match the code:
- GroveSensorKitFHIR.md ("Publish an anchored batch") told apps to
abandon a batch whose digests drifted with
SensorKit.resetQueryAnchors(for:), which resets the committed cursor
and refetches already-processed samples. The recovery is
SensorKit.discardPendingBatches(for:) (SensorKit.swift), which keeps
the acknowledged cursor and fetches the same range again under a new
reset generation, so fresh coordinates and ids; AnchoredFetcher names
it as the recovery for pendingBatchMismatch.
- The SensorKit overview said a receiver can retract a graph and "take
back by identity", while the same page and SensorKitFHIRExporter state
that SensorKit records cannot be retracted yet. The overview now says
the receiver can store and compare a graph on a retry, and that
SensorKit records cannot be taken back yet, linking the section that
says why.
- GroveHealthKitFHIR.md said an ECG passed to export(_:at:receive:) is
refused after its event is reserved while a heartbeat series or a
workout route is refused outright. All three have a content plan, so
each reserves its event and is then refused by the assembly
(HealthKitAssembly.convert: ecgEvidence(.evidenceRequired) for the ECG,
platformExclusiveSourceType for the recording route). The page now
says so and names both errors, and Record.sample's doc comment, which
named only the ECG, lists all three.
Step 12 verifier findings (SensorKit recovery API, HealthKit bare-record
refusals, SensorKit overview). Output unchanged: documentation and one
doc comment only.
Size (A): code 10,090 (+385), public 676 (-197); unchanged by this series
(documentation and tests only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Converter
Owner decision 2026-10-05 ("Delete now"). Nothing in Grove's exporters or
in MyHeartCounts converts through the caller-managed path, and both halves
already declared themselves removal candidates.
GroveSensorKitFHIR loses the generic SensorConverter family:
SensorConverter, SensorConversionContext, SensorConversion,
SensorGraphIdentifiers, SensorPrimaryResource, SensorConversionWarning,
SensorBatchResult, SensorRecordFailure, SensorConversionError,
SensorRecord, SensorSampledDataRecord, SensorECGRecord, SensorECGChannel,
SensorRecordingDocument, SensorCode and SensorRecordError
(SensorConverter.swift, SensorConverter+Resources.swift,
SensorConversionError.swift, SensorRecords.swift). The one piece the
SensorKit exporter shared, the relative sidecar-path check, moves into
SensorKitNativeRecording as a private helper, unchanged.
GroveFHIRContract loses ExchangeEventContext, ExchangeGraphNode and the
package helpers in ExchangeEventContext+Graph.swift, plus the
Instant(utc:)/DateTime(utc:) initializers only the sensor converter
called (UTCTimestamps.swift becomes TimeZone+UTC.swift, keeping
TimeZone.utc). ConverterRole, still the assembler's input, becomes a
package enum beside ExchangeGraphDraft. RepositoryID stays public:
GroveQuestionnaireFHIR's builders take it.
RetractionEvent no longer takes an ExchangeEventContext. It builds from
what a retraction graph states: the reserved event and its instant, the
envelope's identity scope, the converting application the event's
frozen facts name, and the optional legacy Bundle.id. HealthKitAssembly
passes its ExchangeEnvelope scope and request facts (eventContext(for:)
is gone); the Questionnaire exporter passes its producer's identity
scope and the reservation's facts. Validation order and every emitted
byte are unchanged. It takes the identity scope and application rather
than a whole ExchangeEnvelope because a retraction states no adapter
profile, subject or repository scope, and the Questionnaire exporter
has no adapter contract to build an envelope from.
Tests: SensorConverterTests and SensorContextValidationTests exercised
only the deleted API and are removed; ProducerDefaultsTests drops its
ExchangeEventContext-defaults case. Behaviour the exporters still have
stays covered through them: gateway applications on Observations versus
documents, the converter-as-gateway rule and repository ids
(HealthKit exporter tests), UTC clock instants, DST-edge bounds and raw
payload admission (SensorKit exporter tests). Two cases move over: the
identity-fault registry codes now pin SensorKitConversionError alone,
and a new RegisteredRecordingPayloadTests case pins the sidecar-path
refusal through SensorKitNativeRecording, which had no test of its own.
The HealthKit tests' fixture becomes a test-only TestEvent with the same
values, and their retraction cases build through it.
DocC: GroveFHIRContract.md drops the caller-managed section and topic
group and lists RepositoryID under the graph topics with the one line
that says what takes it; GroveSensorKitFHIR.md drops the source-neutral
producer section and topic group. No article names a deleted symbol.
Output unchanged: all HealthKit goldens, outlines.json, the content
corpus, the SensorKit goldens and the Questionnaire graph digests pass
unregenerated in the GroveHealthKitFHIR, GroveSensorKitFHIR and
GroveQuestionnaire suites at this head.
Size (A): code 9,997 (+292), public 637 (-236); against 879651a: code
-93, public -39. Level B: code 18,561 (-1,206), public 1,357 (-419);
against 879651a: code -1,153, public -199.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every HealthKit export keeps its per-sample warnings in Export.warnings, unchanged. A historical export raises the same few warnings for almost every sample (a device without localIdentifier, no HKMetadataKeyTimeZone), so logging each one floods the log. HealthKitFHIRExporter.WarningReport is the second mechanism: add every export of a batch, then log its description once. It names each kind of warning with its HealthKit cause and counts the samples of each type that raised it. The report counts samples, not located elements: a step count without a time zone raises the source offset at both ends of its Period and counts once. It keeps no sample identity, so its memory does not grow with the batch. The three codes GroveHealthKitFHIR raises (mobile-omission.source-offset, .recording-device, .unmodeled-metadata; all from HealthKitAssembly) each get a HealthKit cause sentence; any other code falls back to its IG reason. Refusals carry no warnings today (the exporter always passes none), so they add nothing; add() reads the warnings of every outcome alike. Kinds render the most samples first, then by code; types the most first, then by identifier; counts with grouped thousands in a fixed locale, so the text is deterministic. The DocC article and the Export.warnings comment point to the report. Tests: HealthKitFHIRExporterWarningReportTests exports, through a real exporter, two heart rates without a time zone (one on a device without a per-unit token), a step count over a minute without a time zone carrying an unmodeled key, a zoned heart rate with that key, a zoned heart rate on a device without a token, a clean heart rate and a bare ECG refusal, and pins the kinds, per-type sample counts, locations, ordering, the exact description (also for the reversed order), the empty report, grouped thousands and the unknown-code fallback. Output unchanged: no Sources change touches conversion; goldens and the content corpus pass unchanged in the GroveHealthKitFHIR suite. Size (A): code 10,069 (+364), public 650 (-223); against 7965f5a: code +72, public +13 (the report type, its Kind and their members). Level B: code 18,633 (-1,134), public 1,370 (-406). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
♻️ Current situation & Problem
#67 replaced the previous HealthKit → FHIR conversion layer (which simply produced FHIR Observations with informal and practically undocumented and non-guaranteed semantics and structure) with a new HealthKit → FHIR API whose output is in compliance with our implementation guide
however, this new API was cumbersome to use and understand, and has significantly worse performance (e.g. w.r.t. HKSample → FHIR resource conversion throughput) than what it was replacing.
this PR aims to rework it again, in a way that keeps the fundamental structure and most of the implementation in place, but simplifies and streamlines the public API around the new conversion layer, and also optimizes the implementation to (vastly) improve performance, to a level that at least matches (and in the best case will hopefully exceed) that of the original implementation both PRs intend to replace.
terminology:
new high level API
instead of the TODO TOD OTODO
performance
overall, A still is by far the most performant implementation: this is because it doesn't perform any validation, and because its output (FHIR Observations) is much smaller and simpler than B and C's output (FHIR Bundles containing a graph of Observations and other, related/referenced data)
⚙️ Release Notes
todo
📚 Documentation
yes
✅ Testing
yes
Code of Conduct & Contributing Guidelines
By creating and submitting this pull request, you agree to follow our Code of Conduct and Contributing Guidelines: