@factory/shared
The shared domain layer for AI Software Factory: the canonical error model, zod-validated DTOs shared between the frontend and the API, the fractional position engine, the domain event catalog, and platform-wide constants.
Every other package and app imports its contracts from here, so the FE and BE
can never drift (ARCHITECTURE.md §3: "DTOs + Zod validation shared with the
frontend via packages/shared").
Source-first:
main/typespoint atsrc/index.ts— apps import this package without a build step. A proper build (tsc →dist/) is part of the Phase 0 completion checklist.
Public API
| Module | Exports | Purpose |
|---|---|---|
@factory/shared (root) |
everything below | single import surface |
@factory/shared/errors |
ApiError, ErrorCode, ErrorCodes, ERROR_STATUS, toApiError, fromException |
canonical typed error model (12 stable codes) |
@factory/shared/env |
appEnvSchema, loadEnv, withDotEnv, parseDotEnv |
zod env validation mirroring .env.example (no dotenv dependency) |
@factory/shared/positions |
betweenPosition, afterPosition, beforePosition, generatePosition, rebalancePositions, needsRebalance, comparePositions, minPosition, maxPosition, positionForSlotNeighbors, sortPositions, isValidPosition, POSITION_ALPHABET |
fractional (LexoRank-style) ordering — see below |
@factory/shared/events |
DomainEventName, WEBHOOK_EVENTS, DomainEvent, createDomainEvent, generateId |
event catalog (18 events) + envelope + ids |
@factory/shared/constants |
LIMITS, TOKEN_LIFETIMES, RATE_LIMITS, BLOCK_COLORS, STATUS_COLORS, DENSITIES, ID_PREFIXES |
limits, token lifetimes, rate limits, 18-color palette |
@factory/shared/dto |
registerInputSchema, loginInputSchema, …, blockTypeSchema, blockPayloadSchema, batchBlockOperationsSchema, propertyConfigSchema, filterNodeSchema, recordQuerySchema, createViewSchema, createCommentSchema, createUploadSchema, searchRequestSchema, createWebhookSchema, createNotificationSchema, createAutomationSchema, chatRequestSchema, createShareSchema, … |
all FE/BE zod schemas (auth, workspace, page, block, database, comment, file, search, webhook, notification, automation, ai, share) |
Fractional positions
position is a string over 0123456789abcdefghijklmnopqrstuvwxyz compared
lexicographically (prefix rule: "a" < "a0"). Never array indexes — inserting
between two siblings rewrites exactly one row (ARCHITECTURE.md invariant 4).
import { betweenPosition, afterPosition, positionForSlotNeighbors, needsRebalance, rebalancePositions } from "@factory/shared/positions";
betweenPosition("a0", "b0"); // → "a8" (midpoint, shortest)
afterPosition("a0"); // → "a0i" (append to list end)
positionForSlotNeighbors("a0", "b0"); // → "a8" (DnD drop between neighbors)
positionForSlotNeighbors(null, null); // → "a0" (fresh list, matches prisma @default)
needsRebalance(["a0", "a".repeat(65)]); // true →
rebalancePositions(positions); // → { "a0": "a0", "…": "a1", … } uniform re-spacing
Rules:
betweenPosition(a, b)requiresa < b(throws otherwise) and throws on the empty gap (b === a + "0") or when the result would exceed 64 chars — both mean "rebalance the sibling list now".- Repeated inserts between the same neighbors grow positions by ≤ 1 char each; the API re-spaces preemptively (and hard-fails at 64 chars).
- Full algorithm + invariants: see the JSDoc in
src/positions.tsand the property tests insrc/positions.test.ts(fast-check, 500+ runs).
Errors
import { ApiError, fromException } from "@factory/shared/errors";
throw new ApiError("NOT_FOUND", { message: "Page not found" }); // → 404
const err = fromException(errFromController, requestId); // normalize anything
res.status(err.status).json({ error: err.toJSON() });
Codes: VALIDATION_ERROR, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, RATE_LIMITED, NOT_IMPLEMENTED, SEARCH_UNAVAILABLE, STORAGE_ERROR, AI_ERROR, TIMEOUT, INTERNAL with default HTTP statuses in ERROR_STATUS.
Usage example
// apps/api/modules/pages/pages.service.ts
import { createPageSchema, movePageSchema, positionForSlotNeighbors, ApiError, DomainEventName, createDomainEvent } from "@factory/shared";
const dto = createPageSchema.parse(req.body); // shared validation
const position = positionForSlotNeighbors(beforePos, afterPos);
await this.bus.publish(createDomainEvent(DomainEventName.PAGE_CREATED, { pageId: page.id }, { workspaceId, actorId }));
Tests
Vitest (node environment) + fast-check for the position engine:
src/positions.test.ts— ordering property, bounded growth, 100× halving, empty-gap/max-length refusals, rebalance order preservation + fixed-width encoding (thea10vsaalex-order trap), uniqueness.src/dto/auth.dto.test.ts— password policy (min 10, letter+number, ≤72).src/dto/database.dto.test.ts— per-type property configs, recursive filter trees (nested AND/OR/NOT), query page size, all 9 view types.src/events.test.ts— envelope shape, id generator, catalog completeness.
ROADMAP phases that build on this
- Phase 1–3 (Auth, Workspace, Pages):
auth.dto,workspace.dto,page.dto, error model, env schema, positions (page tree). - Phase 4 (Block Editor):
block.dto(28→30 block types, per-type content schemas, 500-op batches),positions(block ordering), color palette. - Phase 5–6 (Databases, Relations):
database.dto(24 property types, filter tree, views, record query) consumed by@factory/database-engine. - Phase 8–13 (Search, Files, AI, API):
search.dto,file.dto,ai.dto,webhook.dto, event catalog (webhooks + automations).
Recorded spec conflicts
- Block types: the task brief said "28 values (per PRODUCT-SPEC §9)" but
PRODUCT-SPEC §4 and the prisma
BlockTypeenum both define 30 values — implemented all 30 (superset of both). - Property types: the spec header says "22" but the spec list and prisma
PropertyTypeenum define 24 — implemented all 24 (superset).