@ultimat3/cli 22.15.0 → 24.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 (267) hide show
  1. package/CLAUDE.md +36 -13
  2. package/README.md +31 -8
  3. package/package.json +29 -28
  4. package/src/admin-catalog-keys.ts +16 -0
  5. package/src/admin-registration.ts +192 -0
  6. package/src/affected.ts +27 -35
  7. package/src/api-registration.ts +125 -11
  8. package/src/app-artifacts.ts +1 -1
  9. package/src/app-auth.ts +4 -27
  10. package/src/app-boundaries.ts +6 -20
  11. package/src/app-config-load.ts +51 -0
  12. package/src/app-env.ts +5 -11
  13. package/src/app-load.ts +55 -11
  14. package/src/app-manifest.ts +16 -8
  15. package/src/app-mcp.ts +10 -23
  16. package/src/app-permissions-borrowed.ts +129 -0
  17. package/src/app-permissions-site.ts +24 -0
  18. package/src/app-permissions.ts +0 -0
  19. package/src/app-reload-graph.ts +7 -30
  20. package/src/app-transport.ts +219 -0
  21. package/src/boundary-findings.ts +16 -4
  22. package/src/browser-transport.ts +160 -0
  23. package/src/budgets.ts +27 -34
  24. package/src/cdp-shot-driver.ts +3 -1
  25. package/src/cmd-auth-spec.ts +16 -0
  26. package/src/cmd-auth.ts +65 -0
  27. package/src/cmd-build-spec.ts +10 -3
  28. package/src/cmd-build.ts +107 -11
  29. package/src/cmd-db-backfill.ts +25 -12
  30. package/src/cmd-db.ts +66 -16
  31. package/src/cmd-deploy-helm.ts +7 -6
  32. package/src/cmd-deploy.ts +4 -4
  33. package/src/cmd-dev.ts +9 -8
  34. package/src/cmd-doctor-spec.ts +2 -9
  35. package/src/cmd-doctor.ts +75 -24
  36. package/src/cmd-env.ts +26 -2
  37. package/src/cmd-generate.ts +195 -47
  38. package/src/cmd-help.ts +19 -6
  39. package/src/cmd-i18n.ts +10 -9
  40. package/src/cmd-jobs-spec.ts +19 -3
  41. package/src/cmd-jobs.ts +136 -7
  42. package/src/cmd-manifest-spec.ts +1 -1
  43. package/src/cmd-manifest.ts +6 -25
  44. package/src/cmd-mcp.ts +46 -15
  45. package/src/cmd-new.ts +15 -1
  46. package/src/cmd-planned.ts +15 -8
  47. package/src/cmd-routes.ts +27 -3
  48. package/src/cmd-secrets-ring.ts +76 -0
  49. package/src/cmd-secrets-spec.ts +10 -2
  50. package/src/cmd-secrets.ts +95 -18
  51. package/src/cmd-shot-matrix.ts +1 -3
  52. package/src/cmd-shot.ts +12 -3
  53. package/src/cmd-tasks.ts +84 -15
  54. package/src/cmd-test-spec.ts +1 -7
  55. package/src/cmd-test.ts +13 -29
  56. package/src/cmd-verify.ts +13 -5
  57. package/src/comment-safe.ts +16 -0
  58. package/src/coverage-floor.ts +334 -0
  59. package/src/coverage-lcov.ts +193 -0
  60. package/src/coverage-source.ts +136 -0
  61. package/src/db-object-drift.ts +30 -0
  62. package/src/db-replay-engine.ts +115 -0
  63. package/src/db-schema-dump.ts +150 -0
  64. package/src/db-schema-refresh.ts +122 -0
  65. package/src/db-subscribes.ts +12 -4
  66. package/src/dev-dashboard-guard.ts +76 -0
  67. package/src/dev-dashboard.ts +51 -29
  68. package/src/dev-lock.ts +4 -7
  69. package/src/dev-route-table.ts +9 -0
  70. package/src/dispatch.ts +3 -6
  71. package/src/doctor-app-url.ts +39 -0
  72. package/src/doctor-auth.ts +95 -0
  73. package/src/doctor-coverage.ts +39 -0
  74. package/src/doctor-guards.ts +54 -0
  75. package/src/doctor-sealed.ts +132 -0
  76. package/src/document-graph.ts +82 -0
  77. package/src/drift-replica-identity.ts +42 -0
  78. package/src/error-catalog.ts +6 -9
  79. package/src/error-codes.ts +55 -3
  80. package/src/error-unthrown.ts +29 -11
  81. package/src/errors.ts +54 -83
  82. package/src/framework-schema-apply.ts +78 -0
  83. package/src/framework-schema-stamp.ts +96 -0
  84. package/src/framework-schema.ts +28 -2
  85. package/src/generate-files.ts +79 -12
  86. package/src/generate-grant-findings.ts +76 -0
  87. package/src/generate-grants.ts +77 -3
  88. package/src/generate-kinds.ts +52 -1
  89. package/src/generate-shadow.ts +46 -0
  90. package/src/generate-write.ts +62 -40
  91. package/src/generated-imports.ts +59 -0
  92. package/src/gh-target.ts +12 -7
  93. package/src/guard-sources.ts +104 -0
  94. package/src/guards.ts +23 -7
  95. package/src/handle-registration.ts +217 -0
  96. package/src/i18n-audit.ts +4 -1
  97. package/src/i18n-registration.ts +36 -0
  98. package/src/image-prepare.ts +51 -8
  99. package/src/import-closure.ts +165 -0
  100. package/src/index.ts +12 -5
  101. package/src/island-bundle.ts +3 -15
  102. package/src/island-store.ts +24 -16
  103. package/src/jobs-drain.ts +95 -21
  104. package/src/jobs-json.ts +50 -0
  105. package/src/jobs-report.ts +7 -4
  106. package/src/json-merge.ts +46 -0
  107. package/src/loopback-host.ts +13 -0
  108. package/src/manifest-staleness.ts +62 -0
  109. package/src/mcp-errors.ts +41 -4
  110. package/src/mcp-host.ts +16 -9
  111. package/src/mcp-test-run.ts +38 -0
  112. package/src/messages.ts +35 -0
  113. package/src/metrics-endpoint.ts +95 -24
  114. package/src/migration-extensions.ts +29 -0
  115. package/src/module-imports.ts +47 -0
  116. package/src/output.ts +24 -9
  117. package/src/page-navigation.ts +7 -23
  118. package/src/page-speculation.ts +5 -20
  119. package/src/permission-grants.ts +41 -9
  120. package/src/prerender-out.ts +34 -7
  121. package/src/prerender.ts +8 -5
  122. package/src/publish-closure.ts +101 -0
  123. package/src/pwa-artifacts.ts +26 -94
  124. package/src/registry.ts +2 -0
  125. package/src/revalidated-response.ts +4 -3
  126. package/src/role-load.ts +143 -0
  127. package/src/role-replicator.ts +16 -0
  128. package/src/role-start.ts +27 -5
  129. package/src/role-sync.ts +55 -20
  130. package/src/role-wake.ts +45 -0
  131. package/src/runtime-admin.ts +141 -0
  132. package/src/runtime-assets.ts +11 -7
  133. package/src/runtime-bindings.ts +4 -4
  134. package/src/runtime-cache.ts +38 -47
  135. package/src/runtime-hooks.ts +5 -2
  136. package/src/runtime-idempotency-scope.ts +23 -0
  137. package/src/runtime-isr.ts +36 -0
  138. package/src/runtime-jobs.ts +8 -33
  139. package/src/runtime-mfa-warning.ts +29 -0
  140. package/src/runtime-notify-retention.ts +10 -46
  141. package/src/runtime-overrides.ts +1 -1
  142. package/src/runtime-purge.ts +38 -16
  143. package/src/runtime-queue.ts +39 -11
  144. package/src/runtime-realtime.ts +9 -45
  145. package/src/runtime-render.ts +68 -35
  146. package/src/runtime-services.ts +22 -18
  147. package/src/runtime-storage.ts +10 -5
  148. package/src/scaffold-typecheck.ts +1 -0
  149. package/src/schema-diff.ts +16 -0
  150. package/src/schema-drift.ts +45 -15
  151. package/src/schema-dump-drift.ts +64 -0
  152. package/src/seo-routes.ts +80 -9
  153. package/src/serve-boot.ts +114 -158
  154. package/src/serve-drain.ts +9 -33
  155. package/src/serve-env.ts +9 -6
  156. package/src/serve-prebuilt-paths.ts +19 -0
  157. package/src/serve-prebuilt.ts +84 -0
  158. package/src/serve-web.ts +169 -0
  159. package/src/serve.ts +6 -9
  160. package/src/server-barrels.ts +54 -0
  161. package/src/shot-locale.ts +5 -23
  162. package/src/site-assets.ts +3 -5
  163. package/src/site-config.ts +9 -48
  164. package/src/site-seo.ts +1 -1
  165. package/src/sitemap-lastmod.ts +5 -0
  166. package/src/solid-loader.ts +12 -3
  167. package/src/source-edit.ts +49 -0
  168. package/src/source-files.ts +6 -0
  169. package/src/stored-object-headers.ts +43 -0
  170. package/src/sync-url.ts +2 -2
  171. package/src/tasks-facts.ts +3 -26
  172. package/src/templates/action.ts +147 -11
  173. package/src/templates/admin-catalog.ts +58 -0
  174. package/src/templates/admin-page.ts +36 -8
  175. package/src/templates/admin.ts +57 -17
  176. package/src/templates/backfill.ts +121 -87
  177. package/src/templates/entity.ts +140 -44
  178. package/src/templates/guard-animated-layout-property.ts +4 -11
  179. package/src/templates/guard-bare-error.ts +9 -13
  180. package/src/templates/guard-focus-visible.ts +4 -11
  181. package/src/templates/guard-image-dimensions.ts +6 -6
  182. package/src/templates/guard-island-without-states.ts +2 -4
  183. package/src/templates/guard-raw-breakpoint.ts +210 -0
  184. package/src/templates/guard-raw-colour.ts +4 -11
  185. package/src/templates/guard-raw-length.ts +375 -0
  186. package/src/templates/guard-raw-motion.ts +213 -0
  187. package/src/templates/guard-raw-shadow.ts +187 -0
  188. package/src/templates/guard-raw-z-index.ts +166 -0
  189. package/src/templates/guard-repo-raw-sql.ts +345 -0
  190. package/src/templates/guard-semantic-interactive.ts +6 -6
  191. package/src/templates/guard-undeclared-custom-property.ts +217 -0
  192. package/src/templates/guard-undefined-style-class.ts +215 -0
  193. package/src/templates/guard-untranslated-string.ts +6 -6
  194. package/src/templates/guard-unzoned-date.ts +9 -13
  195. package/src/templates/index.ts +3 -2
  196. package/src/templates/island.ts +34 -51
  197. package/src/templates/job-body-test.ts +121 -0
  198. package/src/templates/job.ts +23 -8
  199. package/src/templates/naming.ts +28 -0
  200. package/src/templates/policy.ts +16 -9
  201. package/src/templates/query.ts +242 -26
  202. package/src/templates/resource-create.ts +62 -18
  203. package/src/templates/resource-form-island.ts +58 -80
  204. package/src/templates/resource-page.ts +269 -0
  205. package/src/templates/resource-service.ts +96 -0
  206. package/src/templates/resource.ts +86 -61
  207. package/src/templates/route.ts +41 -35
  208. package/src/templates/scaffold-api.ts +55 -1
  209. package/src/templates/scaffold-app.ts +177 -53
  210. package/src/templates/scaffold-auth.ts +83 -19
  211. package/src/templates/scaffold-browser-client.ts +90 -0
  212. package/src/templates/scaffold-container-compose.ts +10 -1
  213. package/src/templates/scaffold-container.ts +20 -2
  214. package/src/templates/scaffold-dashboard-bare.ts +64 -2
  215. package/src/templates/scaffold-dashboard-example.ts +124 -31
  216. package/src/templates/scaffold-dashboard-shared.ts +1 -20
  217. package/src/templates/scaffold-dashboard.ts +1 -2
  218. package/src/templates/scaffold-db-client.ts +144 -0
  219. package/src/templates/scaffold-db-package.ts +90 -8
  220. package/src/templates/scaffold-docs.ts +50 -5
  221. package/src/templates/scaffold-guards.ts +54 -11
  222. package/src/templates/scaffold-helm-fences.ts +76 -0
  223. package/src/templates/scaffold-helm-templates.ts +32 -7
  224. package/src/templates/scaffold-helm.ts +31 -1
  225. package/src/templates/scaffold-http.ts +9 -7
  226. package/src/templates/scaffold-repo.ts +61 -3
  227. package/src/templates/scaffold-roles.ts +56 -9
  228. package/src/templates/scaffold-shell.ts +74 -105
  229. package/src/templates/scaffold-site.ts +45 -19
  230. package/src/templates/scaffold-ui-package.ts +18 -0
  231. package/src/templates/slice-foundation.ts +46 -8
  232. package/src/test-counts.ts +14 -2
  233. package/src/test-ignores.ts +25 -0
  234. package/src/test-passes.ts +0 -15
  235. package/src/test-select.ts +5 -14
  236. package/src/test-shards.ts +41 -93
  237. package/src/test-workers.ts +26 -4
  238. package/src/theme-boot.ts +6 -31
  239. package/src/transport-calls.ts +105 -0
  240. package/src/trusted-hops-error.ts +17 -0
  241. package/src/unscanned-admin.ts +54 -0
  242. package/src/unthrown-codes.ts +26 -0
  243. package/src/verify-checks.ts +43 -53
  244. package/src/verify-coverage-run.ts +190 -0
  245. package/src/verify-deadline.ts +353 -0
  246. package/src/verify-floor.ts +36 -9
  247. package/src/verify-merge.ts +171 -45
  248. package/src/verify-part-step.ts +90 -0
  249. package/src/verify-progress.ts +25 -0
  250. package/src/verify-role-load-probe.ts +54 -0
  251. package/src/verify-role-load.ts +148 -0
  252. package/src/verify-run.ts +116 -37
  253. package/src/verify-shard.ts +33 -11
  254. package/src/verify-stalled.ts +152 -0
  255. package/src/verify-step.ts +42 -1
  256. package/src/verify-tests.ts +83 -8
  257. package/src/workspace-checks.ts +50 -8
  258. package/src/workspace-dep-edit.ts +127 -0
  259. package/src/build-errors.ts +0 -54
  260. package/src/cmd-dev-fixture.ts +0 -204
  261. package/src/dev-live-fixture.ts +0 -122
  262. package/src/policy-fixture.ts +0 -78
  263. package/src/realtime-browser-probe-fixture.ts +0 -9
  264. package/src/role-start-fixture.ts +0 -78
  265. package/src/scaffold-fixture.ts +0 -160
  266. package/src/templates/island-fixture.ts +0 -76
  267. package/src/verify-errors.ts +0 -49
