@ultimat3/cli 11.3.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,9 +63,9 @@ Built with [Ultimate](https://github.com/developerz-ai/ultimate). Bun-only, Post
63
63
  ## 🚀 Start
64
64
 
65
65
  \`\`\`sh
66
- bin/setup # prerequisites, deps, env, the first migration, migrate, seed
67
- x dev # all roles in one process, embedded Postgres, /_x mounted
68
- x verify # the gate: typecheck, lint, boundaries, tests, drift, budgets
66
+ bin/setup # prerequisites, deps, env, the first migration, migrate, seed, the manifest
67
+ bin/dev # all roles in one process, embedded Postgres, /_x mounted
68
+ bin/check # the gate: typecheck, lint, boundaries, tests, drift, budgets
69
69
  \`\`\`
70
70
 
71
71
  \`packages/db/migrations\` starts empty and \`x db gen\` is its only writer — \`bin/setup\` runs
@@ -104,7 +104,13 @@ bunx x db migrate "$@"
104
104
  # \`postgres:\` DATABASE_URL and so dies on a clone with no Postgres — one line after reporting a
105
105
  # successful migration.
106
106
  bunx x db seed
107
- echo "setup complete — next: x dev"
107
+ # The file \`AGENTS.md\` line 3 tells an agent facts live in, and \`x dev\` prints the path of. It
108
+ # is a projection of the loaded app, so \`x new\` cannot write it — node_modules does not exist
109
+ # yet — and nothing else ever ran the command: after \`x new\`, \`bin/setup\` and all 13
110
+ # generators, \`find . -name '*.manifest.json'\` returned nothing while \`x verify\` reported
111
+ # \`\u2713 manifest\`. \`x verify\`'s manifest step now refuses its absence (X_MANIFEST_MISSING).
112
+ bunx x manifest
113
+ echo "setup complete — next: bin/dev"
108
114
  `;
109
115
 
110
116
  const binDev = (): string => `#!/usr/bin/env bash
@@ -0,0 +1,84 @@
1
+ // The scaffold's answer to "how does this server bind, and what does it admit?", which it did not
2
+ // have. `configureHttp()` is the one registration site an app has for CORS origins, the body
3
+ // limit, the request deadline, the in-flight ceiling and the rate-limit buckets — and until it
4
+ // shipped, the only `HttpConfig` any process built was a fixed literal inside `@ultimat3/cli`.
5
+ //
6
+ // `DEFAULT_CORS.origins` is `[]`, so a scaffolded app refuses every cross-origin browser call. The
7
+ // most common homework-scale need — a Vite front end on `localhost:5173` calling the app — was
8
+ // inexpressible; now it is one uncommented line, and this file is where an agent finds it.
9
+
10
+ import type { GeneratedFile, NameSet } from './naming';
11
+
12
+ const httpConfig = (
13
+ app: NameSet,
14
+ ): string => `// What this app declares about HTTP. Module scope IS the wiring: the boot scan imports every
15
+ // module under \`apps/*\` before a listener binds, and \`x dev\` and the container both read the
16
+ // configured value back at start — the same seam \`app/auth/dev-actor.ts\` installs through.
17
+ //
18
+ // The BOOT lays its own facts over whatever this says: \`port\`, \`hostname\`, \`dev\`, \`buildId\`,
19
+ // \`signInPath\`, \`trustProxy\`, \`trustedProxyHops\` and \`rateLimit.scope\` are all facts about the
20
+ // PROCESS, so writing one here is a type error rather than a value silently overwritten at the
21
+ // next boot.
22
+
23
+ import { configureHttp } from '@ultimat3/http';
24
+
25
+ configureHttp({
26
+ // EMPTY by default, and that is a refusal rather than an oversight: an origin list is a list of
27
+ // sites allowed to make credentialed calls with this app's cookies, and a framework may not
28
+ // guess one. A browser app served from another origin — a Vite dev server, a separate marketing
29
+ // site — goes here, exactly spelled, scheme and port included:
30
+ //
31
+ // cors: { origins: ['http://localhost:5173'] },
32
+ //
33
+ // \`credentials\` is \`true\` by default, and \`'*'\` with credentials is the one combination a
34
+ // browser refuses — \`@ultimat3/http\` refuses it here instead, at the moment you can act on it.
35
+ cors: { origins: [] },
36
+ // The two bounds a request is measured against. Both are the framework's defaults spelled out,
37
+ // so raising one for an endpoint that really does take a 4 MB CSV or five minutes is an edit to
38
+ // a number that is already in front of you rather than a search for the knob.
39
+ bodyLimitBytes: 1024 * 1024,
40
+ requestTimeoutMs: 30_000,
41
+ });
42
+
43
+ /** Named so the module has an export; importing it for the side effect alone is the wiring. */
44
+ export const ${app.camel}Http = 'configured';
45
+ `;
46
+
47
+ const httpConfigTest =
48
+ (): string => `// The declaration reached the registry. \`configureHttp()\` is a module-scope side effect, so the
49
+ // only thing that can go wrong is nobody importing the module — which is exactly how a shipped app
50
+ // rendered every string as ⟦key⟧ for a whole release (issue #249), one seam along.
51
+ import { configuredHttp, resetHttpConfig } from '@ultimat3/http';
52
+ import { expect, unitTest } from '@ultimat3/testing';
53
+ import './http';
54
+
55
+ unitTest('importing the module IS the registration', () => {
56
+ const declared = configuredHttp();
57
+ expect(declared).toBeDefined();
58
+ // The list is empty on a fresh scaffold and that is the shipped default; what is asserted is
59
+ // that the KEY reaches the boot, so adding an origin to it takes effect.
60
+ expect(declared?.cors?.origins).toEqual([]);
61
+ });
62
+
63
+ unitTest('the boot-owned keys are absent — the boot measures them, an app can only guess', () => {
64
+ const declared = configuredHttp() ?? {};
65
+ for (const key of ['port', 'hostname', 'dev', 'buildId', 'signInPath']) {
66
+ expect({ key, declared: Object.hasOwn(declared, key) }).toEqual({ key, declared: false });
67
+ }
68
+ });
69
+
70
+ // The registration is process-global, so a suite that left it set would hand the next file this
71
+ // app's config. \`resetHttpConfig()\` is the seam; this is the one place it is called.
72
+ unitTest('and it is resettable, so no test file inherits the server config of another', () => {
73
+ resetHttpConfig();
74
+ expect(configuredHttp()).toBeUndefined();
75
+ });
76
+ `;
77
+
78
+ /** `apps/web/app/http.ts` and its test. Written by `x new`, with or without the example slice. */
79
+ export function httpFiles(app: NameSet): readonly GeneratedFile[] {
80
+ return [
81
+ { path: 'apps/web/app/http.ts', contents: httpConfig(app) },
82
+ { path: 'apps/web/app/http.test.ts', contents: httpConfigTest() },
83
+ ];
84
+ }
@@ -82,6 +82,21 @@ const rootPackage = (app: NameSet, version: string): string => `{
82
82
  }
83
83
  `;
84
84
 
85
+ /**
86
+ * `"incremental": true` is ONE line and it is the difference between a 4.9s typecheck and a 92s
87
+ * one. `x verify`'s first step is `tsc -b`, and `-b` decides "up to date?" by comparing emitted
88
+ * OUTPUTS against inputs — with `noEmit` and no `composite`/`references`, the output it looks for
89
+ * is an `app.config.js` that will never exist (`Project 'tsconfig.json' is out of date because
90
+ * output file 'app.config.js' does not exist`), so every run rebuilt the whole program from
91
+ * scratch, forever. Measured on a 166-file scaffold with no source change between runs: 92s wall
92
+ * / 43s user CPU without it, 4.9s / 8.8s warm with it, and the whole gate at 12s rather than
93
+ * 24-71s. This was the only tree in the framework without incremental typechecking — the repo
94
+ * root has 32 `references` and `examples/dummy/tsconfig.json` sets `composite`.
95
+ *
96
+ * The note lives HERE and not in the emitted file, for the reason `biome.json` below gives: an
97
+ * app author has no use for eight lines of framework archaeology in their own tsconfig, and
98
+ * `*.tsbuildinfo` is already in the scaffold's `.gitignore`.
99
+ */
85
100
  const rootTsconfig = (app: NameSet): string => `{
86
101
  "compilerOptions": {
87
102
  "target": "ES2023",
@@ -101,6 +116,7 @@ const rootTsconfig = (app: NameSet): string => `{
101
116
  "isolatedModules": true,
102
117
  "skipLibCheck": true,
103
118
  "noEmit": true,
119
+ "incremental": true,
104
120
  "resolveJsonModule": true,
105
121
  "jsx": "preserve",
106
122
  "jsxImportSource": "solid-js"
@@ -258,6 +274,10 @@ const SCAFFOLD_FLOOR: readonly VerifyStepName[] = [
258
274
  'eval',
259
275
  'drift',
260
276
  'budgets',
277
+ // Always applicable to a scaffolded app — it has an `app.config.ts`, roles and two guarded
278
+ // routes — so a run that reports it skipped is a gate that lost the step, not an app with
279
+ // nothing to check. It is the step that would have caught the 500 `x new` used to ship.
280
+ 'policy',
261
281
  'manifest',
262
282
  ];
263
283
 
@@ -19,7 +19,15 @@ const rolesSource =
19
19
  // \`x g policy <feature>\` declares \`<feature>:read\` and \`<feature>:write\`. Granting them is this
20
20
  // file's job — a permission no role holds is one no actor can ever exercise.
21
21
 
22
- import { defineRoles } from '@ultimat3/policy';
22
+ import { definePermissions, defineRoles } from '@ultimat3/policy';
23
+
24
+ // DECLARED before it is granted, and that order is the whole point. \`can()\` calls
25
+ // \`assertPermission\`, which refuses a name no \`definePermissions()\` call registered
26
+ // (X_PERMISSION_UNKNOWN) — and \`defineRoles()\` does NOT: it took \`grants: ['dashboard:read']\`
27
+ // in silence while nothing declared it, so every scaffolded app answered HTTP 500 on /dashboard
28
+ // and /admin from its first \`x dev\`, under a green gate. A permission a role grants and a
29
+ // permission a route requires both belong here.
30
+ export const appPermissions = definePermissions(['admin:read', 'dashboard:read']);
23
31
 
24
32
  export const roles = defineRoles({
25
33
  member: {
@@ -37,9 +45,9 @@ export const roles = defineRoles({
37
45
  const rolesTest =
38
46
  (): string => `// The app's role map, expanded: what each role grants once inheritance is flattened, and which
39
47
  // roles hold a given permission. An undeclared role must grant nothing at all.
40
- import { expandRoles, rolesGranting } from '@ultimat3/policy';
48
+ import { expandRoles, isKnownPermission, rolesGranting } from '@ultimat3/policy';
41
49
  import { expect, unitTest } from '@ultimat3/testing';
42
- import { roles } from './roles';
50
+ import { appPermissions, roles } from './roles';
43
51
 
44
52
  // The map is passed explicitly rather than read off the module-global one: a test that depended on
45
53
  // which module imported first would pass alone and fail inside a suite.
@@ -57,6 +65,26 @@ unitTest('every permission the app enforces is held by some role', () => {
57
65
  expect(rolesGranting('dashboard:read', roles)).toEqual(['admin', 'member']);
58
66
  expect(rolesGranting('admin:read', roles)).toEqual(['admin']);
59
67
  });
68
+
69
+ // The assertion whose absence shipped a 500. Expansion above proves the MAP is right and says
70
+ // nothing about the registry \`can()\` actually consults: a grant naming a permission no
71
+ // \`definePermissions()\` declared expands perfectly and then throws X_PERMISSION_UNKNOWN on the
72
+ // first request to the route that requires it.
73
+ unitTest('every granted permission is in the registry can() asks', () => {
74
+ for (const permission of new Set(Object.values(roles).flatMap((role) => role.grants))) {
75
+ expect({ permission, known: isKnownPermission(permission) }).toEqual({
76
+ permission,
77
+ known: true,
78
+ });
79
+ }
80
+ });
81
+
82
+ unitTest('the routes this app ships require permissions this app declares', () => {
83
+ // The two \`defineRoute({ policy: { permission } })\` values \`x new\` writes. \`RouteGuard\`
84
+ // keeps a bare string, so nothing but this holds them to the declared set.
85
+ expect(appPermissions.has('dashboard:read')).toBe(true);
86
+ expect(appPermissions.has('admin:read')).toBe(true);
87
+ });
60
88
  `;
61
89
 
62
90
  /** `apps/web/shared/roles.ts` and its test. Written by `x new`, with or without the example slice. */
@@ -19,6 +19,7 @@ import { checkAppBoundaries } from './app-boundaries';
19
19
  import { envExampleFindings } from './app-env';
20
20
  import { appManifest, readAppManifest } from './app-manifest';
21
21
  import { OPENAPI_FILE, openApiJson } from './app-openapi';
22
+ import { policyFindings } from './app-permissions';
22
23
  import { APP_CONFIG_FILE } from './app-root';
23
24
  import { checkBudgets, readBuildStats } from './budgets';
24
25
  import { checkDestructiveMigrations } from './db-destructive';
@@ -265,6 +266,15 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
265
266
  applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
266
267
  run: async (ctx) => fromFindings(await catalogFindings(ctx.root)),
267
268
  },
269
+ {
270
+ name: 'policy',
271
+ summary: 'every permission this app grants or requires is one it declares',
272
+ // A repo with no `app.config.ts` is the framework monorepo, which declares no roles and
273
+ // registers no routes — SKIPPED there, never passed, for the reason `i18n` gives: a step that
274
+ // answers `ok` about nothing is the vacuous green these checks exist to refuse.
275
+ applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
276
+ run: async (ctx) => fromFindings(await policyFindings(ctx.root)),
277
+ },
268
278
  {
269
279
  name: 'manifest',
270
280
  summary: 'the files an agent reads: generated facts, hand-written conventions, the env example',
@@ -283,6 +293,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
283
293
  async run(ctx) {
284
294
  const agents = await checkAgentsMd(ctx.root);
285
295
  const findings = [
296
+ ...manifestMissingFindings(ctx.root),
286
297
  ...(await driftFindings(ctx.root)),
287
298
  ...(await envExampleFindings(ctx.root)),
288
299
  ...floorProblemFindings(await readVerifyFloor(ctx.root)),
@@ -312,6 +323,31 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
312
323
  },
313
324
  ];
314
325
 
326
+ /**
327
+ * The half that had never been asked: is the file there at all? `driftFindings` returns nothing
328
+ * when it is absent — correctly, it has nothing to compare — and `AGENTS.md` line 3 tells an agent
329
+ * that facts live in `x.manifest.json`, `x dev` prints its path, and `x manifest` is the only
330
+ * thing that writes it. Nothing ran it: after `x new`, `bin/setup` and all thirteen generators,
331
+ * `find . -name '*.manifest.json'` returned nothing while this step reported green (#F7).
332
+ *
333
+ * An app root, never this repo: the framework monorepo emits `framework.manifest.json` and has no
334
+ * `x.manifest.json` to be missing, so the `app.config.ts` is what decides — the same discriminator
335
+ * `drift`, `budgets`, `seo`, `i18n` and `policy` each use.
336
+ */
337
+ function manifestMissingFindings(root: string): readonly Finding[] {
338
+ if (!existsSync(join(root, APP_CONFIG_FILE))) return [];
339
+ if (existsSync(join(root, MANIFEST_FILENAME))) return [];
340
+ return [
341
+ {
342
+ code: 'X_MANIFEST_MISSING',
343
+ cause: `${MANIFEST_FILENAME} does not exist, so every fact an agent reads about this app — route table, action schemas, policies, error codes — is unavailable`,
344
+ fix: 'x manifest',
345
+ docs: ERROR_DOCS_URL,
346
+ at: MANIFEST_FILENAME,
347
+ },
348
+ ];
349
+ }
350
+
315
351
  /** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
316
352
  async function driftFindings(root: string): Promise<readonly Finding[]> {
317
353
  const path = join(root, MANIFEST_FILENAME);
@@ -41,6 +41,14 @@ export const VERIFY_STEP_NAMES = [
41
41
  // registries that load filled. Until it existed, an app could ship every user-facing string as
42
42
  // `⟦key⟧` with `x verify` green, because nothing in the gate ever asked (issue #249).
43
43
  'i18n',
44
+ // Twentieth, by the same test `seo` and `i18n` each passed: a rider must ask the SAME question
45
+ // off the same data, and "was this import legal?" is not "does the permission this app grants
46
+ // and requires exist?". Reported under `budgets` it would hand the reader a byte budget for an
47
+ // authz defect (axiom 4). Until it existed, `x new` shipped an app that answered HTTP 500 with
48
+ // X_PERMISSION_UNKNOWN on two of its three routes under a green gate: `defineRoles()` accepts an
49
+ // undeclared grant in silence and `RouteGuard.permission` is a bare string. It costs no second
50
+ // app load — `budgets` already imported every module, and this reads the registries that filled.
51
+ 'policy',
44
52
  'manifest',
45
53
  'roadmap',
46
54
  ] as const;