@ultimat3/cli 7.0.0 → 8.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.
Files changed (67) hide show
  1. package/CLAUDE.md +15 -1
  2. package/README.md +8 -3
  3. package/package.json +25 -25
  4. package/src/app-boundaries.ts +55 -5
  5. package/src/bin.ts +6 -3
  6. package/src/ci-log.ts +0 -0
  7. package/src/cmd-db-backfill.ts +240 -0
  8. package/src/cmd-db-branch.ts +3 -2
  9. package/src/cmd-db.ts +35 -156
  10. package/src/cmd-deploy.ts +37 -3
  11. package/src/cmd-dev.ts +7 -1
  12. package/src/cmd-errors.ts +2 -3
  13. package/src/cmd-fix.ts +3 -3
  14. package/src/cmd-i18n.ts +67 -5
  15. package/src/cmd-jobs.ts +27 -4
  16. package/src/cmd-mcp.ts +18 -9
  17. package/src/cmd-new.ts +91 -4
  18. package/src/cmd-policy.ts +3 -2
  19. package/src/cmd-pr.ts +55 -4
  20. package/src/cmd-registries.ts +3 -2
  21. package/src/cmd-shot.ts +68 -6
  22. package/src/cmd-tasks.ts +9 -4
  23. package/src/cmd-verify.ts +47 -6
  24. package/src/dev-cache.ts +1 -1
  25. package/src/dev-lock.ts +124 -12
  26. package/src/dev-queue.ts +12 -7
  27. package/src/dev-replicator.ts +3 -7
  28. package/src/dev-roles-fixture.ts +1 -1
  29. package/src/dev-roles.ts +40 -8
  30. package/src/dev-runtime.ts +96 -4
  31. package/src/dev-sync.ts +9 -4
  32. package/src/dispatch.ts +35 -5
  33. package/src/drift.ts +52 -7
  34. package/src/error-codes.ts +5 -0
  35. package/src/framework-scope.ts +57 -5
  36. package/src/generate-kinds.ts +19 -1
  37. package/src/i18n-registration.ts +67 -4
  38. package/src/index.ts +1 -1
  39. package/src/jobs-report.ts +10 -13
  40. package/src/mcp-errors.ts +3 -0
  41. package/src/messages.ts +12 -0
  42. package/src/output.ts +22 -2
  43. package/src/parse.ts +81 -37
  44. package/src/realtime-browser-probe-fixture.ts +9 -0
  45. package/src/runtime-overrides.ts +11 -3
  46. package/src/shot-settle.ts +57 -0
  47. package/src/shot-verdict.ts +27 -4
  48. package/src/sync-authenticator.ts +86 -14
  49. package/src/templates/guard-bare-error.ts +122 -0
  50. package/src/templates/guard-raw-colour.ts +138 -0
  51. package/src/templates/guard-untranslated-string.ts +138 -0
  52. package/src/templates/guard-unzoned-date.ts +142 -0
  53. package/src/templates/index.ts +3 -0
  54. package/src/templates/island.ts +2 -1
  55. package/src/templates/route.ts +1 -1
  56. package/src/templates/scaffold-app.ts +3 -82
  57. package/src/templates/scaffold-container.ts +30 -4
  58. package/src/templates/scaffold-db-package.ts +14 -6
  59. package/src/templates/scaffold-docs.ts +24 -13
  60. package/src/templates/scaffold-entries.ts +131 -0
  61. package/src/templates/scaffold-guards.ts +26 -0
  62. package/src/templates/scaffold-repo.ts +37 -6
  63. package/src/test-select.ts +4 -3
  64. package/src/verify-run.ts +25 -3
  65. package/src/verify-step.ts +11 -2
  66. package/src/verify-tests.ts +11 -3
  67. package/src/write-line.ts +23 -5
