← all branches

exp/core-dossiers

needs attentionviewing older commit
7640070 · incrementalPR #270reviewed 2026-07-18 02:11 UTC10H · 18M · 14L · 5I
The branch
Purpose
Establish a canonical NON-RUNNING reference for the core domain (org/profile/membership) that implementers can follow when porting the real domains/core/src/ code to the prescriptive FCIS target architecture.
Goal
Provide a machine-readable + human-readable ground truth for: (1) every entity's full FCIS layer stack (type→decisions→errors→queries→shells→writes→mapper), (2) the capability registry mapping every entity×op to all write paths including coordinator flows, (3) scope-tagged contracts distinguishing entity routes from cross-entity flows, (4) enforcement placeholders for the 3-layer compliance wall, and (5) migration forwarding guides per entity showing where routes moved and why.
Sub-goals
  • SG-dossier: .claude/skills/dossier/ + .claude/rules/capability-dossier.md — skill for generating entity dossiers
  • SG-canonical: canonical-reference/ — NON-RUNNING reference implementations for org/profile/membership
  • SG-eslint: apps/platform/eslint.config.mjs — ship the FCIS boundary ESLint rule (Layer 1)
  • SG-registry: canonical-reference/registry/capability-registry.ts — capability registry closing the coordinator write-map gap
  • SG-migration: canonical-reference/_migration/ — per-entity forwarding guides for route changes
The changes (whole branch)
What
This incremental review covers the canonical-reference/ directory added in the latest commit: 49 new files totalling ~2,700 lines. Includes FCIS entity skeletons for membership (full), organization (shaped), profile (shaped); 3 flow coordinator shells; scope-tagged contracts; Zod schemas; core event SSOT; enforcement scripts (Layer 1 real, Layers 2-3 stubs); capability registry; and _migration forwarding guides. The previous review (a516a9da) covered the dossier skill and ESLint config.
Why
The codebase has several architectural gaps the canonical-reference exposes: core.profile.created never fires (V1/BAT-269), core.member.added fires from 3 sites with 3 different payload shapes, authz guards live in handlers not in decision cores (unreachable by coordinator callers), and org/admin contracts are split making scope non-derivable. This reference demonstrates the target state before touching production code.
Areas
canonical-reference/domains/core+6200canonical-reference/apps/platform+4100canonical-reference/packages/api/src/schemas+1070canonical-reference/registry+1530canonical-reference/enforcement+930canonical-reference/_migration+990canonical-reference (docs)+1360.claude/skills/dossier+11700.claude/rules/capability-dossier.md+790apps/platform/eslint.config.mjs+450
Blast
NON-RUNNING reference + tooling only — no production code changes. The ESLint rule (apps/platform/eslint.config.mjs) is live but set to warn-only. Zero risk of runtime regression. High risk if reference is followed verbatim without addressing the 4 high-severity correctness issues.
NON-RUNNING reference — all canonical-reference/ files are design templates, not production code V1/BAT-269 acknowledged — signup flow shows the fix structurally but doesn't apply it to real code yet Enforcement Layers 2 & 3 are placeholder stubs — no CI wiring yet
CI checks· No CI checks found for this PRCodeRabbit· No .coderabbit.yaml in repo

Findings · 47

correctness10

high

signupShell hardcodes null for existingByUsername — username collision guard never executes

canonical-reference/domains/core/src/flows/signup.flow.ts:29

The shell comment says 'fetched-uniqueness passed to decide', but line 29 passes `{ existingByUsername: null, existingByAuthId: null }` as a hardcoded literal. An implementer following this reference would build a shell that bypasses the UsernameTaken guard entirely. The 6-hex-char authId suffix (16M space) is insufficient to make collisions negligible at scale.

high

decideUpdateRole missing guard for caller.role === 'member' — R4 authz invariant violated

canonical-reference/domains/core/src/membership/membership.decisions.ts:36

Guards at lines 38–39 only restrict admin callers. A member-role caller passes all guards and reaches the write decision. The reference's stated R4 invariant is that the decision core is the authoritative enforcement point for all callers including coordinators — but member callers are not blocked here.

high

applyRemove missing discriminator param — cannot select between core.member.removed and core.member.left events

canonical-reference/domains/core/src/membership/membership.writes.ts:41

core.member.removed requires `removedBy + removedAt`; core.member.left requires only `leftAt`. Without a discriminator parameter the function cannot emit the correct event, violating the single-source-for-event-payloads invariant.

high

accept-invitation: ALREADY_MEMBER 409 in contract but no guard in flow or error types

canonical-reference/apps/platform/src/api/contracts/flows/accept-invitation.contract.ts:18

