@ultimat3/cli 19.4.0 → 20.1.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.
@@ -2,9 +2,11 @@
2
2
  // the bin/ shims and the optional dev compose. Separated from the config half so neither file has
3
3
  // to be scrolled to find the other — one file, one job applies to templates too. The image, its
4
4
  // ignore file, the production topology and the deploy page are `scaffold-container.ts`; the
5
- // `.claude/` harness that reads AGENTS.md is `scaffold-claude.ts`.
5
+ // `.claude/` harness that reads AGENTS.md is `scaffold-claude.ts`; the one workflow is
6
+ // `github/ci.yml.ts`.
6
7
 
7
8
  import { LINE_CEILING } from '../workspace-checks';
9
+ import { githubFiles } from './github/ci.yml';
8
10
  import type { GeneratedFile, NameSet } from './naming';
9
11
  import { claudeFiles } from './scaffold-claude';
10
12
  import { containerFiles } from './scaffold-container';
@@ -19,7 +21,7 @@ not exist — five of these had an empty column and were each measured green on
19
21
 
20
22
  | Rule | Detail | Refused by |
21
23
  |---|---|---|
22
- | One gate | \`x verify\` — green means shippable. Never merge red. | the gate itself |
24
+ | One gate | \`bin/check\` — \`x build\` and then \`x verify\`. Green means shippable; never merge red. | the gate itself, and \`.github/workflows/ci.yml\` on every push and pull request |
23
25
  | One way | generators, not hand-rolled files: \`x g resource\`, \`x g action\`, \`x g route\` | review |
24
26
  | Surfaces | \`site/\` is 0kb JS and may not import \`app/\`; \`shared/\` is a leaf | \`X_BOUNDARY_SITE_TO_APP\` |
25
27
  | Data | routes call actions and queries; only \`repo.ts\` touches the database | \`X_BOUNDARY_ROUTE_TO_DB\` |
@@ -28,11 +30,22 @@ not exist — five of these had an empty column and were each measured green on
28
30
  | Time | store UTC, format with an explicit IANA time zone | \`guards/unzoned-date.ts\` |
29
31
  | Strings | every user-facing string goes through \`t()\` | \`guards/untranslated-string.ts\` |
30
32
  | Colour | semantic tokens only, never a raw hex | \`guards/raw-colour.ts\` |
33
+ | Interaction | a click is answered by a control — never a \`<div onClick>\`, and never a \`role=\` where the native tag exists | \`guards/semantic-interactive.ts\` |
34
+ | Focus | \`outline: none\` replaces the ring in the same rule or the one beside it, or it does not remove it | \`guards/focus-visible.ts\` |
35
+ | Images | every image carries width + height or an aspect-ratio, and the priority one is never \`loading="lazy"\` | \`guards/image-dimensions.ts\` |
36
+ | Motion | animate \`transform\` and \`opacity\` — never a layout property, never \`transition: all\` | \`guards/animated-layout-property.ts\` |
37
+ | Islands | every \`*.island.tsx\` has a sibling \`*.island.states.ts\`, so \`x shot --island\` can photograph its failures | \`guards/island-without-states.ts\` |
31
38
  | Size | one file, one job — ${LINE_CEILING} lines of reviewable logic, and split past it | \`X_FILE_TOO_LONG\` |
32
39
 
33
40
  \`guards/\` is yours: each file is one rule, discovered by \`x verify\` and run inside its
34
41
  \`boundaries\` step. Delete one to drop the rule, and \`x g guard <name>\` writes the next.
35
42
 
43
+ The last five rows are about what this app is like to USE, and each is decidable from the file
44
+ alone — which is why they are those five and not the many that are not. A role is a promise:
45
+ \`role="button"\` obliges you to answer Space, Enter, focus and disabled, and the native element
46
+ already does. Only \`transform\` and \`opacity\` animate without a layout pass. An image with no box
47
+ moves everything under it when its bytes land.
48
+
36
49
  Size is a hard line and not a style note: past ${LINE_CEILING} lines a file has stopped being the
37
50
  unit of review, and \`x verify\` refuses it. The one exemption is a file that is nothing but re-exports —
38
51
  it has one job by construction, and its length tracks the API's size rather than its complexity;
@@ -43,7 +56,19 @@ see, and the type already fires — measured, \`price: 19.99\` in a seed is
43
56
  \`TS2322: Type 'number' is not assignable to type 'MoneyInput'\`. A guard that pretended to check
44
57
  it would be worse than the type that really does.
45
58
 
46
- Commands: \`x dev\`, \`x verify\`, \`x g <primitive>\`, \`x g guard <name>\`, \`x db branch create <name>\`, \`x doctor\`.
59
+ \`bin/check\` is the gate and \`x verify\` is only its second half: the first is
60
+ \`x build --target static\`, and the build is what writes the \`.x/build-stats.json\` the
61
+ \`budgets\` step measures. Run \`x verify\` on a tree nobody has built and \`budgets\` is red with
62
+ X_BUDGET_UNMEASURED — the gate reporting on a file that does not exist, not on your code.
63
+
64
+ The platform's \`.dz/\` is ADDITIVE, in both directions. developerz.ai keeps its own files under
65
+ \`.dz/maintainer/\` and \`.dz/pipeline/\`; nothing \`x new\` writes lands under \`.dz/\` and nothing
66
+ here is generated from it. So the scaffold never clobbers a maintainer policy, the platform never
67
+ clobbers \`AGENTS.md\`, \`bin/\`, \`.claude/\` or \`.github/\`, and deleting either side leaves the
68
+ other exactly as it was.
69
+
70
+ Commands: \`bin/setup\`, \`bin/dev\`, \`bin/check\`, \`x g <primitive>\`, \`x g guard <name>\`,
71
+ \`x db branch create <name>\`, \`x doctor\`.
47
72
 
48
73
  Project notes for ${app.kebab}: replace this line with the conventions a newcomer could not guess.
49
74
  `;