@@ -0,0 +1,138 @@
1
+ // The `raw-colour` guard `x new` ships: no stylesheet in this app names a colour.
2
+ // `AGENTS.md` has always stated the rule and NOTHING enforced it — `verify-checks.ts` said it rode
3
+ // on `packages/ui/src/tokens/tokens.test.ts`, which covers the framework's stylesheets and never
4
+ // the app's, so `color: #ff0000` in a scaffolded `page.module.scss` was green on `x verify`.
5
+
6
+ import { guardCode } from './guard';
7
+ import type { GeneratedFile } from './naming';
8
+
9
+ /**
10
+ * Derived from the guard's name, never written as a literal — the same rule `x g guard` follows.
11
+ * An `X_*` literal in framework source is a FRAMEWORK code: `error-catalog.test.ts` refuses one the
12
+ * registry does not hold, and `wiki/Error-Codes.md` would owe it a row. The APP owns the codes its
13
+ * own conventions raise, so this one is spelled by the file it lands in and nowhere else.
14
+ */
15
+ const NAME = 'raw-colour';
16
+ const CODE = guardCode(NAME);
17
+
18
+ const source =
19
+ (): string => `// raw-colour: every colour in this app is a semantic token, so dark theme is not a later project.
20
+ // \`x verify\` discovers every file in \`guards/\` and runs its \`guard\` inside the \`boundaries\`
21
+ // step — nothing registers this file, so nothing can forget to. Delete it to drop the rule.
22
+
23
+ import type { Finding, Guard } from '@ultimat3/cli';
24
+
25
+ /** The app owns the codes its own conventions raise — this one is named for the guard. */
26
+ const CODE = '${CODE}';
27
+
28
+ /** A hex literal. \`#{$x}\` is Sass interpolation, not a colour, and \`{\` is not a hex digit. */
29
+ const HEX = /#[0-9a-fA-F]{3,8}\\b/;
30
+ const CHANNEL_FUNCTION = /\\b(?:rgba?|hsla?|lab|lch|oklab|oklch|color)\\(/i;
31
+ /** The named colours a human actually types. The full CSS list would report \`.item\` selectors. */
32
+ const NAMED =
33
+ /\\b(?:white|black|red|green|blue|yellow|orange|purple|pink|brown|gray|grey|silver|navy|teal|olive|lime|aqua|maroon|fuchsia|gold|beige|coral|crimson|indigo|violet|khaki|salmon|tan|turquoise|wheat)\\b/i;
34
+
35
+ /**
36
+ * A DECLARATION, never a whole line: a selector carries no colon, so \`#hero { … }\` is not a value
37
+ * and is never reported. The value stops at the first \`;\`, \`{\` or \`}\`.
38
+ */
39
+ const DECLARATION = /([\\w-]+)\\s*:\\s*([^;{}]+)/g;
40
+
41
+ export interface StyleFile {
42
+ /** App-root-relative POSIX path, so the finding names the file an author opens. */
43
+ readonly path: string;
44
+ readonly scss: string;
45
+ }
46
+
47
+ /**
48
+ * Comments blanked rather than removed, so the reported line number still points at the source
49
+ * line. \`//\` is skipped when a \`:\` precedes it — \`url(https://…)\` is a value, not a comment.
50
+ */
51
+ const blankComments = (scss: string): string =>
52
+ scss
53
+ .replaceAll(/\\/\\*[\\s\\S]*?\\*\\//g, (match) => match.replaceAll(/[^\\n]/g, ' '))
54
+ .replaceAll(/(?<![:\\w])\\/\\/[^\\n]*/g, (match) => ' '.repeat(match.length));
55
+
56
+ /** Quoted text is a filename or a token name, never a colour: \`url('red.png')\`, \`role('bg')\`. */
57
+ const unquote = (value: string): string => value.replaceAll(/'[^']*'|"[^"]*"/g, ' ');
58
+
59
+ const lineOf = (text: string, index: number): number => text.slice(0, index).split('\\n').length;
60
+
61
+ /** Pure — the caller does the I/O — so the rule is testable without a filesystem. */
62
+ export function rawColours(files: readonly StyleFile[]): readonly Finding[] {
63
+ const findings: Finding[] = [];
64
+ for (const file of files) {
65
+ const scss = blankComments(file.scss);
66
+ for (const match of scss.matchAll(DECLARATION)) {
67
+ const property = match[1] ?? '';
68
+ const value = unquote(match[2] ?? '');
69
+ const literal = HEX.exec(value) ?? CHANNEL_FUNCTION.exec(value) ?? NAMED.exec(value);
70
+ if (literal === null) continue;
71
+ findings.push({
72
+ code: CODE,
73
+ cause: \`\${file.path}:\${lineOf(scss, match.index)} sets \${property} to the raw colour \${literal[0]} — a value no theme can restate, so dark theme renders it unchanged\`,
74
+ fix: \`replace \${literal[0]} in \${file.path} with tokens.role('fg'), tokens.role('bg') or the role this element means, then: x verify\`,
75
+ at: file.path,
76
+ });
77
+ }
78
+ }
79
+ return findings;
80
+ }
81
+
82
+ export const guard: Guard = {
83
+ summary: 'a stylesheet names a semantic token, never a colour',
84
+ async check(root) {
85
+ const files: StyleFile[] = [];
86
+ for await (const entry of new Bun.Glob('{apps,packages}/**/*.scss').scan({
87
+ cwd: root,
88
+ absolute: false,
89
+ })) {
90
+ const path = entry.split('\\\\').join('/');
91
+ if (path.includes('node_modules/')) continue;
92
+ files.push({ path, scss: await Bun.file(\`\${root}/\${path}\`).text() });
93
+ }
94
+ return rawColours(files);
95
+ },
96
+ };
97
+ `;
98
+
99
+ const test =
100
+ (): string => `// The rule, driven directly. Failure case first: a guard whose rule silently stopped matching is
101
+ // a green gate over the convention it was written to enforce.
102
+
103
+ import { expect, unitTest } from '@ultimat3/testing';
104
+ import { rawColours } from './raw-colour';
105
+
106
+ const sheet = (scss: string) => [{ path: 'apps/web/site/page.module.scss', scss }];
107
+
108
+ unitTest('a hex literal in a declaration is refused', () => {
109
+ const findings = rawColours(sheet('.hero {\\n color: #ff0000;\\n}\\n'));
110
+ expect(findings).toHaveLength(1);
111
+ expect(findings[0]?.code).toBe('${CODE}');
112
+ expect(findings[0]?.cause).toContain('#ff0000');
113
+ expect(findings[0]?.cause).toContain(':2');
114
+ });
115
+
116
+ unitTest('rgb(), hsl() and a named colour are the same rule', () => {
117
+ expect(rawColours(sheet('.a { background: rgb(1 2 3); }'))).toHaveLength(1);
118
+ expect(rawColours(sheet('.a { background: hsl(1 2% 3%); }'))).toHaveLength(1);
119
+ expect(rawColours(sheet('.a { border-color: white; }'))).toHaveLength(1);
120
+ });
121
+
122
+ unitTest('a token, a selector and a quoted filename are not colours', () => {
123
+ expect(rawColours(sheet(".a { background: tokens.role('bg'); }"))).toEqual([]);
124
+ expect(rawColours(sheet('#hero { padding: 0; }'))).toEqual([]);
125
+ expect(rawColours(sheet(".a { background: url('red-hero.png'); }"))).toEqual([]);
126
+ });
127
+
128
+ unitTest('a commented-out colour is a note, not a declaration', () => {
129
+ expect(rawColours(sheet('// color: #ff0000;\\n.a { padding: 0; }'))).toEqual([]);
130
+ expect(rawColours(sheet('/* color: #ff0000; */\\n.a { padding: 0; }'))).toEqual([]);
131
+ });
132
+ `;
133
+
134
+ /** `guards/raw-colour.ts` and its test. No index, no registry — the directory is the registration. */
135
+ export const rawColourGuardFiles = (): readonly GeneratedFile[] => [
136
+ { path: 'guards/raw-colour.ts', contents: source() },
137
+ { path: 'guards/raw-colour.test.ts', contents: test() },
138
+ ];
@@ -0,0 +1,138 @@
1
+ // The `untranslated-string` guard `x new` ships: no user-facing string is typed into a page.
2
+ // `AGENTS.md` has always stated the rule and NOTHING enforced it — a hardcoded JSX string sitting
3
+ // beside a `t()` call in a scaffolded page was green on `x verify`, and `x i18n check` cannot see
4
+ // it either: a literal that is in no catalog is a literal the catalog audit has no key for.
5
+
6
+ import { guardCode } from './guard';
7
+ import type { GeneratedFile } from './naming';
8
+
9
+ /**
10
+ * Derived from the guard's name, never written as a literal — the same rule `x g guard` follows.
11
+ * An `X_*` literal in framework source is a FRAMEWORK code: `error-catalog.test.ts` refuses one the
12
+ * registry does not hold, and `wiki/Error-Codes.md` would owe it a row. The APP owns the codes its
13
+ * own conventions raise, so this one is spelled by the file it lands in and nowhere else.
14
+ */
15
+ const NAME = 'untranslated-string';
16
+ const CODE = guardCode(NAME);
17
+
18
+ const source =
19
+ (): string => `// untranslated-string: every user-facing string on a rendered surface goes through \`t()\`.
20
+ // \`x verify\` discovers every file in \`guards/\` and runs its \`guard\` inside the \`boundaries\`
21
+ // step — nothing registers this file, so nothing can forget to.
22
+
23
+ import type { Finding, Guard } from '@ultimat3/cli';
24
+
25
+ /** The app owns the codes its own conventions raise — this one is named for the guard. */
26
+ const CODE = '${CODE}';
27
+
28
+ /**
29
+ * \`<tag …>text</tag>\`, matched on the CLOSING tag rather than on the next \`<\`.
30
+ *
31
+ * That is the whole reason this rule can run over TypeScript at all: \`createSignal<State>('idle')\`
32
+ * is a \`>\` followed by prose-shaped source, and a pattern reading to the next \`<\` reports every
33
+ * generic in the file. A closing tag that names the same element cannot be a type argument.
34
+ *
35
+ * Only the INNERMOST element matches — the content class excludes \`<\` and \`>\` — which is what the
36
+ * rule wants: a parent whose children are elements has no text of its own.
37
+ */
38
+ const ELEMENT = /<([A-Za-z][\\w.:-]*)(?:\\s[^<>]*)?>([^<>]*?)<\\/\\1>/g;
39
+ /** A \`{…}\` child is an expression — \`{t('key')}\`, \`{props.row.title}\` — never typed prose. */
40
+ const EXPRESSION = /\\{[^{}]*\\}/g;
41
+ /** Two word characters in a row. One is \`&\`, \`×\`, an initial — never a sentence. */
42
+ const PROSE = /[\\p{L}\\p{N}]{2,}/u;
43
+
44
+ export interface SourceFile {
45
+ /** App-root-relative POSIX path, so the finding names the file an author opens. */
46
+ readonly path: string;
47
+ readonly source: string;
48
+ }
49
+
50
+ /** Comments blanked IN PLACE — not deleted — so a reported line number still points at the source. */
51
+ const blank = (text: string): string =>
52
+ text
53
+ .replaceAll(/\\/\\*[\\s\\S]*?\\*\\//g, (match) => match.replaceAll(/[^\\n]/g, ' '))
54
+ .replaceAll(/(?<![:\\w])\\/\\/[^\\n]*/g, (match) => ' '.repeat(match.length));
55
+
56
+ const lineOf = (text: string, index: number): number => text.slice(0, index).split('\\n').length;
57
+
58
+ /** Pure — the caller does the I/O — so the rule is testable without a filesystem. */
59
+ export function untranslatedStrings(files: readonly SourceFile[]): readonly Finding[] {
60
+ const findings: Finding[] = [];
61
+ for (const file of files) {
62
+ const text = blank(file.source);
63
+ for (const match of text.matchAll(ELEMENT)) {
64
+ const typed = (match[2] ?? '').replaceAll(EXPRESSION, ' ').trim();
65
+ if (!PROSE.test(typed)) continue;
66
+ findings.push({
67
+ code: CODE,
68
+ cause: \`\${file.path}:\${lineOf(text, match.index)} renders the typed string "\${typed}" inside <\${match[1] ?? 'element'}> — it is in no catalog, so every locale but the one it was typed in reads it verbatim\`,
69
+ fix: \`add a key for "\${typed}" to packages/i18n/catalogs/en.json, render it as {t('…')} in \${file.path}, then: x i18n check\`,
70
+ at: file.path,
71
+ });
72
+ }
73
+ }
74
+ return findings;
75
+ }
76
+
77
+ export const guard: Guard = {
78
+ summary: 'a rendered string comes from t(), never typed into the page',
79
+ async check(root) {
80
+ const files: SourceFile[] = [];
81
+ // Every rendered surface: an app's \`site/\` and \`app/\`, and the shared components under
82
+ // \`packages/*/src\` — \`x new\` scaffolds a \`packages/ui\` whose components render to a user, so
83
+ // a hardcoded string there used to be green. \`api/\` renders nothing and \`shared/\` is a leaf of
84
+ // helpers; \`packages/*/dist\` is a build output, not source.
85
+ //
86
+ // TWO globs, never one with a leading \`{a,b}\` group: \`Bun.Glob.scan()\` matches nothing at all
87
+ // for a pattern that starts with a brace group — measured — so folding these into one line
88
+ // silently turns the guard off, which is worse than the hole it closes.
89
+ for (const pattern of ['apps/*/{site,app}/**/*.tsx', 'packages/*/src/**/*.tsx']) {
90
+ for await (const entry of new Bun.Glob(pattern).scan({ cwd: root, absolute: false })) {
91
+ const path = entry.split('\\\\').join('/');
92
+ if (path.includes('node_modules/') || /\\.test\\.tsx?$/.test(path)) continue;
93
+ files.push({ path, source: await Bun.file(\`\${root}/\${path}\`).text() });
94
+ }
95
+ }
96
+ return untranslatedStrings(files);
97
+ },
98
+ };
99
+ `;
100
+
101
+ const test =
102
+ (): string => `// The rule, driven directly. Failure case first: a guard whose rule silently stopped matching is
103
+ // a green gate over the convention it was written to enforce.
104
+
105
+ import { expect, unitTest } from '@ultimat3/testing';
106
+ import { untranslatedStrings } from './untranslated-string';
107
+
108
+ const file = (source: string) => [{ path: 'apps/web/site/page.tsx', source }];
109
+
110
+ unitTest('a typed JSX string is refused, and the finding quotes it', () => {
111
+ const findings = untranslatedStrings(file('<h1>Welcome back</h1>'));
112
+ expect(findings).toHaveLength(1);
113
+ expect(findings[0]?.code).toBe('${CODE}');
114
+ expect(findings[0]?.cause).toContain('Welcome back');
115
+ });
116
+
117
+ unitTest('a t() child satisfies it, and so does any other expression', () => {
118
+ expect(untranslatedStrings(file("<h1>{t('site.home.title')}</h1>"))).toEqual([]);
119
+ expect(untranslatedStrings(file('<li class={styles.item}>{row.title}</li>'))).toEqual([]);
120
+ });
121
+
122
+ // The reason the rule reads a CLOSING tag: a generic type argument is a > followed by source that
123
+ // looks exactly like prose, and a pattern reading to the next < reports every one of them.
124
+ unitTest('a generic type argument is not a JSX text node', () => {
125
+ const generic = "const [state, setState] = createSignal<SaveState>('idle');\\nconst n = 1;";
126
+ expect(untranslatedStrings(file(generic))).toEqual([]);
127
+ });
128
+
129
+ unitTest('one word character is a symbol, not a sentence', () => {
130
+ expect(untranslatedStrings(file('<span>&</span>'))).toEqual([]);
131
+ });
132
+ `;
133
+
134
+ /** `guards/untranslated-string.ts` and its test. The directory is the registration. */
135
+ export const untranslatedStringGuardFiles = (): readonly GeneratedFile[] => [
136
+ { path: 'guards/untranslated-string.ts', contents: source() },
137
+ { path: 'guards/untranslated-string.test.ts', contents: test() },
138
+ ];
@@ -0,0 +1,142 @@
1
+ // The `unzoned-date` guard `x new` ships: no date is formatted without an explicit IANA zone.
2
+ // `AGENTS.md` has always stated the rule and NOTHING enforced it — `toLocaleDateString('en-US')`
3
+ // in a scaffolded page was green on `x verify`, and it renders the SERVER's zone, so the same row
4
+ // reads as two different days depending on which container answered.
5
+
6
+ import { guardCode } from './guard';
7
+ import type { GeneratedFile } from './naming';
8
+
9
+ /**
10
+ * Derived from the guard's name, never written as a literal — the same rule `x g guard` follows.
11
+ * An `X_*` literal in framework source is a FRAMEWORK code: `error-catalog.test.ts` refuses one the
12
+ * registry does not hold, and `wiki/Error-Codes.md` would owe it a row. The APP owns the codes its
13
+ * own conventions raise, so this one is spelled by the file it lands in and nowhere else.
14
+ */
15
+ const NAME = 'unzoned-date';
16
+ const CODE = guardCode(NAME);
17
+
18
+ const source =
19
+ (): string => `// unzoned-date: a date is stored in UTC and formatted with an explicit IANA zone, never with the
20
+ // host's ambient one. \`x verify\` discovers every file in \`guards/\` and runs its \`guard\` inside
21
+ // the \`boundaries\` step — nothing registers this file, so nothing can forget to.
22
+
23
+ import type { Finding, Guard } from '@ultimat3/cli';
24
+
25
+ /** The app owns the codes its own conventions raise — this one is named for the guard. */
26
+ const CODE = '${CODE}';
27
+
28
+ /** Every call whose output depends on a zone. The \`(\` is the start of the argument list. */
29
+ const FORMATTER = /(?:\\.toLocale(?:Date|Time)?String|Intl\\.DateTimeFormat)\\s*\\(/g;
30
+
31
+ export interface SourceFile {
32
+ /** App-root-relative POSIX path, so the finding names the file an author opens. */
33
+ readonly path: string;
34
+ readonly source: string;
35
+ }
36
+
37
+ /**
38
+ * Comments blanked IN PLACE — not deleted — so the line number a finding reports still points at
39
+ * the source line. What it does not blank is a string body: a call spelled inside a quoted string
40
+ * is reported, which is the one false positive this rule can produce and the reason its own test
41
+ * lives in \`guards/\`, which nothing here scans.
42
+ */
43
+ const blank = (text: string): string =>
44
+ text
45
+ .replaceAll(/\\/\\*[\\s\\S]*?\\*\\//g, (match) => match.replaceAll(/[^\\n]/g, ' '))
46
+ .replaceAll(/(?<![:\\w])\\/\\/[^\\n]*/g, (match) => ' '.repeat(match.length));
47
+
48
+ /**
49
+ * The call's own argument list, from its \`(\` to the \`)\` that closes it. Depth-counted rather
50
+ * than "up to the next \`)\`": \`toLocaleDateString(locale, { timeZone: zoneFor(x) })\` closes twice
51
+ * before the one that ends the call, and reading only the first would report a zoned call.
52
+ */
53
+ function argumentsOf(text: string, open: number): string {
54
+ let depth = 0;
55
+ for (let index = open; index < text.length; index += 1) {
56
+ const char = text[index];
57
+ if (char === '(') depth += 1;
58
+ else if (char === ')') {
59
+ depth -= 1;
60
+ if (depth === 0) return text.slice(open + 1, index);
61
+ }
62
+ }
63
+ return text.slice(open + 1);
64
+ }
65
+
66
+ const lineOf = (text: string, index: number): number => text.slice(0, index).split('\\n').length;
67
+
68
+ /** Pure — the caller does the I/O — so the rule is testable without a filesystem. */
69
+ export function unzonedDates(files: readonly SourceFile[]): readonly Finding[] {
70
+ const findings: Finding[] = [];
71
+ for (const file of files) {
72
+ const text = blank(file.source);
73
+ for (const match of text.matchAll(FORMATTER)) {
74
+ const open = match.index + match[0].length - 1;
75
+ if (argumentsOf(text, open).includes('timeZone')) continue;
76
+ const line = lineOf(text, match.index);
77
+ findings.push({
78
+ code: CODE,
79
+ cause: \`\${file.path}:\${line} calls \${match[0].trim()}) with no timeZone — it formats in whatever zone the process happens to run in, so one row reads as two different days across two containers\`,
80
+ fix: \`pass an explicit IANA zone in \${file.path} — toLocaleDateString(locale, { timeZone: 'UTC' }) — then: x verify\`,
81
+ at: file.path,
82
+ });
83
+ }
84
+ }
85
+ return findings;
86
+ }
87
+
88
+ export const guard: Guard = {
89
+ summary: 'a date is never formatted without an explicit IANA time zone',
90
+ async check(root) {
91
+ const files: SourceFile[] = [];
92
+ for await (const entry of new Bun.Glob('{apps,packages}/**/*.{ts,tsx}').scan({
93
+ cwd: root,
94
+ absolute: false,
95
+ })) {
96
+ const path = entry.split('\\\\').join('/');
97
+ // A test's subject is often the wrong shape on purpose, and \`node_modules\` is not this
98
+ // app's source. Neither exclusion hides a rendered date from a user.
99
+ if (path.includes('node_modules/') || /\\.(?:test|d)\\.tsx?$/.test(path)) continue;
100
+ files.push({ path, source: await Bun.file(\`\${root}/\${path}\`).text() });
101
+ }
102
+ return unzonedDates(files);
103
+ },
104
+ };
105
+ `;
106
+
107
+ const test =
108
+ (): string => `// The rule, driven directly. Failure case first: a guard whose rule silently stopped matching is
109
+ // a green gate over the convention it was written to enforce.
110
+
111
+ import { expect, unitTest } from '@ultimat3/testing';
112
+ import { unzonedDates } from './unzoned-date';
113
+
114
+ const file = (source: string) => [{ path: 'apps/web/app/dashboard/page.tsx', source }];
115
+
116
+ unitTest('toLocaleDateString with no timeZone is refused', () => {
117
+ const findings = unzonedDates(file("const shown = at.toLocaleDateString('en-US');"));
118
+ expect(findings).toHaveLength(1);
119
+ expect(findings[0]?.code).toBe('${CODE}');
120
+ expect(findings[0]?.fix).toContain('timeZone');
121
+ });
122
+
123
+ unitTest('an explicit zone satisfies it, even nested behind another call', () => {
124
+ const zoned = "at.toLocaleDateString('en-US', { timeZone: zoneFor(actor) });";
125
+ expect(unzonedDates(file(zoned))).toEqual([]);
126
+ });
127
+
128
+ unitTest('Intl.DateTimeFormat and toLocaleTimeString are the same rule', () => {
129
+ expect(unzonedDates(file("new Intl.DateTimeFormat('en-US').format(at);"))).toHaveLength(1);
130
+ expect(unzonedDates(file("at.toLocaleTimeString('en-US');"))).toHaveLength(1);
131
+ });
132
+
133
+ unitTest('a commented-out call is a note, not a call', () => {
134
+ expect(unzonedDates(file("// at.toLocaleDateString('en-US');"))).toEqual([]);
135
+ });
136
+ `;
137
+
138
+ /** `guards/unzoned-date.ts` and its test. No index, no registry — the directory registers it. */
139
+ export const unzonedDateGuardFiles = (): readonly GeneratedFile[] => [
140
+ { path: 'guards/unzoned-date.ts', contents: source() },
141
+ { path: 'guards/unzoned-date.test.ts', contents: test() },
142
+ ];
@@ -39,6 +39,9 @@ export { claudeAgentFiles } from './scaffold-claude-agents';
39
39
  export { claudeCommandFiles } from './scaffold-claude-commands';
