Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,8 @@ temp/

# Generated site output
site/dist/

# CLI test fixtures (written and removed per run; a crashed run leaves them)
packages/cli/.app-fixture/
packages/cli/.dev-fixture/
packages/cli/.mcp-fixture/
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ A package may import from strictly lower tiers. Never sideways within a tier, ne
| 4 | `render`, `pwa`, `mcp`, `ai`, `manifest`, `mail` |
| 5 | `ui`, `admin`, `testing`, `cli` |

Declared sideways edges, each earning its line: `admin → ui`, `realtime → query`, `create-ultimate → cli`.
Declared sideways edges, each earning its line: `admin → ui`, `realtime → query`, `cli → admin`, `create-ultimate → cli`.

**`db` is tier 1, decided 2026-08.** It imports `core` and nothing else, so tier 1 is the lowest its real imports allow — and that is what lets `entity` (tier 2) hold its own Postgres driver (`postgresDriver()`) instead of exiling it to a tier-3 package. Two things would have been wrong: a second package owning `Driver`'s only production implementation (two places to look for "where rows live"), and `database()` callers importing the seam from one package and the driver from another. Same shape as `auth → db`.

Expand Down
2 changes: 2 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/architecture/01-package-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ tier 5 ui, admin, testing, cli (may import tier 0-4)
| `schema` → `core` | needs `UltimateError` for parse failures. `core` imports nothing. |
| `admin` → `ui` | the admin dashboard *is* the ui kit, composed. Inverting it ships every widget through props. |
| `realtime` → `query` | tier 3 is one feature: a live query is a query plus a subscription. Splitting duplicates the SQL shape. |
| `cli` → `admin` | `x dev` **mounts** `/_x`; it does not reimplement it. The panels are a tier-5 product, and the alternative is a second dev dashboard inside the CLI. |
| `create-ultimate` → `cli` | a published shim whose whole job is `x new`. The alternative is a second copy of the templates. |
| everything else | none. Siblings share **types only**, declared in the lowest tier that needs them. |

Expand Down
4 changes: 2 additions & 2 deletions packages/admin/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# @ultimat3/admin — boundary

Tier 5. May import tiers 0–4. Nothing imports this.
Tier 5. May import tiers 0–4. Exactly one importer: `@ultimat3/cli`, and only of `@ultimat3/admin/dev` — `cli → admin` is a declared sideways edge (`scripts/lib/tiers.ts`) so `x dev` can **mount** `/_x`. The root barrel `@ultimat3/admin` has no importer.