@@ -52,7 +77,12 @@ const claude = (app: NameSet): string => `# CLAUDE.md
52
77
 
53
78
  ${app.kebab} — Ultimate app. Read AGENTS.md first; it is the same content in the same order.
54
79
 
55
- - Gate: \`x verify\` (add \`--json\` for machine output).
80
+ - Gate: \`bin/check\` (add \`--json\` for machine output — it reaches both halves). It is
81
+ \`x build --target static\` and then \`x verify\`; \`x verify\` alone leaves \`budgets\` with
82
+ nothing to measure. \`.github/workflows/ci.yml\` runs \`bin/setup && bin/check\` on every push
83
+ and pull request, so CI and your terminal run the same two commands.
84
+ - \`.dz/\` belongs to the developerz.ai platform and is additive both ways: the scaffold writes
85
+ nothing there, and nothing there is generated from this repo. Neither side clobbers the other.
56
86
  - Scaffold, do not hand-write: \`x g <kind> <name>\` — \`x g --help\` lists every kind, and is the
57
87
  only place that list is stated.
58
88
  - Destructive DB work goes in a branch: \`x db branch create <name>\`, never the shared dev DB.
@@ -72,7 +102,7 @@ Built with [Ultimate](https://github.com/developerz-ai/ultimate). Bun-only, Post
72
102
  \`\`\`sh
73
103
  bin/setup # prerequisites, deps, env, the first migration, migrate, seed, the manifest
74
104
  bin/dev # all roles in one process, embedded Postgres, /_x mounted
75
- bin/check # the gate: typecheck, lint, boundaries, tests, drift, budgets
105
+ bin/check # the gate: a static build, then typecheck, lint, boundaries, tests, drift, budgets
76
106
  \`\`\`
77
107
 
78
108
  \`packages/db/migrations\` starts empty and \`x db gen\` is its only writer — \`bin/setup\` runs
@@ -91,6 +121,7 @@ with \`X_DB_DRIFT\`, and that is the fix it names.
91
121
  | \`packages/*\` | domain, db, i18n, ui, mcp |
92
122
  | \`app.config.ts\` | the one config file |
93
123
  | \`x.manifest.json\` | generated facts: routes, actions, jobs, policies |
124
+ | \`.github/workflows/ci.yml\` | \`bin/setup\` then \`bin/check\`, on push and pull request |
94
125
  `;
95
126
 