The contract declares `409: JSendFail(['ALREADY_MEMBER'])` but acceptInvitationShell has no active-membership check, and MembershipErrors has no `alreadyMember` constructor. The 409 is a dead response with nothing in the domain layer to produce it.

medium

capability-registry leave route: op:'delete' for a POST method — semantic mismatch

canonical-reference/registry/capability-registry.ts:63

Leave is a soft-delete/transition via POST, not a hard DELETE. Using op:'delete' for a POST endpoint misleads deriveWriteMap() consumers correlating op with HTTP method. 'transition' is more accurate.

medium

UpdateRoleError / RemoveMemberError include MembershipDatabaseError — impossible from pure decisions

canonical-reference/domains/core/src/membership/membership.errors.ts:21

Pure decision functions cannot produce a DB error. Including MembershipDatabaseError in their error unions forces dead branches in handler error mappers that exhaustively match errors.

medium

OrganizationSettings index signature missing readonly — silent mutability hole in strict mode

canonical-reference/domains/core/src/organization/organization.type.ts:16

Named fields are `readonly` but `[key: string]: unknown` lacks `readonly`. In strict mode, `settings['timezone'] = 'foo'` compiles while `settings.timezone = 'foo'` does not.

low

signup contract scope 'public' conflicts with capability-registry 'machine'

canonical-reference/apps/platform/src/api/contracts/flows/signup.contract.ts:16

Contradictory scope values across two reference files will cause mount-layer disagreement.

low

MembershipResponseSchema.removedBy typed uuid; core-events.ts uses z.string() — minor inconsistency

canonical-reference/packages/api/src/schemas/membership.schemas.ts:34

The response schema uses z.string().uuid().nullable(), the event payload uses z.string(). Same field, different constraints across the two representations.

low

decideCreateMembership Result<..., never> misleads on ALREADY_MEMBER idempotency

canonical-reference/domains/core/src/membership/membership.decisions.ts:67

The never error type signals create is infallible, contradicting the accept-invitation contract's 409 ALREADY_MEMBER. No guidance on where the duplicate check belongs.

security9

high

Signup scope inconsistency: contract says 'public', capability-registry says 'machine'

canonical-reference/apps/platform/src/api/contracts/flows/signup.contract.ts:16

'public' means no auth required (user-facing browser call); 'machine' means service-to-service with a token. An implementer generating a scope-enforcement table from the registry will misclassify signup. The Scope union in the registry doesn't even include 'public'.

high

acceptInvitationShell missing ALREADY_MEMBER guard before membership insert

canonical-reference/domains/core/src/flows/accept-invitation.flow.ts:28

No check that acceptedByProfileId is already an active member before calling applyCreateMembership. Accepting twice before step 3 marks the token accepted could insert a duplicate membership row, surfacing a 500 instead of 409.

medium

OrgSettingsSchema.passthrough() leaks unknown DB settings fields through API response

canonical-reference/packages/api/src/schemas/organization.schemas.ts:22

OrgSettingsSchema is reused verbatim in OrganizationResponseSchema. Any keys in the settings JSONB column (internal flags, billing metadata) pass through to member-scoped callers of GET /organizations/:publicId/settings.

medium

scope: 'authenticated' on POST /organizations — no plan gate or rate limit in reference

canonical-reference/apps/platform/src/api/contracts/flows/create-organization.contract.ts:20

Any authenticated user can create orgs with no billing gate. Reference should document that the handler must enforce a per-user limit or Stripe checkout gate before the shell runs.

medium

createMembershipShell exported without actor-relative guard — potential footgun

canonical-reference/domains/core/src/membership/membership.shells.ts:20

decideCreateMembership has no actor-relative guard. If an implementer wires this shell to any handler route, there is nothing in the decision to stop unauthorized callers from adding arbitrary members to any org.

medium

decideUpdateRole relies solely on contract scope for member-role enforcement

canonical-reference/domains/core/src/membership/membership.decisions.ts:38

R4 goal is unifying authz in the decision core for all callers. Without a member-role guard in the decision, any coordinator or machine caller bypassing the contract scope guard faces no fallback.

low

acceptInvitationShell: 410 responses are dead code if stub is copied without replacement

canonical-reference/domains/core/src/flows/accept-invitation.flow.ts:24

INVITATION_EXPIRED, INVITATION_REVOKED, INVITATION_ALREADY_ACCEPTED are only reachable if the commented-out decideAcceptInvitation runs.

low

FCIS boundary lint rule is warn-only — violations can ship silently

canonical-reference/enforcement/fcis-boundary.eslint.mjs:15