Two products, one package: `src/dev/*` = the `/_x` dev dashboard (dev only), `src/*` = the generated app admin (production, authz'd, AI-first). Keep them apart; nothing in `src/dev/` may be imported by an admin view.
Two products, one package, **two entry points**: `@ultimat3/admin/dev` (`src/dev/index.ts`) = the `/_x` dev dashboard, `@ultimat3/admin` (`src/index.ts`) = the generated app admin (production, authz'd, AI-first). Keep them apart; nothing in `src/dev/` may be imported by an admin view, and the root barrel does not re-export the dev half — a host that mounts `/_x` (the CLI) must not load a Solid component to do it.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Rules

Expand Down
8 changes: 6 additions & 2 deletions packages/admin/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# @ultimat3/admin 🛠️

**Two dashboards live here. They are not the same thing.**
**Two dashboards live here. They are not the same thing — and they have two doors.**

| | `/_x` — framework dev dashboard | `admin` — generated app admin |
|---|---|---|
| Import | `@ultimat3/admin/dev` | `@ultimat3/admin` |
| Audience | you, debugging the framework | your operators, and their agents |
| Environment | development **only** — mounting it with `env=production` or `role=production` throws `X_DEV_DASHBOARD_IN_PROD` | production |
| Authz | none: it is your own machine | the app's policies, one decision per surface |
Expand All @@ -27,13 +28,16 @@ One panel per file. Each kills one question, and each is available as `--json`
| `manifest` | emitted `x.manifest.json` diffed against the committed one |

```ts
import { devDashboard, defaultDevSources } from '@ultimat3/admin';
import { devDashboard, defaultDevSources } from '@ultimat3/admin/dev';

const dev = devDashboard({ sources: defaultDevSources({ authz, actors }) }); // throws in prod
const response = await dev.handle(request); // null when the path is not /_x
await dev.json('jobs'); // the same payload `x dev --panel jobs --json` prints
```

The root barrel does not re-export any of this: `x dev` mounts `/_x` without pulling a Solid
component tree into the process, and an admin view cannot reach a dev panel by accident.

## The generated admin

One call, a working CRUD admin: columns from the entity's columns, filters from indexed columns, validation from the entity's schema, labels from i18n keys.
Expand Down
3 changes: 2 additions & 1 deletion packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"provenance": true
},
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./dev": "./src/dev/index.ts"
},
"files": [
"src",
Expand Down
47 changes: 47 additions & 0 deletions packages/admin/src/dev/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// The `/_x` dev dashboard's own door, reached as `@ultimat3/admin/dev`.
//
// A separate door from the package root on purpose: the CLI's only job here is to MOUNT `/_x`,
// so it must never pull `src/*`'s production Solid component tree into every `x dev` process.

export { type DevSourceOptions, defaultDevSources, staticDevSources } from './data';
export type {
CacheEdgeFact,
ColumnFact,
DevSources,
DriftFact,
InvalidationFact,
JobDefFact,
JobRunFact,
JobStepFact,
LiveQueryFact,
LiveSubscriberFact,
MailFact,
ManifestFact,
PolicyFact,
QueueFact,
RequestTrace,
RouteFact,
SpanKind,
SqlResult,
TableFact,
TaskFact,
TimelineSpan,
} from './facts';
export { type DevPanel, type PanelPayload, panelPayload } from './panel';
export { type CachePanelData, cachePanel } from './panel-cache';
export { assertReadOnly, type DbPanelData, dbPanel } from './panel-db';
export { type JobsPanelData, jobsPanel } from './panel-jobs';
export { type LivePanelData, livePanel } from './panel-live';
export { type MailPanelData, mailPanel } from './panel-mail';
export { type ManifestPanelData, manifestPanel } from './panel-manifest';
export { type PolicyPanelData, policyPanel } from './panel-policy';
export { type RoutesPanelData, routesPanel } from './panel-routes';
export { type TimelinePanelData, timelinePanel } from './panel-timeline';
export {
assertDevOnly,
DEV_BASE_PATH,
DEV_PANELS,
type DevDashboard,
type DevDashboardOptions,
devDashboard,
} from './server';
44 changes: 2 additions & 42 deletions packages/admin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,48 +77,8 @@ export {
permissionsForOperation,
} from './crud';
export { AdminDetail, type AdminDetailProps } from './detail';
export { type DevSourceOptions, defaultDevSources, staticDevSources } from './dev/data';
export type {
CacheEdgeFact,
ColumnFact,
DevSources,
DriftFact,
InvalidationFact,
JobDefFact,
JobRunFact,
JobStepFact,
LiveQueryFact,
LiveSubscriberFact,
MailFact,
ManifestFact,
PolicyFact,
QueueFact,
RequestTrace,
RouteFact,
SpanKind,
SqlResult,
TableFact,
TaskFact,
TimelineSpan,
} from './dev/facts';
export { type DevPanel, type PanelPayload, panelPayload } from './dev/panel';
export { type CachePanelData, cachePanel } from './dev/panel-cache';
export { assertReadOnly, type DbPanelData, dbPanel } from './dev/panel-db';
export { type JobsPanelData, jobsPanel } from './dev/panel-jobs';
export { type LivePanelData, livePanel } from './dev/panel-live';
export { type MailPanelData, mailPanel } from './dev/panel-mail';
export { type ManifestPanelData, manifestPanel } from './dev/panel-manifest';
export { type PolicyPanelData, policyPanel } from './dev/panel-policy';
export { type RoutesPanelData, routesPanel } from './dev/panel-routes';
export { type TimelinePanelData, timelinePanel } from './dev/panel-timeline';
export {
assertDevOnly,
DEV_BASE_PATH,
DEV_PANELS,
type DevDashboard,
type DevDashboardOptions,
devDashboard,
} from './dev/server';
// The /_x dashboard is NOT re-exported here — it has its own door, `@ultimat3/admin/dev`, so a
// host that only mounts the dev panels never loads a production admin component.
export {
ADMIN_ERROR_CODES,
AdminEntityUnknownError,
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,39 @@ Tier 5. May import tiers 0–4. Nothing imports this except `create-ultimate`.
| Subprocesses | only through `exec.ts`, so a test can inject a fake `Runner` |
| Templates | `templates/*.ts` return strings; no fixture files on disk |
| Strings | `messages.ts` flat catalog, missing key renders `⟦key⟧` |
| Facts | load the app (`app-load.ts`), then project it — never parse source for primitives |

Every fact the CLI reports comes from a framework package: the manifest from
`@ultimat3/manifest`, `openapi.json` from `@ultimat3/action`, the route table from
`@ultimat3/render`, budget units from `@ultimat3/render`, the `/_x` panels from
`@ultimat3/admin`, the MCP tool catalog from `@ultimat3/mcp`. A check that reimplements one
of those here is the bug, not the fix.

`cli → admin` is a declared sideways edge (`scripts/lib/tiers.ts`): `x dev` **mounts** the
dashboard, it never grows a second one. The CLI's only contribution is the facts no registry
holds — a SQL runner, the committed manifest, the process's own services — supplied as
`defaultDevSources({ hooks })`.

## `x dev` boots the app; it does not simulate one

| File | Job |
|---|---|
| `dev-services.ts` | resolve which service each binding points at — embedded or external |
| `dev-runtime.ts` | start them and install the ambient accessors (`db()`, `jobDriver()`, storage, transport) |
| `dev-render.ts` | one HTTP route per registered `route`, through render's own mode function |
| `dev-hooks.ts` | the pipeline's `authorize` seam, decided from the app's own `Policy` objects |
| `dev-roles.ts` | `--role` selection plus start/stop for `web`, `sync`, `worker`, `scheduler` |
| `dev-dashboard.ts` | the `DevSources` hooks only this process can answer, and the two CLI panels |
| `cmd-dev.ts` | boot order, mounting `/_x`, the file watcher |
| `mcp-host.ts` | the `DevCapabilities` half of `@ultimat3/mcp`'s `DevHost` — db, tests, logs, verify |
| `mcp-db-target.ts` | which database the host is pointed at, and whether it is a branch |
| `mcp-errors.ts` | `errors.explain`: one runnable command per code, typed over `CliErrorCode` |
| `mcp-test-output.ts` | reading `bun test`'s own summary back into a `TestRun` |
| `cmd-mcp.ts` | `x mcp serve`: the two transports, and the local developer's caller |

The roles live in `@ultimat3/core` (`ROLES`, `isRole`), never in a second list here. A dev-only
driver, a dev-only authorizer or a dev-only queue is the bug this design exists to prevent — the
only thing dev changes is which driver is behind an interface.

Commands: `bun test`, `bunx tsc --noEmit -p tsconfig.json`.

Expand Down
16 changes: 13 additions & 3 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The `x` binary. One char during dev, one command per job, `--json` on every one
| `x verify` | **the gate** | 15 named steps, each with pass/fail + duration |
| `x g <primitive> <name>` | scaffolds a primitive **with a passing test** | never a TODO stub |
| `x db gen\|migrate\|reset\|studio\|branch` | everything DB | `branch` = copy-on-write clone + preview URL |
| `x mcp serve` | MCP over stdio or HTTP | read tools unrestricted in dev |
| `x mcp serve` | `@ultimat3/mcp`'s 13 dev tools, over stdio or HTTP | one catalog, one scope set, both transports |
| `x doctor` | environment, ports, drift, PWA prerequisites | every finding carries a fix command |
| `x deploy` | container deploy plan | compose or helm; zero platform primitives |
| `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
Expand Down Expand Up @@ -54,11 +54,21 @@ There is no `--only` and no `--skip`; the exit code is non-zero if any step fail
| `registry.ts` | the one command list |
| `cmd-*.ts` | one command group each |
| `templates/` | scaffolding as typed string modules, not copied fixtures |
| `surfaces.ts` | app import boundaries (site→app, shared leaf, route→DB) |
| `app-load.ts` | import an app's modules so the framework registries hold it |
| `app-manifest.ts` | `x.manifest.json`, projected by `@ultimat3/manifest` |
| `app-openapi.ts` | `openapi.json`, projected by `@ultimat3/action` |
| `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
| `dev-*.ts` | what `x dev` boots: services, runtime, routes, hooks, roles, the `/_x` mount |
| `mcp-host.ts` | the shell-side half of `@ultimat3/mcp`'s dev server — db, tests, logs, verify |
| `verify-step.ts` | the step shape, the step names, the host-check hook |
| `verify-tests.ts` | one `bun test` invocation per test type |
| `workspace-checks.ts` | file-size ceiling and package contract files |
| `drift.ts` `openapi.ts` `budgets.ts` `manifest-scan.ts` | the checks `x verify` composes |
| `drift.ts` `budgets.ts` | the checks `x verify` composes |

The CLI describes an app by **loading** it, never by parsing it: `action()`, `entity()`,
`job()` and `defineRoute()` register themselves, and `x manifest`, `x routes` and `x verify`
read the same tables the running server reads. There is no second definition of a primitive,
no second OpenAPI builder and no second surface-boundary walk anywhere in this package.

## Generated file layout

Expand Down
5 changes: 5 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,21 @@
},
"dependencies": {
"@ultimat3/action": "0.0.1",
"@ultimat3/admin": "0.0.1",
"@ultimat3/core": "0.0.1",
"@ultimat3/db": "0.0.1",
"@ultimat3/entity": "0.0.1",
"@ultimat3/http": "0.0.1",
"@ultimat3/i18n": "0.0.1",
"@ultimat3/jobs": "0.0.1",
"@ultimat3/mail": "0.0.1",
"@ultimat3/manifest": "0.0.1",
"@ultimat3/mcp": "0.0.1",
"@ultimat3/policy": "0.0.1",
"@ultimat3/query": "0.0.1",
"@ultimat3/realtime": "0.0.1",
"@ultimat3/render": "0.0.1",
"@ultimat3/storage": "0.0.1",
"@ultimat3/testing": "0.0.1"
}
}
90 changes: 90 additions & 0 deletions packages/cli/src/app-boundaries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, test } from 'bun:test';
import type { SourceFile } from './app-boundaries';
import { checkImportRules, resolveSpecifier } from './app-boundaries';