96
127
  const binSetup = (): string => `#!/usr/bin/env bash
@@ -193,6 +224,10 @@ export function docsFiles(app: NameSet): readonly GeneratedFile[] {
193
224
  // The harness half of the same job AGENTS.md does. It lands in the app's own repo rather than
194
225
  // in a global config, so it is visible in the scaffold's diff and deletable in one line.
195
226
  ...claudeFiles(app),
227
+ // The same two commands `README.md` opens with, run by a machine that has never seen this
228
+ // repository. Registered here and not in `scaffold-repo.ts` because it is documentation of the
229
+ // gate in executable form, which is the job this file has.
230
+ ...githubFiles(app),
196
231
  ...containerFiles(app),
197
232
  ];
198
233
  }
@@ -6,9 +6,19 @@
6
6
  // (`packages/cli/src/guards.ts`). What was missing is any guard to discover. Four of the five are
7
7
  // here; the fifth, money-as-float, has no static signature and is answered by the `Money` type
8
8
  // instead — `scaffold-docs.ts` says so where an author reads it.
9
+ //
10
+ // The other five are about what the app is like to USE, which no row of `AGENTS.md` had ever been
11
+ // about: a control a keyboard cannot reach, a focus ring taken away and not replaced, an image with
12
+ // no box, an animation the compositor cannot run, and an island nobody has seen fail. Each is
13
+ // statically decidable from the file alone — the reason those five and not the many that are not.
9
14
 
15
+ import { animatedLayoutPropertyGuardFiles } from './guard-animated-layout-property';
10
16
  import { bareErrorGuardFiles } from './guard-bare-error';
17
+ import { focusVisibleGuardFiles } from './guard-focus-visible';
18
+ import { imageDimensionsGuardFiles } from './guard-image-dimensions';
19
+ import { islandWithoutStatesGuardFiles } from './guard-island-without-states';
11
20
  import { rawColourGuardFiles } from './guard-raw-colour';
21
+ import { semanticInteractiveGuardFiles } from './guard-semantic-interactive';
12
22
  import { untranslatedStringGuardFiles } from './guard-untranslated-string';
13
23
  import { unzonedDateGuardFiles } from './guard-unzoned-date';
14
24
  import type { GeneratedFile } from './naming';
@@ -19,8 +29,13 @@ import type { GeneratedFile } from './naming';
19
29
  * `x g guard <name>`, the same shape.
20
30
  */
21
31
  export const scaffoldGuardFiles = (): readonly GeneratedFile[] => [
32
+ ...animatedLayoutPropertyGuardFiles(),
22
33
  ...bareErrorGuardFiles(),
34
+ ...focusVisibleGuardFiles(),
35
+ ...imageDimensionsGuardFiles(),
36
+ ...islandWithoutStatesGuardFiles(),
23
37
  ...rawColourGuardFiles(),
38
+ ...semanticInteractiveGuardFiles(),
24
39
  ...untranslatedStringGuardFiles(),
25
40
  ...unzonedDateGuardFiles(),
26
41
  ];
@@ -5,6 +5,7 @@
5
5
  // container files scaffold-container.ts.
6
6
 
7
7
  import { ENV_EXAMPLE_PATH } from '@ultimat3/core';
8
+ import { REQUIRED_BUN } from '../app-root';
8
9
  import { VERIFY_FLOOR_FILE } from '../verify-floor';
9
10
  import type { VerifyStepName } from '../verify-step';
10
11
  import type { GeneratedFile, NameSet } from './naming';
@@ -26,6 +27,25 @@ import { uiPackageFiles } from './scaffold-ui-package';
26
27
  */
27
28
  const BIOME_VERSION = '2.5.8';
28
29
 
30
+ /**
31
+ * The checker a scaffolded app typechecks with, and it must not lag the one the FRAMEWORK is built
32
+ * and gated on: `@ultimat3/*` ships `.d.ts` emitted by this compiler, so an app pinned a major
33
+ * behind reads the types its own dependencies were written against through an older checker.
34
+ * It drifted for exactly the reason `BIOME_VERSION` did not — Biome's was spelled once as a
35
+ * constant and TypeScript's was a literal buried in a dependency block, so the framework moved to
36
+ * 7.x and every `x new` kept scaffolding `^6.0.3`. `scaffold-repo.test.ts` now pins this against
37
+ * the repo's own root `package.json`, so the next bump cannot leave the scaffold behind in silence.
38
+ */
39
+ const TYPESCRIPT_VERSION = '^7.0.2';
40
+
41
+ // `engines.bun` is `REQUIRED_BUN`, the floor the SHIPPED `x` enforces
42
+ // (`packages/cli/src/app-root.ts`), and not a second literal. It was one: `>=1.3.0`, a whole minor
43
+ // BELOW the CLI the app then runs, so on a box with Bun 1.3.x `bun install` succeeded and the very
44
+ // next line of `bin/setup` died `X_BUN_VERSION: Bun 1.3.14 is older than the required 1.4.0` — a
45
+ // floor admitting a runtime the app's own first command refuses. Interpolated, the two cannot
46
+ // disagree; `scripts/bun-pin.test.ts` reads the emitted string as one more pin site, and
47
+ // `github/ci.yml.ts` pins the workflow's Bun to the same constant.
48
+ //
29
49
  // `version` is not decoration: the manifest's app version IS the contract's compatibility gate,
30
50
  // and the manifest never fabricates one — so an app scaffolded without it failed `x manifest`,
31
51
  // the `manifest` verify step and every production boot with X_APP_PACKAGE_INVALID.
@@ -54,7 +74,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
54
74
  "@electric-sql/pglite": "^0.5.4",
55
75
  "@types/bun": "^1.4.0",
56
76
  "@ultimat3/testing": "^${version}",
57
- "typescript": "^6.0.3"
77
+ "typescript": "${TYPESCRIPT_VERSION}"
58
78
  },
59
79
  "dependencies": {
60
80
  "@ultimat3/action": "^${version}",
@@ -78,7 +98,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
78
98
  "solid-js": "1.9.14"
79
99
  },
80
100
  "engines": {
81
- "bun": ">=1.3.0"
101
+ "bun": ">=${REQUIRED_BUN}"
82
102
  }
83
103
  }
84
104
  `;