Server side of haverstack/core#52. That issue affirms the spec's PATCH contract (content-only RFC 7396 merge patch), splits the adapter's updateRecord grab-bag into intent-revealing operations with a 1:1 wire mapping, and — most importantly for this repo — creates a shared conformance fixture suite run by both adapter-api and this server. Nearly every other design-review decision (#53 errors, #55 IDs, #48 concurrency, #65/#66 attachments, #67/#68 invariants) names those fixtures as its enforcement point, so this is the contract backbone for the whole sync batch.
Server divergence that exists today
The PATCH body shape is already wrong. Spec §Records: "PATCH /records/:id accepts a partial content object" — the body is the patch. The handler reads an envelope instead (src/routes/records.ts:224):
const patch = (body.content ?? {}) as Record<string, unknown>;
A spec-conforming client sending { "title": "New" } gets body.content === undefined → empty patch → a silent no-op update that still bumps version and snapshots. Worst-case wire dishonesty: 200, new version, nothing changed. (Our own tests send the envelope, which is how this survived.) Decide the canonical shape with the spec/core — core #52's resolution keeps the spec's raw-patch semantics — and fix whichever side is wrong, then let the fixtures pin it.
Note the null-deletion consequence: with the raw patch traveling, { "title": null } must delete the field server-side (RFC 7396). The route already delegates merge semantics to Stack.update(), so this mostly follows from the body fix.
Work items
Refs haverstack/core#52, haverstack/core#47 (migration commit), haverstack/core#48 (expectedVersion on patch), haverstack/core#50.
Server side of haverstack/core#52. That issue affirms the spec's PATCH contract (content-only RFC 7396 merge patch), splits the adapter's
updateRecordgrab-bag into intent-revealing operations with a 1:1 wire mapping, and — most importantly for this repo — creates a shared conformance fixture suite run by bothadapter-apiand this server. Nearly every other design-review decision (#53 errors, #55 IDs, #48 concurrency, #65/#66 attachments, #67/#68 invariants) names those fixtures as its enforcement point, so this is the contract backbone for the whole sync batch.Server divergence that exists today
The PATCH body shape is already wrong. Spec §Records: "
PATCH /records/:idaccepts a partial content object" — the body is the patch. The handler reads an envelope instead (src/routes/records.ts:224):A spec-conforming client sending
{ "title": "New" }getsbody.content === undefined→ empty patch → a silent no-op update that still bumpsversionand snapshots. Worst-case wire dishonesty: 200, new version, nothing changed. (Our own tests send the envelope, which is how this survived.) Decide the canonical shape with the spec/core — core #52's resolution keeps the spec's raw-patch semantics — and fix whichever side is wrong, then let the fixtures pin it.Note the null-deletion consequence: with the raw patch traveling,
{ "title": null }must delete the field server-side (RFC 7396). The route already delegates merge semantics toStack.update(), so this mostly follows from the body fix.Work items
POST /records/:id/migrate(migration commit — currently has no wire representation at all, somigrateAll()cannot work overadapter-api)PUT /records/:id/permissionsandPOST /records/:id/restore/:versionmatch the intent-split contract 1:1 (they exist; fixtures will pin bodies/status codes)limit= one default page, not "everything")Refs haverstack/core#52, haverstack/core#47 (migration commit), haverstack/core#48 (
expectedVersionon patch), haverstack/core#50.