const file = (path: string, source: string): SourceFile => ({ path, source });

describe('unit · app boundaries', () => {
test('site/ importing app/ is a build error, not a warning', () => {
const findings = checkImportRules([
file('apps/web/site/pricing/page.tsx', "import { Chart } from '../../app/charts';"),
]);
expect(findings.map((finding) => finding.code)).toEqual(['X_BOUNDARY_SITE_TO_APP']);
expect(findings[0]?.at).toBe('apps/web/site/pricing/page.tsx');
expect(findings[0]?.fix.length).toBeGreaterThan(0);
});

test('shared/ is a leaf: it may not import a surface', () => {
const findings = checkImportRules([
file('apps/web/shared/ui/button.tsx', "import { useSession } from '../../app/session';"),
]);
expect(findings[0]?.code).toBe('X_BOUNDARY_SHARED_LEAF');
});

test('app/ may import api/ types but not api/ values', () => {
const typeOnly = checkImportRules([
file('apps/web/app/orders/page.tsx', "import type { Order } from '../../api/orders';"),
]);
expect(typeOnly).toEqual([]);
const runtime = checkImportRules([
file('apps/web/app/orders/page.tsx', "import { placeOrder } from '../../api/orders';"),
]);
expect(runtime[0]?.code).toBe('X_BOUNDARY_APP_TO_API');
});

test('a route may not touch the database', () => {
const findings = checkImportRules([
file('apps/web/app/orders/page.tsx', "import { db } from '@acme/db';"),
]);
expect(findings[0]?.code).toBe('X_BOUNDARY_ROUTE_TO_DB');
// Every fix is a line a caller can paste: a command first, the rest behind a `#`.
expect(findings[0]?.fix).toStartWith('x g query orders');
});

test('a service may not know about HTTP', () => {
const findings = checkImportRules([
file('apps/web/app/orders/service.ts', "import { request } from '@ultimat3/http';"),
]);
expect(findings[0]?.code).toBe('X_BOUNDARY_SERVICE_TO_HTTP');
expect(findings[0]?.fix).toStartWith('x g action orders');
});

test('a legal import graph produces no findings', () => {
expect(
checkImportRules([
file('apps/web/site/page.tsx', "import { Button } from '../shared/ui/button';"),
file('apps/web/app/orders/repo.ts', "import { db } from '@acme/db';"),
]),
).toEqual([]);
});

// The upgrade over the CLI's own former check: the import that costs you is two hops away
// from the file anyone reviewed, and a direct-imports-only walk finds nothing.
test('a transitive site/ -> shared/ -> app/ chain is caught, and the chain is named', () => {
const findings = checkImportRules([
file('apps/web/site/pricing.tsx', "import { Price } from '../shared/price';"),
file('apps/web/shared/price.ts', "import { rate } from '../app/rates';"),
file('apps/web/app/rates.ts', 'export const rate = 1;'),
]);
const codes = findings.map((finding) => finding.code);
expect(codes).toContain('X_BOUNDARY_SITE_TO_APP');
expect(codes).toContain('X_BOUNDARY_SHARED_LEAF');
const chained = findings.find((finding) => finding.code === 'X_BOUNDARY_SITE_TO_APP');
expect(chained?.cause).toContain('apps/web/site/pricing.tsx');
expect(chained?.cause).toContain('apps/web/app/rates.ts');
expect(chained?.at).toBe('apps/web/shared/price.ts');
});

test('a specifier resolves onto the graph key it names, extension and all', () => {
const keys = new Set(['apps/web/shared/price.ts', 'apps/web/shared/ui/index.tsx']);
expect(resolveSpecifier('apps/web/site/page.tsx', '../shared/price', keys)).toBe(
'apps/web/shared/price.ts',
);
expect(resolveSpecifier('apps/web/site/page.tsx', '../shared/ui', keys)).toBe(
'apps/web/shared/ui/index.tsx',
);
expect(resolveSpecifier('apps/web/site/page.tsx', '@ultimat3/http', keys)).toBe(
'@ultimat3/http',
);
});
});
Loading
Loading