exp/core-dossiers
needs attentionviewing older commit7640070 · incrementalPR #270reviewed 2026-07-18 02:11 UTC10H · 18M · 14L · 5I- 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
- 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+620−0canonical-reference/apps/platform+410−0canonical-reference/packages/api/src/schemas+107−0canonical-reference/registry+153−0canonical-reference/enforcement+93−0canonical-reference/_migration+99−0canonical-reference (docs)+136−0.claude/skills/dossier+1170−0.claude/rules/capability-dossier.md+79−0apps/platform/eslint.config.mjs+45−0
- 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.
Findings · 47
correctness10
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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'.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
_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.
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
- 8ff8773needs attentionincremental1H · 3M · 2L2026-07-27 17:46
- 7640070needs attentionincremental10H · 18M · 14L2026-07-18 02:11current
- a516a9dneeds attentionincremental4H · 5M · 4L2026-07-17 04:38
- 910fc6aneeds attentionincremental1H · 9M · 6L2026-07-10 23:48
- 0b75850safeincremental0H · 0M · 0L2026-07-10 23:17
- 380a931safeincremental0H · 0M · 0L2026-07-09 19:17
- 5a9ac90blockedfull5H · 8M · 5L2026-07-07 22:07