feat(effect-app,infra): run jitM at the store boundary as JSON → JSON - #896
Merged
Merged
Conversation
`jitM` was applied by the repository *after* the store had already decoded the document, and was typed `(pm: Encoded) => Encoded` - a lie, since jitMs are written against the stored JSON and `Encoded` holds native Date/Map/Set. The read pipeline is now: raw JSON document -> merge `defaultValues` (unchanged: they only fill absent keys, so an explicitly stored `null` reaches `jitM`) -> `jitM` (JSON -> JSON) -> decode `toCodecJson(toEncoded(schema))` -> Encoded. The repository only decodes Encoded -> the domain type, so `jitM` can repair legacy shapes - including explicit `null`s - before any schema decode sees the document. The JSON->Encoded decode stays strict: `jitM` is the one place a legacy document can be repaired, and a document it does not repair fails loudly at the store boundary rather than being read back half-decoded. - `StoreConfig.jitM?: (json: JsonRecord) => JsonRecord` is new, alongside the exported `JsonRecord` alias; it is not applied on the write/encode path and never receives `_etag` - `RepositoryOptions.jitM` changes to the same signature and is forwarded into the store config instead of into `makeRepoInternal` - `makeRepoInternal` loses its `mapFrom` parameter; `mapReverse` now only splits `_etag` and calls `setEtag` - `Disk` applies `jitM` once at the file-read boundary and passes `undefined` down to its inner Memory store, since jitMs are not guaranteed idempotent - `validateSample` decodes the store's output directly; `ValidationError.jitMResult` is deprecated as it is identical to `rawData` Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add a legacy-document suite for the JSON-shaped `jitM` running at the store boundary, covering what the pipeline makes possible and what it deliberately refuses: - `jitM` repairs a legacy document missing a required key, which decodes end-to-end through `find`/`all`/`validateSample` - `jitM` repairs an explicit `null` (not a valid `S.Date` encoding, so no schema decode could ever fix it) and the document decodes - `jitM` receives the raw JSON document - a Date as an ISO string, a ReadonlySet as an array, a ReadonlyMap as an array of pairs, and no `_etag` - and the JSON it returns is what the decode consumes - `defaultValues` still only fill absent keys, are merged before `jitM` runs, and a stored `null` reaches `jitM` instead of being replaced by the default - a document `jitM` does *not* repair fails loudly at the store boundary (`SchemaError: Missing key at ["vatRate"]`), pinned down so it is not "fixed" into a lenient decode later - `_etag` is not visible to `jitM` and survives the round trip `MemoryStoreLive` cannot seed a legacy document (it strictly `encode`s its seed), so the suite uses a schemaless Memory harness that applies the real read boundary on top. `validateSample`'s corruption cases move to a schema whose Encoded shape is looser than its domain type (`FiniteFromString`): now that the store decode is strict, a `jitM` producing a value the Encoded shape rejects would fail in the store rather than surface as a repository `ValidationError`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 16, 2026
@effect-app/cli
effect-app
@effect-app/eslint-codegen-model
@effect-app/eslint-shared-config
@effect-app/infra
@effect-app/vue
@effect-app/vue-components
commit: |
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.
Supersedes #894 and #895 — both will be closed. This is the same design rebuilt directly on
main, with nothing lenient.The bug
Since #874 ("native Date/Map/Set Encoded"), the document stores decode every stored document with the full schema at the store boundary, and the repository applied
jitM(mapFrom) only afterwards. So a legacy document thatjitMexists to migrate could not be read at all:on every
find/filter/all/validateSample. Found via macs-holding/configurator, where all 7 DB Validation jobs fail against real production data:Shop.vatRatemissing on every shop,User.permissionson most users,Configurator.conditionGroups[].conditions[].rules[].groupIdon legacy rules. Read-only prod inspection confirmed these are genuinely missing keys — there are nonulls involved.jitMwas also mistyped:(pm: Encoded) => Encodedpromised nativeDate/Map/Set, while every jitM is written against the document as stored.The pipeline
makeRepoforwardsjitMinto the store config;makeRepoInternalloses itsmapFromparameter andmapReverseonly splits_etag;validateSampledecodes the store's output directly. New infra helpermakeStoredDecode(codec, jitM)holds the read boundary in one place, splitting_etagoff first sojitMnever sees infrastructure metadata. Wired in Cosmos, SQL, Pg, Memory and Disk (Disk at the file-read boundary, exactly once, since jitMs are not guaranteed idempotent). Never on the write path.Everything stays strict.
makeJsonDocumentCodecis unchanged frommainin both directions, andpackages/infra/src/Store/utils.tsis byte-identical tomain. A documentjitMdoes not repair fails loudly — pinned by a test, with a comment telling future readers not to "fix" it into a lenient decode.defaultValueskeep theirPartial<Encoded>type and their existing lowering viajson.toJson(...); they still fill only absent keys, so an explicitly storednullreachesjitM— which is the point:nullis a value, not a missing key, and onlyjitMcan decide what to do with it.Breaking changes for consumers
jitMis(json: JsonRecord) => JsonRecord, no longer generic overEncoded.Date→ ISO string,ReadonlySet→ array,ReadonlyMap→ array of[k, v]pairs. Returning aDate/Map/Setis now wrong.json["x"]);"x" in jsonstill works._etagis not part of the document passed tojitM.ValidationError.jitMResultis@deprecatedand equalsrawData— the repository can no longer observe a pre-jitMdocument. Kept rather than removed to avoid breaking consumers; happy to delete it instead.Verification
pnpm check(tsgo): clean, no casts added to paper over themapFromremoval.packages/infra: 282 passed / 26 skipped.packages/effect-app: 185 passed. Lint clean.repository-legacy-document.test.ts(new) andvalidateSample.test.ts: missing-key repair throughfind/all/validateSample; explicitnullrepair;jitMsees JSON (ISO string, Set-as-array, Map-as-pairs, no_etag);defaultValuesfill only absent keys with a storednullreachingjitM; an unrepaired document failing loudly;_etaginvisible tojitMand preserved.Notes
MemoryStoreLivecannot seed a legacy-shaped document (its seed path strictly encodes the whole document), so the legacy-document test uses a schemaless Memory harness with the real read boundary on top. Worth deciding separately whether writes should fail loudly or lower leniently.selectand CosmosqueryRawbypass the document decode and therefore do not runjitM. Pre-existing, unchanged.defaultValueson read remain inconsistent across adapters (Cosmos/SQL merge on every read; Memory at seed time; Disk before its inner store). Left alone deliberately.makeJsonDocumentCodec.decodeis sync and throws. Turning that into a typedSchemaErrorfailure needs the read paths effectified (Store.find/all/filterchannels widened, per-document decode returningResult, adapters'maps lifted) — planned as a separate PR.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.