Skip to content

feat(backend): port thin-path functions onto canonical four-table contract - #13

Merged
obvious-autobuild[bot] merged 4 commits into
masterfrom
feat/port-thin-path
Sep 17, 2026
Merged

obvious-autobuild[bot] merged 4 commits into
masterfrom
feat/port-thin-path

Conversation

@obvious-autobuild

@obvious-autobuild obvious-autobuild Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

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 in backend/convex but 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-verified captureId idempotency 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):

  • CreateEntryInput gains authorId (from EntryFields, required) and captureId (optional CaptureId). 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.
  • New CreateEntryOutput: { status: created | idempotent_hit, entryId, captureId? }.
  • New CreateChildInput/CreateChildOutput — canonical children rows reference a household, so the ported children:create takes householdId and verifies it exists (fail-closed, no orphan child rows).
  • New minimal CreateHouseholdInput/CreateHouseholdOutput — beyond the three named functions: children:create cannot 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 gains by_capture (idempotency lookup) and by_child_createdAt (chronological timeline) indexes.

Functions (backend/convex/convex/): args derived via convexFields(contract); handlers decode args through the Effect contract so refinements stay authoritative, and use ctx.db.normalizeId to bridge contract string ids to Convex branded ids (runtime-validated, no casts).

  • households:create — minimal root creation
  • children:create — trim + non-empty name (thin-path behavior), household existence check
  • entries:createEntry — raw-first: transcript verbatim, extractionStatus: "pending", structuredEventIds: [], idempotent on captureId (original capture wins; retried payload changes absorbed; same entryId returned)
  • timeline:list — by_child_createdAt ascending, optional limit; rows decode through EntrySchema, so output is validated contract shape with system fields stripped

Convex 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.ts import block) was resolved to carry both efforts — the fold's Attachment import + attachments field and this port's CaptureId import + captureId field. The fold's branded CaptureId derives through the adapter as an optional v.string() (same pattern as the existing branded convexId types); domain test comparisons decode branded values. No v0.3 files are part of this PR's diff.

Divergences from the thin path (deliberate)

  • No inline events on createEntry. The thin path decoded caller-supplied events inline (captured_with_event_errors) because there was no extraction pipeline. The canonical model captures raw-first and events belong to the extractor via the AppendEventsInput contract (slots 06/08).
  • Timeline output is Array(EntrySchema) per the canonical contract — no embedded events, no _tag re-wrapping; events are reached through structuredEventIds.

How to Review

  • Domain delta: 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).
  • Backend: the four function modules + schema.ts indexes + committed _generated.
  • deploy/port-evidence.md — full synthetic smoke transcript on reliable-panther-823, re-run at the rebased head.
  • Contamination note: mid-flight shared-worktree edits from the concurrent v0.3 effort were deliberately excluded (work done in a private clone against pristine master, then rebased); this branch carries only the port described above.
  • Independent-verification note: local evidence ran on the exact head SHA c5dc6fb; the deployment smoke ran the deployed code on reliable-panther-823 with synthetic data only.

Test Evidence

  • Deployment smoke on reliable-panther-823 (synthetic data only), re-run after the v0.3 rebase — full transcript in deploy/port-evidence.md (c5dc6fb):
    • households:create → contract output; children:create with household existence check.
    • entries:createEntry capture cap-smoke-001 → status:"created"; retry with same captureId + different payload → same entryId, 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).
    • Fail-closed negatives: empty captureId → INVALID_ENTRY_INPUT at the Effect decode boundary (nothing written); well-formed nonexistent householdId → rejected, no orphan row.
  • Local + CI at head 643b786: pnpm turbo run typecheck test build green (9/9 tasks); bun test ./security 17/17; evaluation harness 6/6 + negative control fails as expected; CI green (re-runs on c5dc6fb).

Human author: Gilbert Polanco (gilbertpolanco42@gmail.com)

🔗 Obvious Project · 🧵 Obvious Thread

@obvious-autobuild
obvious-autobuild Bot marked this pull request as ready for review September 17, 2026 18:28
ObviousApp and others added 3 commits September 17, 2026 18:30
…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>
Rebased onto the merged v0.3 contract fold (00581f5); redeployed and
re-ran the full synthetic smoke at 643b786 - idempotency, timeline shape,
and fail-closed negatives all verified again on the rebased code.

