Shared packages
Reusable infrastructure lives under packages/. Apps depend on these; apps never depend on each other.
| Package | Purpose |
|---|---|
@constellation-platform/auth-core | JWT types, clearance levels, permission checking, and the AuthProvider abstraction (token validation, user lookup, and updateTrustedClaims, the typed write for the four provider-held trusted claims, used only by Directory's audited writers) |
@constellation-platform/auth-next | Next.js auth middleware (withAuth, withTenantAuth, locale primitives) and the shared client AuthProvider / useAuth(), whose password signIn resolves to a SignInOutcome ('complete' or 'mfa-required'), and the provider sign-in callback every sign-in zone binds with one call (./provider-sign-in, ./provider-sign-in-route, the test-only ./provider-sign-in-testing, and signInNotice in ./client) |
@constellation-platform/db | Tenant-scoped Prisma client, RLS helpers, auditAction() for routine audit, correlation-id propagation |
@constellation-platform/design-tokens | The Constellation design language (warm-neutral OKLCH foundation, single amber accent, functional status colours, Geist type) + a Path A bridge aliasing every shadcn variable onto a Constellation token, Tailwind v4 @theme mapping, light/dark palettes, and a11y baseline (prefers-reduced-motion, :focus-visible). The bridge covers elevation as well as colour: Tailwind's --shadow-* scale maps onto the warm --e-* ramp, so stock shadow-sm / shadow-md / shadow-lg render warm rather than neutral-black — see Elevation below. It also carries the shared typography baseline (PLT-455): font-semibold resolves to the kit's 550 heading weight and the base body rule applies the kit's Geist feature settings ('ss01', 'cv11'), in all four consuming product apps. CSS-only — import styles.css from each app's globals.css. |
@constellation-platform/errors | Standard error hierarchy and API envelope (AppError, toErrorResponse) |
@constellation-platform/audit | Transactional audit via outbox (auditCritical() — writes audit row + publishes audit.entry.created event in the same txn) |
@constellation-platform/events | Outbox-based event publishing (publish), polling dispatcher, subscribe |
@constellation-platform/jobs | Job queue abstraction (createJobQueue({ type: 'postgres' | 'memory', ... })). Two implementations today: PostgresJobQueue (durable) and InMemoryJobQueue (tests). runInWorkerBypassTransaction is the audited cross-tenant drain transaction every worker shares (PLT-1342). Every transaction the Postgres queue opens declares its purpose (enqueue, lookup, claim or settle), read with jobTransactionPurpose(fn), and JobQueue.registeredJobTypes() lists the job types a handler is registered for (PLT-921) |
@constellation-platform/storage | Object-storage adapter. S3-compatible (MinIO in dev, S3 in cloud, MinIO on-prem) plus a Supabase Storage backend for Supabase-hosted tenants |
@constellation-platform/github | GitHub App provider, one instance per App registration with every credential injected: App JWT, installation tokens minted for exactly one repository and an explicit permission subset, revoke and suspend, a one-repository issues client (search, create, state change, comments) whose token is revoked once its callback settles and every call it started has settled, a one-repository pull-request file listing under a pull_requests: read token (bounded at GitHub's 3000-file ceiling, with a partial marker), the connect flow's user-authorization proof, and X-Hub-Signature-256 verification with current and next secrets. Tokens come back as redacting SecretValues |
@constellation-platform/telemetry | Web-vitals and analytics telemetry behind a provider seam (WebVitals, WebAnalytics). Take no props: the collector path is wrong by default behind the multi-zone rewrites and fails silently, so the package owns the transport paths |
@constellation-platform/error-reporting | Error reporting behind a provider seam (Sentry today): reportError, reportMessage, flushErrorReports, and reportRequestError for Next's onRequestError. Never throws. reportMessage(…, { withoutAmbientContext: true }) sends a message projected to an allowlist, without the request's URL, headers, path-named transaction, user or breadcrumbs (contextFreeMessageEvent, PT-1076). The /scrub subpath (scrubErrorEvent, reportableError) the /init-options subpath (errorReportingInitOptions) and the /environment subpath (resolveErrorReportingEnvironment, the tier an event names, INF-605) hold the SDK-free, shared outgoing allowlist and init options that the wiki, the agents zone and the catalog zone pass to their init files (ADR-049 § D4). Apps must not import @sentry/* outside the framework init files — the constellation/no-error-reporting-sdk lint rule enforces it |
@constellation-platform/i18n | BCP-47 Locale type, resolveLocale(), Intl format helpers |
@constellation-platform/translation | AI-backed translation; wraps ai-core |
@constellation-platform/ai-core | Provider-agnostic LLM abstraction (Claude / mock); audit + budget. Sampling params (temperature/top_p) are model-capability-gated: sent only to issued ids of models that still accept them (Opus 4.6, Sonnet 4.6 and earlier), omitted for Opus 4.7 and later and for unrecognised ids, and never sent together (PLT-831). The budget wrapper (withBudget) refuses a call when no tenant resolves unless onMissingTenant: 'allow' is passed explicitly, sends the output ceiling it reserves as the request's maxTokens, and refuses token counts that are not finite (PLT-358); its usage counter is still kept per process |
@constellation-platform/ai-embeddings | Embedding-API provider abstraction (OpenAI / mock); pgvector-friendly output |
@constellation-platform/ai-rag | Retrieval-augmented generation primitives — combines ai-core and ai-embeddings for grounded responses |
@constellation-platform/coordinator | Hosted reasoning service — stateless consultCoordinator() over initiative-scoped Project Tracker state; every consult it persists lands as one coordinator.consults row plus an audit entry via auditCritical (a consult rejected before persistence writes neither). Reachable via the consult_coordinator MCP tool, npx pt consult, and the PT REST route. synthesizeCycleRetrospective() is the system-fired sibling (SYSTEM-attributed unless the caller names another actor type) the PT-816 cycle-close hook will call. Since PLT-1805 it also owns persistent conversation threads (createConversationThread, listConversationThreads, searchConversationThreads, shareConversationThread, linkConversationThreadToTask, deleteConversationThread): a thread is private to its author until shared read-only, every read re-checks each source it was built from with that source's owning module and fails closed, and deletion purges its content in one transaction, and the same transaction erases every thread that depends on it: a thread started from an excerpt of it, a thread that cited it, and every thread that depends on one of those, through other copies and citations (PLT-1897, PLT-1901). When the author can no longer delete (clearance lowered below the label, suspended or departed), a live HUMAN holder of coordinator.conversation_threads.erase through a GLOBAL assignment erases the thread instead (eraseMemberConversationThread, found per author with listErasableMemberThreads, PLT-1809): the same purge, tombstone and dependent-thread erasure, within the holder's own clearance and with a mandatory reason, while the holder never sees a title, message or count and one metadata-only critical audit entry records it. No route uses them until PT-1268. |
@constellation-platform/testing | Test tenant/user factories, test transaction wrapper. The ./e2e subpath carries the shared Playwright helpers: loginViaApi (programmatic mock login), e2eBaseUrl (the overridable base a suite drives) and assertSkipLinkBypassesChrome (the check every zone shell that mounts SkipLink owes its smoke tier — one Tab focuses the link, the focused link is actually painted and not occluded, and Enter moves focus onto #main-content rather than only the URL fragment; every zone shell's smoke tier uses it) |
@constellation/contracts | Shared Zod schemas and derived types (cross-module event payloads, common shapes) |
@constellation-platform/ui | Shared React components. Notable primitives: LocaleSwitcher; PersonAvatar + avatarTint (palette-tinted initials avatar, hashing a name onto the eight --avatar-* token pairs, with an optional docked agent marker and an optional profile image, avatarUrl, drawn over the initials once it loads); AgentMarker (the platform's single agent-owned signal, badge or icon); ListFilterBar (the simple list filter bar — status + sort + debounced search + clear-all; a surface needing several independent facets composes Selects alongside it rather than widening its props); Breadcrumbs + the composable Breadcrumb* parts (the trail; pass linkComponent — typically next/link — for client-side navigation, and note it is not optional under a zone basePath: the default plain anchor is not basePath-prefixed, so every collapsed crumb resolves against the root zone and 404s in production while looking correct in dev and CI, where the basePath is unset); LoadingSpinner (the spinner. Announcing by default — it renders an <output>, an implicit status live region named "Loading", which is what you want when the spinner IS the announcement: a page or panel loading with nothing else saying so. Pass decorative inside a control, or the live region nests in that control and the control's accessible name absorbs the spinner's — a submit button reads "Loading Save" rather than "Save". decorative renders the same element with aria-hidden, so the exclusion is inherited rather than wrapped. Note that hiding it is only free where the button's label already changes while pending ("Retry" → "Retrying…"): a button with a STATIC label needs a pending label of its own, or it goes disabled under an unchanged name and says nothing at all); RichTextEditor (the shared TipTap editor, HTML or mode="markdown". It is a controlled component and behaves like one (PLT-1295): onChange fires ONLY for a mutation originating inside the editor — a keystroke, a toolbar command, an upload insertion — and never for the initial seeding, an external content push, or an editable change, so a caller's content survives mount byte-for-byte. The corollary is that the CALLER owns canonicalisation: a value the editor would serialise differently (_under_ → *under*, a reference link → an inline link, <b> → **) stays exactly as you hold it until a real edit happens, and a required-field check must use a rich-text-aware predicate rather than a trim(), because an empty editor's content is whatever you passed — possibly <p></p> — rather than ''. Naming the editable editor (PT-1002): the focusable ProseMirror element is its only textbox, named by the element with id="<id>-label" when you pass id — render it yourself (a <Label id="<id>-label">; a <label htmlFor> alone cannot name it) — and otherwise by ariaLabel, then the placeholder, which is also the fallback when that label element is missing); SkipLink (the "Skip to main content" bypass link, WCAG 2.2 SC 2.4.1 — mount it as the first focusable element in the document, since one placed after the chrome cannot skip that chrome, and give its targetId element tabIndex={-1} or activation moves the URL fragment while focus stays behind). See the package's README.md and AGENTS.md for the component guide, and src/index.ts for the authoritative export list |
@constellation-platform/coordinator-chat | Shared coordinator UI rendered by Project Tracker as both the full-page workspace (CoordinatorWorkspace) and the task drawer (PLT-224, PLT-237). Zone-agnostic transport via an injected resolveApiUrl. Consumed as source. |
@constellation/config | Shared ESLint, TypeScript, Vitest configs |
i18n entry points
Internationalisation primitives are split across three packages so apps can compose only what they need:
@constellation-platform/i18n— runtime-agnostic.Localeunion,SUPPORTED_LOCALES,resolveLocale(),negotiateAcceptLanguage(),formatDate/DateTime/Number/Currency, message-loader with regional → base-language → default fallback, and alocalized()JSONB overlay reader.@constellation-platform/auth-next/locale— Next.js wiring. Cookie helpers (readLocaleCookie,serializeLocaleCookie,clearLocaleCookieValue), edge-middleware integration via thelocaleResolveroption oncreateConstellationAuthMiddleware,withVaryCookie()response decorator, andAuthContext.localepopulated bywithAuth.@constellation-platform/translation— AI translation.TranslationProviderinterface andcreateTranslationProvider({ ai, model })that wrapsai-coreso tenant budget + audit are inherited.@constellation-platform/ui— sharedLocaleSwitcherclient component (server-action driven; renders null when fewer than two locales are enabled).
See Multilanguage (i18n) for the architecture overview.
Elevation
Shadows come from the design language's warm elevation ramp, not Tailwind's stock scale. @constellation-platform/design-tokens maps Tailwind's --shadow-* theme steps onto the four --e-* rungs, so the stock utilities already render warm — reach for shadow-sm / shadow-md / shadow-lg as usual and do not write shadow-[var(--e-2)].
| Utility | Rung | Typical surface |
|---|---|---|
shadow / shadow-2xs / shadow-xs / shadow-sm | --e-1 | inputs, switches, cards, active chips |
shadow-md | --e-2 | popover, dropdown menu, select content |
shadow-lg | --e-3 | dialog, sheet, submenus, drag lifts |
shadow-xl / shadow-2xl | --e-pop | drawers, command palette, floating panels |
Three things worth knowing:
- Elevation is deliberately four levels, not Tailwind's seven. The design language is near-flat, so steps collapse onto shared rungs. To retune elevation, change the four
--e-*values in the token package once — never add a role-specific shadow. - Dark mode uses neutral-black shadows, by design. A warm tint needs the higher alpha dark grounds demand to register at all, and at that alpha it reads as haze rather than depth. The warmth in dark mode comes from the neutral ramp underneath.
- A
shadow-[…]holding a literal value (e.g.shadow-[0_4px_10px_rgba(0,0,0,.1)]) opts that element out of both the ramp and the dark theme. An arbitrary value that still references a token —shadow-[var(--e-2)]— keeps resolving per theme; it is merely redundant, sinceshadow-mdalready gives you--e-2.shadow-<color>modifiers (e.g.shadow-black/20) no longer tint these steps, because Tailwind cannot parse a colour out of avar(). The separatedrop-shadow-*filter utility is not remapped and stays neutral — it is for glyph legibility over imagery, not surface elevation.
Quiet text
@constellation-platform/design-tokens ships two quiet text tiers. Choose between them by the surface the text sits on, not by how important the text feels:
| Token | Use on | Contrast (light / dark) |
|---|---|---|
text-muted-foreground (--n-550) | the page canvas, cards and the sunken bg-muted fill | 4.82 / 4.77 on the canvas, 4.94 / 4.89 on a card, 4.57 / 4.59 on bg-muted |
text-muted-foreground-strong (--n-600) | the bg-secondary and bg-accent fills: a quiet filled button, a table header, a hovered or active row | 6.22 on the canvas, 5.55 / 5.59 over bg-accent |
Both clear the WCAG AA 4.5:1 floor for normal-size text on the surfaces listed. Plain text-muted-foreground measures about 4.3:1 on bg-secondary / bg-accent, under that floor, so quiet text on those fills takes -strong.
Before PLT-778 the plain token was --n-500, at 3.74 / 4.14 on the canvas. That is why many existing call sites use -strong on plain surfaces as well; those remain correct. The package's own test, packages/ui/tests/muted-foreground-contrast.test.ts, recomputes these pairings from the stylesheet.
When to use which
- AI features. Start with
@constellation-platform/ai-corefor provider-agnostic completion calls (with budget + audit). Add@constellation-platform/ai-embeddingswhen you need vector embeddings (semantic search, RAG retrieval). Add@constellation-platform/ai-ragwhen you want grounded answers — it composesai-core+ai-embeddingsand handles the retrieval / context-injection wiring. For initiative-scoped reasoning over live PT + GitHub state (prioritisation, duplicate-effort detection, "what next"), call@constellation-platform/coordinator'sconsultCoordinator()— it wrapsai-coreand writes an append-onlycoordinator.consultsrow plus acoordinator.consult.createdaudit entry for every call it gets far enough to persist (input rejected up front, and a persist that itself fails, write neither). The same package'ssynthesizeCycleRetrospective()is the system-fired entry point for cycle-close retrospectives, attributed to SYSTEM unless the caller names another actor type — the PT-816 cycle-close hook is what will call it. Its host-suppliedCycleRetrospectiveContextoptionally carriesoverCapacityAdmissions(PLT-722): the work whose admission took a cycle past its points target — including the one that first crosses it, not only those admitted when it was already over — which the synthesised retro reports as interruption pressure alongside the delivery figures. Omitting the field is valid and produces the retrospective it produced before the field existed. The figures it carries are never summed — each is the cycle's excess after that admission, not the points it added — and the rendered list is bounded both by entry count and by a character budget, with an omission line naming the true total. It persists identically, is idempotent on a successful retro per cycle (a failed attempt stays retryable), emits the knowledge-base file-back event even with zero citations (the wiki's own space and dedup gates still apply), and is best-effort — every failure resolves as an outcome rather than throwing, so a retro can never fail the cycle close. - Persistence.
@constellation-platform/dbfor any tenant-scoped DB access. UsewithTenantContext()to open a transaction withapp.tenant_idset. UseauditAction()for routine audit; reach for@constellation-platform/audit'sauditCritical()only when the mutation must publish anaudit.entry.createdoutbox event for downstream SIEM. - Async work.
@constellation-platform/jobsfor one-off background work.@constellation-platform/eventsfor cross-module domain events (always wrap in the originating transaction). - Files.
@constellation-platform/storage(S3 / MinIO). - Errors you did not map to a response.
@constellation-platform/error-reporting. On a serverless path,await flushErrorReports()before responding: the function is frozen once it answers, and an unflushed report is lost. - Auth. Apps wire
@constellation-platform/auth-nextand call into@constellation-platform/auth-corefor permission helpers. - UI. Always import from
@constellation-platform/ui, never from app-localcomponents/ui/. Overlays (Dialog,Sheet,Popover,DropdownMenu,Select,Tooltip,AlertDialog) all portal intodocument.body, so they stack byz-indexalone — never set az-*class on one; passlayerand let nested overlays derive from it. See overlay-layering. Ask a primitive for a variant rather than copying its classes: a destructive confirmation is<AlertDialogAction variant="destructive">, which takes the Button's variants (PLT-464). A copiedbg-destructive … hover:bg-destructive/90list missed the Button's later fix to a solid--destructive-hover, and kept a hover that measured 3.89:1 on the light canvas.