40
40
  export { containerFiles } from './scaffold-container';
41
41
  export { docsFiles, EXECUTABLE_FILES } from './scaffold-docs';
42
+ export { entryFiles } from './scaffold-entries';
43
+ // The four guards `x new` ships, distinct from `guardFiles` above, which is `x g guard <name>`.
44
+ export { scaffoldGuardFiles } from './scaffold-guards';
42
45
  export { i18nIndex } from './scaffold-i18n';
43
46
  export { repoFiles } from './scaffold-repo';
44
47
  export type { SliceModule } from './slice-foundation';
@@ -78,7 +78,8 @@ export function mount(el: HTMLElement, props: ${Name}Props): void {
78
78
  `;
79
79
  };
80
80
 
81
- const islandStyle = (): string => `// Semantic tokens only — a raw hex here is a dark-theme bug and
81
+ const islandStyle =
82
+ (): string => `// Semantic tokens only — a raw hex here is refused by the boundaries step, not by lint — a dark-theme bug and
82
83
  // a lint failure. Scoped by the island build, with the class names the server hashed.
83
84
  @use '@ultimat3/ui/tokens' as tokens;
84
85
 
@@ -138,7 +138,7 @@ export function ${name}Page() {${translatorBinding(module)}
138
138
  };
139
139
 
140
140
  const styleSource =
141
- (): string => `// Semantic tokens only — a raw hex here is a dark-theme bug and a lint failure.
141
+ (): string => `// Semantic tokens only — a raw hex here is refused by the boundaries step (guards/raw-colour.ts), not by lint — a dark-theme bug in every scheme but the one it was written in.
142
142
  @use '@ultimat3/ui/tokens' as tokens;
143
143
 
144
144
  .page {
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { GeneratedFile, NameSet } from './naming';
6
6
  import { apiFiles } from './scaffold-api';
7
+ import { entryFiles } from './scaffold-entries';
7
8
  import { icon } from './scaffold-icon';
8
9
  import { rolesFiles } from './scaffold-roles';
9
10
 
@@ -349,86 +350,6 @@ export function AdminHome() {
349
350
  }
350
351
  `;
351
352
 
352
- // The two entry files a deploy needs. Both are deliberately thin: which role a container is, which
353
- // port it binds, how it drains and what a static build enumerates are the framework's answers, so
354
- // an upgrade moves them without a codemod in every app that ever shipped.
355
-
356
- const server =
357
- (): string => `// The production entry. \`docker/Dockerfile\` starts this, and \`x build --target binary\` compiles it.
358
- // ROLE selects what this process is — web, sync, worker, scheduler, replicator, or migrate, which
359
- // applies the migrations and exits. PORT is bound on every interface, because a container bound to
360
- // localhost is unreachable through its own port mapping.
361
-
362
- import { join } from 'node:path';
363
- import { runRole } from '@ultimat3/cli';
364
-
365
- // MORE THAN ONE REPLICA? Add these two lines, above \`runRole\`:
366
- //
367
- // import { configureIdempotency } from '@ultimat3/action';
368
- // configureIdempotency({ scope: 'shared' });
369
- //
370
- // \`idempotent: true\` on an action promises that a retry does not repeat the work. Under the
371
- // process-scoped default that promise holds inside ONE process — a client retrying
372
- // \`POST /api/payments/charge\` after a timeout lands on another replica, which has never seen the
373
- // key, and charges the card twice, silently, with \`x verify\` green. Declaring \`'shared'\` is what
374
- // makes that a boot error (\`X_IDEMPOTENCY_NOT_SHARED\`) unless a shared store is installed.
375
- // \`runRole\` installs the Postgres one for you, on the connection it resolved from \`DATABASE_URL\`,
376
- // so the declaration is all this app owes. It must run before \`runRole\` imports the actions.
377
-
378
- /**
379
- * Where the app is. From this file normally — the image's WORKDIR is not the app root's business.
380
- * A \`--compile\` binary is the exception: its \`import.meta.dir\` is Bun's virtual filesystem, which
381
- * holds this module's bundled imports and none of the app's source, and the framework's registries
382
- * are filled by scanning that source at boot. So a binary reads its root from the directory it is
383
- * started in — it is a launcher for an app tree, not a self-contained copy of one.
384
- */
385
- const root = import.meta.dir.startsWith('/$bunfs')
386
- ? process.cwd()
387
- : join(import.meta.dir, '..', '..');
388
-
389
- // Guarded, because the framework's module scan imports every file under apps/*/ to fill its
390
- // registries — an unguarded boot would start a server inside \`x verify\`.
391
- if (import.meta.main) {
392
- await runRole({ root, env: Bun.env });
393
- }
394
- `;
395
-
396
- const prerender =
397
- (): string => `// The static entry. \`x build --target static\` runs this with \`--out <dir>\` and it writes one HTML
398
- // file per \`render: 'static'\` route — a CDN or an object store then serves site/ with no process
399
- // behind it. Every other render mode needs a running app and is reported as skipped, never emitted.
400
- //
401
- // Skipped is not unweighed: a route that declares a \`budget:\` is rendered in memory and measured
402
- // whatever its mode, so \`x verify\`'s \`budgets\` step has a number for it. \`unmeasured\` is the list
403
- // this build could not render — each one is an X_BUDGET_UNMEASURED at the gate, and this is where
404
- // the reason is.
405
- //
406
- // It writes the whole of \`pages\` and \`skipped\`, never a COUNT of either. A count is what let a
407
- // partial artifact read as a complete one: someone pointed a screenshot tool at \`.x/static\` and
408
- // filed "the island did not mount" against a route that had never been emitted (issue #242). Each
409
- // skipped route carries its own \`reason\` and \`why\`, and \`report\` is where the same inventory
410
- // landed on disk — which is what \`x build --target static --json\` reads back.
411
-
412
- import { join } from 'node:path';
413
- import { prerenderSite } from '@ultimat3/cli';
414
-
415
- const root = join(import.meta.dir, '..', '..');
416
- const flag = Bun.argv.indexOf('--out');
417
- const out = (flag === -1 ? undefined : Bun.argv[flag + 1]) ?? join(root, '.x', 'static');
418
- // SITE_ORIGIN is what canonical and og:url are built from; the default is only ever a local build.
419
- // Property access, not \`Bun.env['SITE_ORIGIN']\`: the scaffolded tsconfig does not set
420
- // \`noPropertyAccessFromIndexSignature\`, so the bracket form is the one biome's useLiteralKeys
421
- // reports — a diagnostic in an app's first lint run over a file the app never wrote.
422
- const origin = Bun.env.SITE_ORIGIN;
423
-
424
- if (import.meta.main) {
425
- const report = await prerenderSite({ root, out, ...(origin === undefined ? {} : { origin }) });
426
- await Bun.stdout.write(
427
- \`\${JSON.stringify({ ok: true, out: report.out, emitted: report.pages, skipped: report.skipped, unmeasured: report.unmeasured, report: report.report })}\\n\`,
428
- );
429
- }
430
- `;
431
-
432
353
  const placeholder = (surface: string, app: NameSet): string => `# ${surface}
433
354
 
434
355
  Placeholder. The monorepo shape exists now so adding ${surface} later is a new directory, not a
@@ -447,8 +368,8 @@ export function appFiles(app: NameSet, example: boolean): readonly GeneratedFile
447
368
  return [
448
369
  { path: 'apps/web/package.json', contents: webPackage(app) },
449
370
  { path: 'apps/web/tsconfig.json', contents: tsconfig() },
450
- { path: 'apps/web/server.ts', contents: server() },
451
- { path: 'apps/web/prerender.ts', contents: prerender() },
371
+ // The process a container starts and the artifact a CDN is handed — `scaffold-entries.ts`.
372
+ ...entryFiles(),
452
373
  { path: 'apps/web/site/icon.png', contents: icon() },
453
374
  { path: 'apps/web/site/page.tsx', contents: sitePage(app) },
454
375
  { path: 'apps/web/site/page.module.scss', contents: siteStyle() },
@@ -52,8 +52,10 @@ ENV NODE_ENV=production \\
52
52
  # own port mapping, its load balancer and every health probe alike.
53
53
  EXPOSE 3000
54
54
 
55
- # Every role serves /healthz and /readyz. /readyz flips to 503 on SIGTERM *before* the socket
56
- # closes, so a rolling restart drains in-flight work instead of dropping it.
55
+ # The probe for the roles that SERVE HTTP — \`web\` and \`sync\`. /readyz flips to 503 on SIGTERM
56
+ # *before* the socket closes, so a rolling restart drains in-flight work instead of dropping it.
57
+ # Every other role opens the scrape listener alone and never binds $PORT, so each one overrides
58
+ # this in docker/docker-compose.prod.yml rather than reporting \`unhealthy\` for its whole life.
57
59
  HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 CMD \\
58
60
  bun --eval "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/readyz').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"
59
61
 
@@ -120,6 +122,22 @@ x-image: &image
120
122
  depends_on:
121
123
  db: { condition: service_healthy }
122
124
 
125
+ # The image's own HEALTHCHECK fetches \`/readyz\` on $PORT, and only \`web\` and \`sync\` open an HTTP
126
+ # socket — every other role gets the scrape listener and nothing else. A service that inherits that
127
+ # probe is fetching a port it never binds: it reports \`unhealthy\` for its whole life and anything
128
+ # gated on it never starts. Probes follow the role here, exactly as they do in \`docker/helm\`.
129
+ x-metrics-probe: &metrics-probe
130
+ test: ['CMD', 'bun', '--eval', "fetch('http://127.0.0.1:'+(process.env.METRICS_PORT||9090)+'/metrics').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
131
+ interval: 10s
132
+ timeout: 3s
133
+ start_period: 30s
134
+ retries: 3
135
+
136
+ # Run-once services exit. A probe against an exited container reports \`unhealthy\` forever, and
137
+ # nothing waits on their health — \`service_completed_successfully\` is what the others gate on.
138
+ x-run-once-probe: &run-once-probe
139
+ disable: true
140
+
123
141
  services:
124
142
  db:
125
143
  image: postgres:17-alpine
@@ -138,6 +156,7 @@ services:
138
156
  <<: *image
139
157
  environment: [ROLE=migrate]
140
158
  restart: 'no'
159
+ healthcheck: *run-once-probe
141
160
 
142
161
  # Run-once, AFTER the new version serves. Deliberately NOT part of the release gate: a slow
143
162
  # UPDATE there holds the deploy open against a database still serving the previous version.
@@ -158,6 +177,7 @@ services:
158
177
  # listing this last would only look like "after". The image's HEALTHCHECK is what makes it true.
159
178
  web: { condition: service_healthy }
160
179
  restart: 'no'
180
+ healthcheck: *run-once-probe
161
181
 
162
182
  web:
163
183
  <<: *image
@@ -184,6 +204,7 @@ services:
184
204
  depends_on:
185
205
  db: { condition: service_healthy }
186
206
  migrate: { condition: service_completed_successfully }
207
+ healthcheck: *metrics-probe
187
208
  deploy: { replicas: 1 } # scales on queue depth
188
209
 
189
210
  scheduler:
@@ -192,7 +213,12 @@ services:
192
213
  depends_on:
193
214
  db: { condition: service_healthy }
194
215
  migrate: { condition: service_completed_successfully }
195
- deploy: { replicas: 1 } # fixed 1; leadership is a Postgres advisory lock
216
+ # Fixed 1. Leadership is an EXPIRING LEASE ROW in \`x_scheduler_leader\` (dev-roles.ts,
217
+ # driver-pg-ddl.ts), NOT an advisory lock: that grant belongs to the session, not to the
218
+ # process — it outlives every transaction and no pooled node can renew it or prove it still
219
+ # holds one. A second instance is harmless but idle.
220
+ healthcheck: *metrics-probe
221
+ deploy: { replicas: 1 }
196
222
 
197
223
  volumes:
198
224
  pgdata:
@@ -208,7 +234,7 @@ nothing to rebuild between staging and production.
208
234
  | \`web\` | HTTP: pages, actions, assets | \`$PORT\` (default 3000) |
209
235
  | \`sync\` | websockets for live queries | \`$PORT + 1\` |
210
236
  | \`worker\` | the job queue | — |
211
- | \`scheduler\` | cron tasks; leadership is a Postgres advisory lock | — |
237
+ | \`scheduler\` | cron tasks; leadership is an expiring lease row in \`x_scheduler_leader\` | — |
212
238
  | \`replicator\` | the logical replication slot, exactly one per database | — |
213
239
  | \`migrate\` | applies pending migrations and **exits** | — |
214
240