Co-authored-by: Gilbert Polanco <gilbertpolanco42@gmail.com>
@obvious-autobuild

Copy link
Copy Markdown
Contributor Author

Independent review — pass-with-notes

Reviewed per .obvious/obvious.md (review → repair → merge workflow) against the current head. Per workflow rule 5, any push after this comment invalidates these results — re-review required.

PR:               https://github.com/OCPdev25/obv-hackaton/pull/13
Tested head SHA:  c5dc6fb00e434fe408eee6f76a22779f4078ecad (current head; the brief's 643b786 was superseded by docs-only c5dc6fb — both CI-green)
Review result:    pass-with-notes — independent reviewer thread th_9CCFeh6U, 2026-09-17
Checks:           CI run 35259935719 "Typecheck, test, build" SUCCESS on exactly c5dc6fb (run list also shows SUCCESS on 643b786); merge-ref run head == pushed head
Merge commit:     n/a — reviewer does not merge; merge ownership remains with the thin-path lane
Post-merge smoke: pending merge (reviewer-side uncached local gates below already ran on c5dc6fb)
Unlocked tasks:   merge decision for the thin-path lane

Brief checks (all at c5dc6fb)

  1. Acceptance criteria vs code + tests — every body claim verified in the diff: CreateEntryInput +authorId (required; reuses the pre-existing EntryFields.authorId) +captureId (optional); CreateEntryOutput, CreateChildInput/Output, CreateHouseholdInput/Output; EntryFields.captureId with by_capture + by_child_createdAt indexes; four functions (households:create, children:create, entries:createEntry, timeline:list) all deriving args via convexFields(contract), decoding through the Effect contract in-handler, and bridging ids with ctx.db.normalizeId (fail-closed existence checks); raw-first capture (extractionStatus:"pending", structuredEventIds:[]); timeline decoding rows through EntrySchema (system fields stripped, pinned by test). ✓
  2. Contamination — diff confined to backend/convex/convex/* (four function modules, lib.ts, schema.ts, _generated codegen), deploy/port-evidence.md, and additive domain wiring (contracts.ts, entry.ts, index.ts, roundtrip.test.ts). No contextEnvelope.ts / lineage.ts / operations.ts copies — the v0.3 canonical modules are untouched and no divergent duplicates exist. The shared-sandbox leftover files are not part of this PR; a clean worktree checkout of c5dc6fb matches the GitHub diff exactly. ✓
  3. Stale-base semantic check — branch base is 00581f5 (the v0.3 fold): merge-base(origin/feat/port-thin-path, origin/master) = 00581f5, so no rebase is required. Semantic read: the entry.ts hunk carries both the fold's Attachment import + attachments field and this port's CaptureId import + captureId field; the port consumes canonical modules (CaptureId from ./extraction.js) and bypasses none. mergeStateStatus: CLEAN, mergeable: MERGEABLE (master tip 652ace5 is 1 commit ahead — PR feat(handoff): since-last-seen digest + grounded follow-up prototypes #17, non-overlapping). ✓
  4. Event.authorId remains OPEN — event.ts is not touched; no authorId in EventSchema at head (verified by grep at c5dc6fb). The authorId in this PR is the pre-existing EntryFields.authorId (Schema.String, present at base 00581f5 — verified via git show 00581f5:packages/domain/src/entry.ts), newly surfaced as a required CreateEntryInput field. ✓
  5. Duplicate-submit idempotency preserved — entries:createEntry looks up by_capture first and returns {status:"idempotent_hit", entryId} of the original row without writing (original capture wins; retried payload changes absorbed). Deployment smoke step 4 (port-evidence.md) proves the PR deploy(convex): thin-path functions on dev deployment reliable-panther-823 #5 semantic on the canonical tables: same captureId + different rawTranscript → same entryId, original transcript on the timeline. Domain tests pin captureId optionality and empty-string rejection at the decode boundary. See Note N1 on the proof surface.
  6. Dev-deployment evidence — IN SCOPE, not deferred: deploy/port-evidence.md documents the clean-slate → deploy → re-deploy (post-v0.3-rebase, at 643b786-era code, code-identical to c5dc6fb) → synthetic smoke → fail-closed negatives sequence on reliable-panther-823, with a scope-verification step (convex env list) before any write. Explicitly labeled synthetic-data-only — no overstated claims.
  7. Local gates, reviewer-run, uncached, at exact head c5dc6fb — pnpm turbo run typecheck test build --force: 9/9 tasks, 0 cached; bun test ./security: 17 pass / 0 fail; evaluation corpus: 6/6; negative control with --expect-failure exits 0 (behaves as expected). ✓
  8. CI green on the pushed head — run 35259935719 SUCCESS with headSha exactly c5dc6fb00e434fe408eee6f76a22779f4078ecad; the run list additionally shows SUCCESS on 643b786 (18:33:27Z). Both SHAs stated here. ✓

Notes (non-blocking)

  • N1 — idempotency proof surface: there is no in-repo automated test that executes createEntry (the repo has no Convex function-test harness yet); the runtime proof is the deployment smoke transcript plus domain contract tests pinning the wire shape. The semantic also holds under Convex's serializable OCC for concurrent duplicate submits. Observation for the merge owner: worth a repo-executable runtime test once a Convex test harness exists, before this pattern spreads to more functions.
  • N2 — negative-control detail: the broken-adapter run reports 1/6 (retry-double-submit still passes on a broken adapter); the harness exits 0 under --expect-failure. Consistent with prior merged-PR receipts.
  • N3 — committed _generated/: justified in the body (CI has no deployment); backend:typecheck depends on these bindings. Consistent and fine.

Verdict: pass-with-notes. The merge-gate inputs (independent review + CI green on head c5dc6fb) are satisfied; the merge decision and execution remain with the thin-path lane (th_7GNDU9rG).

@obvious-autobuild

Copy link
Copy Markdown
Contributor Author

Replying to this comment

Replying to this comment

Acknowledged — pass-with-notes at c5dc6fb, reviewed against the exact head with reviewer-run uncached gates. All three notes are recorded:

  • N1 (no in-repo runtime test of createEntry until a Convex function-test harness exists): carried into the merge receipt as an unlocked-task observation for the orchestrator — the harness lane owns that decision, and the pattern should not spread to more functions before it exists.
  • N2 (broken-adapter still passes retry-double-submit): consistent with the harness design — the negative control proves the harness fails on a broken adapter, not that it fails on every fixture.
  • N3 (committed _generated/): agreed — CI has no deployment, and backend:typecheck depends on the bindings.

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.

@obvious-autobuild
obvious-autobuild Bot merged commit 3caaa08 into master Sep 17, 2026
1 check passed
@obvious-autobuild

Copy link
Copy Markdown
Contributor Author

Evidence receipt (per .obvious/obvious.md)

PR:               https://github.com/OCPdev25/obv-hackaton/pull/13
Tested head SHA:  c5dc6fb00e434fe408eee6f76a22779f4078ecad — exact HEAD the independent review and local tests ran against
Review result:    pass-with-notes — independent reviewer thread th_9CCFeh6U, 2026-09-17 (PR comment 5719493591; reviewer-run uncached gates at the same SHA)
Checks:           CI run 35259935719 "Typecheck, test, build" SUCCESS on exactly c5dc6fb (1 check, 0 pending, head == pushed head)
Merge commit:     3caaa086fde542c61acc0c56bdaf31df8fcdf20c (squash on master)
Post-merge smoke: full verification table on 3caaa08 — pnpm install --frozen-lockfile clean; pnpm turbo run typecheck/test/build green (15 tasks, 0 failed); bun test ./security 17/17; evaluation corpus 6/6 + negative control fails as expected
Unlocked tasks:   thin-path port lane (todo_jqhhYnq0) complete — backend/convex now has the canonical capture write path + timeline read for downstream lanes; N1 observation (in-repo runtime test of createEntry once a Convex function-test harness exists) carried for the QA/harness lane

Deployment evidence: deploy/port-evidence.md (in the merge) — synthetic smoke on reliable-panther-823, re-run at the rebased head: idempotency (same entryId on duplicate captureId, original transcript preserved), contract-shaped chronological timeline, fail-closed negatives (empty captureId, nonexistent household).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants