1# Lofi-owned schema facade over the Jazz 2 DSL23Date: 2026-07-18\4Reviewed dependency: `jazz-tools@2.0.0-alpha.53`\5Decision: adopt a curated one-to-one re-export; reject a translating facade67## Question89The developer-experience contract deferred whether a lofi-owned schema facade is "honest and10affordable" (previously gated on #7). Until now `src/schema.ts` and `src/permissions.ts` were the11two deliberate raw-Jazz surfaces in author source, importing `schema as s` from `jazz-tools`12directly.1314## Context and control1516The baseline (control) is the M1 arrangement: direct pinned `jazz-tools` imports in author schema17and permission files, with the author-boundary test exempting those two files from its raw-Jazz18rule. Two facts changed the trade-off:19201. Jazz 2 replaced the publicly documented 1.x CoValue API (`co.map`, `co.list`, `co.feed`, CoText,21 FileStream) with a relational table DSL. Public Jazz documentation describes 1.x, so a22 transparent surface delegates authors to documentation that is wrong for the pinned version.232. Alpha releases change behavior without notice (see24 [group-creator-authority-alpha53](group-creator-authority-alpha53.md)); every schema author was25 exposed to that churn directly.2627## Decision2829Adopt the **re-export kind** of facade and reject the **translating kind**:3031- `@nzip/lofi/schema` exports `s`, a curated `Pick` of the pinned Jazz 2 `schema` namespace. Every32 exposed member is the Jazz original by object identity — verified by test — so names, semantics,33 error messages, and tooling output cannot drift from what runs underneath. The module also34 re-exports the DSL's schema-authoring types (`App`, `Schema`, `InsertOf`, `WhereOf`, and related).35- Curation is by omission only: the deprecated `col.rename` and the unexercised `defineSliceableApp`36 are left out. Nothing is renamed, wrapped, or reinterpreted; a translating facade (a lofi-invented37 schema language) remains rejected as a non-goal.38- Author files declare data exclusively through `@nzip/lofi/schema`. The generated author-boundary39 test now applies the raw-Jazz rule to every author source file with no exemptions. Direct40 `jazz-tools` imports remain possible as an explicitly unsupported escape hatch, per the existing41 escape-hatch row of the contract.4243Honest: the facade translates nothing, so it cannot misrepresent the vendor surface. Affordable: it44re-exports rather than reimplements, so its maintenance cost is the curation list and one identity45test.4647## Procedure and evidence4849- `package/schema/mod.ts` — the facade; `package/schema/mod_test.ts` verifies member identity50 against `jazz-tools` and round-trips `defineApp`/`definePermissions` through the facade.51- `package/schema/merge_sync_test.ts` — concurrent-writer merge semantics (lww, counter, g-set) over52 a real JazzServer with two synced clients and a fresh observer.53- `package/schema/migration_test.ts` — the schema-evolution surface (`defineSchema`,54 `defineMigration`, `renameTableFrom`, `add`/`drop`/`renameFrom`) over a real JazzServer: forward55 and backward lens directions plus the missing-migration deploy guardrail.56- `apps/reference/tests/author-boundary_test.ts` — rejects `jazz-tools` imports in all author57 source, including `schema.ts` and `permissions.ts`.58- `deno task check` and `deno task build` pass with the reference app (and therefore the starter,59 which copies it) migrated to the facade.6061## Contract delta6263The author-boundary rows for `schema.ts` and `permissions.ts` no longer allow direct pinned Jazz64declarations; both declare through `@nzip/lofi/schema`. The schema safe default changes from "use65the smallest verified Jazz 2 surface" to "expose the pinned DSL through the re-export, never a66second schema language." Version bumps of `jazz-tools` become package releases: upstream renames are67absorbed by the facade instead of breaking application schemas.6869## Conformance findings (alpha.53)7071The column-type conformance suite (`package/schema/conformance_test.ts`, task72`deno task test:conformance`, part of `deno task check`) exercises every facade column type through73the official policy test harness against the real engine. Verified working end to end: string,74boolean, int (within i32), float, timestamp storage, enum, ref, json, array, bytes (≥32-byte75payloads observed reliable), `.optional()`, `.default()`, `.transform()` (view type on76insert/read/update; `where` filters take the stored type), lww merge (the default; concurrent77conflicts resolve to the last write to reach the server on every replica), counter merge78(single-session), g-set merge (single-table apps, cross-writer union), policy conditions over typed79columns with `anyOf` alternatives, `permissionIntrospectionColumns` (`$canRead`/`$canEdit`/80`$canDelete` selected per row, mirroring the deployed policy), and the full migration surface81(`defineSchema`, `defineMigration`, `renameTableFrom`, `add`/`drop`/`renameFrom`; forward and82backward lens directions carry data correctly, and deploying a changed schema without its migration83reports `status: "missing"` and withholds permissions). The three merge strategies (`lww`,84`counter`, `g-set`) are the complete collaborative-value surface of the pinned alpha — Jazz 285exports no successor to the 1.x CoValue types. Engine and type bugs found and pinned in the suite so86an alpha bump surfaces any change:87881. **`.merge()` and `.transform()` erase column typing.** The legacy untyped signatures89 (`merge(): this`, `transform(): ColumnBuilder`) shadow the typed overloads, degrading the column90 to `ColumnBuilder` and poisoning the whole table's row types. Runtime unaffected. Workaround91 documented on `@nzip/lofi/schema`: cast the result back (e.g.92 `as unknown as93 IntColumn<false, true>`, `as unknown as StringColumn<false, false, string[]>`).942. **Int columns are i32 at runtime.** Values outside i32 are rejected with95 `InvalidArg … expected96 i32` despite the `number` static type. Pinned.973. **Timestamp where-equality matches every row** (verified with distinct non-epoch dates), and98 running that query wedges the FFI driver: the next harness boot in the same process hangs on its99 first write. Pinned; the scalar test deliberately runs last.1004. **A g-set column destabilizes cross-table writes.** In a process that has booted harnesses101 before, writing a sibling table before the g-set table's first write hangs indefinitely102 (reproduced five times in bisection); a fresh process may not reproduce it. Covered by a tolerant103 subprocess canary; g-set coverage stays isolated in a single-table app.1045. **Short byte payloads are unreliable.** A 2-byte insert has been observed succeeding, failing105 with `WriteError … data too short for column value`, and hanging, depending on process context.106 Covered by a tolerant subprocess canary.1076. **Counter merge semantics are inconsistent across replica lifetimes.** Verified with two synced108 clients plus a fresh observer (`package/schema/merge_sync_test.ts`): the server keeps the last109 causally ordered update value and sums only concurrent updates, while live replicas apply every110 update — including the echo of their own reconnected write — as a delta. A replica that watched111 the history diverges permanently from what a fresh boot reads (observed: canonical 8, first112 writer 13 after a concurrent 5/3 conflict on 0; canonical 3, live replicas 5 after causal 2 then113 3). The full matrix is pinned. G-set union merge, by contrast, is verified convergent across114 writers and fresh boots.115116Hangs cannot be cancelled through FFI, so the canaries run in child processes that force-exit117(`package/testdata/gset_hang_canary.ts`, `package/testdata/bytes_short_payload_canary.ts`). The118concurrent-writer suite runs before the conformance suite in a separate process because the scalar119test wedges the FFI driver last.120121## Follow-up122123- Re-verify all pins on each alpha bump and remove the ones upstream fixes; findings 2–6 stay124 internal until our own testing is complete and a report is explicitly decided.125- Merge the g-set table back into the main conformance app when the cross-table hang is gone.126- Revisit the curation list on each alpha bump; `defineSliceableApp` joins the surface when a use127 case and tests exist. — Done: it joined with conformance coverage as the base of nested app128 namespaces; see [nested-namespaces-alpha53](nested-namespaces-alpha53.md).