@ultimat3/cli 21.0.0 → 22.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.
- package/CLAUDE.md +84 -1650
- package/README.md +40 -2
- package/package.json +30 -30
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +11 -1
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +111 -43
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +55 -36
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +3 -3
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +28 -30
- package/src/error-pages.ts +19 -7
- package/src/errors.ts +14 -0
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -348
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +6 -1
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +15 -8
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +1 -1
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +28 -27
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +23 -17
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +22 -8
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +39 -16
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +18 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -347
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sync-url.ts +21 -1
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +49 -12
- package/src/templates/resource.ts +10 -4
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +24 -159
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +10 -7
- package/src/verify-run.ts +11 -1
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +1 -26
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -94
- package/src/cdp-connection.ts +0 -247
- package/src/cdp-e2e-page.ts +0 -180
- package/src/cdp-e2e-session.ts +0 -199
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -193
- package/src/cdp-offline-script.ts +0 -73
- package/src/cdp-pipe.ts +0 -77
- package/src/e2e-app.ts +0 -103
- package/src/e2e-browser-handle.ts +0 -55
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -117
- package/src/e2e-errors.ts +0 -117
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -153
- package/src/e2e-preload.ts +0 -64
- package/src/e2e-probe.ts +0 -23
- package/src/e2e-selection.ts +0 -182
- package/src/e2e-spawn.ts +0 -169
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
- /package/src/{dev-render.ts → runtime-render.ts} +0 -0
|
@@ -12,8 +12,18 @@ const helpers = (
|
|
|
12
12
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
|
13
13
|
{{- end -}}
|
|
14
14
|
|
|
15
|
+
{{/*
|
|
16
|
+
helm create's rule: a release whose name already contains the chart's is the full name on its own.
|
|
17
|
+
x deploy --method helm names the release after the app, which IS the chart name, so without it
|
|
18
|
+
every object would be ${app.kebab}-${app.kebab}-web.
|
|
19
|
+
*/}}
|
|
15
20
|
{{- define "${app.kebab}.fullname" -}}
|
|
16
|
-
{{-
|
|
21
|
+
{{- $name := include "${app.kebab}.name" . -}}
|
|
22
|
+
{{- if contains $name .Release.Name -}}
|
|
23
|
+
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
|
24
|
+
{{- else -}}
|
|
25
|
+
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
|
26
|
+
{{- end -}}
|
|
17
27
|
{{- end -}}
|
|
18
28
|
|
|
19
29
|
{{/*
|
|
@@ -92,15 +102,38 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
92
102
|
HTTP and get both. worker, scheduler and replicator open no HTTP socket at all — the scrape
|
|
93
103
|
listener is their only port — so they take liveness on it and NO readiness: nothing routes to
|
|
94
104
|
them, and a readiness flap would drop the pod out of the Service and so out of the scrape.
|
|
105
|
+
|
|
106
|
+
Every one of them takes a startupProbe too, because no listener opens early: the server builds
|
|
107
|
+
the app's islands before any role binds a port, and a liveness probe counting from container
|
|
108
|
+
start restarts a pod that is merely booting. 30 x 5s = 150s of boot, then the ordinary checks.
|
|
95
109
|
*/}}
|
|
96
110
|
{{- if $cfg.port }}
|
|
111
|
+
startupProbe:
|
|
112
|
+
httpGet: { path: /healthz, port: http }
|
|
113
|
+
periodSeconds: 5
|
|
114
|
+
failureThreshold: 30
|
|
97
115
|
readinessProbe:
|
|
98
116
|
httpGet: { path: /readyz, port: http }
|
|
99
117
|
periodSeconds: 5
|
|
100
118
|
livenessProbe:
|
|
101
119
|
httpGet: { path: /healthz, port: http }
|
|
102
120
|
periodSeconds: 15
|
|
121
|
+
{{- /*
|
|
122
|
+
Holds SIGTERM back while the pod is already out of its Service's endpoints, so a proxy that has
|
|
123
|
+
not caught up still reaches a listener that answers. lifecycle.preStop.sleep exists from
|
|
124
|
+
Kubernetes 1.30 and this chart's floor is 1.27, so it renders only where the API server knows it;
|
|
125
|
+
below that the framework's own readiness grace (/readyz at 503, listener still open) covers it.
|
|
126
|
+
*/}}
|
|
127
|
+
{{- if semverCompare ">=1.30-0" $root.Capabilities.KubeVersion.Version }}
|
|
128
|
+
lifecycle:
|
|
129
|
+
preStop:
|
|
130
|
+
sleep: { seconds: {{ $root.Values.drain.preStopSleepSeconds | int }} }
|
|
131
|
+
{{- end }}
|
|
103
132
|
{{- else if $scraped }}
|
|
133
|
+
startupProbe:
|
|
134
|
+
httpGet: { path: /metrics, port: metrics }
|
|
135
|
+
periodSeconds: 5
|
|
136
|
+
failureThreshold: 30
|
|
104
137
|
livenessProbe:
|
|
105
138
|
httpGet: { path: /metrics, port: metrics }
|
|
106
139
|
periodSeconds: 15
|
|
@@ -114,8 +147,9 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
114
147
|
`;
|
|
115
148
|
|
|
116
149
|
const deployments = (app: NameSet): string => `{{/*
|
|
117
|
-
One Deployment per enabled role, from one image. terminationGracePeriodSeconds
|
|
118
|
-
framework's SIGTERM drain: in-flight requests, open websockets
|
|
150
|
+
One Deployment per enabled role, from one image. terminationGracePeriodSeconds covers the preStop
|
|
151
|
+
sleep, the framework's readiness grace and its SIGTERM drain: in-flight requests, open websockets
|
|
152
|
+
and running job steps finish.
|
|
119
153
|
*/}}
|
|
120
154
|
{{- range $role, $cfg := .Values.roles }}
|
|
121
155
|
{{- if $cfg.enabled }}
|
|
@@ -151,9 +185,11 @@ spec:
|
|
|
151
185
|
code that reaches the filesystem, which for a web role is one path traversal. */}}
|
|
152
186
|
automountServiceAccountToken: false
|
|
153
187
|
securityContext: {{- toYaml $.Values.podSecurityContext | nindent 8 }}
|
|
154
|
-
{{/*
|
|
155
|
-
|
|
156
|
-
|
|
188
|
+
{{/* Spent in order: the preStop sleep (drain.preStopSleepSeconds, 1.30+), the framework's
|
|
189
|
+
readiness grace (5s outside local environments) and its drain deadline (25s, which is what
|
|
190
|
+
X_SHUTDOWN_TIMEOUT's fix line names) — 35s, so 45 leaves 10s before SIGKILL. Raise this WITH
|
|
191
|
+
configureLifecycle({ deadlineMs, readinessGraceMs }), never instead of it: the drain
|
|
192
|
+
abandons its hooks at its own deadline. */}}
|
|
157
193
|
terminationGracePeriodSeconds: 45
|
|
158
194
|
containers:
|
|
159
195
|
{{- include "${app.kebab}.container" (dict "role" $role "cfg" $cfg "root" $) | nindent 8 }}
|
|
@@ -259,12 +295,16 @@ spec:
|
|
|
259
295
|
paths:
|
|
260
296
|
{{- /* The path the sync node actually serves. Routing /_sync instead sends every
|
|
261
297
|
websocket to the web role, which answers no upgrade. */}}
|
|
298
|
+
{{- /* Only when the sync role runs: a rule naming a Service the chart did not render is an
|
|
299
|
+
ingress that 503s every websocket instead of letting it reach nothing at all. */}}
|
|
300
|
+
{{- if .Values.roles.sync.enabled }}
|
|
262
301
|
- path: /_x/sync
|
|
263
302
|
pathType: Prefix
|
|
264
303
|
backend:
|
|
265
304
|
service:
|
|
266
305
|
name: {{ include "${app.kebab}.fullname" . }}-sync
|
|
267
306
|
port: { name: http }
|
|
307
|
+
{{- end }}
|
|
268
308
|
- path: /
|
|
269
309
|
pathType: Prefix
|
|
270
310
|
backend:
|
|
@@ -277,8 +317,13 @@ spec:
|
|
|
277
317
|
const hpa = (app: NameSet): string => `{{/*
|
|
278
318
|
Per-role autoscalers, off until you turn one on. Each role scales on the signal that predicts ITS
|
|
279
319
|
saturation — CPU is a lagging proxy for all three and scales the wrong thing at the wrong time.
|
|
280
|
-
|
|
320
|
+
Either metric type needs a metrics adapter in the cluster; without one the HPA reads <unknown> and
|
|
281
321
|
holds at minReplicas, which is why these are opt-in rather than a default nobody wired.
|
|
322
|
+
|
|
323
|
+
autoscaling.type says whose number it is. Pods (the default) is a per-pod series — rps, open
|
|
324
|
+
sockets — averaged across pods. External is ONE series for the whole role, divided by the replica
|
|
325
|
+
count: queue_depth is that shape, because every worker publishes the same global backlog, and read
|
|
326
|
+
as Pods it asked for N times the workers the queue needed. Any other type fails the render.
|
|
282
327
|
*/}}
|
|
283
328
|
{{- range $role, $cfg := .Values.roles }}
|
|
284
329
|
{{- if and $cfg.enabled $cfg.autoscaling $cfg.autoscaling.enabled }}
|
|
@@ -297,7 +342,20 @@ spec:
|
|
|
297
342
|
name: {{ include "${app.kebab}.fullname" $ }}-{{ $role }}
|
|
298
343
|
minReplicas: {{ $cfg.autoscaling.minReplicas }}
|
|
299
344
|
maxReplicas: {{ $cfg.autoscaling.maxReplicas }}
|
|
345
|
+
{{- $type := default "Pods" $cfg.autoscaling.type }}
|
|
346
|
+
{{- if not (has $type (list "Pods" "External")) }}
|
|
347
|
+
{{- fail (printf "roles.%s.autoscaling.type is %q — set it to Pods (a per-pod series) or External (one series for the whole role, e.g. queue_depth)" $role $type) }}
|
|
348
|
+
{{- end }}
|
|
300
349
|
metrics:
|
|
350
|
+
{{- if eq $type "External" }}
|
|
351
|
+
- type: External
|
|
352
|
+
external:
|
|
353
|
+
metric:
|
|
354
|
+
name: {{ $cfg.autoscaling.metric }}
|
|
355
|
+
target:
|
|
356
|
+
type: AverageValue
|
|
357
|
+
averageValue: {{ $cfg.autoscaling.targetAverageValue | quote }}
|
|
358
|
+
{{- else }}
|
|
301
359
|
- type: Pods
|
|
302
360
|
pods:
|
|
303
361
|
metric:
|
|
@@ -305,6 +363,7 @@ spec:
|
|
|
305
363
|
target:
|
|
306
364
|
type: AverageValue
|
|
307
365
|
averageValue: {{ $cfg.autoscaling.targetAverageValue | quote }}
|
|
366
|
+
{{- end }}
|
|
308
367
|
behavior:
|
|
309
368
|
scaleUp:
|
|
310
369
|
stabilizationWindowSeconds: 30
|
|
@@ -34,7 +34,7 @@ image:
|
|
|
34
34
|
env:
|
|
35
35
|
NODE_ENV: production
|
|
36
36
|
|
|
37
|
-
existingSecret: ${app.kebab}-secrets # DATABASE_URL, NATS_URL, S3_*, AUTH_SECRET
|
|
37
|
+
existingSecret: ${app.kebab}-secrets # DATABASE_URL, NATS_URL, S3_*, AUTH_SECRET, ULTIMATE_CURSOR_SECRET
|
|
38
38
|
|
|
39
39
|
# The scrape listener every serving role opens, on its own port and never the app's: the ingress
|
|
40
40
|
# routes / to web, so /metrics beside /healthz on 3000 is /metrics on the internet. This is
|
|
@@ -51,6 +51,12 @@ securityContext:
|
|
|
51
51
|
readOnlyRootFilesystem: true
|
|
52
52
|
capabilities: { drop: [ALL] }
|
|
53
53
|
|
|
54
|
+
# The stop sequence: out of the Service, then this preStop sleep (Kubernetes 1.30+ only), then
|
|
55
|
+
# SIGTERM, then the framework's readiness grace and drain. terminationGracePeriodSeconds in
|
|
56
|
+
# templates/deployments.yaml is sized to all three, so move them together.
|
|
57
|
+
drain:
|
|
58
|
+
preStopSleepSeconds: 5
|
|
59
|
+
|
|
54
60
|
# The release phase. Runs to completion before any serving role starts.
|
|
55
61
|
migrate:
|
|
56
62
|
enabled: true
|
|
@@ -63,8 +69,10 @@ migrate:
|
|
|
63
69
|
# PORT: they differ for sync, which binds PORT + 1, and the chart derives the env from this number
|
|
64
70
|
# so there is only ever one to move.
|
|
65
71
|
#
|
|
66
|
-
# Autoscaling is off until you wire a metrics adapter — each \`metric\` below is a
|
|
67
|
-
#
|
|
72
|
+
# Autoscaling is off until you wire a metrics adapter — each \`metric\` below is a series the role
|
|
73
|
+
# exports, and an HPA with no adapter behind it reads <unknown> and holds at minReplicas.
|
|
74
|
+
# \`type\` is Pods (the default: a per-pod series averaged across pods) or External (one series for
|
|
75
|
+
# the whole role, divided by the replica count).
|
|
68
76
|
roles:
|
|
69
77
|
web:
|
|
70
78
|
enabled: true
|
|
@@ -81,7 +89,14 @@ roles:
|
|
|
81
89
|
targetAverageValue: "50"
|
|
82
90
|
|
|
83
91
|
sync:
|
|
84
|
-
|
|
92
|
+
# OFF by default. A sync pod on a real database hears committed changes only from a replicator
|
|
93
|
+
# it can reach, and a fresh deploy has none: booted anyway it is refused (X_REALTIME_TOPOLOGY).
|
|
94
|
+
# To turn live queries and channels on: declare realtime: { enabled: true, transport: 'nats',
|
|
95
|
+
# urlEnv: 'NATS_URL' } in app.config.ts (a NATS_URL under transport 'memory' is refused, and
|
|
96
|
+
# 'nats' without it is too); run NATS and set the SAME NATS_URL for web, sync and the
|
|
97
|
+
# replicator; enable exactly one replicator (below) against a Postgres started with
|
|
98
|
+
# wal_level=logical (the replicator creates its publication at boot) and a role with REPLICATION on a cluster dedicated to this app; then set this to true.
|
|
99
|
+
enabled: false
|
|
85
100
|
replicas: 2
|
|
86
101
|
port: 3001
|
|
87
102
|
resources:
|
|
@@ -104,6 +119,10 @@ roles:
|
|
|
104
119
|
enabled: false
|
|
105
120
|
minReplicas: 1
|
|
106
121
|
maxReplicas: 50
|
|
122
|
+
# External: every worker publishes the SAME global backlog, so averaged as a Pods metric it
|
|
123
|
+
# asked for N times the workers the queue needed. Expose it from your adapter deduplicated —
|
|
124
|
+
# max(queue_depth), never sum, which would multiply it by the pod count again.
|
|
125
|
+
type: External
|
|
107
126
|
metric: queue_depth # jobs waiting, exported by the worker role
|
|
108
127
|
targetAverageValue: "100"
|
|
109
128
|
|
|
@@ -112,9 +131,12 @@ roles:
|
|
|
112
131
|
# Fixed 1, and a second replica is safe but pointless: leadership is an expiring row in
|
|
113
132
|
# x_scheduler_leader, so the extra pod stands by.
|
|
114
133
|
replicas: 1
|
|
134
|
+
# 512Mi, not 256Mi: a scaffolded app measured ~325 MiB at boot as ROLE=scheduler (2026-09-23),
|
|
135
|
+
# when every role still built the app's islands first; at 256Mi the pod was OOMKilled on every
|
|
136
|
+
# boot. Only web builds islands now, so the peak is lower but unmeasured, and the limit stays.
|
|
115
137
|
resources:
|
|
116
138
|
requests: { cpu: 50m, memory: 128Mi }
|
|
117
|
-
limits: { memory:
|
|
139
|
+
limits: { memory: 512Mi }
|
|
118
140
|
|
|
119
141
|
replicator:
|
|
120
142
|
enabled: false # exactly one per database; enable when the change feed is in use
|
|
@@ -28,27 +28,31 @@ const i18nPackage = (app: NameSet, version: string): string => `{
|
|
|
28
28
|
`;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
* locale a run actually added
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* The default locale first, then every other one alphabetically — a stable order so a diff shows
|
|
32
|
+
* only the locale a run actually added. The default is `en` when the set holds it (and when the set
|
|
33
|
+
* is empty, which is `x new`), else the first tag: importing an `en.json` the app does not have is
|
|
34
|
+
* a file that does not compile.
|
|
35
35
|
*/
|
|
36
|
+
export const defaultLocaleOf = (locales: readonly string[]): string =>
|
|
37
|
+
locales.length === 0 || locales.includes('en') ? 'en' : ([...locales].sort()[0] ?? 'en');
|
|
38
|
+
|
|
36
39
|
const orderedLocales = (locales: readonly string[]): readonly string[] => {
|
|
40
|
+
const first = defaultLocaleOf(locales);
|
|
37
41
|
const rest = new Set(locales);
|
|
38
|
-
rest.delete(
|
|
39
|
-
return [
|
|
42
|
+
rest.delete(first);
|
|
43
|
+
return [first, ...[...rest].sort()];
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
/** A locale tag is not always a valid JS binding (`zh-hant`) — `camel()` is the one identifier
|
|
43
47
|
* derivation every generated file already uses for names, so the import agrees with the rest of
|
|
44
48
|
* the app's own naming instead of inventing a second casing rule. */
|
|
45
|
-
const localeImport = (locale: string): string =>
|
|
49
|
+
export const localeImport = (locale: string): string =>
|
|
46
50
|
`import ${camel(locale)} from '../catalogs/${locale}.json';`;
|
|
47
51
|
|
|
48
52
|
/** The object-literal entry for one locale: shorthand when the binding IS the tag (`en`, `es`, …),
|
|
49
53
|
* `'tag': binding` when `camel()` had to reshape it (`zh-hant` → `zhHant`) — `defineCatalogs` reads
|
|
50
54
|
* the locale from the key, never the identifier, so the quoted form is what keeps it addressable. */
|
|
51
|
-
const localeEntry = (locale: string): string => {
|
|
55
|
+
export const localeEntry = (locale: string): string => {
|
|
52
56
|
const binding = camel(locale);
|
|
53
57
|
return binding === locale ? binding : `'${locale}': ${binding}`;
|
|
54
58
|
};
|
|
@@ -61,6 +65,7 @@ const localeEntry = (locale: string): string => {
|
|
|
61
65
|
*/
|
|
62
66
|
export function i18nIndex(locales: readonly string[]): string {
|
|
63
67
|
const ordered = orderedLocales(locales);
|
|
68
|
+
const fallback = defaultLocaleOf(locales);
|
|
64
69
|
const imports = ordered.map(localeImport).join('\n');
|
|
65
70
|
const entries = ordered.map(localeEntry).join(', ');
|
|
66
71
|
return `// The app's catalog, registered once and typed against English. Every surface resolves strings
|
|
@@ -75,13 +80,13 @@ import {
|
|
|
75
80
|
} from '@ultimat3/i18n';
|
|
76
81
|
${imports}
|
|
77
82
|
|
|
78
|
-
export const catalogs = defineCatalogs({ default: '
|
|
83
|
+
export const catalogs = defineCatalogs({ default: '${fallback}', locales: { ${entries} } });
|
|
79
84
|
|
|
80
85
|
/**
|
|
81
86
|
* English is the source of truth for the key space — a second locale must match it exactly, or
|
|
82
87
|
* \`x verify\` fails.
|
|
83
88
|
*/
|
|
84
|
-
export type AppCatalog = typeof
|
|
89
|
+
export type AppCatalog = typeof ${camel(fallback)};
|
|
85
90
|
|
|
86
91
|
/** Every key this app's catalog defines — dot-paths, plus the stem of each plural family. */
|
|
87
92
|
export type TranslationKey = KeyOf<AppCatalog>;
|
|
@@ -64,9 +64,9 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
64
64
|
"dev": "x dev",
|
|
65
65
|
"check": "bin/check",
|
|
66
66
|
"verify": "x verify",
|
|
67
|
-
"typecheck": "tsc -
|
|
67
|
+
"typecheck": "tsc -p . --pretty",
|
|
68
68
|
"lint": "biome check .",
|
|
69
|
-
"test": "bun test",
|
|
69
|
+
"test": "bun test --isolate",
|
|
70
70
|
"db:migrate": "x db migrate",
|
|
71
71
|
"db:seed": "x db seed"
|
|
72
72
|
},
|
|
@@ -89,6 +89,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
89
89
|
"@ultimat3/i18n": "^${version}",
|
|
90
90
|
"@ultimat3/jobs": "^${version}",
|
|
91
91
|
"@ultimat3/mcp": "^${version}",
|
|
92
|
+
"@ultimat3/money": "^${version}",
|
|
92
93
|
"@ultimat3/policy": "^${version}",
|
|
93
94
|
"@ultimat3/pwa": "^${version}",
|
|
94
95
|
"@ultimat3/query": "^${version}",
|
|
@@ -106,14 +107,16 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
109
|
* `"incremental": true` is ONE line and it is the difference between a 4.9s typecheck and a 92s
|
|
109
|
-
* one.
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
110
|
+
* one. Measured on a 166-file scaffold with no source change between runs: 92s wall / 43s user CPU
|
|
111
|
+
* without it, 4.9s / 8.8s warm with it.
|
|
112
|
+
*
|
|
113
|
+
* The typecheck is `tsc -p .`, never `-b`, `As of 2026-09-23` (#450) — here, in the scaffold's
|
|
114
|
+
* `typecheck` script, and in `x verify`'s step (`verify-typecheck.ts`, for any root without
|
|
115
|
+
* `references`). `-b` decides "up to date?" by MTIME, and Bun installs a dependency as hardlinks
|
|
116
|
+
* carrying its cache's old mtimes: upgrading to a version already in the cache left every input
|
|
117
|
+
* older than the buildinfo, so `-b` skipped the program and reported green while a cold CI was
|
|
118
|
+
* red. `-p` with `incremental` compares content hashes. `-b` also wanted an emitted
|
|
119
|
+
* `app.config.js` that `noEmit` never writes, which is why this line was first needed.
|
|
117
120
|
*
|
|
118
121
|
* The note lives HERE and not in the emitted file, for the reason `biome.json` below gives: an
|
|
119
122
|
* app author has no use for eight lines of framework archaeology in their own tsconfig, and
|
|
@@ -4,9 +4,23 @@
|
|
|
4
4
|
// many answers as the app has folders, and the framework's two tracked apps already disagree.
|
|
5
5
|
|
|
6
6
|
import type { GeneratedFile } from './naming';
|
|
7
|
+
import { wrapList } from './wrap';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* A role's grants as Biome prints them. The example slice's \`post:read\`/\`post:write\` ride along
|
|
11
|
+
* with it: \`x g resource post\` would have granted them, and the scaffold writes that result.
|
|
12
|
+
*/
|
|
13
|
+
const grants = (base: string, example: string | undefined): string =>
|
|
14
|
+
wrapList(
|
|
15
|
+
' ',
|
|
16
|
+
'grants: [',
|
|
17
|
+
[base, ...(example === undefined ? [] : [example])].map((grant) => `'${grant}'`),
|
|
18
|
+
'],',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const rolesSource = (
|
|
22
|
+
example: boolean,
|
|
23
|
+
): string => `// Who holds which permission, for the whole app. Roles are sugar: every one expands to a flat
|
|
10
24
|
// permission set before any policy runs, so a rule never reasons about the hierarchy.
|
|
11
25
|
//
|
|
12
26
|
// ONE file, and it lives in shared/ — the leaf both site/ and app/ already import, and the one the
|
|
@@ -16,8 +30,9 @@ const rolesSource =
|
|
|
16
30
|
// role HERE; calling defineRoles() again from a feature folder works and is the drift this file
|
|
17
31
|
// exists to prevent.
|
|
18
32
|
//
|
|
19
|
-
// \`x g policy <feature>\` declares \`<feature>:read\` and \`<feature>:write
|
|
20
|
-
//
|
|
33
|
+
// \`x g policy <feature>\` declares \`<feature>:read\` and \`<feature>:write\` and grants them below —
|
|
34
|
+
// read to member, write to admin. A permission no role holds is one no actor can ever exercise,
|
|
35
|
+
// and \`x verify\`'s policy step refuses it (X_PERMISSION_UNGRANTED).
|
|
21
36
|
|
|
22
37
|
import { definePermissions, defineRoles } from '@ultimat3/policy';
|
|
23
38
|
|
|
@@ -32,11 +47,11 @@ export const appPermissions = definePermissions(['admin:read', 'dashboard:read']
|
|
|
32
47
|
export const roles = defineRoles({
|
|
33
48
|
member: {
|
|
34
49
|
description: 'Signed in. Reads the app surface.',
|
|
35
|
-
|
|
50
|
+
${grants('dashboard:read', example ? 'post:read' : undefined)}
|
|
36
51
|
},
|
|
37
52
|
admin: {
|
|
38
53
|
description: 'Runs the app: the /admin surface, plus everything a member may do.',
|
|
39
|
-
|
|
54
|
+
${grants('admin:read', example ? 'post:write' : undefined)}
|
|
40
55
|
inherits: ['member'],
|
|
41
56
|
},
|
|
42
57
|
});
|
|
@@ -49,12 +64,25 @@ import { expandRoles, isKnownPermission, rolesGranting } from '@ultimat3/policy'
|
|
|
49
64
|
import { expect, unitTest } from '@ultimat3/testing';
|
|
50
65
|
import { appPermissions, roles } from './roles';
|
|
51
66
|
|
|
67
|
+
// Every feature's policy.ts, imported the way the boot scan imports it: \`x g policy\` grants the
|
|
68
|
+
// permissions a policy.ts DECLARES, so the registry below is only whole once they have run.
|
|
69
|
+
const features = new Bun.Glob('{app,site}/*/policy.ts');
|
|
70
|
+
for await (const file of features.scan({ cwd: \`\${import.meta.dir}/..\` })) {
|
|
71
|
+
await import(\`\${import.meta.dir}/../\${file}\`);
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
// The map is passed explicitly rather than read off the module-global one: a test that depended on
|
|
53
75
|
// which module imported first would pass alone and fail inside a suite.
|
|
54
76
|
|
|
77
|
+
// Subsets, never exact lists: every \`x g policy\` adds a grant here, and a pinned list would make
|
|
78
|
+
// the generator's correct edit a red test.
|
|
55
79
|
unitTest('admin inherits every member grant and adds its own', () => {
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
const member = expandRoles(['member'], roles);
|
|
81
|
+
const admin = expandRoles(['admin'], roles);
|
|
82
|
+
expect(member).toContain('dashboard:read');
|
|
83
|
+
expect(member).not.toContain('admin:read');
|
|
84
|
+
expect(admin).toContain('admin:read');
|
|
85
|
+
for (const permission of member) expect(admin).toContain(permission);
|
|
58
86
|
});
|
|
59
87
|
|
|
60
88
|
unitTest('a role nobody declared grants nothing', () => {
|
|
@@ -88,9 +116,9 @@ unitTest('the routes this app ships require permissions this app declares', () =
|
|
|
88
116
|
`;
|
|
89
117
|
|
|
90
118
|
/** `apps/web/shared/roles.ts` and its test. Written by `x new`, with or without the example slice. */
|
|
91
|
-
export function rolesFiles(): readonly GeneratedFile[] {
|
|
119
|
+
export function rolesFiles(example = false): readonly GeneratedFile[] {
|
|
92
120
|
return [
|
|
93
|
-
{ path: 'apps/web/shared/roles.ts', contents: rolesSource() },
|
|
121
|
+
{ path: 'apps/web/shared/roles.ts', contents: rolesSource(example) },
|
|
94
122
|
{ path: 'apps/web/shared/roles.test.ts', contents: rolesTest() },
|
|
95
123
|
];
|
|
96
124
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `policyFiles`; the five generators that write *into* a slice imported the same files and wrote
|
|
4
4
|
// none of them, so each emitted TS2307 in any slice a resource had not been run in first.
|
|
5
5
|
|
|
6
|
-
import { stripComments } from '
|
|
6
|
+
import { stripComments } from '@ultimat3/core';
|
|
7
7
|
import type { FeatureTarget } from './entity';
|
|
8
8
|
import { entityFiles } from './entity';
|
|
9
9
|
import type { GeneratedFile, NameSet } from './naming';
|
package/src/templates/wrap.ts
CHANGED
|
@@ -80,7 +80,10 @@ const compareSpecifiers = (left: string, right: string): number => {
|
|
|
80
80
|
* than spelled in each template, because the correct spelling is not knowable at authoring time.
|
|
81
81
|
*/
|
|
82
82
|
export const sortSpecifiers = (names: readonly string[]): readonly string[] =>
|
|
83
|
-
[...names].sort(compareSpecifiers);
|
|
83
|
+
[...names].sort((left, right) => compareSpecifiers(bare(left), bare(right)));
|
|
84
|
+
|
|
85
|
+
/** An inline \`type X\` sorts as \`X\`: Biome orders by the imported name, never by the modifier. */
|
|
86
|
+
const bare = (name: string): string => (name.startsWith('type ') ? name.slice(5) : name);
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
89
|
* A named import, sorted and wrapped. Its own function because the braces are spaced on one line
|
package/src/test-passes.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// this file decides how many, and `test-shards.ts` spends them.
|
|
4
4
|
|
|
5
5
|
import type { TestFile } from './test-select';
|
|
6
|
+
import { SERIAL_TYPES } from './test-workers';
|
|
6
7
|
import type { TestType } from './verify-tests';
|
|
7
|
-
import { ownerOf
|
|
8
|
+
import { ownerOf } from './verify-tests';
|
|
8
9
|
|
|
9
10
|
/** One `bun test` invocation: which files, how wide, and the type its reproduce line names. */
|
|
10
11
|
export interface TestPass {
|
package/src/test-workers.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// Bun ships no CPU-count primitive; `cpus()` is the fallback when navigator cannot answer.
|
|
7
7
|
import { cpus } from 'node:os';
|
|
8
|
+
import type { TestType } from '@ultimat3/testing';
|
|
8
9
|
|
|
9
10
|
/** navigator first: it is the runtime's own answer, and it respects a container's CPU limit. */
|
|
10
11
|
export function availableCpus(): number {
|
|
@@ -48,3 +49,28 @@ export const WORKER_FLOOR = 2;
|
|
|
48
49
|
|
|
49
50
|
export const defaultWorkers = (available: number = availableCpus()): number =>
|
|
50
51
|
Math.max(WORKER_FLOOR, Math.min(WORKER_CEILING, Math.round(available * WORKER_OVERSUBSCRIBE)));
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Which types run across worker processes, and why the other two cannot.
|
|
55
|
+
*
|
|
56
|
+
* Parallel is safe when the only thing a test file shares with another file is the database, and
|
|
57
|
+
* the database is per worker by construction (`ULTIMATE_TEST_WORKER` → one clone of the migrated
|
|
58
|
+
* template, `@ultimat3/testing`'s `acquireWorkerDatabase`). Every other process-global in this
|
|
59
|
+
* framework — the permission set, the roles, the entity/action/query registries, the error-code
|
|
60
|
+
* titles, the fixture bag — is handled by `--isolate` giving each FILE its own module registry.
|
|
61
|
+
*
|
|
62
|
+
* | Type | Why |
|
|
63
|
+
* |---|---|
|
|
64
|
+
* | `live` | **serial.** A logical replication slot and a publication are named at the Postgres
|
|
65
|
+
* CLUSTER level, not inside a database, and this repo's own feed tests hard-code
|
|
66
|
+
* `x_live_slot` / `x_live_pub` against `TEST_REPLICATION_URL` — the one server, never a per-worker
|
|
67
|
+
* clone. Two workers would race `pg_create_logical_replication_slot` and the loser's failure would
|
|
68
|
+
* read as a flake. A per-worker database does not isolate a cluster-wide object |
|
|
69
|
+
* | `e2e` | **serial.** It runs against the *built output*: one `dist/`, one service-worker
|
|
70
|
+
* registration, one browser profile. There is nothing per-worker to hand it, and the type is
|
|
71
|
+
* seconds at most, so a split would buy a race and no time |
|
|
72
|
+
*
|
|
73
|
+
* The two are named here rather than tested for, because "can this type be sharded?" is a design
|
|
74
|
+
* fact about the type, not something a run can discover about itself.
|
|
75
|
+
*/
|
|
76
|
+
export const SERIAL_TYPES: readonly TestType[] = ['live', 'e2e'];
|
package/src/ts-scan.ts
CHANGED
|
@@ -30,14 +30,11 @@ export interface CodeScan {
|
|
|
30
30
|
readonly unresolved: readonly UnresolvedCodeSite[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// The masking itself — `QUOTES`, `endOfLiteral`, `stripComments`, `maskLiterals` —
|
|
34
|
-
// `@ultimat3/core`'s `source-mask.ts`
|
|
35
|
-
//
|
|
36
|
-
// this package, and this package's public API, keep the names they had.
|
|
33
|
+
// The masking itself — `QUOTES`, `endOfLiteral`, `stripComments`, `maskLiterals` — is
|
|
34
|
+
// `@ultimat3/core`'s (`source-mask.ts`), imported from there by every scanner. It was re-exported
|
|
35
|
+
// here and from this package's barrel: two import paths for one function (22.0.0 removed both).
|
|
37
36
|
import { endOfLiteral, maskLiterals, QUOTES, stripComments } from '@ultimat3/core';
|
|
38
37
|
|
|
39
|
-
export { endOfLiteral, maskLiterals, QUOTES, stripComments };
|
|
40
|
-
|
|
41
38
|
// `ReadonlySet`, so a consumer cannot mutate what every scan in this package reads.
|
|
42
39
|
export const OPENERS: ReadonlySet<string> = new Set(['(', '[', '{']);
|
|
43
40
|
export const CLOSERS: ReadonlySet<string> = new Set([')', ']', '}']);
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
// root that has no `tsconfig.json` at all, so an `existsSync` ahead of it was a second question
|
|
8
8
|
// with one answer.
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
10
|
+
import { ERROR_DOCS_URL, maskLiterals, stripComments } from '@ultimat3/core';
|
|
11
11
|
import type { Finding } from './output';
|
|
12
|
-
import { maskLiterals, stripComments } from './ts-scan';
|
|
13
12
|
|
|
14
13
|
const ROOT_TSCONFIG = 'tsconfig.json';
|
|
15
14
|
|