The rule is 'warn', meaning a handler calling a query mutation directly passes CI. Should graduate to 'error' once existing violations are remediated.

info

MembershipResponseSchema exposes removedBy UUID to member-scoped callers

canonical-reference/packages/api/src/schemas/membership.schemas.ts:32

UUID of the admin who removed a member returned to all member-scoped list/get callers. Consider restricting to admin scope or resolving to display name.

conventions10

medium

Local Result<T,E> stub instead of @batu/result import — may be adopted verbatim

canonical-reference/domains/core/src/membership/membership.decisions.ts:12

Real import is commented out. Implementers may keep the inline stub. Same pattern in organization.decisions.ts and profile.decisions.ts.

medium

organization.errors.ts and profile.errors.ts absent — inconsistent with canonical form

canonical-reference/domains/core/src/organization/

Membership has a complete errors.ts with smart constructors. Organization and Profile inline ad-hoc error objects in decision functions, contradicting the canonical form requirement for a dedicated {entity}.errors.ts.

medium

FCIS namespace not exported — {Entity}FCIS pattern not demonstrated in index.ts

canonical-reference/domains/core/src/membership/index.ts:11

CLAUDE.md requires {Entity}FCIS as the named namespace export. All three index.ts use flat re-exports. The ESLint rule in fcis-boundary.eslint.mjs guards against `ProfileFCIS.profileQueries.insert(...)` but the index.ts doesn't produce that call shape.

low

declare function stubs in profile/index.ts use `unknown` param types

canonical-reference/domains/core/src/profile/index.ts:9

Membership and organization shells use correct typed signatures. The asymmetry creates false impression that profile shells have looser types.

low

organization/index.ts missing errors.ts / queries.ts / mapper.ts from FCIS barrel

canonical-reference/domains/core/src/organization/index.ts:13

Acknowledged in a comment. Combined with absent organization.errors.ts, an implementer cannot derive the organization error pattern from the reference alone.

low

membership.shells.ts FETCH step hardcoded null — inverts the fetch→decide→write pattern

canonical-reference/domains/core/src/membership/membership.shells.ts:33

Queries are commented out and replaced with `{ target: null, ... }`, causing decideUpdateRole to immediately return notFound on every call. The placeholder value actively inverts the pattern's intent.

low

satisfies z.ZodType<...> appears only as comments, not as compile-time assertions

canonical-reference/packages/api/src/schemas/membership.schemas.ts:10

Implementers will treat these as documentation annotations, not live compile-time constraints.

low

PublicIdSchema is bare z.string() — weakens the 'malformed id → curated 4xx' claim

canonical-reference/packages/api/src/schemas/common.schemas.ts:6

With bare z.string(), the 4xx comes from DB not-found, not from validation. Should add regex for {3char}_{ULID} format or note the real constraint.

info

decideUpdateOrganization/decideDeleteOrganization are declare stubs — intentional but asymmetric

canonical-reference/domains/core/src/organization/organization.decisions.ts:21

Intentional skeleton per comment. Implementers must cross-reference membership.decisions.ts.

info

fcis-boundary.eslint.mjs message text differs slightly from the shipped rule — cosmetic

canonical-reference/enforcement/fcis-boundary.eslint.mjs:18

Reference message shorter than the shipped message in apps/platform/eslint.config.mjs. Selectors match exactly.

tests7

high

New authz guards in decideUpdateRole (R4) have no unit tests

canonical-reference/domains/core/src/membership/membership.decisions.ts:37

Three new guards added vs production: cannotModifyOwnRole, admin→owner escalation block, admin-demote-owner block. None have tests. The admin→owner escalation bypass is a privilege-escalation bug if missed when implementing.

high

No __tests__ directory in canonical-reference — enforcement scripts entirely untested

canonical-reference/

Zero test files. deriveWriteMap() is the only runnable logic — untested. The two placeholder enforcement scripts (event-completeness, flow-registry) export status strings and perform no checks. A broken enforcement script passes every CI run silently.

medium

event-completeness.check.mjs is a placeholder — performs no actual check

canonical-reference/enforcement/event-completeness.check.mjs:15

Exports only `status = 'PLACEHOLDER — Layer 2 not built'`. Cannot verify that every CORE_EVENT_TYPES value has a CoreEventPayloads schema.

medium

flow-registry.check.mjs is a placeholder — no actual coordinator-vs-registry validation

canonical-reference/enforcement/flow-registry.check.mjs:15

Pseudocode only. Does not verify that every coordinator writing an entity is registered in FLOWS.

medium

deriveWriteMap() has no unit test

canonical-reference/registry/capability-registry.ts:97

