@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
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { sortedImports } from './imports';
|
|
7
7
|
import type { GeneratedFile, NameSet } from './naming';
|
|
8
8
|
import { examplePageTest, exampleViewTest } from './scaffold-dashboard-example-tests';
|
|
9
|
+
import { exampleView } from './scaffold-dashboard-example-view';
|
|
9
10
|
import { DASHBOARD_DIR, routeConfig, themeActions, themeIsland } from './scaffold-dashboard-shared';
|
|
10
11
|
|
|
11
12
|
// Plain strings for the framework lines, never template literals: the workspace-dependency scanner
|
|
@@ -24,7 +25,7 @@ ${sortedImports([
|
|
|
24
25
|
"import { isUltimateError } from '@ultimat3/core';",
|
|
25
26
|
"import { currentLocale } from '@ultimat3/i18n';",
|
|
26
27
|
"import { formatMoney } from '@ultimat3/money';",
|
|
27
|
-
"import { defineRoute, island } from '@ultimat3/render';",
|
|
28
|
+
"import { defineRoute, island, type RouteContext } from '@ultimat3/render';",
|
|
28
29
|
[
|
|
29
30
|
'import {',
|
|
30
31
|
' AreaChart,',
|
|
@@ -38,6 +39,8 @@ ${sortedImports([
|
|
|
38
39
|
' Section,',
|
|
39
40
|
' StatTile,',
|
|
40
41
|
' ThemeToggle,',
|
|
42
|
+
' TimeRangePicker,',
|
|
43
|
+
' timeRangeUrl,',
|
|
41
44
|
"} from '@ultimat3/ui';",
|
|
42
45
|
].join('\n'),
|
|
43
46
|
])}
|
|
@@ -45,7 +48,10 @@ import { Shell } from '../../shared/shell';
|
|
|
45
48
|
import { postList } from '../post/live/post-list';
|
|
46
49
|
import {
|
|
47
50
|
bucketByDay,
|
|
48
|
-
|
|
51
|
+
CHART_RANGES,
|
|
52
|
+
CHART_ZONE,
|
|
53
|
+
chartDays,
|
|
54
|
+
DEFAULT_CHART_RANGE,
|
|
49
55
|
formatCount,
|
|
50
56
|
type PostRow,
|
|
51
57
|
postStats,
|
|
@@ -66,6 +72,8 @@ export interface DashboardData {
|
|
|
66
72
|
readonly rows: readonly PostRow[];
|
|
67
73
|
/** The instant the windows were cut at, so server and test agree on "the last 7 days". */
|
|
68
74
|
readonly now: string;
|
|
75
|
+
/** The request's URL: the chart's window lives in it (\`?range=\`, or a custom \`from\`/\`to\`). */
|
|
76
|
+
readonly url: string;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/**
|
|
@@ -76,12 +84,13 @@ export interface DashboardData {
|
|
|
76
84
|
*/
|
|
77
85
|
export async function dashboardData(
|
|
78
86
|
read: () => Promise<readonly Parameters<typeof toPostRow>[0][]>,
|
|
87
|
+
url: string,
|
|
79
88
|
): Promise<DashboardData> {
|
|
80
89
|
const now = new Date().toISOString();
|
|
81
90
|
try {
|
|
82
|
-
return { rows: (await read()).map(toPostRow), now };
|
|
91
|
+
return { rows: (await read()).map(toPostRow), now, url };
|
|
83
92
|
} catch (error) {
|
|
84
|
-
if (isUltimateError(error) && error.code === 'X_DB_UNAVAILABLE') return { rows: [], now };
|
|
93
|
+
if (isUltimateError(error) && error.code === 'X_DB_UNAVAILABLE') return { rows: [], now, url };
|
|
85
94
|
throw error;
|
|
86
95
|
}
|
|
87
96
|
}
|
|
@@ -93,7 +102,8 @@ export async function dashboardData(
|
|
|
93
102
|
* read to the request's actor, so the viewer \`auth/dev-actor.ts\` resolves sees the rows
|
|
94
103
|
* \`packages/db/src/seed.ts\` wrote, and a real session sees its own org's the day one exists.
|
|
95
104
|
*/
|
|
96
|
-
const load = (): Promise<DashboardData> =>
|
|
105
|
+
const load = ({ url }: RouteContext): Promise<DashboardData> =>
|
|
106
|
+
dashboardData(() => postList({ limit: ROW_LIMIT }), url);
|
|
97
107
|
|
|
98
108
|
${themeIsland}
|
|
99
109
|
|
|
@@ -108,7 +118,27 @@ export function Page(props: PageProps) {
|
|
|
108
118
|
const locale = currentLocale();
|
|
109
119
|
const now = new Date(props.data.now);
|
|
110
120
|
const stats = postStats(props.data.rows, now);
|
|
111
|
-
|
|
121
|
+
// The chart's window, from the URL: a preset link or the custom form, kept across reload and
|
|
122
|
+
// share. A window the URL cannot hold (backwards, malformed) is the default, and the picker says so.
|
|
123
|
+
const labels = new Map<string, string>([
|
|
124
|
+
['week', t('app.dashboard.rangeWeek')],
|
|
125
|
+
['fortnight', t('app.dashboard.rangeFortnight')],
|
|
126
|
+
['month', t('app.dashboard.rangeMonth')],
|
|
127
|
+
]);
|
|
128
|
+
const range = timeRangeUrl(props.data.url, {
|
|
129
|
+
presets: CHART_RANGES.map((preset) => ({
|
|
130
|
+
id: preset.id,
|
|
131
|
+
label: labels.get(preset.id) ?? preset.id,
|
|
132
|
+
seconds: preset.days * 86_400,
|
|
133
|
+
})),
|
|
134
|
+
default: DEFAULT_CHART_RANGE,
|
|
135
|
+
timeZone: CHART_ZONE,
|
|
136
|
+
now,
|
|
137
|
+
});
|
|
138
|
+
// A capped read cannot see the days before its oldest row: those are unknown, never zero.
|
|
139
|
+
const points = bucketByDay(props.data.rows, range.to, chartDays(range.from, range.to), {
|
|
140
|
+
capped: props.data.rows.length >= ROW_LIMIT,
|
|
141
|
+
});
|
|
112
142
|
const mix = priceMix(props.data.rows);
|
|
113
143
|
const latest = props.data.rows.slice(0, TABLE_ROWS);
|
|
114
144
|
// Per currency: a bar is a share of the dearest post IN ITS CURRENCY, never of another's minor units.
|
|
@@ -137,6 +167,8 @@ export function Page(props: PageProps) {
|
|
|
137
167
|
label={t('app.dashboard.statWeek')}
|
|
138
168
|
value={formatCount(stats.lastWeek, locale)}
|
|
139
169
|
delta={stats.delta}
|
|
170
|
+
goodWhen="up"
|
|
171
|
+
trend={points}
|
|
140
172
|
hint={t('app.dashboard.hintVsPriorWeek')}
|
|
141
173
|
/>
|
|
142
174
|
<StatTile
|
|
@@ -157,6 +189,11 @@ export function Page(props: PageProps) {
|
|
|
157
189
|
title={t('app.dashboard.chartTitle')}
|
|
158
190
|
description={t('app.dashboard.chartRange')}
|
|
159
191
|
>
|
|
192
|
+
<TimeRangePicker
|
|
193
|
+
range={range}
|
|
194
|
+
timeZone={CHART_ZONE}
|
|
195
|
+
label={t('app.dashboard.rangeLabel')}
|
|
196
|
+
/>
|
|
160
197
|
<AreaChart
|
|
161
198
|
label={t('app.dashboard.chartLabel')}
|
|
162
199
|
keys={points.map((point) => point.key)}
|
|
@@ -225,131 +262,6 @@ export function Page(props: PageProps) {
|
|
|
225
262
|
}
|
|
226
263
|
`;
|
|
227
264
|
|
|
228
|
-
const exampleView =
|
|
229
|
-
(): string => `// The dashboard's numbers, apart from its markup: pure functions over the rows the page loaded,
|
|
230
|
-
// so the server render and a test cannot disagree about a figure. No I/O, no \`t()\`, no JSX.
|
|
231
|
-
|
|
232
|
-
import type { Money } from '@ultimat3/money';
|
|
233
|
-
import { type ChartPoint, deltaOf, type StatDelta } from '@ultimat3/ui';
|
|
234
|
-
|
|
235
|
-
/** Days of history the chart shows, oldest first. */
|
|
236
|
-
export const CHART_DAYS = 14;
|
|
237
|
-
|
|
238
|
-
const DAY_MS = 86_400_000;
|
|
239
|
-
|
|
240
|
-
/** One post as the page reads it. \`createdAt\` is ISO text: a \`Date\` cannot cross a JSON seam. */
|
|
241
|
-
export interface PostRow {
|
|
242
|
-
readonly id: string;
|
|
243
|
-
readonly title: string;
|
|
244
|
-
/** Integer minor units plus an ISO code, never a float — the entity's \`money()\` column. */
|
|
245
|
-
readonly price: Money;
|
|
246
|
-
readonly createdAt: string;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Duck-typed rather than the entity's own row: the page only ever reads these four columns. The
|
|
251
|
-
* query reads through the typed handle, so \`createdAt\` arrives as the \`Date\` the entity declares.
|
|
252
|
-
*/
|
|
253
|
-
export function toPostRow(row: {
|
|
254
|
-
readonly id: string;
|
|
255
|
-
readonly title: string;
|
|
256
|
-
readonly price: Money;
|
|
257
|
-
readonly createdAt: Date | string;
|
|
258
|
-
}): PostRow {
|
|
259
|
-
const at = row.createdAt;
|
|
260
|
-
return {
|
|
261
|
-
id: row.id,
|
|
262
|
-
title: row.title,
|
|
263
|
-
price: row.price,
|
|
264
|
-
createdAt: at instanceof Date ? at.toISOString() : at,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** Free (a zero price) against paid, over every row loaded — the two halves of the ring. */
|
|
269
|
-
export interface PriceMix {
|
|
270
|
-
readonly free: number;
|
|
271
|
-
readonly paid: number;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function priceMix(rows: readonly PostRow[]): PriceMix {
|
|
275
|
-
const paid = rows.filter((row) => row.price.minor > 0).length;
|
|
276
|
-
return { free: rows.length - paid, paid };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* The dearest price per currency, in minor units — what each row's in-cell bar is a share of.
|
|
281
|
-
* Per currency because 1,900 cents and 1,900 yen are not the same height.
|
|
282
|
-
*/
|
|
283
|
-
export function priceCeilings(rows: readonly PostRow[]): ReadonlyMap<string, number> {
|
|
284
|
-
const ceilings = new Map<string, number>();
|
|
285
|
-
for (const row of rows) {
|
|
286
|
-
const { currency, minor } = row.price;
|
|
287
|
-
ceilings.set(currency, Math.max(ceilings.get(currency) ?? 0, minor));
|
|
288
|
-
}
|
|
289
|
-
return ceilings;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export interface PostStats {
|
|
293
|
-
readonly total: number;
|
|
294
|
-
/** Created in the seven days ending at \`now\`. */
|
|
295
|
-
readonly lastWeek: number;
|
|
296
|
-
/** \`lastWeek\` against the seven days before it; absent when that baseline is zero. */
|
|
297
|
-
readonly delta: StatDelta | undefined;
|
|
298
|
-
/** Created in the UTC day \`now\` falls in — a partial day, so it carries no delta. */
|
|
299
|
-
readonly today: number;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const createdAtOf = (row: PostRow): number => Date.parse(row.createdAt);
|
|
303
|
-
|
|
304
|
-
const within = (rows: readonly PostRow[], from: number, to: number): number =>
|
|
305
|
-
rows.filter((row) => {
|
|
306
|
-
const at = createdAtOf(row);
|
|
307
|
-
return Number.isFinite(at) && at > from && at <= to;
|
|
308
|
-
}).length;
|
|
309
|
-
|
|
310
|
-
export function postStats(rows: readonly PostRow[], now: Date): PostStats {
|
|
311
|
-
const end = now.getTime();
|
|
312
|
-
const weekAgo = end - 7 * DAY_MS;
|
|
313
|
-
const dayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
314
|
-
const lastWeek = within(rows, weekAgo, end);
|
|
315
|
-
return {
|
|
316
|
-
total: rows.length,
|
|
317
|
-
lastWeek,
|
|
318
|
-
delta: deltaOf(lastWeek, within(rows, weekAgo - 7 * DAY_MS, weekAgo)),
|
|
319
|
-
today: within(rows, dayStart - 1, end),
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** \`MM-DD\` of a UTC day. ISO, never \`toLocaleDateString\`: the chart's axis is data, not prose. */
|
|
324
|
-
const dayKey = (at: number): string => new Date(at).toISOString().slice(5, 10);
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* One bucket per UTC day, the last one being the day \`now\` falls in, oldest first — the shape
|
|
328
|
-
* \`BarChart\` draws. Every day is present even when nothing was created on it: a chart with the
|
|
329
|
-
* quiet days removed reads as busier than the app is.
|
|
330
|
-
*/
|
|
331
|
-
export function bucketByDay(
|
|
332
|
-
rows: readonly PostRow[],
|
|
333
|
-
now: Date,
|
|
334
|
-
days: number,
|
|
335
|
-
): readonly ChartPoint[] {
|
|
336
|
-
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
337
|
-
const start = today - (days - 1) * DAY_MS;
|
|
338
|
-
const counts: number[] = Array.from({ length: days }, () => 0);
|
|
339
|
-
for (const row of rows) {
|
|
340
|
-
const at = createdAtOf(row);
|
|
341
|
-
if (!Number.isFinite(at)) continue;
|
|
342
|
-
const index = Math.floor((at - start) / DAY_MS);
|
|
343
|
-
if (index >= 0 && index < days) counts[index] = (counts[index] ?? 0) + 1;
|
|
344
|
-
}
|
|
345
|
-
return counts.map((value, index) => ({ key: dayKey(start + index * DAY_MS), value }));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/** A figure for a tile, in the page's locale. \`Intl.NumberFormat\` needs no time zone. */
|
|
349
|
-
export const formatCount = (value: number, locale: string): string =>
|
|
350
|
-
new Intl.NumberFormat(locale).format(value);
|
|
351
|
-
`;
|
|
352
|
-
|
|
353
265
|
/** The example dashboard's page, view module and the tests beside both. */
|
|
354
266
|
export const exampleDashboardFiles = (app: NameSet): readonly GeneratedFile[] => [
|
|
355
267
|
{ path: `${DASHBOARD_DIR}/page.tsx`, contents: examplePage(app) },
|
|
@@ -5,12 +5,8 @@
|
|
|
5
5
|
export const DASHBOARD_DIR = 'apps/web/app/dashboard';
|
|
6
6
|
|
|
7
7
|
export const routeConfig = (load: string): string => `export const config = defineRoute({
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
// Solid renderer, and the server JSX factory is inert on purpose. A scaffolded 'stream' route
|
|
11
|
-
// therefore failed x routes with X_ROUTE_MODE_INVALID on the first run, printing a fix nobody
|
|
12
|
-
// could follow. Ship the mode that works. Async data needs no boundary: await it in the page.
|
|
13
|
-
render: 'ssr',
|
|
8
|
+
// The app/ default: the shell flushes first, and a read moved into a <Hole> arrives after it.
|
|
9
|
+
render: 'stream',
|
|
14
10
|
// 'visible' for the one island on this page, the theme toggle in the header.
|
|
15
11
|
hydrate: 'visible',
|
|
16
12
|
offline: 'runtime',
|
|
@@ -22,7 +18,7 @@ export const routeConfig = (load: string): string => `export const config = defi
|
|
|
22
18
|
// toggle, provider, the ui runtime they share and the error registry are the rest. It was
|
|
23
19
|
// 60.9kb before the framework stopped shipping solid-js twice and the i18n catalog with it
|
|
24
20
|
// (issue #490), which is what put this at 64kb; 60kb is the figure the scaffold budgets held
|
|
25
|
-
// before that, kept rather than tightened so a Bun patch cannot red a first \`bun run
|
|
21
|
+
// before that, kept rather than tightened so a Bun patch cannot red a first \`bun run verify\`.
|
|
26
22
|
budget: { js: '60kb' },${load}
|
|
27
23
|
meta: ({ t }) => ({
|
|
28
24
|
title: t('app.dashboard.title'),
|
|
@@ -75,7 +75,7 @@ export const dbClientSource = (
|
|
|
75
75
|
|
|
76
76
|
${sortedImports([
|
|
77
77
|
"import { storeMode } from '@ultimat3/core';",
|
|
78
|
-
"import { type Driver, database,
|
|
78
|
+
"import { type Driver, database, postgresDriver, testDriver } from '@ultimat3/entity';",
|
|
79
79
|
...entries.map(handleImportLine),
|
|
80
80
|
])}
|
|
81
81
|
|
|
@@ -84,13 +84,13 @@ ${sortedImports([
|
|
|
84
84
|
* reads through (\`driver.reset?.()\`).
|
|
85
85
|
*
|
|
86
86
|
* \`postgresDriver()\` takes no connection: it resolves \`@ultimat3/db\`'s process client, which is
|
|
87
|
-
* the embedded PGlite under \`x dev\` and the \`DATABASE_URL\` pool in a container.
|
|
88
|
-
*
|
|
89
|
-
* \`
|
|
90
|
-
*
|
|
87
|
+
* the embedded PGlite under \`x dev\` and the \`DATABASE_URL\` pool in a container. Under
|
|
88
|
+
* \`bun test\` it is \`testDriver()\`: memory until a test installs its migrated database with
|
|
89
|
+
* \`setDbClient\`, that database while it is installed — decided per call, so this module-load
|
|
90
|
+
* constant follows the client. \`storeMode\` is core's one answer to which store a seam takes.
|
|
91
91
|
*/
|
|
92
92
|
export const selectDriver = (env: Readonly<Record<string, string | undefined>>): Driver =>
|
|
93
|
-
storeMode(env) === 'memory' ?
|
|
93
|
+
storeMode(env) === 'memory' ? testDriver() : postgresDriver();
|
|
94
94
|
|
|
95
95
|
export const driver = selectDriver(Bun.env);
|
|
96
96
|
|
|
@@ -111,6 +111,7 @@ const clientTest = (
|
|
|
111
111
|
): string => `// Which store the handle reads: memory under \`bun test\`, the process's Postgres client
|
|
112
112
|
// everywhere else. A handle that silently took the memory driver in a container would serve an
|
|
113
113
|
// empty database with a green gate.
|
|
114
|
+
import { recordingClient, setDbClient } from '@ultimat3/db';
|
|
114
115
|
import { expect, unitTest } from '@ultimat3/testing';
|
|
115
116
|
import { db, driver, selectDriver } from './client';
|
|
116
117
|
|
|
@@ -126,6 +127,19 @@ unitTest('the handle is built over the driver this module exports', () => {
|
|
|
126
127
|
expect(driver.reset).toBeDefined();
|
|
127
128
|
expect(typeof db).toBe('object');
|
|
128
129
|
});
|
|
130
|
+
|
|
131
|
+
// The test driver decides per call: a migrated database installed with \`setDbClient\` is what
|
|
132
|
+
// \`db\` reaches, and clearing it hands the handle back to memory.
|
|
133
|
+
unitTest('under test the handle follows an installed client', async () => {
|
|
134
|
+
const client = recordingClient();
|
|
135
|
+
setDbClient(client);
|
|
136
|
+
try {
|
|
137
|
+
await driver.transactor().run(async () => undefined);
|
|
138
|
+
} finally {
|
|
139
|
+
setDbClient(undefined);
|
|
140
|
+
}
|
|
141
|
+
expect(client.texts).toContain('BEGIN');
|
|
142
|
+
});
|
|
129
143
|
`;
|
|
130
144
|
|
|
131
145
|
/**
|
|
@@ -101,19 +101,6 @@ export {};
|
|
|
101
101
|
*
|
|
102
102
|
* `x db seed` owns the connection, the tier and the per-seed transaction. One runner, one answer.
|
|
103
103
|
*/
|
|
104
|
-
/**
|
|
105
|
-
* The seed's two package imports in the order Biome sorts them: the app's own scope can sort on
|
|
106
|
-
* either side of \`@ultimat3\`, and a fixed order is a lint failure for half of all app names.
|
|
107
|
-
*/
|
|
108
|
-
const seedImports = (app: NameSet): string =>
|
|
109
|
-
[
|
|
110
|
-
[`@${app.kebab}/web/shared/demo-org`, 'DEMO_ORG_LABEL'],
|
|
111
|
-
['@ultimat3/entity', 'defineSeed'],
|
|
112
|
-
]
|
|
113
|
-
.sort(([a = ''], [b = '']) => (a < b ? -1 : a > b ? 1 : 0))
|
|
114
|
-
.map(([from, name]) => `import { ${name} } from '${from}';`)
|
|
115
|
-
.join('\n');
|
|
116
|
-
|
|
117
104
|
const dbSeed = (app: NameSet, example: boolean): string =>
|
|
118
105
|
example
|
|
119
106
|
? `// Deterministic fixtures: the same rows every time, so a test, a demo and a branch database
|
|
@@ -124,9 +111,13 @@ const dbSeed = (app: NameSet, example: boolean): string =>
|
|
|
124
111
|
// included), and wraps each seed in its own transaction. Never a plain \`bun run\` script: that
|
|
125
112
|
// reaches the database through \`db()\`, which needs a \`postgres:\` \`DATABASE_URL\` and so cannot
|
|
126
113
|
// see the embedded database at all.
|
|
127
|
-
|
|
114
|
+
import { defineSeed } from '@ultimat3/entity';
|
|
128
115
|
import { post } from './schema';
|
|
129
116
|
|
|
117
|
+
// \`apps/web/shared/demo-org.ts\`'s label, spelled here because a package imports no app
|
|
118
|
+
// (X_BOUNDARY_PACKAGE_TO_APP). \`seed.test.ts\` reads that module and holds the two equal.
|
|
119
|
+
const DEMO_ORG_LABEL = 'org:demo';
|
|
120
|
+
|
|
130
121
|
/** Stable across runs: \`id('post:hello')\` is a UUID v5 of the label, not a random one. */
|
|
131
122
|
export const ${app.camel}Seed = defineSeed('${app.kebab}', async ({ insert, id }) => {
|
|
132
123
|
await insert(post, [
|
|
@@ -2,34 +2,52 @@
|
|
|
2
2
|
// the bin/ scripts and the optional dev compose. Separated from the config half so neither file has
|
|
3
3
|
// to be scrolled to find the other — one file, one job applies to templates too. The image, its
|
|
4
4
|
// ignore file, the production topology and the deploy page are `scaffold-container.ts`; the
|
|
5
|
-
// `.claude/` harness that reads
|
|
5
|
+
// `.claude/` harness that reads CLAUDE.md is `scaffold-claude.ts`; the one workflow is
|
|
6
6
|
// `github/ci.yml.ts`.
|
|
7
7
|
|
|
8
8
|
import { LINE_CEILING } from '../workspace-checks';
|
|
9
9
|
import { githubFiles } from './github/ci.yml';
|
|
10
10
|
import type { GeneratedFile, NameSet } from './naming';
|
|
11
|
+
import { S3_GATEWAY_IMAGE } from './pinned-images';
|
|
11
12
|
import { binFiles } from './scaffold-bin';
|
|
12
13
|
import { claudeFiles } from './scaffold-claude';
|
|
13
14
|
import { containerFiles } from './scaffold-container';
|
|
15
|
+
import { mcpJsonFile } from './scaffold-mcp-json';
|
|
14
16
|
import { testServicesFiles } from './scaffold-test-services';
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The one convention file, under two names: `CLAUDE.md` holds it and `AGENTS.md` is a relative
|
|
20
|
+
* symlink to it (the owner's ruling, 2026-11-03), so there is no second copy to drift. Every word
|
|
21
|
+
* stays vendor-neutral, because every agent reads it — `scaffold-agents-md.test.ts` refuses one.
|
|
22
|
+
*/
|
|
23
|
+
const conventions = (app: NameSet): string => `# ${app.pascal}
|
|
17
24
|
|
|
18
|
-
Human-authored, short, stable
|
|
19
|
-
agent cannot infer from the code.
|
|
25
|
+
Human-authored, short, stable — one file under two names, \`AGENTS.md\` being a symlink to it.
|
|
26
|
+
Facts live in \`x.manifest.json\`; this file holds only what an agent cannot infer from the code.
|
|
20
27
|
|
|
21
28
|
Every row below names what refuses it. A rule with nothing in the last column is a rule that does
|
|
22
|
-
not exist — five of these had an empty column and were each measured green on \`x verify\`.
|
|
29
|
+
not exist — five of these had an empty column and were each measured green on \`bunx x verify\`.
|
|
30
|
+
|
|
31
|
+
There is no global \`x\`: every command here is \`bunx x …\`, the CLI this app installs.
|
|
32
|
+
|
|
33
|
+
Before stating or working around a framework limit, ask the installed framework:
|
|
34
|
+
\`bunx x docs "<question>"\`, and \`bunx x errors explain <CODE>\` for an error code.
|
|
35
|
+
|
|
36
|
+
Before an edit, ask what it reaches: \`bunx x impact <file> --json\` names the routes, actions,
|
|
37
|
+
queries, jobs, MCP tools, published operations and tests it touches, each with provenance.
|
|
23
38
|
|
|
24
39
|
| Rule | Detail | Refused by |
|
|
25
40
|
|---|---|---|
|
|
26
|
-
| One gate | \`bun run
|
|
41
|
+
| One gate | \`bun run verify\` — \`bunx x verify\`, which builds what its \`budgets\` step weighs. Green means shippable; never merge red. CI runs the same two commands as your terminal: \`bun run setup && bun run verify\` | the gate itself, and \`.github/workflows/ci.yml\` on every push and pull request; \`.github/workflows/image.yml\` publishes only a commit it passed |
|
|
27
42
|
| Coverage | the unit suite covers 95% of the lines and functions of this app's own source — every file under \`apps/\` and \`packages/\`, one no test loads counted at 0%. A floor, not the goal: a test added to raise it is proven by mutation — break the source, watch it go red, restore | \`X_COVERAGE_BELOW_FLOOR\` on the \`unit\` step; \`coverage\` in \`x.verify.json\` states the floor, and it only rises |
|
|
28
|
-
| One way | generators, not hand-rolled files: \`x g resource\`, \`x g action\`, \`x g route\` | review |
|
|
43
|
+
| One way | generators, not hand-rolled files: \`bunx x g resource\`, \`bunx x g action\`, \`bunx x g route\` | review |
|
|
29
44
|
| Surfaces | \`site/\` is 0kb JS and may not import \`app/\`; \`shared/\` is a leaf | \`X_BOUNDARY_SITE_TO_APP\` |
|
|
30
45
|
| Data | routes call actions and queries; only \`repo.ts\` touches the database | \`X_BOUNDARY_ROUTE_TO_DB\` |
|
|
31
46
|
| Repos | a \`repo.ts\` reads through the typed handle, never a \`sql\` literal the handle can express | \`guards/repo-raw-sql.ts\` |
|
|
32
|
-
|
|
|
47
|
+
| Vendors | a server \`fetch(\` carries a \`signal\` in its own options — better, it is \`outbound()\` from \`@ultimat3/core/outbound\`: a deadline, the caller's signal, safe-only retry | \`guards/raw-fetch.ts\` |
|
|
48
|
+
| Polling | an island re-reads on a timer with \`visibleInterval(run, ms)\` from \`@ultimat3/core/page\` — no call while the tab is hidden, none after a lost session — never a bare \`setInterval(\`; a \`// why:\` on the line admits a tick that is not a read | \`guards/raw-interval.ts\` |
|
|
49
|
+
| Confirmation | a destructive act asks through \`ConfirmDialog\` from \`@ultimat3/ui\` — Cancel focused, a native post, stays open on a refusal — never \`confirm(\`; a \`// why:\` on the line admits a call that is not that question | \`guards/native-confirm.ts\` |
|
|
50
|
+
| Errors | never \`throw new Error\` — declare each code once, \`defineError({ code, title, status, cause, fix })\` from \`@ultimat3/core\`, and throw its class; never a handwritten \`UltimateError\` subclass | \`guards/bare-error.ts\`, \`guards/error-declared.ts\` |
|
|
33
51
|
| Money | \`{ minor, currency }\`, never a float — \`money()\` on the column | the type: \`price: 19.99\` is TS2322, \`number\` is not \`MoneyInput\` |
|
|
34
52
|
| Time | store UTC, format with an explicit IANA time zone | \`guards/unzoned-date.ts\` |
|
|
35
53
|
| Strings | every user-facing string goes through \`t()\` | \`guards/untranslated-string.ts\` |
|
|
@@ -45,11 +63,11 @@ not exist — five of these had an empty column and were each measured green on
|
|
|
45
63
|
| Focus | \`outline: none\` replaces the ring in the same rule or the one beside it, or it does not remove it | \`guards/focus-visible.ts\` |
|
|
46
64
|
| Images | every image carries width + height or an aspect-ratio, and the priority one is never \`loading="lazy"\` | \`guards/image-dimensions.ts\` |
|
|
47
65
|
| Motion | animate \`transform\` and \`opacity\` — never a layout property, never \`transition: all\` | \`guards/animated-layout-property.ts\` |
|
|
48
|
-
| Islands | every \`*.island.tsx\` has a sibling \`*.island.states.ts\`, so \`x shot --island\` can photograph its failures | \`guards/island-without-states.ts\` |
|
|
66
|
+
| Islands | every \`*.island.tsx\` has a sibling \`*.island.states.ts\`, so \`bunx x shot --island\` can photograph its failures | \`guards/island-without-states.ts\` |
|
|
49
67
|
| Size | one file, one job — ${LINE_CEILING} lines of reviewable logic, and split past it | \`X_FILE_TOO_LONG\` |
|
|
50
68
|
|
|
51
|
-
\`guards/\` is yours: each file is one rule, discovered by \`x verify\` and run inside its
|
|
52
|
-
\`boundaries\` step. Delete one to drop the rule, and \`x g guard <name>\` writes the next.
|
|
69
|
+
\`guards/\` is yours: each file is one rule, discovered by \`bunx x verify\` and run inside its
|
|
70
|
+
\`boundaries\` step. Delete one to drop the rule, and \`bunx x g guard <name>\` writes the next.
|
|
53
71
|
|
|
54
72
|
The last five rows are about what this app is like to USE, and each is decidable from the file
|
|
55
73
|
alone — which is why they are those five and not the many that are not. A role is a promise:
|
|
@@ -58,21 +76,21 @@ already does. Only \`transform\` and \`opacity\` animate without a layout pass.
|
|
|
58
76
|
moves everything under it when its bytes land.
|
|
59
77
|
|
|
60
78
|
Size is a hard line and not a style note: past ${LINE_CEILING} lines a file has stopped being the
|
|
61
|
-
unit of review, and \`x verify\` refuses it. The one exemption is a file that is nothing but re-exports —
|
|
79
|
+
unit of review, and \`bunx x verify\` refuses it. The one exemption is a file that is nothing but re-exports —
|
|
62
80
|
it has one job by construction, and its length tracks the API's size rather than its complexity;
|
|
63
81
|
one statement of logic in such a file re-arms the ceiling on the same save.
|
|
64
82
|
|
|
65
83
|
Coverage counts execution, not validation: 100% is reachable with zero assertions, and a covered
|
|
66
84
|
branch whose test cannot fail is worse than an uncovered one, because it reads as done. An
|
|
67
85
|
\`exclude\` entry under \`coverage\` is for code a unit test cannot execute — an island's
|
|
68
|
-
browser-only mount, a container entry point — carries its \`why\`, and is printed by \`x doctor\`.
|
|
86
|
+
browser-only mount, a container entry point — carries its \`why\`, and is printed by \`bunx x doctor\`.
|
|
69
87
|
|
|
70
88
|
Every test the scaffold and the generators write is the idiom to copy. All of them run on the
|
|
71
89
|
\`unit\` step — in process, on the in-memory driver, with no database, browser or server.
|
|
72
90
|
|
|
73
91
|
A suite that needs a real server reads it from \`docker/test-services.env\`: Postgres 18, Redis, NATS
|
|
74
92
|
and S3 from \`docker/docker-compose.test.yml\`, in RAM, with durability and logging off — CI starts
|
|
75
|
-
it before \`bun run
|
|
93
|
+
it before \`bun run verify\`, a laptop with \`docker compose -f docker/docker-compose.test.yml up -d
|
|
76
94
|
--wait\` then \`set -a; . docker/test-services.env; set +a\`. A resource a test opens is held with
|
|
77
95
|
\`await using\` — \`await using db = await acquireWorkerDatabase()\` drops its database however
|
|
78
96
|
the test ends:
|
|
@@ -83,7 +101,7 @@ the test ends:
|
|
|
83
101
|
| a component | \`renderView(Component, props)\` | \`apps/web/shared/shell.test.ts\` |
|
|
84
102
|
| an island | \`mountIsland\`, once per state its \`*.island.states.ts\` declares | \`apps/web/shared/theme-toggle.island.test.ts\` |
|
|
85
103
|
| an action or a query | \`target.as(actor, input)\` — the refusal first, then what it answers | \`apps/web/api/health.test.ts\` |
|
|
86
|
-
| a job, a task, a backfill | \`test('…', async ({ runJobs }) => …)\` — a worker in this process; assert on \`trace.steps\` and each run's \`outcome\` | what \`x g job\` writes |
|
|
104
|
+
| a job, a task, a backfill | \`test('…', async ({ runJobs }) => …)\` — a worker in this process; assert on \`trace.steps\` and each run's \`outcome\` | what \`bunx x g job\` writes |
|
|
87
105
|
| a repo, a seed | as an actor, \`runWithContext(ctxOf({ actor }), …)\`, with \`driver.reset?.()\` after each | \`packages/db/src/seed.test.ts\` |
|
|
88
106
|
|
|
89
107
|
Money is the one row with no guard, deliberately: a float has no static signature a text rule can
|
|
@@ -91,45 +109,37 @@ see, and the type already fires — measured, \`price: 19.99\` in a seed is
|
|
|
91
109
|
\`TS2322: Type 'number' is not assignable to type 'MoneyInput'\`. A guard that pretended to check
|
|
92
110
|
it would be worse than the type that really does.
|
|
93
111
|
|
|
94
|
-
\`bun run
|
|
95
|
-
\`x build --target static
|
|
96
|
-
|
|
97
|
-
X_BUDGET_UNMEASURED — the gate reporting on a file that does not exist, not on your code.
|
|
112
|
+
\`bun run verify\` is \`bunx x verify\`, the whole gate: its \`budgets\` step runs
|
|
113
|
+
\`bunx x build --target static\` itself when \`.x/build-stats.json\` is missing or older than the
|
|
114
|
+
source, so nothing has to be built first.
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
clobbers \`AGENTS.md\`, \`bin/\`, \`.claude/\` or \`.github/\`, and deleting either side leaves the
|
|
103
|
-
other exactly as it was.
|
|
116
|
+
Commands: \`bun run setup\`, \`bun run dev\`, \`bun run verify\`, \`bunx x g <primitive>\`,
|
|
117
|
+
\`bunx x g guard <name>\`, \`bunx x db branch create <name>\`, \`bunx x doctor\`. \`setup\` is
|
|
118
|
+
\`bin/setup.ts\`, a file Bun runs, so all three are the same command in PowerShell, cmd and bash.
|
|
104
119
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
- \`bunx x g --help\` lists every generator kind, and is the only place that list is stated.
|
|
121
|
+
- Destructive database work goes in a branch, \`bunx x db branch create <name>\`, never the shared
|
|
122
|
+
dev database.
|
|
123
|
+
- \`bunx x db gen\` also writes \`packages/db/schema/\`, the whole schema as SQL, one file per
|
|
124
|
+
table: read it, never edit it. \`bunx x verify\` fails with \`X_SCHEMA_DUMP_DRIFT\` when it is not
|
|
125
|
+
what the migrations produce, and \`bunx x db gen\` rewrites it.
|
|
126
|
+
- \`bunx x doctor\` explains a broken environment and prints the fix for every finding.
|
|
127
|
+
- \`.claude/\` holds a harness that reads this file: \`/feature\`, \`/planx\`, \`/verify\` and four
|
|
128
|
+
boundary-scoped subagents. \`.claude/README.md\` says what each costs; every file is deletable.
|
|
108
129
|
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
## Agent tools
|
|
131
|
+
|
|
132
|
+
| Tool | What it answers | How to reach it |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| \`x.manifest.json\` | every route, action, query, job and policy, generated — read it before searching the tree | \`bunx x manifest\` rewrites it |
|
|
135
|
+
| \`bunx x impact <file|primitive> --json\` · \`bunx x graph --json\` | what an edit reaches, and the app as one graph (declared, import and observed edges) | rebuilt from the loaded app on every call |
|
|
136
|
+
| \`.mcp.json\` | the dev tools over MCP: routes, schema, policies, the database, tests, the gate — \`bunx x mcp tools\` lists them | a client that reads \`.mcp.json\` starts it; from any other, run \`bunx @ultimat3/cli mcp serve --transport stdio\` here |
|
|
137
|
+
| \`POST /mcp\` | this app's own tools — every action declaring \`mcp: { expose: true }\` — for a remote agent | \`apps/web/mcp.ts\`; under \`bun run dev\`, \`Authorization: Bearer dev-agent.admin\`, else an API key |
|
|
138
|
+
| \`.claude/commands/feature.md\` | build or fix one thing end to end, done when \`bun run verify\` is green | plain markdown: any agent can read one and follow it |
|
|
139
|
+
| \`.claude/commands/planx.md\` | write a plan to \`docs/plans/\` for another agent to execute | the same |
|
|
140
|
+
| \`.claude/commands/verify.md\` | run the gate and fix what it reports | the same |
|
|
111
141
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
${app.kebab} — Ultimate app. Read AGENTS.md first; it is the same content in the same order.
|
|
115
|
-
|
|
116
|
-
- Gate: \`bun run check\` (add \`--json\` for machine output — it reaches both halves). It is
|
|
117
|
-
\`x build --target static\` and then \`x verify\`; \`x verify\` alone leaves \`budgets\` with
|
|
118
|
-
nothing to measure. \`.github/workflows/ci.yml\` runs \`bun run setup && bun run check\` on
|
|
119
|
-
every push and pull request, so CI and your terminal run the same two commands.
|
|
120
|
-
- \`.dz/\` belongs to the developerz.ai platform and is additive both ways: the scaffold writes
|
|
121
|
-
nothing there, and nothing there is generated from this repo. Neither side clobbers the other.
|
|
122
|
-
- Scaffold, do not hand-write: \`x g <kind> <name>\` — \`x g --help\` lists every kind, and is the
|
|
123
|
-
only place that list is stated.
|
|
124
|
-
- Destructive DB work goes in a branch: \`x db branch create <name>\`, never the shared dev DB.
|
|
125
|
-
- \`x db gen\` also writes \`packages/db/schema/\` — the whole schema as SQL, one file per table.
|
|
126
|
-
Read it; never edit it. \`x verify\` fails with \`X_SCHEMA_DUMP_DRIFT\` when it is not what the
|
|
127
|
-
migrations produce, and \`x db gen\` rewrites it.
|
|
128
|
-
- \`x doctor\` explains a broken environment and prints the fix command for every finding.
|
|
129
|
-
|
|
130
|
-
\`.claude/\` holds the harness that reads this file: \`/feature\`, \`/planx\`, \`/verify\` and four
|
|
131
|
-
boundary-scoped subagents. It is yours — \`.claude/README.md\` says what each one costs, and every
|
|
132
|
-
file in it is deletable.
|
|
142
|
+
Project notes for ${app.kebab}: replace this line with the conventions a newcomer could not guess.
|
|
133
143
|
`;
|
|
134
144
|
|
|
135
145
|
const readme = (app: NameSet): string => `# ${app.pascal}
|
|
@@ -141,7 +151,7 @@ Built with [Ultimate](https://github.com/developerz-ai/ultimate). Bun-only, Post
|
|
|
141
151
|
\`\`\`sh
|
|
142
152
|
bun run setup # prerequisites, deps, env, the first migration, migrate, seed, the manifest
|
|
143
153
|
bun run dev # all roles in one process, embedded Postgres, /_x mounted
|
|
144
|
-
bun run
|
|
154
|
+
bun run verify # the gate: typecheck, lint, boundaries, tests, drift, budgets (it builds to weigh)
|
|
145
155
|
\`\`\`
|
|
146
156
|
|
|
147
157
|
Tests against real servers — Postgres 18, Redis, NATS, S3, in RAM and silent, as CI runs them:
|
|
@@ -149,7 +159,7 @@ Tests against real servers — Postgres 18, Redis, NATS, S3, in RAM and silent,
|
|
|
149
159
|
\`\`\`sh
|
|
150
160
|
docker compose -f docker/docker-compose.test.yml up -d --wait
|
|
151
161
|
set -a; . docker/test-services.env; set +a
|
|
152
|
-
bun run
|
|
162
|
+
bun run verify
|
|
153
163
|
\`\`\`
|
|
154
164
|
|
|
155
165
|
\`packages/db/migrations\` starts empty and \`x db gen\` is its only writer — \`bun run setup\` runs
|
|
@@ -168,7 +178,9 @@ with \`X_DB_DRIFT\`, and that is the fix it names.
|
|
|
168
178
|
| \`packages/*\` | domain, db, i18n, ui, mcp |
|
|
169
179
|
| \`app.config.ts\` | the one config file |
|
|
170
180
|
| \`x.manifest.json\` | generated facts: routes, actions, jobs, policies |
|
|
171
|
-
| \`.
|
|
181
|
+
| \`.mcp.json\` | MCP client config: the dev tools, \`bunx @ultimat3/cli mcp serve --transport stdio\` |
|
|
182
|
+
| \`apps/web/mcp.ts\` | the app's own \`POST /mcp\`: exposed actions, behind a bearer token |
|
|
183
|
+
| \`.github/workflows/ci.yml\` | \`bun run setup\` then \`bun run verify\`, on push and pull request |
|
|
172
184
|
| \`.github/workflows/image.yml\` | after a green \`ci\` on the default branch: \`docker/Dockerfile\` to \`ghcr.io\`, tagged \`sha-<7>\`, always on \`ubuntu-latest\` (bash and Docker). \`ci.yml\` runs on \`vars.CI_RUNNER\`, default \`ubuntu-latest\` |
|
|
173
185
|
`;
|
|
174
186
|
|
|
@@ -206,7 +218,7 @@ services:
|
|
|
206
218
|
# the region a request was signed for, and an app that sets no S3_REGION signs for \`auto\` — so
|
|
207
219
|
# that is this gateway's region. A server for a laptop; production is any S3-compatible endpoint.
|
|
208
220
|
s3:
|
|
209
|
-
image:
|
|
221
|
+
image: ${S3_GATEWAY_IMAGE}
|
|
210
222
|
environment:
|
|
211
223
|
VGW_BACKEND: posix
|
|
212
224
|
VGW_BACKEND_ARG: /data
|
|
@@ -225,9 +237,13 @@ volumes:
|
|
|
225
237
|
export function docsFiles(app: NameSet): readonly GeneratedFile[] {
|
|
226
238
|
return [
|
|
227
239
|
{ path: 'README.md', contents: readme(app) },
|
|
228
|
-
{ path: '
|
|
229
|
-
|
|
230
|
-
//
|
|
240
|
+
{ path: 'CLAUDE.md', contents: conventions(app) },
|
|
241
|
+
// A link, after its target. \`contents\` is the copy written only where the OS refuses a
|
|
242
|
+
// symlink (Windows without the privilege): \`write-generated.ts\` decides, never this list.
|
|
243
|
+
{ path: 'AGENTS.md', contents: conventions(app), link: 'CLAUDE.md' },
|
|
244
|
+
// The one MCP client config — what AGENTS.md's "Agent tools" block names first.
|
|
245
|
+
mcpJsonFile(app),
|
|
246
|
+
// The script `bun run setup` runs — TypeScript, so one file per OS.
|
|
231
247
|
...binFiles(),
|
|
232
248
|
{ path: 'docker/docker-compose.dev.yml', contents: composeDev(app) },
|
|
233
249
|
// What the suites run against — in RAM, silent; ci.yml starts it before the gate.
|