FEAT-021: Segments — Zealot Groups ported onto CustomerRef - #100
Merged
Merged
Conversation
agreenspan
force-pushed
the
FEAT-021-segments
branch
from
September 10, 2026 14:06
f919dd6 to
897bdac
Compare
agreenspan
changed the base branch from
main
to
INFRA-030-reference-registry
September 10, 2026 14:06
agreenspan
force-pushed
the
FEAT-021-segments
branch
from
September 10, 2026 14:37
897bdac to
09669e1
Compare
…-driven reconcile, hooks, jobs, routes, web page Port of Zealot's Groups onto the template's CustomerRef. Segment is false-polymorphically owned (User|Organization|Space = the provider), static or dynamic; SegmentMember carries source rule|manual. segmentLensFor(owner) is the single source for validation, evaluation, hydration and reference extraction. Inbound triggers are a post-commit DB hook on every model the lens reaches; recalculation lives in reconcileSegment (toPrisma, set-based) and reconcileCustomerRefSegments (fetchLens + check), with sweepSegments as the cron backstop and segment.membershipChanged as the outbound app event. Routes under /segment, /segmentMember, /me, /organization/:id, /space/:id; Segments page in all three web contexts. Adversarial-review fixes folded in: sender bind on received communications, in-memory segment-of-segment row, retryable evaluation-error pause, customer-facing membership pick, unowned references refused at save, freeze on flip to static. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjP2vYXsVd8AELiHnRvQzK
- ProviderModel grows to User | Organization | Space and becomes Segment.ownerModel; CustomerRef gains providerUserId / providerOrganizationId with relations, partial uniques, indexes, registry + factory + include coverage - static = computed at create / conditions change, dynamic = continuous; a hand-picked audience is a static `id in [...]` rule, so SegmentMember.source, the freeze hook, and the manual add / delete member routes go - /v1/user mounted with segments + segmentMemberships read-many (self or admin), completing the me / user / organization / space pair - tests, docs, ticket, kanban follow Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VjP2vYXsVd8AELiHnRvQzK
Segment registers on both RuleReference axes; the edge writer moves to packages/db as syncRuleReferenceEdges so email and segments share it. The pause triple and its enum are gone: a segment's edges are written by the segmentRuleReferences after-write hook, both reconcile rails go through withRule (degraded evaluates nothing new, never a match), and the owner's reads carry ruleIssues. A membership loop is refused at save. Ruling 2026-09-10 on INFRA-030. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uyo2oHpjDz4zjRyqRWH6bP
…on narrowed updates, held dead references stay editable, owner id required, batched edges on the entity rail Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uyo2oHpjDz4zjRyqRWH6bP
agreenspan
force-pushed
the
FEAT-021-segments
branch
from
September 10, 2026 16:36
8944a97 to
a673a15
Compare
agreenspan
changed the base branch from
INFRA-030-reference-registry
to
main
September 10, 2026 16:36
… additions Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uyo2oHpjDz4zjRyqRWH6bP
agreenspan
added a commit
that referenced
this pull request
Sep 13, 2026
)" This reverts commit 0b28aed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FEAT-021: Segments
Zealot's
Groups/FanUsersGroupported onto the template'sCustomerRef. Ticket:tickets/FEAT-021-segments.md(decisions, what shipped, open items).Shape (after the 2026-09-10 rulings)
Segment— owner false-polymorphic overProviderModel(the provider whose customers are segmented, so the owner enum isCustomerRef.providerModel);conditions(required) json-rules tree over a lens rooted atCustomerRef;typestatic | dynamic says when the rule runs — static is computed once when created or its conditions change, dynamic continuously; reconcile-pause triple (cycle | danglingReference | dynamicReference | evaluationError); soft-delete; audit-enabled.SegmentMember—segmentId×customerRefId, written only by reconcile. A hand-picked audience is a static segment whose rule isid in [...]; there is nosourcecolumn and no member add/remove endpoint.ProviderModelgrew to User | Organization | Space —CustomerRef.providerUserId/providerOrganizationId, relations, every customer × provider partial unique, registry provider axis, factory deps,includeProvider/ provider schema carry all three.segmentLensFor(ownerModel): tenancy bind on the provider FK, customer branches (User / Organization / Space → contacts, tag attachments, received communications),segmentMembers → segment.idwith aSegment.idsource so "members of segment X" is a picker. A boot probe asserts the membership source stays reachable for every provider branch.segmentConditions(required on every write, validate against the lens, normalize, refuse self-reference / path-read references / references to segments the owner does not have),segmentMemberOwner(member's customer ref must belong to the owner as provider),segmentReconcile(post-commit enqueue: a segment's own row →reconcileSegmentfor either type; writes on every other model the lens reaches → per-customer-ref job, dynamic segments only; the model→customer-ref reverse map refuses to boot if the lens reaches a model it cannot map).reconcileSegment(set-based viatoPrisma, superseding per segment, fans out to dynamic dependents),reconcileCustomerRefSegments(hydrate one ref viafetchLens,check()per dynamic segment in dependency order),sweepSegments(cron 04:00 UTC: pause verdicts + dependency-ordered enqueue of dynamic segments).segment.membershipChanged→ websocket refetch of the member list + per-usermeReadManySegmentMembershipsrefetch./segment/:idread/update/delete,/segment/:id/segmentMemberslist;segments+segmentMembershipsread-many on/me,/user/:id,/organization/:id,/space/:id(/v1/useris mounted for the first time;db:userread = self or admin);segmentscreate on/me,/organization/:id,/space/:id. The customer-facing membership shape carries id / name / owner / type / createdAt only — the rule is opaque to members.One call to review
Inbound triggers are DB hooks, outbound is the app event. Zealot triggers off app events; the template's row-level event log is AuditLog, and every write goes through the hooked client, so the webhook hook's shape was used. Flipping to app events is a small change (hook body → emits in controllers; handler → job stays).
Validation
apps/api/src/modules/segment/tests/— 35 tests: conditions hook (required on both types; user / organization owners validate against their own lens), reference graph, pause verdicts, reconcile (set-based, provider scoping, static computed at save and inert afterwards, hand-picked ids scoped to the owner's customers, user and organization as providers, user-write trigger, contact-write trigger through the owner, cross-provider communications excluded, segment-of-segment in one pass and in dependency order, evaluation-error retry, flip to static keeps members / flip back catches up), routes end to end (customer-facing shape, user-context reads self-only, hand-picked static segment through the API).communicationsReceived, in-memory segment-of-segment row, permanent evaluation-error pause, provider rule leaking through/me/segmentMemberships, unowned references accepted at save) are fixed and pinned by tests.bun run typecheck: clean for every workspace except@template/sdk, whose trackedsrc/client/index.tsimports untracked../core/*.genfiles that exist only in the main checkout as stale artifacts (pre-existing; not touched here).bun run typecheck:fe: clean.bun test(api): 1065 pass, 2 pre-existing failures inlensWhere.test.ts(atLeastrules written without acondition, untouched here). db 293, permissions 81, ui 244 pass.biome checkon every file this branch touches: clean. Repo-wide lint has 134 pre-existing errors in files this branch does not touch.no-raw-global-assign-in-testsfails on two pre-existing files not touched here.🤖 Generated with Claude Code
https://claude.ai/code/session_01VjP2vYXsVd8AELiHnRvQzK