Only runnable logic in canonical-reference. Expected output is documented in comments at line 111 — those three assertions would be a trivial but valuable unit test.

low

Production decideUpdateRole lacks test for same-role no-op path

domains/core/src/membership/__tests__/membership.decisions.test.ts

Canonical reference returns ok({ noChange: true }) for same-role. Existing tests don't cover this idempotency behavior.

info

Existing production decision tests are well-structured — no gaps in current shipped code

domains/core/src/membership/__tests__/membership.decisions.test.ts

decideUpdateRole/decideRemoveMember/decideLeaveMembership/decideCreateMembership all have comprehensive coverage in the production decision layer as shipped today.

improvement11

high

Enforcement Layers 2 & 3 are placeholders with no CI wiring — registry guarantee unenforceable

canonical-reference/enforcement/README.md

No CI workflow references flow-registry.check.mjs or event-completeness.check.mjs. A new coordinator can write an entity without registering in FLOWS with no detection. Reference should either point to the Linear issue tracking this, or include a stub that exits non-zero so a CI step can be trivially added.

high

Missing invitation entity reference — accept-invitation flow's dependency has no canonical template

canonical-reference/domains/core/src/flows/accept-invitation.flow.ts

accept-invitation.flow.ts comments out invitation.decisions and invitation.writes with no canonical-reference/domains/core/src/invitation/ counterpart. Implementers porting the invitation entity have no template. A _migration/invitation.md forwarding guide exists but the canonical form does not.

medium

Scope union in registry missing 'public' — signup and future public routes cannot be typed correctly

canonical-reference/registry/capability-registry.ts

The Scope union (line 17) doesn't include 'public'. Signup is the one clearly public route. Either add 'public' to Scope and use it consistently, or explain why 'machine' is the intended classification.

medium

getSession returns memberships: z.array(z.unknown()) — too loose for a reference

canonical-reference/apps/platform/src/api/contracts/profile.contract.ts

getSession is the most frequently-read endpoint (chrome/RBAC for every authenticated UI). z.unknown() means implementers lose type safety here. A minimal SessionMembershipSchema should be defined.

medium

ROUTE-FORMS.md covers only mutation flows — no canonical form for GET/list routes

canonical-reference/ROUTE-FORMS.md

All three forms describe write paths. Read paths have no canonical form — no guidance on when a handler may call a query directly, or how pagination is typed. Implementers porting list routes have no reference.

medium

declare function stubs visually indistinguishable from real implementations

canonical-reference/domains/core/src/organization/organization.shells.ts

TypeScript ambient declarations look like real signatures unless the reader knows the syntax. A brief inline comment on the first occurrence ('← stub: body not shown; follows membership shell pattern') would reduce confusion.

medium

MembershipResponseSchema omits phone from joined profile fields

canonical-reference/packages/api/src/schemas/membership.schemas.ts

ProfileResponseSchema includes phone. MembershipResponseSchema joins email/username/fullName/avatarUrl but omits phone. The reference should be explicit about whether this is intentional.

low

Database = DbOrTx alias in _kernel.ts adds no information

canonical-reference/domains/core/src/membership/_kernel.ts

Shells use Database while *.writes.ts helpers use DbOrTx — same type. The asymmetry suggests false distinction. Either remove the alias or document the intended difference.

low

PublicIdSchema bare z.string() contradicts the curated 4xx claim in organization.contract.ts

canonical-reference/packages/api/src/schemas/common.schemas.ts

Duplicate of correctness finding — also an improvement opportunity to add the {3char}_{ULID} regex.

low

_migration/README.md doesn't enumerate coverage scope

canonical-reference/_migration/README.md

Five guides cover auth/invitation/membership/organization/profile. No note that other platform contracts (sites, billing) are out of scope. An implementer may misread this as complete coverage.

info

FLOWS-CLASSIFICATION.md not referenced from README.md — discovery gap

canonical-reference/FLOWS-CLASSIFICATION.md

Valuable content explaining the two-bucket folder rule is not linked from README's on-ramp section.

History · 7 commits

  1. 8ff8773needs attentionincremental1H · 3M · 2L2026-07-27 17:46
  2. 7640070needs attentionincremental10H · 18M · 14L2026-07-18 02:11current
  3. a516a9dneeds attentionincremental4H · 5M · 4L2026-07-17 04:38
  4. 910fc6aneeds attentionincremental1H · 9M · 6L2026-07-10 23:48
  5. 0b75850safeincremental0H · 0M · 0L2026-07-10 23:17
  6. 380a931safeincremental0H · 0M · 0L2026-07-09 19:17
  7. 5a9ac90blockedfull5H · 8M · 5L2026-07-07 22:07