@@ -0,0 +1,187 @@
1
+ // The `raw-shadow` guard `x new` ships: elevation is `tokens.shadow()` or `@include tokens.surface`.
2
+ // The shadow scale is THEMED — dark gets deeper, higher-alpha shadows — so a hand-written offset
3
+ // and blur is the one elevation that does not change when the theme does.
4
+
5
+ import { guardCode } from './guard';
6
+ import type { GeneratedFile } from './naming';
7
+
8
+ /** Derived from the guard's name, never a literal: an `X_*` literal here is a FRAMEWORK code. */
9
+ const NAME = 'raw-shadow';
10
+ const CODE = guardCode(NAME);
11
+
12
+ const source =
13
+ (): string => `// raw-shadow: elevation comes off the shadow scale, so dark theme deepens it instead of losing it.
14
+ // \`x verify\` discovers every file in \`guards/\` and runs its \`guard\` inside the \`boundaries\`
15
+ // step — nothing registers this file, so nothing can forget to. Delete it to drop the rule.
16
+ //
17
+ // A shadow is RAW when a length in it was written by hand. \`tokens.shadow('md')\`, \`none\`, a
18
+ // hairline ring (\`inset 0 0 0 1px …\`) and a line drawn from tokens (\`inset 0 #{tokens.stroke(thick)} …\`)
19
+ // carry no such length and are never reported.
20
+
21
+ import type { Finding, Guard } from '@ultimat3/cli';
22
+
23
+ /** The app owns the codes its own conventions raise — this one is named for the guard. */
24
+ const CODE = '${CODE}';
25
+
26
+ export interface StyleFile {
27
+ /** App-root-relative POSIX path, so the finding names the file an author opens. */
28
+ readonly path: string;
29
+ readonly scss: string;
30
+ }
31
+
32
+ const blank = (text: string): string => text.replaceAll(/[^\\n]/g, ' ');
33
+
34
+ /** Comments blanked rather than removed, so a reported line number still points at the source. */
35
+ const blankComments = (scss: string): string =>
36
+ scss.replaceAll(/\\/\\*[\\s\\S]*?\\*\\//g, blank).replaceAll(/(?<![:\\w])\\/\\/[^\\n]*/g, blank);
37
+
38
+ const lineOf = (text: string, index: number): number => text.slice(0, index).split('\\n').length;
39
+
40
+ /** The \`as <name>\` the token entry point is used under in this sheet; \`tokens\` when it is not. */
41
+ const namespaceOf = (scss: string): string =>
42
+ /@use\\s+['"][^'"]*tokens[^'"]*['"]\\s+as\\s+([\\w-]+)/.exec(scss)?.[1] ?? 'tokens';
43
+
44
+ /** A declaration; a Sass \`#{…}\` interpolation is a value that carries braces. */
45
+ const DECLARATION = /(?<![\\w$-])([\\w-]+)\\s*:\\s*((?:#\\{[^}]*\\}|[^;{}])+)/g;
46
+ const SHADOW_PROPERTY = /^(?:box-shadow|text-shadow)$/;
47
+
48
+ /** The index just past the \`)\` that closes the \`(\` at \`open\`, or \`undefined\` when none does. */
49
+ const closeOf = (text: string, open: number): number | undefined => {
50
+ let depth = 0;
51
+ for (let index = open; index < text.length; index += 1) {
52
+ const character = text.charAt(index);
53
+ if (character === '(') depth += 1;
54
+ else if (character === ')') {
55
+ depth -= 1;
56
+ if (depth === 0) return index + 1;
57
+ }
58
+ }
59
+ return undefined;
60
+ };
61
+
62
+ /**
63
+ * A token read — \`var(--shadow-md)\`, a module call (\`tokens.space(2)\`, \`t.rem(-4px)\`) or the bare
64
+ * \`shadow(md)\` — with its whole argument list, so the px handed to \`rem()\` is not read as raw.
65
+ */
66
+ const TOKEN_CALL = /(?<![\\w-])(?:var|(?:[\\w-]+\\.)[\\w-]+|shadow|rem|space|stroke)\\(/g;
67
+
68
+ const withoutTokens = (value: string): string => {
69
+ let out = value;
70
+ for (const match of value.matchAll(TOKEN_CALL)) {
71
+ const open = match.index + match[0].length - 1;
72
+ const close = closeOf(out, open);
73
+ if (close !== undefined) {
74
+ out = out.slice(0, match.index) + blank(out.slice(match.index, close)) + out.slice(close);
75
+ }
76
+ }
77
+ return out;
78
+ };
79
+
80
+ /** A hand-written length: any unit, any sign — except \`0\` and the \`1px\` of a hairline ring. */
81
+ const LENGTH = /(?<![\\w.#-])-?(\\d*\\.?\\d+)(px|rem|em|vh|vw|%)/g;
82
+
83
+ const rawLength = (value: string): string | undefined => {
84
+ for (const match of withoutTokens(value).matchAll(LENGTH)) {
85
+ const amount = Number(match[1]);
86
+ if (amount === 0 || (amount === 1 && match[2] === 'px')) continue;
87
+ return match[0];
88
+ }
89
+ return undefined;
90
+ };
91
+
92
+ /** Pure — the caller does the I/O — so the rule is testable without a filesystem. */
93
+ export function rawShadows(files: readonly StyleFile[]): readonly Finding[] {
94
+ const findings: Finding[] = [];
95
+ for (const file of files) {
96
+ const scss = blankComments(file.scss);
97
+ const ns = namespaceOf(file.scss);
98
+ for (const match of scss.matchAll(DECLARATION)) {
99
+ const property = match[1] ?? '';
100
+ const value = (match[2] ?? '').trim().replaceAll(/\\s+/g, ' ');
101
+ const dropShadow = /drop-shadow\\(/.test(value);
102
+ if (!SHADOW_PROPERTY.test(property) && !dropShadow) continue;
103
+ const literal = rawLength(value);
104
+ if (literal === undefined) continue;
105
+ const line = lineOf(scss, match.index);
106
+ // An inset shadow is a LINE drawn inside the box, not elevation: its repair is the stroke
107
+ // scale, and telling its author to reach for \`shadow()\` would be telling them to delete it.
108
+ const written = /\\binset\\b/.test(value)
109
+ ? \`write each length of this inset line as \${ns}.stroke(thick), \${ns}.space(<step>) or \${ns}.rem(<px>)\`
110
+ : \`\${property}: \${ns}.shadow('sm') — or 'xs' 'md' 'lg' 'xl'; a card or popover takes @include \${ns}.surface\`;
111
+ findings.push({
112
+ code: CODE,
113
+ cause: \`\${file.path}:\${line} writes \\\`\${property}: \${value}\\\` — a shadow with the hand-written length \${literal}, which no theme restates: dark mode keeps the light theme's elevation\`,
114
+ fix: \`\${written} — at \${file.path}:\${line}, then: x verify\`,
115
+ at: \`\${file.path}:\${line}\`,
116
+ });
117
+ }
118
+ }
119
+ return findings;
120
+ }
121
+
122
+ export const guard: Guard = {
123
+ summary: 'a shadow comes off the shadow scale, never a hand-written offset and blur',
124
+ async check(_root, sources) {
125
+ const sheets = await sources.files('{apps,packages}/**/*.scss');
126
+ return rawShadows(sheets.map((file) => ({ path: file.path, scss: file.text })));
127
+ },
128
+ };
129
+ `;
130
+
131
+ const test =
132
+ (): string => `// The rule, driven directly. Failure case first: a guard whose rule silently stopped matching is
133
+ // a green gate over the convention it was written to enforce.
134
+
135
+ import { expect, unitTest } from '@ultimat3/testing';
136
+ import { rawShadows } from './raw-shadow';
137
+
138
+ const sheet = (scss: string) => [{ path: 'apps/web/site/page.module.scss', scss }];
139
+
140
+ unitTest('a hand-written box-shadow is refused, and the fix is the declaration to write', () => {
141
+ const findings = rawShadows(sheet('.card {\\n box-shadow: 0 4px 12px rgb(0 0 0 / 0.2);\\n}\\n'));
142
+ expect(findings).toHaveLength(1);
143
+ expect(findings[0]?.code).toBe('${CODE}');
144
+ expect(findings[0]?.at).toBe('apps/web/site/page.module.scss:2');
145
+ expect(findings[0]?.cause).toContain('4px');
146
+ expect(findings[0]?.fix).toStartWith("box-shadow: tokens.shadow('sm') — ");
147
+ });
148
+
149
+ unitTest('text-shadow, a rem length and a drop-shadow filter are the same rule', () => {
150
+ expect(rawShadows(sheet('.a { text-shadow: 0 0.25rem 0 currentcolor; }'))).toHaveLength(1);
151
+ expect(rawShadows(sheet('.a { filter: drop-shadow(0 2px 4px currentcolor); }'))).toHaveLength(1);
152
+ });
153
+
154
+ unitTest('an inset line is repaired with the stroke scale, not the elevation scale', () => {
155
+ const findings = rawShadows(sheet('.tab { box-shadow: inset 0 -2px 0 0 currentcolor; }'));
156
+ expect(findings).toHaveLength(1);
157
+ expect(findings[0]?.fix).toContain('tokens.stroke(thick)');
158
+ expect(findings[0]?.fix).not.toContain('tokens.shadow(');
159
+ });
160
+
161
+ unitTest('the fix speaks the namespace the sheet uses the tokens under', () => {
162
+ const scss = "@use '../shared/tokens' as t;\\n.a { box-shadow: 0 2px 6px black; }";
163
+ expect(rawShadows(sheet(scss))[0]?.fix).toStartWith("box-shadow: t.shadow('sm') — ");
164
+ });
165
+
166
+ // The legitimate lookalikes — the reason the rule can stay switched on.
167
+ unitTest('a token, none, a hairline ring and a line drawn from tokens are silent', () => {
168
+ const silent = [
169
+ ".a { box-shadow: tokens.shadow('md'); }",
170
+ '.a { box-shadow: var(--shadow-sm); }',
171
+ '.a { box-shadow: none; }',
172
+ ".a { box-shadow: inset 0 0 0 1px tokens.role('accent', 0.24); }",
173
+ '.a { box-shadow: inset 0 calc(-1 * #{tokens.stroke(thick)}) 0 0 currentcolor; }',
174
+ '.a { box-shadow: inset #{tokens.rem(-4.5px)} #{tokens.rem(-3px)} 0 0 currentcolor; }',
175
+ ".a { box-shadow: tokens.shadow('sm'), inset 0 0 0 1px currentcolor; }",
176
+ '// box-shadow: 0 4px 12px black;\\n.a { box-shadow: none; }',
177
+ '.a { padding: 4px 12px; }',
178
+ ];
179
+ for (const scss of silent) expect(rawShadows(sheet(scss))).toEqual([]);
180
+ });
181
+ `;
182
+
183
+ /** `guards/raw-shadow.ts` and its test. No index, no registry — the directory is the registration. */
184
+ export const rawShadowGuardFiles = (): readonly GeneratedFile[] => [
185
+ { path: `guards/${NAME}.ts`, contents: source() },
186
+ { path: `guards/${NAME}.test.ts`, contents: test() },
187
+ ];
@@ -0,0 +1,166 @@
1
+ // The `raw-z-index` guard `x new` ships: a `z-index` is a named layer of `@ultimat3/ui/tokens`' z
2
+ // ladder. `_z.scss` has always said "a magic number in a component is a review failure"; this is
3
+ // that sentence as a build error.
4
+
5
+ import { guardCode } from './guard';
6
+ import type { GeneratedFile } from './naming';
7
+
8
+ /** Derived from the guard's name, never a literal: an `X_*` literal here is a FRAMEWORK code. */
9
+ const NAME = 'raw-z-index';
10
+ const CODE = guardCode(NAME);
11
+
12
+ const source =
13
+ (): string => `// raw-z-index: a layer is a name on the one z ladder, so "above the dialog" means one thing.
14
+ // \`x verify\` discovers every file in \`guards/\` and runs its \`guard\` inside the \`boundaries\`
15
+ // step — nothing registers this file, so nothing can forget to. Delete it to drop the rule.
16
+ //
17
+ // The NEGATIVE of a named layer is still a named layer — \`calc(-1 * #{tokens.z(raised)})\` puts a
18
+ // decoration behind its own stacking context — so a \`-1 *\` beside a token is not a number.
19
+
20
+ import type { Finding, Guard } from '@ultimat3/cli';
21
+
22
+ /** The app owns the codes its own conventions raise — this one is named for the guard. */
23
+ const CODE = '${CODE}';
24
+
25
+ /** \`@ultimat3/ui/tokens\`' z ladder. */
26
+ export const LAYERS: readonly (readonly [string, number])[] = [
27
+ ['base', 0],
28
+ ['raised', 10],
29
+ ['sticky', 100],
30
+ ['dropdown', 200],
31
+ ['drawer', 300],
32
+ ['dialog', 400],
33
+ ['popover', 500],
34
+ ['tooltip', 600],
35
+ ['toast', 700],
36
+ ['skip-nav', 800],
37
+ ];
38
+
39
+ export interface StyleFile {
40
+ /** App-root-relative POSIX path, so the finding names the file an author opens. */
41
+ readonly path: string;
42
+ readonly scss: string;
43
+ }
44
+
45
+ const blank = (text: string): string => text.replaceAll(/[^\\n]/g, ' ');
46
+
47
+ /** Comments blanked rather than removed, so a reported line number still points at the source. */
48
+ const blankComments = (scss: string): string =>
49
+ scss.replaceAll(/\\/\\*[\\s\\S]*?\\*\\//g, blank).replaceAll(/(?<![:\\w])\\/\\/[^\\n]*/g, blank);
50
+
51
+ const lineOf = (text: string, index: number): number => text.slice(0, index).split('\\n').length;
52
+
53
+ /** The \`as <name>\` the token entry point is used under in this sheet; \`tokens\` when it is not. */
54
+ const namespaceOf = (scss: string): string =>
55
+ /@use\\s+['"][^'"]*tokens[^'"]*['"]\\s+as\\s+([\\w-]+)/.exec(scss)?.[1] ?? 'tokens';
56
+
57
+ const Z_INDEX = /(?<![\\w$-])z-index\\s*:\\s*((?:#\\{[^}]*\\}|[^;{}])+)/g;
58
+ /** A token read: \`tokens.z(dialog)\`, \`z('dialog')\`, \`var(--z-dialog)\` — and the negation of one. */
59
+ const TOKEN = /(?:[\\w-]+\\.)?z\\([^)]*\\)|var\\(\\s*--z-[\\w-]+\\s*\\)|-1\\s*\\*|\\*\\s*-1/g;
60
+
61
+ /** The nearest layer to a number. Never \`base\` for a non-zero one: \`-0\` is not behind anything. */
62
+ const layerFor = (value: number): string => {
63
+ let best: readonly [string, number] | undefined;
64
+ for (const layer of LAYERS) {
65
+ if (value !== 0 && layer[1] === 0) continue;
66
+ if (
67
+ best === undefined ||
68
+ Math.abs(layer[1] - Math.abs(value)) < Math.abs(best[1] - Math.abs(value))
69
+ ) {
70
+ best = layer;
71
+ }
72
+ }
73
+ return best?.[0] ?? 'raised';
74
+ };
75
+
76
+ /** Pure — the caller does the I/O — so the rule is testable without a filesystem. */
77
+ export function rawZIndexes(files: readonly StyleFile[]): readonly Finding[] {
78
+ const findings: Finding[] = [];
79
+ for (const file of files) {
80
+ const scss = blankComments(file.scss);
81
+ const ns = namespaceOf(file.scss);
82
+ for (const match of scss.matchAll(Z_INDEX)) {
83
+ const value = (match[1] ?? '').trim();
84
+ const literal = /-?\\d+/.exec(value.replaceAll(TOKEN, ' '))?.[0];
85
+ if (literal === undefined) continue;
86
+ const line = lineOf(scss, match.index);
87
+ const layer = \`\${ns}.z(\${layerFor(Number(literal))})\`;
88
+ const written = Number(literal) < 0 ? \`calc(-1 * #{\${layer}})\` : layer;
89
+ findings.push({
90
+ code: CODE,
91
+ cause: \`\${file.path}:\${line} writes \\\`z-index: \${value}\\\` — a number only this file knows, on no ladder another layer can be placed against\`,
92
+ fix: \`z-index: \${written} — at \${file.path}:\${line} (layers: \${LAYERS.map(([name]) => name).join(' ')}), then: x verify\`,
93
+ at: \`\${file.path}:\${line}\`,
94
+ });
95
+ }
96
+ }
97
+ return findings;
98
+ }
99
+
100
+ export const guard: Guard = {
101
+ summary: 'a z-index is a named layer, never a number',
102
+ async check(_root, sources) {
103
+ const sheets = await sources.files('{apps,packages}/**/*.scss');
104
+ return rawZIndexes(sheets.map((file) => ({ path: file.path, scss: file.text })));
105
+ },
106
+ };
107
+ `;
108
+
109
+ const test =
110
+ (): string => `// The rule, driven directly. Failure case first: a guard whose rule silently stopped matching is
111
+ // a green gate over the convention it was written to enforce.
112
+
113
+ import { expect, unitTest } from '@ultimat3/testing';
114
+ import { rawZIndexes } from './raw-z-index';
115
+
116
+ const sheet = (scss: string) => [{ path: 'apps/web/site/page.module.scss', scss }];
117
+ const fixes = (scss: string) => rawZIndexes(sheet(scss)).map((finding) => finding.fix);
118
+
119
+ unitTest('a numeric z-index is refused, and the fix is the layer it means', () => {
120
+ const findings = rawZIndexes(sheet('.menu {\\n z-index: 200;\\n}\\n'));
121
+ expect(findings).toHaveLength(1);
122
+ expect(findings[0]?.code).toBe('${CODE}');
123
+ expect(findings[0]?.at).toBe('apps/web/site/page.module.scss:2');
124
+ expect(findings[0]?.fix).toStartWith('z-index: tokens.z(dropdown) — ');
125
+ });
126
+
127
+ unitTest('a number off the ladder gets the nearest layer', () => {
128
+ expect(fixes('.a { z-index: 9999; }')[0]).toStartWith('z-index: tokens.z(skip-nav) — ');
129
+ expect(fixes('.a { z-index: 1; }')[0]).toStartWith('z-index: tokens.z(raised) — ');
130
+ expect(fixes('.a { z-index: 0; }')[0]).toStartWith('z-index: tokens.z(base) — ');
131
+ });
132
+
133
+ unitTest('a negative number is the negation of a layer, never of base', () => {
134
+ expect(fixes('.a { z-index: -1; }')[0]).toStartWith('z-index: calc(-1 * #{tokens.z(raised)}) — ');
135
+ });
136
+
137
+ unitTest('a number beside a token is still a number', () => {
138
+ expect(rawZIndexes(sheet('.a { z-index: calc(#{tokens.z(dialog)} + 1); }'))).toHaveLength(1);
139
+ });
140
+
141
+ unitTest('the fix speaks the namespace the sheet uses the tokens under', () => {
142
+ const scss = "@use '../shared/tokens' as t;\\n.a { z-index: 400; }";
143
+ expect(fixes(scss)[0]).toStartWith('z-index: t.z(dialog) — ');
144
+ });
145
+
146
+ // The legitimate lookalikes — the reason the rule can stay switched on.
147
+ unitTest('a token, its negation and a keyword are silent', () => {
148
+ const silent = [
149
+ '.a { z-index: tokens.z(dialog); }',
150
+ ".a { z-index: tokens.z('skip-nav'); }",
151
+ '.a { z-index: var(--z-toast); }',
152
+ '.a { z-index: calc(-1 * #{tokens.z(raised)}); }',
153
+ '.a { z-index: auto; }',
154
+ '.a { z-index: inherit; }',
155
+ '// z-index: 5;\\n.a { z-index: auto; }',
156
+ '$z-index: 5;',
157
+ ];
158
+ for (const scss of silent) expect(rawZIndexes(sheet(scss))).toEqual([]);
159
+ });
160
+ `;
161
+
162
+ /** `guards/raw-z-index.ts` and its test. No index, no registry — the directory is the registration. */
163
+ export const rawZIndexGuardFiles = (): readonly GeneratedFile[] => [
164
+ { path: `guards/${NAME}.ts`, contents: source() },
165
+ { path: `guards/${NAME}.test.ts`, contents: test() },
166
+ ];