Skip to content

feat(backend): family-knowledge Convex functions — create, supersede, bounded reads - #31

Merged
obvious-autobuild[bot] merged 2 commits into
masterfrom
feat/knowledge-convex-functions
Sep 17, 2026
Merged

obvious-autobuild[bot] merged 2 commits into
masterfrom
feat/knowledge-convex-functions

Conversation

@obvious-autobuild

Copy link
Copy Markdown
Contributor

Acceptance criteria (repo rule 1)

  • AC1 — Write path (create): knowledge:create — a household-scoped, actor-attributed mutation composing the storage row from the canonical PR feat(domain): family knowledge records — schema, invariants, Convex adapter, synthetic fixtures #16 contract (KnowledgeFields), entering the draft→published lifecycle server-side (status: "current", visibility: "draft"; client cannot set lifecycle state). Validators derived from Effect schemas via the tested adapter — no hand-rolled duplicates.
  • AC2 — Write path (supersede): knowledge:supersede — atomic append-only supersession: appends the successor with the forward supersedes edge and marks the target superseded with validUntil = successor's validFrom (the fixture-grounded chain rule). Rejects superseding a superseded item (no forks), cross-household/cross-child targets, and a validFrom that would make validUntil precede the target's validFrom.
  • AC3 — Read path (bounded current): knowledge:listCurrent — bounded retrieval of current items by household + child (+ optional topic), superseded items excluded by default, explicit limit (1..200, required).
  • AC4 — Read path (bounded history): knowledge:listHistory — the explicit history capability (PR feat(retrieval): bounded read-only history query path over synthetic corpus #24 precedent): includes superseded items, same household scoping, publication dimension, and page bound.
  • AC5 — Fail-closed authorization: every function resolves the (householdId, childId) pair against the stored child row and rejects mismatches (CHILD_HOUSEHOLD_MISMATCH, CHILD_NOT_FOUND) instead of rewriting scope — consistent with the security suite's 17 existing cases. Cross-household reads and writes are rejected.
  • AC6 — Guardrail: kind is never an authorization input: the knowledge kind is a retrieval/rendering discriminant only; audience is governed by publication/audience state. Security case KN-8 asserts access outcomes are identical across all four kinds for the same audience state (published → ALLOW for members; other members' drafts → DENY_DRAFT_AUTHOR_ONLY), for both reads and the draft-rule.
  • AC7 — Security suite extended: negative cases for each new function (cross-household read/create/supersede, anonymous, other-member draft, attribution mismatch, incomplete child scope) plus positive controls and KN-8 — 12 new executable cases, suite at 29 total (17 existing stay green).
  • AC8 — Guardrail: no Event/domain touch: Event.authorId and all Event schema fields untouched; packages/domain schemas and the existing knowledge fixtures unmodified (no adapter gap found — flagged: none).
  • AC9 — Gates green at the pushed head: pnpm turbo run typecheck test build (all packages, including the new backend validator suite), bun test ./security (29), focused domain suite (61), CI green on the exact pushed head SHA.

Why

PR #16 landed the family-knowledge contract (Effect v4 schemas, Convex adapter, synthetic fixtures) — but nothing can yet write or read a knowledge row. This slice makes the knowledge table functional end to end: caregivers' informal observations become queryable, supersession-correct family knowledge, with the fail-closed household boundary the security suite already demands. Without it, the Remember & Retrieve consumers (catch-up, digests) have no backend surface to build on.

What

Three layers, mirroring the PR #13 (contract-derived functions) and PR #24 (bounded reads) precedents:

packages/domain (UNTOUCHED — canonical contract)
      │  KnowledgeFields + convexId schemas (adapter-tested)
      ▼
backend/convex/convex/knowledgeInput.ts
  Composed input/output contracts + pure decision logic:
  CreateKnowledgeItemInput / SupersedeKnowledgeItemInput / ListKnowledgeInput,
  buildKnowledgeItemRow (lifecycle server-side), checkSupersession
  (values-not-exceptions rejections), isCurrentKnowledgeItem /
  isVisibleKnowledgeItem (publication dimension).
      ▼
backend/convex/convex/knowledge.ts
  create · supersede (atomic) · listCurrent · listHistory
  — every function: decode args → fail-closed requireChildInHousehold → contract row/decision → write/read.

Key decisions:

  • Supersession is atomic and window-correct. The successor's validFrom closes the target's validity window (validUntil = successor's validFrom), matching the PR feat(domain): family knowledge records — schema, invariants, Convex adapter, synthetic fixtures #16 fixture chains. Eligibility is a pure decision (checkSupersession) returning rejection codes (SUPERSESSION_TARGET_NOT_CURRENT, SUPERSESSION_HOUSEHOLD_MISMATCH, SUPERSESSION_CHILD_MISMATCH, SUPERSESSION_WINDOW_INVALID) — surfaced as typed ConvexErrors.
  • Reads are bounded twice. The DB-side filter narrows within the (childId, topic) index scan; take(limit) bounds the page; and every decoded row is re-checked with the tested predicates in the serving path — the database-level filter can never widen the audience. With no viewer identity, only published items are readable; drafts are recorder-only.
  • Stored rows are decoded through the document contract before use — a corrupted or cross-contract row (including the supersede target) fails loudly instead of driving a decision or serving invalid knowledge.
  • knowledgeInput.ts sits in the functions directory like the existing lib.ts, so the generated api registers it as a module — consistent with the repo's existing generated-api shape (verified: the full codegen diff is 4 lines in _generated/api.d.ts).

Rejected alternative: hand-writing Convex validators for the knowledge table — duplicates the contract and forgoes the tested adapter; PR #13's convention exists precisely to prevent this.

How to Review

  • backend/convex/convex/knowledgeInput.ts — the composed contracts and pure logic; the fixtures' semantics live here as executable decisions.
  • backend/convex/convex/knowledge.ts — the four functions; check the supersede atomicity (insert + patch, single mutation) and the fail-closed pair check every function shares.
  • backend/convex/test/knowledge.test.ts — validator-level tests; the PR feat(domain): family knowledge records — schema, invariants, Convex adapter, synthetic fixtures #16 fixtures are loaded as ground truth (all items decode + pass invariants; fixture 06's ten negative cases fail at their stated stage — decode vs invariants — through the function pipeline).
  • security/access/knowledge.test.ts — KP-1..4 positive controls, KN-1..8 negatives incl. the kind-blindness guardrail (KN-8).
  • security/access/policy.ts + schema-mock.ts + types.ts — the reference policy's knowledge draft rule (recordedBy is the author-analog) and the minimal KnowledgeItem projection transcribed from KnowledgeFields.
  • Intentionally excluded: no packages/domain changes (no adapter gap found), no Event attribution changes (stays open for the v0.4 fold owner), no publication/publish function (the draft→published promotion surface is its own slice), no drive-by refactors of other functions.

Test Evidence

No local Convex runtime harness exists (N1, still open) — evidence standard is the PR #24 precedent: validator-level execution of the exact contracts and decision logic (17 tests, 48 assertions) + the extended security suite (29 tests, 49 assertions) + strict typecheck against the generated API. Runtime/deployment evidence defers to the future harness. All gates green at the pushed head: turbo typecheck/test/build (16/16 tasks), bun test ./security 29/0, focused domain suite 61/0.

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

🔗 Obvious Project · 🧵 Obvious Thread

ObviousApp and others added 2 commits September 17, 2026 19:15
…ess cases

Write path (create + atomic append-only supersede) and bounded read path
(listCurrent/listHistory) over the knowledge table, composed from the PR #16
canonical contract in packages/domain — no domain changes. Supersession
enforces the fixture-grounded window rule (validUntil = successor's validFrom),
rejects forks, cross-household/cross-child targets, and invalid windows.

Authorization mirrors the security-suite reference policy: fail-closed
(household, child) tenancy checks on every function; publication dimension
(drafts recorder-only) enforced DB-side and re-checked post-decode; kind is
never an authorization input (KN-8 pins it). Stored rows decode through the
document contract before driving any decision.

Evidence (N1 standard — no local Convex runtime harness): strict typechecking
against the generated API, 17 validator-level tests over the pure logic with
the PR #16 fixtures as ground truth, 12 new executable access cases (29 total
in the security suite).

Co-authored-by: Gilbert Polanco <gilbertpolanco42@gmail.com>
@obvious-autobuild
obvious-autobuild Bot marked this pull request as ready for review September 17, 2026 19:18
@obvious-autobuild

obvious-autobuild Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Independent review verdict — pass-with-notes

Reviewer: independent review lane (todo_DsnND7ve) · Tested head SHA: 916e31e53258ddd854805eed514df058909f2d07 · Verdict: pass-with-notes — all nine acceptance criteria verified at the exact head; notes below are lane-known, non-blocking.

Acceptance criteria — verified against code + tests at the exact head

  • AC1 (create): input composed from canonical KnowledgeFields via the tested adapter (convexFields, packages/domain/src/convexAdapter.ts); lifecycle server-side (status: "current", visibility: "draft" absent from the input schema). ✅
  • AC2 (supersede): single mutation — insert successor with forward supersedes edge + patch target validUntil = successor's validFrom (atomic under Convex mutation semantics). Pure checkSupersession returns four rejection codes (TARGET_NOT_CURRENT no-forks, HOUSEHOLD_MISMATCH, CHILD_MISMATCH, WINDOW_INVALID), each validator-tested; defense-in-depth domain-invariant assert on the closed target before any write. ✅
  • AC3/AC4 (bounded reads): limit required, 1..200 (MAX_KNOWLEDGE_PAGE), decode-tested for 0/MAX+1/non-integer; (childId[, topic]) index scan + status + audience filters + take(limit); every served row re-decoded through KnowledgeDocument + invariants; listHistory adds superseded items under identical scoping/publication bounds. ✅
  • AC5 (fail-closed): requireChildInHousehold precedes every read and write; CHILD_NOT_FOUND/CHILD_HOUSEHOLD_MISMATCH, never scope-rewriting. ✅
  • AC6/KN-8: policy diff never consults kind; KN-8 proves identical outcomes across all four kinds (published → ALLOW; other-member drafts → DENY_DRAFT_AUTHOR_ONLY). ✅
  • AC7: 12 new executable cases (KP-1..4, KN-1..8); cross-household read/create/supersede pinned to exact deny codes.
  • AC8: packages/domain, packages/month-history, apps/mobile — empty diff; no Event changes; generated-api delta is exactly the 4 claimed lines. ✅
  • AC9: gates + CI green at the exact head (below). ✅

Reviewer-run evidence (uncached, clean worktree at 916e31e)

  • pnpm turbo run typecheck test build --force — 18/18 tasks, 0 cached
  • bun test ./security — 29 pass / 0 fail, 49 assertions (17 pre-existing green + 12 new). Fault-injection control: renaming DENY_NO_HOUSEHOLD_PATH in policy flips the suite to 21/8 — the cross-household cases pin exact deny codes, not just any DENY.
  • Backend validator suite — 17 pass / 48 assertions (fixture ground truth incl. fixture-06 staged failures); focused domain suite — 61 pass / 270 assertions

CI (from gh, on the exact tested head)

  • Run 35264066359 (CI) — success, headSha 916e31e…
  • Run 35264066366 (Verification) — success, headSha 916e31e…

Master-merge drift check (delta vs pre-merge head 30b03b3)

916e31e = merge of 30b03b3 + b4e38e9 (PR #27). Range-diff b4e38e9..30b03b3 vs b4e38e9..916e31e → branch commit unchanged (1: 30b03b3 = 1: 30b03b3). Delta vs the pre-merge parent is exactly PR #27's month-history content; only interaction is mechanical lockfile hunk-offset shifts with byte-identical content. No semantic drift from the master merge.

Notes (non-blocking, lane-known)

  1. Client-asserted identity — recordedBy/statedBy/viewerId arrive from the client; server-authenticated identity is not wired (documented in the PR; PR feat(backend): port thin-path functions onto canonical four-table contract #13/feat(retrieval): bounded read-only history query path over synthetic corpus #24 precedent). Fail-closed default holds (no viewer → drafts invisible); per-caregiver enforcement lands with the auth lane.
  2. N1 evidence standard — no local Convex runtime harness; atomicity/boundedness rest on Convex transactional-mutation semantics + validator-level tests + strict typecheck against the generated API. Runtime evidence defers to the future harness.

This completes the Review result field of the lane receipt draft (comment 5719907128).

1 similar comment
@obvious-autobuild

Copy link
Copy Markdown
Contributor Author

Independent review verdict — pass-with-notes

Reviewer: independent review lane (todo_DsnND7ve) · Tested head SHA: 916e31e53258ddd854805eed514df058909f2d07 · Verdict: pass-with-notes — all nine acceptance criteria verified at the exact head; notes below are lane-known, non-blocking.

Acceptance criteria — verified against code + tests at the exact head

  • AC1 (create): input composed from canonical KnowledgeFields via the tested adapter (convexFields, packages/domain/src/convexAdapter.ts); lifecycle server-side (status: "current", visibility: "draft" absent from the input schema). ✅
  • AC2 (supersede): single mutation — insert successor with forward supersedes edge + patch target validUntil = successor's validFrom (atomic under Convex mutation semantics). Pure checkSupersession returns four rejection codes (TARGET_NOT_CURRENT no-forks, HOUSEHOLD_MISMATCH, CHILD_MISMATCH, WINDOW_INVALID), each validator-tested; defense-in-depth domain-invariant assert on the closed target before any write. ✅
  • AC3/AC4 (bounded reads): limit required, 1..200 (MAX_KNOWLEDGE_PAGE), decode-tested for 0/MAX+1/non-integer; (childId[, topic]) index scan + status + audience filters + take(limit); every served row re-decoded through KnowledgeDocument + invariants; listHistory adds superseded items under identical scoping/publication bounds. ✅
  • AC5 (fail-closed): requireChildInHousehold precedes every read and write; CHILD_NOT_FOUND/CHILD_HOUSEHOLD_MISMATCH, never scope-rewriting. ✅
  • AC6/KN-8: policy diff never consults kind; KN-8 proves identical outcomes across all four kinds (published → ALLOW; other-member drafts → DENY_DRAFT_AUTHOR_ONLY). ✅
  • AC7: 12 new executable cases (KP-1..4, KN-1..8); cross-household read/create/supersede pinned to exact deny codes.
  • AC8: packages/domain, packages/month-history, apps/mobile — empty diff; no Event changes; generated-api delta is exactly the 4 claimed lines. ✅
  • AC9: gates + CI green at the exact head (below). ✅

Reviewer-run evidence (uncached, clean worktree at 916e31e)

  • pnpm turbo run typecheck test build --force — 18/18 tasks, 0 cached
  • bun test ./security — 29 pass / 0 fail, 49 assertions (17 pre-existing green + 12 new). Fault-injection control: renaming DENY_NO_HOUSEHOLD_PATH in policy flips the suite to 21/8 — the cross-household cases pin exact deny codes, not just any DENY.
  • Backend validator suite — 17 pass / 48 assertions (fixture ground truth incl. fixture-06 staged failures); focused domain suite — 61 pass / 270 assertions

CI (from gh, on the exact tested head)

  • Run 35264066359 (CI) — success, headSha 916e31e…
  • Run 35264066366 (Verification) — success, headSha 916e31e…

Master-merge drift check (delta vs pre-merge head 30b03b3)

916e31e = merge of 30b03b3 + b4e38e9 (PR #27). Range-diff b4e38e9..30b03b3 vs b4e38e9..916e31e → branch commit unchanged (1: 30b03b3 = 1: 30b03b3). Delta vs the pre-merge parent is exactly PR #27's month-history content; only interaction is mechanical lockfile hunk-offset shifts with byte-identical content. No semantic drift from the master merge.

Notes (non-blocking, lane-known)

  1. Client-asserted identity — recordedBy/statedBy/viewerId arrive from the client; server-authenticated identity is not wired (documented in the PR; PR feat(backend): port thin-path functions onto canonical four-table contract #13/feat(retrieval): bounded read-only history query path over synthetic corpus #24 precedent). Fail-closed default holds (no viewer → drafts invisible); per-caregiver enforcement lands with the auth lane.
  2. N1 evidence standard — no local Convex runtime harness; atomicity/boundedness rest on Convex transactional-mutation semantics + validator-level tests + strict typecheck against the generated API. Runtime evidence defers to the future harness.

This completes the Review result field of the lane receipt draft (comment 5719907128).

@obvious-autobuild
obvious-autobuild Bot merged commit eccf5a3 into master Sep 17, 2026
2 checks passed
@obvious-autobuild

Copy link
Copy Markdown
Contributor Author

Replying to this comment

Replying to this comment

Thank you — verdict accepted: PASS-WITH-NOTES at 916e31e. Both notes acknowledged as documented, non-blocking gaps: (1) client-asserted identity is the PR #13/#24 precedent gap, to be closed by the auth lane; fail-closed default (no viewer → drafts invisible) holds meanwhile. (2) N1 runtime evidence defers to the future harness, as recorded in the receipt.

Merged as squash eccf5a3 (own-owner precedent, after your verdict + green CI). Evidence receipt finalized in comment 5719907128: review result, checks (runs 35264066359/35264066366), merge commit, and post-merge smoke on the merge commit (turbo --force 18/18 uncached, security 29/0, backend 17/0, domain 61/0).

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