feat(domain): fold contract v0.3 — context envelope, operation outputs, lineage - #14
Conversation
…edBy
Contract v0.3 fold (art_bKoYFzs7), part 1 of 3:
- CaptureId is branded at the type level (Schema.brand); wire format and
Convex mapping unchanged (adapter maps brands to v.string()).
- The monotonic attempt counter renames ExtractionAttempt -> AttemptNumber;
ExtractionAttempt now names the lineage record (landing next).
- Event carries optionalKey producedBy { attemptId, extractorVersion,
schemaVersion } per the operator-surface lineage proposal.
- New shared Attachment shape (one shape, two consumers: Entry.attachments
and ContextEnvelope.attachments).
Delta: art_bKoYFzs7. Sources: art_lyBemdV9 SS2-4, art_rBKvvzIa SS3-4.
Human author: Gilbert Polanco (gilbertpolanco42@gmail.com)
Co-authored-by: Gilbert Polanco <gilbertpolanco42@gmail.com>
…neage Contract v0.3 fold (art_bKoYFzs7), part 2 of 3: - contextEnvelope.ts: the schema-defined record traveling with every captured utterance — verbatim utterance, actor, household root, explicit child/view/timezone context, attachments, prior references. Every contextual value carries provenance (user-asserted | app-known | inferred); missing context stays absent, never null. - operations.ts: closed tagged extraction-outcome union (fact-candidates | question-intent | write-proposal | unresolved-reference) plus Answered and the inert-until-authorized Authorization execute input; tagged via an explicit `outcome` literal (Convex forbids _-prefixed stored fields). - lineage.ts: append-only ExtractionAttempt record (triggeredBy, inputHash, outcome/failure with the v0.2 pipeline taxonomy) + deriveExtractionStatus — attempt history is the source of truth; Entry.extractionStatus is its materialized read-model. - entry.ts: optionalKey attachments (shared Attachment shape) alongside the legacy photoId scalar. - barrel exports for all of the above; ExtractionAttempt now names the lineage record (the counter is AttemptNumber). - contract-v03.test.ts: 25 new tests pinning envelope provenance, closed union discrimination and exhaustivity, derived status, producedBy round-trip, brand-safe captureId mapping (v.string, not v.id), and attachment shape sharing. roundtrip.test.ts widens one branded assertion. Verified locally (worktree at this commit): turbo typecheck+build+test 9/9 tasks, 36/36 domain tests, 17/17 security cases, 6/6 evaluation fixtures, negative control fails loudly. Delta: art_bKoYFzs7. Sources: art_lyBemdV9 SS2-4, art_rBKvvzIa SS3-4. Human author: Gilbert Polanco (gilbertpolanco42@gmail.com) Co-authored-by: Gilbert Polanco <gilbertpolanco42@gmail.com>
Evidence receipt (workflow rule)Delta document (published before implementation, governance rule-2): art_bKoYFzs7. |
Independent post-merge verification receipt (todo_HF7TF08w)Verdict: pass-with-notes. Independent review thread (reviewer for todo_HF7TF08w), 2026-09-17. This review ran against both the PR head and the merge commit; the pre-merge pass landed post-merge — timing deviation is recorded by the coordinator. Criterion results: (1) merge diff Notes (documentation-level, no action required): one compile-only widened assertion in |
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>
…tract (#13) Ports 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 (capture session id, required for PR #5's idempotency semantic); new CreateEntryOutput, CreateChildInput/Output, minimal CreateHouseholdInput/Output; 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) - deploy/port-evidence.md: synthetic smoke on reliable-panther-823 - idempotency, contract-shaped timeline, fail-closed negatives - re-run at the rebased head after the v0.3 fold (PR #14) merged mid-flight 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. Review: pass-with-notes at c5dc6fb (independent reviewer, th_9CCFeh6U); CI run 35259935719 SUCCESS on c5dc6fb. Co-authored-by: Gilbert Polanco <gilbertpolanco42@gmail.com>
Acceptance criteria
visibleToActor.fact-candidates | question-intent | write-proposal | unresolved-reference) +Answered+ inert-until-Authorizationexecute input; question-intent carries no write payload.ExtractionAttemptrecord (triggeredBy,inputHash, outcome/failure on the v0.2 pipeline taxonomy),Event.producedBy,deriveExtractionStatus(no attempt → pending; latest attempt → structured/failed).Entry.attachments(optionalKey, sharedAttachmentshape); legacyphotoIdscalar kept until readers migrate.CaptureIdbranded at the type level; Convex adapter still maps it tov.string()(brand-safe, notv.id).outcomeliteral discrimination instead of_tag.Why
Three contract additions have accumulated as separate proposals: the context envelope (what the app already knows when a capture happens), the operation-output contracts (extraction may not guess or write without authorization), and extraction lineage (reruns must supersede, not mutate, and stay auditable). None of them were canonical, so downstream slots were about to hardcode three different partial shapes. This PR folds the approved rule-2 proposals into the canonical contract as v0.3, in
packages/domain— the single source of truth — so every consumer derives from one set of schemas.What
Two commits behind the published delta (art_bKoYFzs7):
CaptureIdbranded (Schema.brand, type-level only), sharedAttachmentshape extracted to its own module (one shape, two consumers),Event.producedBy(optionalKey: attemptId + extractorVersion + schemaVersion).contextEnvelope.ts,operations.ts,lineage.tsmodules;Entry.attachments; barrel wiring; 25 new tests + one widened assertion in the existing roundtrip test.Adaptations where the proposals met the merged scaffold's reality (all recorded in the delta):
outcomeliteral discrimination instead of_tag(Convex forbids_-prefixed stored fields); the monotonic attempt counter renamedAttemptNumberbecauseExtractionAttemptnow names the lineage record;Entry.extractionStatusstays as the materialized read-model ofderiveExtractionStatus(no destructive removal); unix-msSchema.Numberwire dates instead ofDateFromMillis.Intentionally NOT here: no Convex table or function changes (lineage table lands with the extraction pipeline work), no LLM call, no mobile UI. The contract moves; consumers follow.
How to Review
packages/domain/src/contextEnvelope.ts— provenance vocabulary and the requiredcapturedAtTimezonedecision.packages/domain/src/operations.ts— closed union: check that question-intent structurally cannot carry a write payload, and thatAuthorizationis the only mutating input.packages/domain/src/lineage.ts— attempt record + the status derivation pure function.contract-v03.test.ts) encodes the wire pins as executable assertions.Note on process: the shared repo sandbox working tree was carrying another worker's in-flight thin-path changes; this PR's diff contains only the two fold commits above (verified commit-by-commit; implemented and verified in an isolated worktree at the pushed SHAs).
Test Evidence
Local verification in an isolated worktree at 233a91d (exact pushed HEAD):
pnpm turbo run typecheck build test— 9/9 tasks greenbun test packages/domain— 36/36 (11 pre-existing + 25 new)bun test ./security— 17/17 fail-closed access casesevaluation/bun src/run.ts— 6/6 fixtures; broken-adapter negative control fails loudly as requiredCI runs the same gates on this SHA.
Human author: Gilbert Polanco (gilbertpolanco42@gmail.com)
🔗 Obvious Project · 🧵 Obvious Thread