feat(backend): port thin-path functions onto canonical four-table contract - #13
Conversation
…tract Port the retired thin-path functions (PR #5) onto backend/convex with validators derived from the packages/domain contracts via the tested Effect->Convex adapter: - contracts: CreateEntryInput gains authorId + optional captureId (the capture session id, required for PR #5's idempotency semantic); new CreateEntryOutput, CreateChildInput/Output, minimal CreateHouseholdInput/ Output (children:create needs a household creation path); EntryFields gains optional captureId - schema: entries gains by_capture (idempotency lookup) and by_child_createdAt (chronological timeline) indexes - functions: households:create, children:create (trim + non-empty name, household existence check), entries:createEntry (raw-first capture, extractionStatus pending, idempotent on captureId - original capture wins, retried payload changes absorbed, same entryId returned), timeline:list (by_child_createdAt asc, optional limit, rows decode through EntrySchema so output is validated contract shape) - convex codegen (_generated) committed per repo policy (CI has no deployment) Divergence from the thin path: createEntry no longer accepts inline events - the canonical model captures raw-first and events belong to the extractor via the AppendEventsInput contract. Co-authored-by: Gilbert Polanco <gilbertpolanco42@gmail.com>
Capture write path, captureId idempotency (original capture wins, retried payload absorbed, same entryId), chronological contract-shaped timeline, and fail-closed negatives (empty captureId, bogus householdId) on dev deployment reliable-panther-823. Synthetic data only. Co-authored-by: Gilbert Polanco <gilbertpolanco42@gmail.com>
PR #14 branded CaptureId (NonEmptyString + brand). Compare decoded values against a branded captureId instead of a plain string literal. Co-authored-by: Gilbert Polanco <gilbertpolanco42@gmail.com>
d818502 to
643b786
Compare
Independent review — pass-with-notesReviewed per Brief checks (all at c5dc6fb)
Notes (non-blocking)
Verdict: pass-with-notes. The merge-gate inputs (independent review + CI green on head |
|
Acknowledged — pass-with-notes at c5dc6fb, reviewed against the exact head with reviewer-run uncached gates. All three notes are recorded:
No push follows this reply (workflow rule 5 — the review results and CI on c5dc6fb remain valid). Proceeding with the serialized squash merge as the thin-path lane merge owner; evidence receipt to follow on this PR once the merge commit and post-merge smoke are in. |
Evidence receipt (per
|
Why
The retired thin path (PR #5,
deploy/thin-path) proved the capture write path on a throwaway two-table schema with hand-written validators. The canonical scaffold (PR #9) landed the four-table, contract-derived schema inbackend/convexbut shipped zero functions — the canonical model could not capture or read anything. This PR ports the thin path's three functions (children:create,entries:createEntry,timeline:list) onto the canonical tables so the deployment-verifiedcaptureIdidempotency semantic (duplicate submit returns the same entry) survives on the real schema.What
Contracts extended (
packages/domain— still the single source of truth; the backend hand-writes no validators):CreateEntryInputgainsauthorId(fromEntryFields, required) andcaptureId(optionalCaptureId). Function args must derive from contracts, and PR deploy(convex): thin-path functions on dev deployment reliable-panther-823 #5's idempotency needs the capture session id at the entry boundary.CreateEntryOutput:{ status: created | idempotent_hit, entryId, captureId? }.CreateChildInput/CreateChildOutput— canonicalchildrenrows reference a household, so the portedchildren:createtakeshouseholdIdand verifies it exists (fail-closed, no orphan child rows).CreateHouseholdInput/CreateHouseholdOutput— beyond the three named functions:children:createcannot be exercised at all without a household-creation path (no other write path for households exists in the deployed surface). Slot 19 (actor/membership/invitation flow) supersedes it.EntryFields.captureId: Schema.optional(CaptureId)— capture session id on the entry row; schema gainsby_capture(idempotency lookup) andby_child_createdAt(chronological timeline) indexes.Functions (
backend/convex/convex/): args derived viaconvexFields(contract); handlers decode args through the Effect contract so refinements stay authoritative, and usectx.db.normalizeIdto bridge contract string ids to Convex branded ids (runtime-validated, no casts).households:create— minimal root creationchildren:create— trim + non-empty name (thin-path behavior), household existence checkentries:createEntry— raw-first: transcript verbatim,extractionStatus: "pending",structuredEventIds: [], idempotent oncaptureId(original capture wins; retried payload changes absorbed; sameentryIdreturned)timeline:list—by_child_createdAtascending, optionallimit; rows decode throughEntrySchema, so output is validated contract shape with system fields strippedConvex codegen (
_generated/) committed per repo policy (CI has no deployment).Rebase note (v0.3 fold)
PR #14 (contract v0.3) merged mid-flight; this branch is rebased onto it. The single textual conflict (
entry.tsimport block) was resolved to carry both efforts — the fold'sAttachmentimport +attachmentsfield and this port'sCaptureIdimport +captureIdfield. The fold's brandedCaptureIdderives through the adapter as an optionalv.string()(same pattern as the existing brandedconvexIdtypes); domain test comparisons decode branded values. No v0.3 files are part of this PR's diff.Divergences from the thin path (deliberate)
captured_with_event_errors) because there was no extraction pipeline. The canonical model captures raw-first and events belong to the extractor via theAppendEventsInputcontract (slots 06/08).Array(EntrySchema)per the canonical contract — no embedded events, no_tagre-wrapping; events are reached throughstructuredEventIds.How to Review
contracts.ts,entry.ts(+captureId),index.ts; tests pin the adapter derivation, the captureId optionality/rejection, and the timeline read-boundary decode (system fields stripped).schema.tsindexes + committed_generated.deploy/port-evidence.md— full synthetic smoke transcript on reliable-panther-823, re-run at the rebased head.c5dc6fb; the deployment smoke ran the deployed code on reliable-panther-823 with synthetic data only.Test Evidence
reliable-panther-823(synthetic data only), re-run after the v0.3 rebase — full transcript indeploy/port-evidence.md(c5dc6fb):households:create→ contract output;children:createwith household existence check.entries:createEntrycapturecap-smoke-001→status:"created"; retry with same captureId + different payload → sameentryId,status:"idempotent_hit", original transcript preserved (PR deploy(convex): thin-path functions on dev deployment reliable-panther-823 #5 semantic on canonical tables).timeline:list→ chronological, contract-shaped rows (rawTranscript verbatim,extractionStatus:"pending",structuredEventIds:[], no_id/_creationTime).captureId→INVALID_ENTRY_INPUTat the Effect decode boundary (nothing written); well-formed nonexistenthouseholdId→ rejected, no orphan row.643b786:pnpm turbo run typecheck test buildgreen (9/9 tasks);bun test ./security17/17; evaluation harness 6/6 + negative control fails as expected; CI green (re-runs onc5dc6fb).Human author: Gilbert Polanco (gilbertpolanco42@gmail.com)
🔗 Obvious Project · 🧵 Obvious Thread