@ultimat3/cli 29.4.0 → 30.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 +64 -40
- package/README.md +61 -11
- package/package.json +42 -28
- package/src/affected-tests.ts +97 -0
- package/src/affected-unowned.ts +43 -0
- package/src/affected.ts +25 -6
- package/src/app-boundaries.ts +29 -1
- package/src/app-evals.ts +12 -4
- package/src/app-load-unresolved.ts +62 -0
- package/src/app-load.ts +30 -37
- package/src/app-mcp.ts +14 -2
- package/src/app-module-graph.ts +232 -0
- package/src/app-reload-graph.ts +2 -2
- package/src/app-root.ts +37 -1
- package/src/bin.ts +1 -0
- package/src/boundary-findings.ts +6 -0
- package/src/budgets-freshness.ts +32 -0
- package/src/budgets.ts +20 -1
- package/src/build-capture-env.ts +9 -0
- package/src/cdp-shot-a11y.ts +2 -39
- package/src/ci-tested.ts +243 -0
- package/src/client-emit-customer-runtime.ts +139 -0
- package/src/client-emit-customer.ts +108 -0
- package/src/client-emit-drift.ts +72 -0
- package/src/client-emit-header.ts +67 -0
- package/src/client-emit-schemas.ts +175 -0
- package/src/client-emit-types.ts +121 -0
- package/src/client-emit-vendor.ts +146 -0
- package/src/client-errors-routes.ts +229 -0
- package/src/client-errors-script.ts +74 -0
- package/src/client-vitals-route.ts +197 -0
- package/src/client-vitals-script.ts +56 -0
- package/src/cmd-affected.ts +23 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +35 -9
- package/src/cmd-ci-spec.ts +42 -6
- package/src/cmd-ci.ts +99 -2
- package/src/cmd-client-customer.ts +134 -0
- package/src/cmd-client-source.ts +81 -0
- package/src/cmd-client-spec.ts +48 -0
- package/src/cmd-client-write.ts +41 -0
- package/src/cmd-client.ts +111 -0
- package/src/cmd-db-recount.ts +120 -0
- package/src/cmd-db-spec.ts +36 -6
- package/src/cmd-db.ts +11 -3
- package/src/cmd-deploy-compose.ts +61 -3
- package/src/cmd-deploy-drain.ts +10 -2
- package/src/cmd-deploy-helm.ts +11 -0
- package/src/cmd-deploy-preflight.ts +118 -0
- package/src/cmd-deploy-spec.ts +58 -6
- package/src/cmd-deploy-verify.ts +380 -0
- package/src/cmd-deploy.ts +42 -9
- package/src/cmd-docs.ts +34 -4
- package/src/cmd-doctor.ts +66 -9
- package/src/cmd-drill-spec.ts +25 -0
- package/src/cmd-drill.ts +176 -0
- package/src/cmd-env-init.ts +102 -0
- package/src/cmd-env-spec.ts +10 -3
- package/src/cmd-env.ts +15 -10
- package/src/cmd-errors-recent.ts +60 -0
- package/src/cmd-errors-spec.ts +9 -1
- package/src/cmd-errors.ts +4 -0
- package/src/cmd-exercise-spec.ts +19 -0
- package/src/cmd-exercise.ts +129 -0
- package/src/cmd-generate-spec.ts +9 -1
- package/src/cmd-generate.ts +32 -27
- package/src/cmd-graph-load.ts +97 -0
- package/src/cmd-graph-spec.ts +16 -0
- package/src/cmd-graph.ts +73 -0
- package/src/cmd-i18n.ts +4 -1
- package/src/cmd-impact-spec.ts +13 -0
- package/src/cmd-impact.ts +64 -0
- package/src/cmd-jobs-resolve.ts +76 -0
- package/src/cmd-jobs-spec.ts +21 -2
- package/src/cmd-jobs.ts +10 -6
- package/src/cmd-mail-spec.ts +28 -0
- package/src/cmd-mail.ts +70 -0
- package/src/cmd-mcp-lock.ts +69 -0
- package/src/cmd-mcp-spec.ts +23 -4
- package/src/cmd-mcp.ts +17 -3
- package/src/cmd-new-spec.ts +8 -1
- package/src/cmd-new.ts +61 -9
- package/src/cmd-planned.ts +0 -6
- package/src/cmd-repro-spec.ts +31 -0
- package/src/cmd-repro.ts +174 -0
- package/src/cmd-stop-spec.ts +21 -0
- package/src/cmd-stop.ts +166 -0
- package/src/cmd-test-spec.ts +17 -4
- package/src/cmd-test.ts +70 -14
- package/src/cmd-upgrade-spec.ts +26 -0
- package/src/cmd-upgrade.ts +274 -0
- package/src/cmd-verify-spec.ts +3 -3
- package/src/db-backfill.ts +6 -0
- package/src/db-diagnostics.ts +106 -0
- package/src/db-generate.ts +9 -0
- package/src/db-reset-template.ts +1 -1
- package/src/db-rolling.ts +45 -0
- package/src/db-seed-generate.ts +188 -0
- package/src/deploy-verify-findings.ts +98 -0
- package/src/deploy-verify-flags.ts +109 -0
- package/src/deploy-verify-grade.ts +185 -0
- package/src/deploy-verify-queues.ts +53 -0
- package/src/dev-boot.ts +35 -15
- package/src/dev-child-watch.ts +16 -4
- package/src/dev-dashboard-guard.ts +8 -1
- package/src/dev-dashboard.ts +7 -0
- package/src/dev-lock-identity.ts +61 -0
- package/src/dev-lock-port-holder.ts +80 -0
- package/src/dev-lock-steal.ts +124 -0
- package/src/dev-lock.ts +92 -120
- package/src/dev-mail-templates.ts +51 -0
- package/src/dev-rebuild.ts +26 -2
- package/src/dev-route-table.ts +17 -0
- package/src/dev-supervisor.ts +5 -2
- package/src/dev-watch-tree.ts +11 -10
- package/src/dev-watch.ts +1 -1
- package/src/dispatch.ts +29 -6
- package/src/docs-section.ts +69 -0
- package/src/docs-sources.ts +100 -0
- package/src/doctor-auth.ts +11 -2
- package/src/doctor-chart-alerts.ts +55 -0
- package/src/doctor-pooler.ts +107 -0
- package/src/doctor-rolling.ts +35 -0
- package/src/doctor-row-security.ts +40 -0
- package/src/document-options.ts +13 -0
- package/src/drill-sync-lane.ts +25 -0
- package/src/drill-targets.ts +39 -0
- package/src/duplicate-packages.ts +1 -1
- package/src/env-production.ts +93 -0
- package/src/error-codes-shot.ts +59 -0
- package/src/error-codes-tooling.ts +29 -0
- package/src/error-codes.ts +100 -54
- package/src/error-contract.ts +2 -2
- package/src/errors.ts +22 -3
- package/src/exercise-database.ts +99 -0
- package/src/exercise-findings.ts +159 -0
- package/src/exercise-overrides.ts +42 -0
- package/src/exercise-registration.ts +58 -0
- package/src/exercise-static.ts +88 -0
- package/src/exercise-subject.ts +69 -0
- package/src/failure-capture-boot.ts +30 -0
- package/src/framework-env.ts +7 -1
- package/src/framework-schema.ts +72 -5
- package/src/generate-catalog-locales.ts +19 -0
- package/src/generate-chart-alerts.ts +156 -0
- package/src/generate-feature-required.ts +65 -0
- package/src/generate-feature.ts +83 -6
- package/src/generate-files.ts +24 -0
- package/src/generate-format-written.ts +32 -0
- package/src/generate-kinds.ts +6 -0
- package/src/generate-sign-in-config.ts +100 -0
- package/src/generate-sign-in-manifest.ts +28 -0
- package/src/generate-sign-in-mode.ts +101 -0
- package/src/generate-sign-in.ts +250 -0
- package/src/generate-write.ts +44 -7
- package/src/hand-rolled-socket.ts +72 -0
- package/src/i18n-app-key-shapes.ts +29 -0
- package/src/i18n-audit.ts +10 -3
- package/src/i18n-registration.ts +18 -3
- package/src/index.ts +2 -0
- package/src/island-bundle.ts +4 -3
- package/src/island-disposer.ts +116 -0
- package/src/island-realtime-state.ts +34 -6
- package/src/island-realtime.ts +6 -2
- package/src/island-routes.ts +3 -2
- package/src/isr-query-advice.ts +6 -11
- package/src/isr-query-guard.ts +4 -3
- package/src/job-budget.ts +95 -0
- package/src/jobs-json.ts +11 -0
- package/src/lint-findings.ts +104 -0
- package/src/local-cli.ts +34 -4
- package/src/loopback-host.ts +2 -2
- package/src/mail-preview-findings.ts +24 -0
- package/src/mcp-errors.ts +82 -2
- package/src/mcp-host.ts +11 -1
- package/src/mcp-lock-findings.ts +57 -0
- package/src/measure-paths.ts +12 -4
- package/src/measure-scope.ts +27 -13
- package/src/messages-client.ts +23 -0
- package/src/messages-db.ts +56 -0
- package/src/messages-gate.ts +107 -0
- package/src/messages-repro.ts +11 -0
- package/src/messages-shot.ts +51 -0
- package/src/messages-upgrade.ts +45 -0
- package/src/messages.ts +53 -178
- package/src/metrics-endpoint.ts +23 -5
- package/src/new-shapes-generate.ts +71 -0
- package/src/new-shapes.ts +63 -0
- package/src/openapi-compile-ir.ts +68 -0
- package/src/openapi-compile-schema.ts +284 -0
- package/src/openapi-compile.ts +273 -0
- package/src/openapi-refuse.ts +45 -0
- package/src/otlp-export.ts +2 -1
- package/src/output-findings.ts +123 -0
- package/src/output.ts +32 -8
- package/src/package-boundaries.ts +110 -0
- package/src/page-pins.ts +23 -0
- package/src/plain-routes.ts +13 -0
- package/src/prerender-entries.ts +59 -0
- package/src/prerender-heaviest.ts +27 -0
- package/src/prerender-preload.ts +6 -0
- package/src/prerender-report.ts +12 -0
- package/src/prerender-seo-reads.ts +38 -0
- package/src/prerender-served.ts +22 -7
- package/src/prerender-vitals.ts +42 -0
- package/src/prerender.ts +61 -32
- package/src/pwa-artifacts.ts +4 -3
- package/src/pwa-manifest-locales.ts +1 -1
- package/src/registry.ts +18 -0
- package/src/repro-bundle.ts +65 -0
- package/src/repro-errors.ts +47 -0
- package/src/repro-job.ts +72 -0
- package/src/repro-literal.ts +41 -0
- package/src/repro-request.ts +48 -0
- package/src/repro-write.ts +100 -0
- package/src/retain-sweep.ts +27 -0
- package/src/retained-assets-boot.ts +113 -0
- package/src/retained-assets-layout.ts +110 -0
- package/src/retained-assets-read.ts +142 -0
- package/src/retained-assets-sweep.ts +70 -0
- package/src/retained-assets.ts +171 -0
- package/src/revalidated-response.ts +8 -5
- package/src/role-replicator.ts +7 -1
- package/src/role-start-devices.ts +119 -0
- package/src/role-start-types.ts +6 -0
- package/src/role-start.ts +22 -5
- package/src/role-sync.ts +7 -1
- package/src/route-islands.ts +42 -0
- package/src/route-table.ts +2 -1
- package/src/row-security-boot.ts +58 -0
- package/src/row-security-wanted.ts +32 -0
- package/src/runtime-admin.ts +0 -1
- package/src/runtime-assets-site-cache.ts +55 -0
- package/src/runtime-assets-site.ts +136 -0
- package/src/runtime-assets.ts +99 -55
- package/src/runtime-bindings.ts +19 -3
- package/src/runtime-cache.ts +23 -14
- package/src/runtime-isr-outcome.ts +114 -60
- package/src/runtime-purge.ts +14 -2
- package/src/runtime-queue.ts +31 -4
- package/src/runtime-render-document.ts +204 -0
- package/src/runtime-render.ts +48 -198
- package/src/runtime-replica-position.ts +62 -0
- package/src/runtime-replica.ts +18 -3
- package/src/runtime-s3-path-style.ts +17 -0
- package/src/runtime-services.ts +19 -6
- package/src/runtime-statement-bound.ts +22 -0
- package/src/runtime-storage-upload.ts +9 -0
- package/src/runtime-storage.ts +41 -81
- package/src/schema-diff-views.ts +54 -0
- package/src/schema-diff.ts +21 -3
- package/src/schema-drift.ts +28 -9
- package/src/schema-dump-drift.ts +91 -7
- package/src/script-csp.ts +1 -1
- package/src/seo-cards.ts +26 -0
- package/src/seo-routes.ts +22 -8
- package/src/serve-boot.ts +54 -21
- package/src/serve-drain.ts +1 -0
- package/src/serve-env.ts +26 -8
- package/src/serve-retire.ts +12 -6
- package/src/serve-web.ts +42 -4
- package/src/serve.ts +9 -7
- package/src/site-asset-routes.ts +7 -1
- package/src/site-config.ts +4 -0
- package/src/site-llms-pages.ts +106 -0
- package/src/site-llms.ts +64 -0
- package/src/site-seo-write.ts +4 -2
- package/src/site-seo.ts +55 -6
- package/src/source-mtime.ts +50 -0
- package/src/static-capture-errors.ts +41 -0
- package/src/static-capture.ts +56 -0
- package/src/static-cards-serve.ts +39 -0
- package/src/static-cards.ts +93 -0
- package/src/static-document.ts +14 -13
- package/src/static-images.ts +115 -0
- package/src/stream-hole-scope.ts +55 -0
- package/src/style-bundle.ts +1 -1
- package/src/style-routes.ts +3 -2
- package/src/sw-precache-plan.ts +2 -2
- package/src/sw-routes.ts +2 -2
- package/src/templates/action-tests.ts +306 -0
- package/src/templates/action.ts +81 -269
- package/src/templates/backfill.ts +1 -1
- package/src/templates/entity.ts +21 -5
- package/src/templates/github/ci.yml.ts +8 -9
- package/src/templates/guard-bare-error.ts +2 -2
- package/src/templates/guard-error-declared.ts +263 -0
- package/src/templates/guard-native-confirm.ts +132 -0
- package/src/templates/guard-raw-fetch.ts +151 -0
- package/src/templates/guard-raw-interval.ts +121 -0
- package/src/templates/guard-repo-raw-sql.ts +71 -2
- package/src/templates/job.ts +1 -1
- package/src/templates/naming.ts +5 -0
- package/src/templates/pinned/docker-compose.yml +6 -0
- package/src/templates/pinned-images.ts +8 -0
- package/src/templates/policy.ts +85 -2
- package/src/templates/query.ts +70 -13
- package/src/templates/resource-create.ts +3 -1
- package/src/templates/resource-form-island.ts +15 -4
- package/src/templates/resource-page-db.ts +83 -0
- package/src/templates/resource-page.ts +161 -52
- package/src/templates/route.ts +4 -6
- package/src/templates/scaffold-app.ts +3 -0
- package/src/templates/scaffold-auth.ts +2 -1
- package/src/templates/scaffold-bin.ts +5 -52
- package/src/templates/scaffold-claude-agents.ts +19 -19
- package/src/templates/scaffold-claude-commands.ts +28 -31
- package/src/templates/scaffold-claude.ts +26 -23
- package/src/templates/scaffold-container-compose.ts +6 -1
- package/src/templates/scaffold-container.ts +24 -4
- package/src/templates/scaffold-dashboard-bare.ts +3 -3
- package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
- package/src/templates/scaffold-dashboard-example-view.ts +156 -0
- package/src/templates/scaffold-dashboard-example.ts +43 -131
- package/src/templates/scaffold-dashboard-shared.ts +3 -7
- package/src/templates/scaffold-db-client.ts +20 -6
- package/src/templates/scaffold-db-package.ts +5 -14
- package/src/templates/scaffold-docs.ts +73 -57
- package/src/templates/scaffold-domain-package.ts +18 -14
- package/src/templates/scaffold-entries.ts +7 -2
- package/src/templates/scaffold-exercise.ts +37 -0
- package/src/templates/scaffold-guards.ts +13 -1
- package/src/templates/scaffold-helm-alerts.ts +234 -0
- package/src/templates/scaffold-helm-templates.ts +7 -5
- package/src/templates/scaffold-helm.ts +46 -12
- package/src/templates/scaffold-i18n.ts +6 -2
- package/src/templates/scaffold-mcp-endpoint.ts +182 -0
- package/src/templates/scaffold-mcp-json.ts +31 -0
- package/src/templates/scaffold-mcp-package.ts +59 -56
- package/src/templates/scaffold-repo.ts +7 -2
- package/src/templates/scaffold-shell.ts +45 -79
- package/src/templates/scaffold-test-services.ts +13 -5
- package/src/templates/shapes/api-keys.ts +170 -0
- package/src/templates/shapes/api.ts +101 -0
- package/src/templates/shapes/blog-site.ts +340 -0
- package/src/templates/shapes/blog-slice.ts +265 -0
- package/src/templates/shapes/blog.ts +114 -0
- package/src/templates/shapes/dashboard-detail.ts +242 -0
- package/src/templates/shapes/dashboard.ts +53 -0
- package/src/templates/shapes/index.ts +17 -0
- package/src/templates/shapes/overlay.ts +99 -0
- package/src/templates/shapes/shared-edits.ts +46 -0
- package/src/templates/sign-in-action-tests.ts +237 -0
- package/src/templates/sign-in-authenticator.ts +112 -0
- package/src/templates/sign-in-oidc.ts +189 -0
- package/src/templates/sign-in-pages.ts +148 -0
- package/src/templates/sign-in-password.ts +216 -0
- package/src/templates/sign-in-shared.ts +268 -0
- package/src/templates/slice-foundation.ts +70 -15
- package/src/test-batches.ts +1 -1
- package/src/test-database.ts +1 -1
- package/src/test-shards.ts +7 -1
- package/src/test-workers.ts +18 -12
- package/src/tool-gate-names.ts +34 -0
- package/src/upgrade-agents-link.ts +49 -0
- package/src/upgrade-bun-floor.ts +225 -0
- package/src/upgrade-gate.ts +228 -0
- package/src/upgrade-pins.ts +63 -0
- package/src/upgrade-plan.ts +214 -0
- package/src/upgrade-registry.ts +53 -0
- package/src/upgrade-report.ts +139 -0
- package/src/upgrade-root.ts +74 -0
- package/src/verify-budgets-build.ts +118 -0
- package/src/verify-checks.ts +67 -24
- package/src/verify-coverage-run.ts +1 -1
- package/src/verify-e2e-lane.ts +41 -0
- package/src/verify-exercise.ts +64 -0
- package/src/verify-floor.ts +41 -0
- package/src/verify-merge.ts +19 -4
- package/src/verify-role-load.ts +2 -0
- package/src/verify-run.ts +28 -8
- package/src/verify-shard.ts +12 -2
- package/src/verify-skip-ceiling.ts +87 -0
- package/src/verify-step.ts +5 -0
- package/src/verify-tests.ts +25 -5
- package/src/view-drift.ts +55 -0
- package/src/worker-bundle.ts +3 -2
- package/src/workspace-graph.ts +38 -19
- package/src/write-generated.ts +43 -0
- package/src/zone-boot.ts +20 -0
- package/wiki/Actions.md +342 -0
- package/wiki/Admin-Dashboard.md +432 -0
- package/wiki/Agents.md +958 -0
- package/wiki/Auth.md +195 -0
- package/wiki/Bare-VM.md +111 -0
- package/wiki/Batching-And-Preloading.md +136 -0
- package/wiki/Building-Your-Own-Base.md +203 -0
- package/wiki/CI-Parallel-Gate.md +220 -0
- package/wiki/CLI-Reference.md +1894 -0
- package/wiki/Caching-And-Invalidation.md +255 -0
- package/wiki/Client-Data.md +194 -0
- package/wiki/Client-Navigation.md +513 -0
- package/wiki/Compared.md +93 -0
- package/wiki/Configuration.md +868 -0
- package/wiki/Contributing.md +12 -0
- package/wiki/Deploy-Recipes.md +122 -0
- package/wiki/Deployment.md +508 -0
- package/wiki/Entities-And-Migrations.md +1116 -0
- package/wiki/FAQ.md +175 -0
- package/wiki/Feature-Flags.md +65 -0
- package/wiki/Getting-Started.md +221 -0
- package/wiki/Home.md +160 -0
- package/wiki/I18n.md +239 -0
- package/wiki/Installation.md +206 -0
- package/wiki/Interface-Rules.md +339 -0
- package/wiki/Jobs-And-Workflows.md +783 -0
- package/wiki/Known-Gaps.md +185 -0
- package/wiki/MCP-And-AI.md +753 -0
- package/wiki/Mail.md +138 -0
- package/wiki/Migrating-An-Existing-App.md +594 -0
- package/wiki/Migrating-From-Django.md +54 -0
- package/wiki/Migrating-From-Laravel.md +53 -0
- package/wiki/Migrating-From-Node-Drizzle.md +53 -0
- package/wiki/Migrating-From-Rails.md +53 -0
- package/wiki/Migrations-And-Backfills.md +552 -0
- package/wiki/Money.md +338 -0
- package/wiki/N-Plus-One-Detection.md +197 -0
- package/wiki/Notify.md +208 -0
- package/wiki/Observability.md +245 -0
- package/wiki/Outbound-HTTP.md +109 -0
- package/wiki/PWA-And-Offline.md +343 -0
- package/wiki/Policies-And-Authz.md +251 -0
- package/wiki/Project-Layout.md +150 -0
- package/wiki/Queries-And-Live-Queries.md +429 -0
- package/wiki/Realtime.md +734 -0
- package/wiki/Resource-Management.md +121 -0
- package/wiki/Routes-And-Render-Modes.md +521 -0
- package/wiki/SEO.md +288 -0
- package/wiki/Scheduled-Tasks.md +175 -0
- package/wiki/Scraping.md +493 -0
- package/wiki/Shapes.md +63 -0
- package/wiki/Stability.md +97 -0
- package/wiki/Static-Assets.md +166 -0
- package/wiki/Storage-And-Uploads.md +404 -0
- package/wiki/Testing.md +1314 -0
- package/wiki/The-Eight-Primitives.md +247 -0
- package/wiki/Theming.md +362 -0
- package/wiki/Timezones-And-Dates.md +194 -0
- package/wiki/Troubleshooting.md +163 -0
- package/wiki/Tutorial-01-First-App.md +289 -0
- package/wiki/Tutorial-02-First-Feature.md +339 -0
- package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
- package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
- package/wiki/Tutorial-05-Deploy-Free.md +231 -0
- package/wiki/Tutorial-06-Growing-Up.md +158 -0
- package/wiki/UI-Components.md +156 -0
- package/wiki/Upgrading.md +3667 -0
- package/src/stored-object-headers.ts +0 -43
|
@@ -18,28 +18,32 @@ const domainIndex = (
|
|
|
18
18
|
): string => `// Pure types and constants. No I/O of any kind: no fs, no network, no database, no env reads.
|
|
19
19
|
// \`Money\` is @ultimat3/schema's, re-exported and never restated: one declaration of
|
|
20
20
|
// { minor, currency } is what makes an entity column, a form field and this module the same type.
|
|
21
|
-
import {
|
|
21
|
+
import { defineError } from '@ultimat3/core';
|
|
22
22
|
import type { MoneyValue as Money } from '@ultimat3/schema';
|
|
23
23
|
|
|
24
24
|
export type { MoneyValue as Money } from '@ultimat3/schema';
|
|
25
25
|
|
|
26
|
+
interface CurrencyPair {
|
|
27
|
+
readonly left: string;
|
|
28
|
+
readonly right: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* Never a bare Error: an agent reading the failure needs the code, the cause and the exact command
|
|
28
33
|
* that resolves it. Adding across currencies is a bug in the caller's data, not a runtime hiccup,
|
|
29
|
-
* so the fix names the conversion that has to happen first.
|
|
30
|
-
*
|
|
31
|
-
* No \`docs:\`:
|
|
32
|
-
* one home. A per-code URL written here is a page that does not exist.
|
|
34
|
+
* so it answers 422 and the fix names the conversion that has to happen first. One declaration
|
|
35
|
+
* — \`defineError\` — holds the title, the status and the class; \`guards/error-declared.ts\` keeps
|
|
36
|
+
* it the only way. No \`docs:\`: the link resolves from the code's registered descriptor.
|
|
33
37
|
*/
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
38
|
+
export const ${app.pascal}CurrencyMismatchError = defineError<CurrencyPair>({
|
|
39
|
+
code: '${currencyCode(app)}',
|
|
40
|
+
title: 'money added across two currencies',
|
|
41
|
+
status: 422,
|
|
42
|
+
cause: (pair) =>
|
|
43
|
+
\`cannot add \${pair.left} to \${pair.right}: Money is only additive inside one currency\`,
|
|
44
|
+
fix: (pair) =>
|
|
45
|
+
\`bun test packages/domain/src/index.test.ts # add() is defined only inside one currency — convert \${pair.right} to \${pair.left} before calling it\`,
|
|
46
|
+
});
|
|
43
47
|
|
|
44
48
|
export const zero = (currency: string): Money => ({ minor: 0, currency });
|
|
45
49
|
|
|
@@ -91,8 +91,10 @@ const origin = Bun.env.SITE_ORIGIN;
|
|
|
91
91
|
* from the CDN and from the container.
|
|
92
92
|
*
|
|
93
93
|
* A DYNAMIC route contributes exactly the URLs this build emitted for it, read back off the report:
|
|
94
|
-
* the sitemap then cannot name a page the artifact does not contain.
|
|
95
|
-
*
|
|
94
|
+
* the sitemap then cannot name a page the artifact does not contain. What each page says of itself —
|
|
95
|
+
* its \`lastmod\`, the \`locales\` it is listed in — is \`report.entries\`: the build's own
|
|
96
|
+
* \`prerender()\` answers, so a route that lists its pages off the app's queries is never asked again
|
|
97
|
+
* once the build has stopped answering them.
|
|
96
98
|
*
|
|
97
99
|
* With \`seo.sitemap.sections\` the answer is an index and its parts: \`/sitemap.xml\` plus
|
|
98
100
|
* \`/sitemaps/<name>-<n>.xml\`, every one written here.
|
|
@@ -110,9 +112,12 @@ async function writeSeoFiles(report: PrerenderReport): Promise<readonly string[]
|
|
|
110
112
|
disallow: settings.disallow,
|
|
111
113
|
robotsGroups: settings.robotsGroups,
|
|
112
114
|
sitemap: settings.sitemap,
|
|
115
|
+
llms: settings.llms,
|
|
113
116
|
root,
|
|
114
117
|
pagesFor: (route) =>
|
|
115
118
|
report.pages.filter((page) => page.route === route).map((page) => page.path),
|
|
119
|
+
entries: report.entries,
|
|
120
|
+
llmsPages: report.llmsPages,
|
|
116
121
|
});
|
|
117
122
|
return await writeSiteSeo(out, seo);
|
|
118
123
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// `apps/web/exercise.ts`: the app's overrides for `x exercise`, empty. Written by `x new` so the
|
|
2
|
+
// file every `X_CONTRACT_EXERCISE_UNREACHED` fix names exists before the first line is pasted in.
|
|
3
|
+
|
|
4
|
+
import type { GeneratedFile } from './naming';
|
|
5
|
+
|
|
6
|
+
const exercise =
|
|
7
|
+
(): string => `// What \`x exercise\` cannot guess about this app: an input a primitive needs bound to a real
|
|
8
|
+
// row, an actor its policy permits, or a primitive that must not run in the pass and why. Empty
|
|
9
|
+
// runs every action, query and job as generated. \`x exercise --json\` prints the entry to paste
|
|
10
|
+
// for each one it could not reach; each value is the \`exercise\` option \`.contract()\` takes.
|
|
11
|
+
import { defineExercise } from '@ultimat3/testing';
|
|
12
|
+
|
|
13
|
+
export const exercise = defineExercise({});
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
// The import IS the check: `defineExercise` refuses a blank `off` as the module loads, so a reason
|
|
17
|
+
// deleted from an entry fails `bun test` here, before the gate's `exercise` step runs at all.
|
|
18
|
+
const exerciseTest =
|
|
19
|
+
(): string => `// The overrides load, and every primitive they turn off says why.
|
|
20
|
+
import { expect, unitTest } from '@ultimat3/testing';
|
|
21
|
+
import { exercise } from './exercise';
|
|
22
|
+
|
|
23
|
+
unitTest('apps/web/exercise.ts loads, and every off entry names its reason', () => {
|
|
24
|
+
for (const [name, entry] of exercise.entries) {
|
|
25
|
+
if ('off' in entry) {
|
|
26
|
+
expect({ name, blank: entry.off.trim() === '' }).toEqual({ name, blank: false });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
expect(exercise.entries).toBeInstanceOf(Map);
|
|
30
|
+
});
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/** Written by `x new`, with or without the example slice: the gate's `exercise` step reads it. */
|
|
34
|
+
export const exerciseFiles = (): readonly GeneratedFile[] => [
|
|
35
|
+
{ path: 'apps/web/exercise.ts', contents: exercise() },
|
|
36
|
+
{ path: 'apps/web/exercise.test.ts', contents: exerciseTest() },
|
|
37
|
+
];
|
|
@@ -18,15 +18,23 @@
|
|
|
18
18
|
// design system's rules a scaffolded app's gate held.
|
|
19
19
|
//
|
|
20
20
|
// And one is about DATA: a `repo.ts` reads through the typed handle. It ships because the generator
|
|
21
|
-
// used to emit string SQL, and every app that started from it copied the shape.
|
|
21
|
+
// used to emit string SQL, and every app that started from it copied the shape. One is about the
|
|
22
|
+
// NETWORK: a server `fetch(` carries a signal, or is `outbound()` from `@ultimat3/core/outbound`. `error-declared`
|
|
23
|
+
// is `bare-error`'s second half: an app code is declared once, with core's `defineError`. One is
|
|
24
|
+
// about the TAB: an island polls with `visibleInterval()`, which a hidden tab and a lost session stop.
|
|
25
|
+
// One is about the QUESTION: a destructive act asks through `ConfirmDialog`, never `confirm(`.
|
|
22
26
|
|
|
23
27
|
import { animatedLayoutPropertyGuardFiles } from './guard-animated-layout-property';
|
|
24
28
|
import { bareErrorGuardFiles } from './guard-bare-error';
|
|
29
|
+
import { errorDeclaredGuardFiles } from './guard-error-declared';
|
|
25
30
|
import { focusVisibleGuardFiles } from './guard-focus-visible';
|
|
26
31
|
import { imageDimensionsGuardFiles } from './guard-image-dimensions';
|
|
27
32
|
import { islandWithoutStatesGuardFiles } from './guard-island-without-states';
|
|
33
|
+
import { nativeConfirmGuardFiles } from './guard-native-confirm';
|
|
28
34
|
import { rawBreakpointGuardFiles } from './guard-raw-breakpoint';
|
|
29
35
|
import { rawColourGuardFiles } from './guard-raw-colour';
|
|
36
|
+
import { rawFetchGuardFiles } from './guard-raw-fetch';
|
|
37
|
+
import { rawIntervalGuardFiles } from './guard-raw-interval';
|
|
30
38
|
import { rawLengthGuardFiles } from './guard-raw-length';
|
|
31
39
|
import { rawMotionGuardFiles } from './guard-raw-motion';
|
|
32
40
|
import { rawShadowGuardFiles } from './guard-raw-shadow';
|
|
@@ -48,11 +56,15 @@ import type { GeneratedFile } from './naming';
|
|
|
48
56
|
const SHIPPED: readonly (readonly [string, () => readonly GeneratedFile[]])[] = [
|
|
49
57
|
['animated-layout-property', animatedLayoutPropertyGuardFiles],
|
|
50
58
|
['bare-error', bareErrorGuardFiles],
|
|
59
|
+
['error-declared', errorDeclaredGuardFiles],
|
|
51
60
|
['focus-visible', focusVisibleGuardFiles],
|
|
52
61
|
['image-dimensions', imageDimensionsGuardFiles],
|
|
53
62
|
['island-without-states', islandWithoutStatesGuardFiles],
|
|
63
|
+
['native-confirm', nativeConfirmGuardFiles],
|
|
54
64
|
['raw-breakpoint', rawBreakpointGuardFiles],
|
|
55
65
|
['raw-colour', rawColourGuardFiles],
|
|
66
|
+
['raw-fetch', rawFetchGuardFiles],
|
|
67
|
+
['raw-interval', rawIntervalGuardFiles],
|
|
56
68
|
['raw-length', rawLengthGuardFiles],
|
|
57
69
|
['raw-motion', rawMotionGuardFiles],
|
|
58
70
|
['raw-shadow', rawShadowGuardFiles],
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// The monitoring objects every Ultimate chart ships: the scrape object and the alert rules. The
|
|
2
|
+
// rules are one table, rendered as the chart's PrometheusRule template and as plain rule groups for
|
|
3
|
+
// a loader outside Kubernetes. Only mechanism ships — a stalled queue, a dead letter, a lost lease,
|
|
4
|
+
// a dropped frame, a 5xx share, a dead target; a latency target is the app's (axiom 8).
|
|
5
|
+
import type { GeneratedFile, NameSet } from './naming';
|
|
6
|
+
|
|
7
|
+
/** How an expression names a series and groups its result, per renderer. */
|
|
8
|
+
export interface AlertScope {
|
|
9
|
+
/** `name{matchers}` — the scope's own matchers, then `extra`. */
|
|
10
|
+
readonly series: (name: string, extra?: string) => string;
|
|
11
|
+
/** The labels every rule groups by, so one release (or one job) fires one alert. */
|
|
12
|
+
readonly by: string;
|
|
13
|
+
/** The label naming a scrape target's role. */
|
|
14
|
+
readonly component: string;
|
|
15
|
+
/** The rule's threshold, as the renderer spells it: a value lookup or the default. */
|
|
16
|
+
readonly threshold: (fallback: number) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ChartAlert {
|
|
20
|
+
readonly alert: string;
|
|
21
|
+
readonly expr: (scope: AlertScope) => string;
|
|
22
|
+
/** The default `alerts.rules.<alert>.threshold`, for a rule whose comparison is a number. */
|
|
23
|
+
readonly threshold?: number;
|
|
24
|
+
readonly for: string;
|
|
25
|
+
readonly severity: 'warning' | 'critical';
|
|
26
|
+
readonly summary: string;
|
|
27
|
+
/** The heading in `docs/ops/06-runbooks.md` the rule's `runbook_url` lands on. */
|
|
28
|
+
readonly anchor: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const rule = (alert: Omit<ChartAlert, 'anchor'>): ChartAlert => ({
|
|
32
|
+
...alert,
|
|
33
|
+
anchor: alert.alert.toLowerCase(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const CHART_ALERTS: readonly ChartAlert[] = [
|
|
37
|
+
rule({
|
|
38
|
+
alert: 'UltimateQueueStalled',
|
|
39
|
+
threshold: 300,
|
|
40
|
+
expr: (s) =>
|
|
41
|
+
`max by (${s.by}, queue) (${s.series('queue_oldest_ready_seconds')}) > ${s.threshold(300)}`,
|
|
42
|
+
for: '5m',
|
|
43
|
+
severity: 'warning',
|
|
44
|
+
summary: 'A queue has a due job nothing has claimed for longer than the threshold',
|
|
45
|
+
}),
|
|
46
|
+
rule({
|
|
47
|
+
alert: 'UltimateDeadLetters',
|
|
48
|
+
expr: (s) => `max by (${s.by}, queue) (${s.series('queue_dead_jobs')}) > 0`,
|
|
49
|
+
for: '5m',
|
|
50
|
+
severity: 'warning',
|
|
51
|
+
summary: 'A queue holds dead-lettered jobs',
|
|
52
|
+
}),
|
|
53
|
+
rule({
|
|
54
|
+
alert: 'UltimateLeaseLost',
|
|
55
|
+
expr: (s) => `sum by (${s.by}, queue) (rate(${s.series('job_leases_lost_total')}[5m])) > 0`,
|
|
56
|
+
for: '5m',
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
summary: 'Job leases are lapsing while their jobs still run, so jobs run twice',
|
|
59
|
+
}),
|
|
60
|
+
rule({
|
|
61
|
+
alert: 'UltimateFramesDropped',
|
|
62
|
+
expr: (s) => `sum by (${s.by}) (rate(${s.series('channel_frames_dropped_total')}[5m])) > 0`,
|
|
63
|
+
for: '5m',
|
|
64
|
+
severity: 'warning',
|
|
65
|
+
summary: 'Sync nodes are dropping channel frames to socket backpressure',
|
|
66
|
+
}),
|
|
67
|
+
rule({
|
|
68
|
+
alert: 'UltimateServerErrors',
|
|
69
|
+
threshold: 0.05,
|
|
70
|
+
expr: (s) =>
|
|
71
|
+
`sum by (${s.by}) (rate(${s.series('http_requests_total', 'status="5xx"')}[5m])) / ` +
|
|
72
|
+
`sum by (${s.by}) (rate(${s.series('http_requests_total')}[5m])) > ${s.threshold(0.05)}`,
|
|
73
|
+
for: '10m',
|
|
74
|
+
severity: 'critical',
|
|
75
|
+
summary: 'The share of requests answered 5xx is above the threshold',
|
|
76
|
+
}),
|
|
77
|
+
rule({
|
|
78
|
+
alert: 'UltimateTargetDown',
|
|
79
|
+
expr: (s) => `count by (${s.by}, ${s.component}) (${s.series('up')} == 0) > 0`,
|
|
80
|
+
for: '5m',
|
|
81
|
+
severity: 'critical',
|
|
82
|
+
summary: 'A role has a scrape target that is down',
|
|
83
|
+
}),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
/** Where `runbook_url` points unless `alerts.runbookBase` says otherwise. */
|
|
87
|
+
export const RUNBOOK_BASE =
|
|
88
|
+
'https://github.com/developerz-ai/ultimate/blob/main/docs/ops/06-runbooks.md';
|
|
89
|
+
|
|
90
|
+
const matchers = (own: string, extra?: string): string =>
|
|
91
|
+
[own, extra].filter((part) => part !== undefined && part !== '').join(',');
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The chart's scope: one release in one namespace, the two labels the ServiceMonitor's
|
|
95
|
+
* `targetLabels` and the operator put on every series. A threshold reads the rule's value.
|
|
96
|
+
*/
|
|
97
|
+
export const K8S_SCOPE: AlertScope = {
|
|
98
|
+
series: (name, extra) =>
|
|
99
|
+
`${name}{${matchers('namespace="{{ $.Release.Namespace }}",app_kubernetes_io_instance="{{ $.Release.Name }}"', extra)}}`,
|
|
100
|
+
by: 'namespace, app_kubernetes_io_instance',
|
|
101
|
+
component: 'app_kubernetes_io_component',
|
|
102
|
+
threshold: (fallback) => `{{ ternary $r.threshold ${fallback} (hasKey $r "threshold") }}`,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const plainScope = (own: string): AlertScope => ({
|
|
106
|
+
series: (name, extra) => {
|
|
107
|
+
const inner = matchers(own, extra);
|
|
108
|
+
return inner === '' ? name : `${name}{${inner}}`;
|
|
109
|
+
},
|
|
110
|
+
by: 'job',
|
|
111
|
+
component: 'instance',
|
|
112
|
+
threshold: (fallback) => String(fallback),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** The rules as one plain Prometheus rule group, defaults in place, scoped by `own` matchers. */
|
|
116
|
+
export function alertRuleGroups(own = ''): string {
|
|
117
|
+
const scope = plainScope(own);
|
|
118
|
+
const rules = CHART_ALERTS.map(
|
|
119
|
+
(a) => ` - alert: ${a.alert}
|
|
120
|
+
expr: >-
|
|
121
|
+
${a.expr(scope)}
|
|
122
|
+
for: ${a.for}
|
|
123
|
+
labels:
|
|
124
|
+
severity: ${a.severity}
|
|
125
|
+
annotations:
|
|
126
|
+
summary: ${JSON.stringify(a.summary)}
|
|
127
|
+
runbook_url: ${JSON.stringify(`${RUNBOOK_BASE}#${a.anchor}`)}
|
|
128
|
+
`,
|
|
129
|
+
).join('');
|
|
130
|
+
return `groups:\n - name: ultimate\n rules:\n${rules}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One rule of the chart template: every field a value under `alerts.rules.<alert>` may replace. */
|
|
134
|
+
const chartRule = (
|
|
135
|
+
a: ChartAlert,
|
|
136
|
+
): string => ` {{- $r := index $rules "${a.alert}" | default dict }}
|
|
137
|
+
{{- if ne (toString $r.enabled) "false" }}
|
|
138
|
+
- alert: ${a.alert}
|
|
139
|
+
expr: >-
|
|
140
|
+
${a.expr(K8S_SCOPE)}
|
|
141
|
+
for: {{ $r.for | default "${a.for}" }}
|
|
142
|
+
labels:
|
|
143
|
+
severity: {{ $r.severity | default "${a.severity}" }}
|
|
144
|
+
annotations:
|
|
145
|
+
summary: ${JSON.stringify(a.summary)}
|
|
146
|
+
runbook_url: {{ printf "%s#${a.anchor}" $base | quote }}
|
|
147
|
+
{{- end }}
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The `enabled` switch both monitoring objects share: `auto` (and a values file with no such block)
|
|
152
|
+
* renders when the cluster serves the operator's kind, `true` forces it, `false` declines it.
|
|
153
|
+
*/
|
|
154
|
+
const operatorGate = (
|
|
155
|
+
key: string,
|
|
156
|
+
kind: string,
|
|
157
|
+
): string => `{{- $cfg := .Values.${key} | default dict }}
|
|
158
|
+
{{- $mode := toString (ternary $cfg.enabled "auto" (hasKey $cfg "enabled")) }}
|
|
159
|
+
{{- if not (has $mode (list "auto" "true" "false")) }}
|
|
160
|
+
{{- fail (printf "${key}.enabled is %q — set it to auto (render when the cluster serves monitoring.coreos.com/v1/${kind}), true or false" $mode) }}
|
|
161
|
+
{{- end }}
|
|
162
|
+
{{- if or (eq $mode "true") (and (eq $mode "auto") (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1/${kind}")) }}`;
|
|
163
|
+
|
|
164
|
+
const alertRulesTemplate = (chart: string): string => `{{/*
|
|
165
|
+
The alert rules for this release's own series, written from @ultimat3/cli's rule table. Each rule
|
|
166
|
+
reads alerts.rules.<alert> for threshold, for, severity and enabled; its runbook_url is
|
|
167
|
+
alerts.runbookBase plus the rule's anchor. Rendered by capability, like servicemonitor.yaml.
|
|
168
|
+
*/}}
|
|
169
|
+
${operatorGate('alerts', 'PrometheusRule')}
|
|
170
|
+
{{- $rules := $cfg.rules | default dict }}
|
|
171
|
+
{{- $base := $cfg.runbookBase | default "${RUNBOOK_BASE}" }}
|
|
172
|
+
apiVersion: monitoring.coreos.com/v1
|
|
173
|
+
kind: PrometheusRule
|
|
174
|
+
metadata:
|
|
175
|
+
name: {{ include "${chart}.fullname" . }}
|
|
176
|
+
labels:
|
|
177
|
+
{{- include "${chart}.labels" . | nindent 4 }}
|
|
178
|
+
{{- with $cfg.labels }}
|
|
179
|
+
{{- toYaml . | nindent 4 }}
|
|
180
|
+
{{- end }}
|
|
181
|
+
spec:
|
|
182
|
+
groups:
|
|
183
|
+
- name: {{ include "${chart}.fullname" . }}.ultimate
|
|
184
|
+
rules:
|
|
185
|
+
${CHART_ALERTS.map(chartRule).join('')}{{- end }}
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
const serviceMonitorTemplate = (chart: string): string => `{{/*
|
|
189
|
+
Prometheus-operator discovery for the scrape listener every serving role opens, rendered by
|
|
190
|
+
capability: serviceMonitor.enabled auto (the default, and a values file with no such block) renders
|
|
191
|
+
it when the cluster serves the kind, so a cluster without the operator still installs. ONE object
|
|
192
|
+
for the release: every role's Service publishes the same \`metrics\` port name. targetLabels carry
|
|
193
|
+
the role and the release onto every series, which is what alert-rules.yaml and an HPA key on.
|
|
194
|
+
*/}}
|
|
195
|
+
${operatorGate('serviceMonitor', 'ServiceMonitor')}
|
|
196
|
+
apiVersion: monitoring.coreos.com/v1
|
|
197
|
+
kind: ServiceMonitor
|
|
198
|
+
metadata:
|
|
199
|
+
name: {{ include "${chart}.fullname" . }}
|
|
200
|
+
labels:
|
|
201
|
+
{{- include "${chart}.labels" . | nindent 4 }}
|
|
202
|
+
{{- with $cfg.labels }}
|
|
203
|
+
{{- toYaml . | nindent 4 }}
|
|
204
|
+
{{- end }}
|
|
205
|
+
spec:
|
|
206
|
+
selector:
|
|
207
|
+
matchLabels:
|
|
208
|
+
app.kubernetes.io/name: {{ include "${chart}.name" . }}
|
|
209
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
210
|
+
targetLabels:
|
|
211
|
+
- app.kubernetes.io/component
|
|
212
|
+
- app.kubernetes.io/instance
|
|
213
|
+
endpoints:
|
|
214
|
+
# The Service port NAME, never the integer: moving metricsPort moves nothing here.
|
|
215
|
+
- port: metrics
|
|
216
|
+
path: /metrics
|
|
217
|
+
interval: {{ $cfg.interval | default "15s" }}
|
|
218
|
+
scrapeTimeout: {{ $cfg.scrapeTimeout | default "10s" }}
|
|
219
|
+
{{- end }}
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The chart's two monitoring templates. The framework chart's copies (\`docker/helm/templates/\`)
|
|
224
|
+
* are these bytes for the chart named \`ultimate\`, held by \`scaffold-helm-parity.test.ts\`.
|
|
225
|
+
*/
|
|
226
|
+
export function helmMonitoringFiles(app: NameSet): readonly GeneratedFile[] {
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
path: 'docker/helm/templates/servicemonitor.yaml',
|
|
230
|
+
contents: serviceMonitorTemplate(app.kebab),
|
|
231
|
+
},
|
|
232
|
+
{ path: 'docker/helm/templates/alert-rules.yaml', contents: alertRulesTemplate(app.kebab) },
|
|
233
|
+
];
|
|
234
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Split from scaffold-helm.ts, which holds the chart's INPUTS (Chart.yaml, values.yaml).
|
|
4
4
|
|
|
5
5
|
import type { GeneratedFile, NameSet } from './naming';
|
|
6
|
+
import { helmMonitoringFiles } from './scaffold-helm-alerts';
|
|
6
7
|
import { helmFenceFiles } from './scaffold-helm-fences';
|
|
7
8
|
|
|
8
9
|
const helpers = (
|
|
@@ -169,7 +170,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
169
170
|
periodSeconds: 15
|
|
170
171
|
failureThreshold: 4
|
|
171
172
|
{{- /*
|
|
172
|
-
The worker's retire,
|
|
173
|
+
The worker's retire, on by default: roles.worker.retireSeconds > 0. The preStop sends PID 1 SIGUSR2 —
|
|
173
174
|
the framework's retire: stop claiming, finish every held job however long it takes, abort
|
|
174
175
|
nothing, then drain and exit 0 — and waits until PID 1 is gone, so the SIGTERM that follows finds
|
|
175
176
|
nothing left to cut off. It re-sends the signal every 5 s while it waits: PID 1 has no default
|
|
@@ -185,7 +186,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
185
186
|
command:
|
|
186
187
|
- bun
|
|
187
188
|
- -e
|
|
188
|
-
- "process.kill(1, 'SIGUSR2'); for (let n = 1; ; n++) { try { process.kill(1, n % 5 === 0 ? 'SIGUSR2' : 0); } catch { break; } await Bun.sleep(1000); }"
|
|
189
|
+
- "process.kill(1, 'SIGUSR2'); const end = Date.now() + {{ int $cfg.retireSeconds }} * 1000; for (let n = 1; Date.now() < end; n++) { try { process.kill(1, n % 5 === 0 ? 'SIGUSR2' : 0); } catch { break; } await Bun.sleep(1000); }"
|
|
189
190
|
{{- end }}
|
|
190
191
|
{{- end }}
|
|
191
192
|
resources: {{- toYaml $cfg.resources | nindent 4 }}
|
|
@@ -196,7 +197,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
196
197
|
|
|
197
198
|
{{/*
|
|
198
199
|
A role's terminationGracePeriodSeconds, in the order the kubelet's budget is spent: the preStop
|
|
199
|
-
sleep (web and sync, 1.30+), the worker's
|
|
200
|
+
sleep (web and sync, 1.30+), the worker's retire (roles.worker.retireSeconds), the
|
|
200
201
|
readiness grace (web and sync — every other role drains with none), the drain budget (app.config.ts drain.deadlineMs, every role;
|
|
201
202
|
the worker's is drain.workerDeadlineSeconds instead when > 0, from drain.workerDeadlineMs) and the teardown margin.
|
|
202
203
|
Defaults: 45s web/sync (40s below 1.30), 35s the rest. x deploy --method helm sets
|
|
@@ -408,7 +409,7 @@ holds at minReplicas, which is why these are opt-in rather than a default nobody
|
|
|
408
409
|
|
|
409
410
|
autoscaling.type says whose number it is. Pods (the default) is a per-pod series — rps, open
|
|
410
411
|
sockets — averaged across pods. External is ONE series for the whole role, divided by the replica
|
|
411
|
-
count:
|
|
412
|
+
count: queue_demand is that shape, because every serving worker publishes the same number, and read
|
|
412
413
|
as Pods it asked for N times the workers the queue needed. Any other type fails the render.
|
|
413
414
|
*/}}
|
|
414
415
|
{{- range $role, $cfg := .Values.roles }}
|
|
@@ -430,7 +431,7 @@ spec:
|
|
|
430
431
|
maxReplicas: {{ $cfg.autoscaling.maxReplicas }}
|
|
431
432
|
{{- $type := default "Pods" $cfg.autoscaling.type }}
|
|
432
433
|
{{- if not (has $type (list "Pods" "External")) }}
|
|
433
|
-
{{- 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.
|
|
434
|
+
{{- 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_demand)" $role $type) }}
|
|
434
435
|
{{- end }}
|
|
435
436
|
metrics:
|
|
436
437
|
{{- if eq $type "External" }}
|
|
@@ -469,5 +470,6 @@ export function helmTemplateFiles(app: NameSet): readonly GeneratedFile[] {
|
|
|
469
470
|
{ path: 'docker/helm/templates/ingress.yaml', contents: ingress(app) },
|
|
470
471
|
{ path: 'docker/helm/templates/hpa.yaml', contents: hpa(app) },
|
|
471
472
|
...helmFenceFiles(app),
|
|
473
|
+
...helmMonitoringFiles(app),
|
|
472
474
|
];
|
|
473
475
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// scaffold-helm-templates.ts.
|
|
7
7
|
|
|
8
8
|
import type { GeneratedFile, NameSet } from './naming';
|
|
9
|
+
import { RUNBOOK_BASE } from './scaffold-helm-alerts';
|
|
9
10
|
import { helmTemplateFiles } from './scaffold-helm-templates';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -17,6 +18,37 @@ import { helmTemplateFiles } from './scaffold-helm-templates';
|
|
|
17
18
|
*/
|
|
18
19
|
export const RUNTIME_UID = 1000;
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The two monitoring blocks of `values.yaml`, each a top-level key with its comment. Exported
|
|
23
|
+
* because `x g chart-alerts` appends whichever one an existing app's `values.yaml` lacks: one
|
|
24
|
+
* copy of the bytes, here, for the scaffold and the backfill.
|
|
25
|
+
*/
|
|
26
|
+
export const SERVICE_MONITOR_VALUES = `# Prometheus-operator discovery for that port (templates/servicemonitor.yaml) and the alert rules on
|
|
27
|
+
# the series it scrapes (templates/alert-rules.yaml). \`enabled\` on each:
|
|
28
|
+
# auto render when the cluster serves the operator's kind, so a cluster without it still
|
|
29
|
+
# installs. \`helm template\` answers from \`--api-versions\`: an offline render (GitOps) gets
|
|
30
|
+
# neither unless it passes --api-versions monitoring.coreos.com/v1/ServiceMonitor and
|
|
31
|
+
# --api-versions monitoring.coreos.com/v1/PrometheusRule
|
|
32
|
+
# true always render; \`helm install\` fails where the CRD is missing
|
|
33
|
+
# false never render
|
|
34
|
+
serviceMonitor:
|
|
35
|
+
enabled: auto
|
|
36
|
+
interval: 15s
|
|
37
|
+
scrapeTimeout: 10s
|
|
38
|
+
labels: {} # what your operator's serviceMonitorSelector matches, if it uses one
|
|
39
|
+
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const ALERTS_VALUES = `# Each rule's runbook_url is runbookBase#<alert, lower-cased>. Per rule, under rules.<alert>:
|
|
43
|
+
# threshold, for, severity, enabled — e.g. rules: { UltimateQueueStalled: { threshold: 600 } }.
|
|
44
|
+
alerts:
|
|
45
|
+
enabled: auto
|
|
46
|
+
runbookBase: ${RUNBOOK_BASE}
|
|
47
|
+
labels: {} # what your operator's ruleSelector matches, if it uses one
|
|
48
|
+
rules: {}
|
|
49
|
+
|
|
50
|
+
`;
|
|
51
|
+
|
|
20
52
|
/**
|
|
21
53
|
* `appVersion` is the default image tag (`image.tag: ""` below), so it must name a tag that can
|
|
22
54
|
* exist. `x deploy --image <ref>` overrides both keys, which is the path that actually ships.
|
|
@@ -80,7 +112,7 @@ networkPolicy:
|
|
|
80
112
|
# METRICS_PORT in the container; move one and the other follows.
|
|
81
113
|
metricsPort: 9090
|
|
82
114
|
|
|
83
|
-
# The image's \`bun\` user, the same uid as the Dockerfile's USER: move one and the other follows.
|
|
115
|
+
${SERVICE_MONITOR_VALUES}${ALERTS_VALUES}# The image's \`bun\` user, the same uid as the Dockerfile's USER: move one and the other follows.
|
|
84
116
|
podSecurityContext:
|
|
85
117
|
runAsNonRoot: true
|
|
86
118
|
runAsUser: ${RUNTIME_UID}
|
|
@@ -167,11 +199,12 @@ roles:
|
|
|
167
199
|
worker:
|
|
168
200
|
enabled: true
|
|
169
201
|
replicas: 2
|
|
170
|
-
# The retire,
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
|
|
202
|
+
# The retire, ON: a deploy or a scale-down never cuts a job it could wait out. app.config.ts
|
|
203
|
+
# drain.workerRetireMs / 1000, set by x deploy, and 0 turns it off. The worker's preStop sends
|
|
204
|
+
# its PID 1 SIGUSR2: stop claiming, finish every held job, abort nothing, exit 0. An idle worker
|
|
205
|
+
# exits at once, and a Terminating pod does not hold a rollout back. Added to this role's
|
|
206
|
+
# terminationGracePeriodSeconds, which the kubelet counts the preStop against.
|
|
207
|
+
retireSeconds: 3600
|
|
175
208
|
resources:
|
|
176
209
|
requests: { cpu: 200m, memory: 256Mi }
|
|
177
210
|
limits: { memory: 1Gi }
|
|
@@ -179,11 +212,12 @@ roles:
|
|
|
179
212
|
enabled: false
|
|
180
213
|
minReplicas: 1
|
|
181
214
|
maxReplicas: 50
|
|
182
|
-
# External: every worker publishes the SAME
|
|
215
|
+
# External: every serving worker publishes the SAME number, so averaged as a Pods metric it
|
|
183
216
|
# asked for N times the workers the queue needed. Expose it from your adapter deduplicated —
|
|
184
|
-
# max(
|
|
217
|
+
# max by (queue) (queue_demand), never sum, which would multiply it by the pod count again.
|
|
185
218
|
type: External
|
|
186
|
-
metric:
|
|
219
|
+
metric: queue_demand # jobs held by serving workers plus jobs due, per queue
|
|
220
|
+
# Set it to one pod's jobs.maxInFlight; x deploy --method helm sets it when app.config.ts declares it.
|
|
187
221
|
targetAverageValue: "100"
|
|
188
222
|
|
|
189
223
|
scheduler:
|
|
@@ -213,9 +247,9 @@ ingress:
|
|
|
213
247
|
`;
|
|
214
248
|
|
|
215
249
|
/**
|
|
216
|
-
* The chart for a new app:
|
|
217
|
-
*
|
|
218
|
-
*
|
|
250
|
+
* The chart for a new app: core Kubernetes objects, plus a ServiceMonitor and a PrometheusRule that
|
|
251
|
+
* render only where the cluster serves those kinds. A PodDisruptionBudget is deliberately absent:
|
|
252
|
+
* it is cluster policy rather than app topology.
|
|
219
253
|
*/
|
|
220
254
|
export function helmFiles(app: NameSet): readonly GeneratedFile[] {
|
|
221
255
|
return [
|
|
@@ -143,8 +143,12 @@ const i18nCatalog = (app: NameSet): string =>
|
|
|
143
143
|
'app.dashboard.statToday': 'Created today',
|
|
144
144
|
'app.dashboard.hintToday': 'so far, UTC',
|
|
145
145
|
'app.dashboard.chartTitle': 'Posts per day',
|
|
146
|
-
'app.dashboard.chartRange': '
|
|
147
|
-
'app.dashboard.
|
|
146
|
+
'app.dashboard.chartRange': 'Per UTC day, over the window you pick',
|
|
147
|
+
'app.dashboard.rangeLabel': 'Chart window',
|
|
148
|
+
'app.dashboard.rangeWeek': 'Last 7 days',
|
|
149
|
+
'app.dashboard.rangeFortnight': 'Last 14 days',
|
|
150
|
+
'app.dashboard.rangeMonth': 'Last 30 days',
|
|
151
|
+
'app.dashboard.chartLabel': 'Posts created per day over the chosen window',
|
|
148
152
|
'app.dashboard.tableTitle': 'Latest posts',
|
|
149
153
|
'app.dashboard.tableCaption': 'The ten newest posts',
|
|
150
154
|
'app.dashboard.columnTitle': 'Title',
|