@ultimat3/cli 29.3.2 → 30.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +13 -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 +28 -8
- 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 +42 -138
- 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 +17 -27
- 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 +1313 -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
package/src/verify-checks.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { APP_CONFIG_FILE, ERROR_DOCS_URL } from '@ultimat3/core';
|
|
|
8
8
|
import type { Manifest } from '@ultimat3/manifest';
|
|
9
9
|
import { AGENTS_MD_FILENAME, MANIFEST_FILENAME, verifyContract } from '@ultimat3/manifest';
|
|
10
10
|
import type { MetaIssue } from '@ultimat3/seo';
|
|
11
|
-
import { validateMeta } from '@ultimat3/seo';
|
|
11
|
+
import { llmsDescriptionIssues, validateMeta } from '@ultimat3/seo';
|
|
12
12
|
import { checkAgentsMd } from './app-agents-md';
|
|
13
13
|
import { readAppSources } from './app-boundaries';
|
|
14
14
|
import { envExampleFindings } from './app-env';
|
|
@@ -17,14 +17,18 @@ import { policyFindings } from './app-permissions';
|
|
|
17
17
|
import { asyncPageFindings } from './async-pages';
|
|
18
18
|
import { appBoundaryFindings } from './boundary-findings';
|
|
19
19
|
import { checkBudgets, readBuildStats } from './budgets';
|
|
20
|
+
import { checkClientDrift } from './client-emit-drift';
|
|
20
21
|
import { checkDestructiveMigrations } from './db-destructive';
|
|
22
|
+
import { checkRollingMigrations } from './db-rolling';
|
|
21
23
|
import { checkUngeneratableMigrations } from './db-ungeneratable';
|
|
22
24
|
import { checkDocumentStyles, documentSurfaces } from './document-styles';
|
|
23
25
|
import { checkErrorCodeResolution, checkErrorFixReport } from './error-contract';
|
|
24
26
|
import { guardFindings } from './guards';
|
|
25
27
|
import { catalogFindings } from './i18n-registration';
|
|
26
28
|
import { isrQueryAdvice } from './isr-query-advice';
|
|
29
|
+
import { readJobBudgetFindings } from './job-budget';
|
|
27
30
|
import { unregisteredJobFindings } from './job-registration';
|
|
31
|
+
import { LINT_ARGV, lintOutcome } from './lint-findings';
|
|
28
32
|
import { liveRouteFindings } from './live-routes';
|
|
29
33
|
import { withLoadFindings, withoutLoadFindings } from './load-findings';
|
|
30
34
|
import {
|
|
@@ -32,14 +36,21 @@ import {
|
|
|
32
36
|
manifestMissingFindings,
|
|
33
37
|
manifestStaleness,
|
|
34
38
|
} from './manifest-staleness';
|
|
39
|
+
import { mcpLockFindings } from './mcp-lock-findings';
|
|
35
40
|
import { msg } from './messages';
|
|
36
41
|
import type { Finding } from './output';
|
|
37
42
|
import { findingFrom } from './output';
|
|
43
|
+
import { retainUnsweptFindings } from './retain-sweep';
|
|
38
44
|
import { checkMigrationDrift } from './schema-drift';
|
|
39
45
|
import { checkSchemaDump } from './schema-dump-drift';
|
|
46
|
+
import { cardRouteFindings } from './seo-cards';
|
|
40
47
|
import { scanSiteMeta } from './seo-meta';
|
|
41
48
|
import { quoteArg } from './shell-quote';
|
|
49
|
+
import { loadSiteSettings } from './site-config';
|
|
42
50
|
import { readStaticReport } from './static-report';
|
|
51
|
+
import { unknownToolGateFindings } from './tool-gate-names';
|
|
52
|
+
import { measureIfStale } from './verify-budgets-build';
|
|
53
|
+
import { exerciseStep } from './verify-exercise';
|
|
43
54
|
import { floorProblemFindings, readVerifyFloor } from './verify-floor';
|
|
44
55
|
import { roleLoadFindings } from './verify-role-load';
|
|
45
56
|
import type { VerifyStep } from './verify-step';
|
|
@@ -52,6 +63,9 @@ import { checkWorkspaceDependencies } from './workspace-graph';
|
|
|
52
63
|
/** The one file that makes the `roadmap` step answerable, and therefore what `applies` reads. */
|
|
53
64
|
const ROADMAP_FILE = join('docs', 'idea', '14-roadmap.md');
|
|
54
65
|
|
|
66
|
+
/** Where `exercise` joins the suites: directly after `job`, as `VERIFY_STEP_NAMES` holds it. */
|
|
67
|
+
const AFTER_JOB = TEST_STEPS.findIndex((step) => step.name === 'job') + 1;
|
|
68
|
+
|
|
55
69
|
/** The whole contract, in cost order. Every check the framework knows how to make lives here. */
|
|
56
70
|
export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
57
71
|
{
|
|
@@ -82,12 +96,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
82
96
|
// the colour rule is `packages/ui/src/tokens/tokens.test.ts`, and rides on the `unit` step.
|
|
83
97
|
summary: 'biome: format, no any, no default exports, no unused imports',
|
|
84
98
|
async run(ctx) {
|
|
85
|
-
|
|
86
|
-
return fromExec(result, {
|
|
87
|
-
code: 'X_LINT_FAILED',
|
|
88
|
-
cause: 'biome reported problems',
|
|
89
|
-
fix: 'bunx biome check --write .',
|
|
90
|
-
});
|
|
99
|
+
return lintOutcome(await ctx.runner(LINT_ARGV, { cwd: ctx.root }));
|
|
91
100
|
},
|
|
92
101
|
},
|
|
93
102
|
{
|
|
@@ -176,7 +185,9 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
176
185
|
};
|
|
177
186
|
},
|
|
178
187
|
},
|
|
179
|
-
...TEST_STEPS,
|
|
188
|
+
...TEST_STEPS.slice(0, AFTER_JOB),
|
|
189
|
+
exerciseStep,
|
|
190
|
+
...TEST_STEPS.slice(AFTER_JOB),
|
|
180
191
|
{
|
|
181
192
|
name: 'drift',
|
|
182
193
|
summary:
|
|
@@ -202,6 +213,12 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
202
213
|
// itself, so CI still needs nothing listening: the committed `packages/db/schema/` against
|
|
203
214
|
// what replaying the migrations produces, and that dump loaded back (`X_SCHEMA_DUMP_DRIFT`).
|
|
204
215
|
...(await checkSchemaDump(ctx.root, ctx.env ?? Bun.env)),
|
|
216
|
+
// The fifth: what the previous release would trip over during a roll. Off until the app
|
|
217
|
+
// sets `rollingSince` in `x.verify.json`, since an applied migration cannot gain a marker.
|
|
218
|
+
...(await checkRollingMigrations(ctx.root)),
|
|
219
|
+
// The sixth: a generated client (`x client`) against its own stamp — a source document that
|
|
220
|
+
// moved (`X_CLIENT_STALE`) or a body edited by hand (`X_CLIENT_EDITED`). No database.
|
|
221
|
+
...(await checkClientDrift(ctx.root)),
|
|
205
222
|
]),
|
|
206
223
|
},
|
|
207
224
|
{
|
|
@@ -235,17 +252,21 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
235
252
|
// no stylesheet registry to read; there is nothing for either half to weigh.
|
|
236
253
|
applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
|
|
237
254
|
async run(ctx) {
|
|
238
|
-
// No stats file is NOT "nothing to weigh" — it is every declared budget unmeasured
|
|
239
|
-
// the
|
|
240
|
-
//
|
|
241
|
-
//
|
|
255
|
+
// No stats file is NOT "nothing to weigh" — it is every declared budget unmeasured. So the
|
|
256
|
+
// step MEASURES first, whenever the stats are missing, of an earlier rule, or older than the
|
|
257
|
+
// newest app source (`verify-budgets-build.ts`): `x verify` alone is the gate, on a fresh
|
|
258
|
+
// tree too, and a heavier island is never weighed at the bytes of a build before it.
|
|
242
259
|
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
260
|
+
// Only then, never on every run: an unedited tree pays nothing, and a rebuild after an edit costs
|
|
261
|
+
// one static build (8.2 s on `dummy/social-media-clone`, As of 2026-11). The builder is
|
|
262
|
+
// `x build`, spawned through `ctx.runner` so it is a tagged child under this step's deadline
|
|
263
|
+
// (`stepTimeoutMs.budgets` overrides). A failing build — `examples/dummy`'s old 5.9 s was a
|
|
264
|
+
// hard `X_PRERENDER_FAILED` — is red under the build's own findings, unchanged. `SITE_ORIGIN`
|
|
265
|
+
// shapes canonical and og:url, never script bytes, and the build writes `.x/verify-static`,
|
|
266
|
+
// so a deployable `.x/static` built with the real origin is never replaced.
|
|
267
|
+
// A failed build's findings stand in for the per-route weighing below, which would only
|
|
268
|
+
// repeat them as `X_BUDGET_UNMEASURED` per route; the other riders still answer.
|
|
269
|
+
const measured = await measureIfStale(ctx);
|
|
249
270
|
const stats = await readBuildStats(ctx.root);
|
|
250
271
|
// The report beside the stats, for the one thing `checkBudgets` reads off it: a route the
|
|
251
272
|
// build rendered and could not weigh because its island was handed props over the cap is
|
|
@@ -265,13 +286,17 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
265
286
|
...(await liveRouteFindings(ctx.root)),
|
|
266
287
|
// The fourth: a data read in an async Page rather than in `load` (`async-pages.ts`).
|
|
267
288
|
...asyncPageFindings(),
|
|
268
|
-
...
|
|
289
|
+
...(measured.findings.length > 0
|
|
290
|
+
? measured.findings
|
|
291
|
+
: checkBudgets(manifest, stats, report?.unmeasured)),
|
|
269
292
|
]);
|
|
270
293
|
// What no build can weigh, said out loud rather than dropped (`measure-paths.ts`).
|
|
271
294
|
const notes = (report?.unmeasured ?? [])
|
|
272
295
|
.filter((one) => one.weighable === false)
|
|
273
296
|
.map((one) => `not weighed: ${one.reason}`);
|
|
274
|
-
const output = [outcome.output, ...notes]
|
|
297
|
+
const output = [measured.output, outcome.output, ...notes]
|
|
298
|
+
.filter((line) => line !== undefined)
|
|
299
|
+
.join('\n');
|
|
275
300
|
return output === '' ? outcome : { ...outcome, output };
|
|
276
301
|
},
|
|
277
302
|
},
|
|
@@ -293,12 +318,19 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
293
318
|
// such key), so canonical checks are skipped rather than run against an origin this file
|
|
294
319
|
// invented. `seo-meta.ts` spells out why that is the honest half.
|
|
295
320
|
const report = validateMeta(scan.records);
|
|
321
|
+
// `/llms.txt` lists these pages by description: while it is on, a page without one is red.
|
|
322
|
+
const llms =
|
|
323
|
+
(await loadSiteSettings(ctx.root)).llms === false
|
|
324
|
+
? []
|
|
325
|
+
: llmsDescriptionIssues(scan.records);
|
|
296
326
|
// The scan filled the route table, so the advice costs no second load. Rides here because
|
|
297
327
|
// this is the step about public, shared-cache pages; it never fails it.
|
|
298
328
|
const warnings = await isrQueryAdvice(ctx.root);
|
|
329
|
+
// A declared `og.capture` that names no site/ route: the card convention's build error.
|
|
330
|
+
const cards = cardRouteFindings(scan.records);
|
|
299
331
|
return {
|
|
300
|
-
ok: scan.findings.length === 0 && report.ok,
|
|
301
|
-
findings: [...scan.findings, ...report.issues.map(seoFinding)],
|
|
332
|
+
ok: scan.findings.length === 0 && report.ok && llms.length === 0 && cards.length === 0,
|
|
333
|
+
findings: [...scan.findings, ...[...report.issues, ...llms].map(seoFinding), ...cards],
|
|
302
334
|
...(warnings.length === 0 ? {} : { warnings }),
|
|
303
335
|
};
|
|
304
336
|
},
|
|
@@ -358,11 +390,19 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
358
390
|
// Started first and awaited last: it is a child process, and it runs beside the rest.
|
|
359
391
|
const roleLoad = app ? roleLoadFindings(ctx.root, ctx.runner) : Promise.resolve([]);
|
|
360
392
|
const drift = await driftFindings(ctx.root);
|
|
393
|
+
// Every `mcpTools()` against `tools.lock.json`: the `x mcp lock --check` of each, at once.
|
|
394
|
+
const remote = app ? await mcpLockFindings(ctx.root) : { findings: [], warnings: [] };
|
|
361
395
|
const findings = [
|
|
362
396
|
...(app ? await withLoadFindings(ctx.root, drift) : drift),
|
|
363
397
|
// An app only: the framework monorepo declares no `defineApi`, so it has no list to be in.
|
|
364
398
|
...(existsSync(join(ctx.root, APP_CONFIG_FILE))
|
|
365
|
-
?
|
|
399
|
+
? [
|
|
400
|
+
...(await unregisteredJobFindings(ctx.root)),
|
|
401
|
+
...(await retainUnsweptFindings(ctx.root)),
|
|
402
|
+
...(await readJobBudgetFindings(ctx.root)),
|
|
403
|
+
...(await unknownToolGateFindings(ctx.root)),
|
|
404
|
+
...remote.findings,
|
|
405
|
+
]
|
|
366
406
|
: []),
|
|
367
407
|
// What a worker never registers: a declaration beside a component the API index does not
|
|
368
408
|
// import (`verify-role-load.ts`). The boot checks jobs and tasks only, and only at boot.
|
|
@@ -374,7 +414,10 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
374
414
|
];
|
|
375
415
|
// Warnings are not findings: `AGENTS.md` tabulating a route table is a smell a human
|
|
376
416
|
// judges, not a build error. They ride in `output`, which `--json` carries verbatim.
|
|
377
|
-
const output =
|
|
417
|
+
const output = [
|
|
418
|
+
...agents.warnings.map((warning) => `${AGENTS_MD_FILENAME}: ${warning}`),
|
|
419
|
+
...remote.warnings,
|
|
420
|
+
];
|
|
378
421
|
return {
|
|
379
422
|
ok: findings.length === 0,
|
|
380
423
|
findings,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// ORDER, NOT MEMBERSHIP, FOLLOWS THE CLOCK. Each slice also writes a junit report; its per-file
|
|
14
14
|
// durations go into the run's timings cache (`TEST_TIMINGS_FILE`, `test-timings.ts`) and the NEXT
|
|
15
15
|
// run starts the slices longest-first from it. The slices themselves stay a pure function of the
|
|
16
|
-
// file list — the coverage a slice measures never depends on a cache. Measured on
|
|
16
|
+
// file list — the coverage a slice measures never depends on a cache. Measured on one adopter's app
|
|
17
17
|
// (1 192 files, 102 slices, 8 processes): sorted order 64.1 s, longest-first 56.3 s, the floor
|
|
18
18
|
// 56.1 s — the pool used to start its 50 s slice last.
|
|
19
19
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The gate's second serial lane: `e2e` beside `live` in a package monorepo, isolated from it by
|
|
2
|
+
// construction. Split from `verify-run.ts`, which owns the order; this file owns the one condition
|
|
3
|
+
// under which two serial suites may share the machine, and the environment that makes it safe.
|
|
4
|
+
|
|
5
|
+
// why: Bun ships no synchronous existence check or path join.
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
// why: Bun ships no path-join primitive.
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { APP_CONFIG_FILE } from '@ultimat3/core';
|
|
10
|
+
import type { Runner } from './exec';
|
|
11
|
+
import type { VerifyContext } from './verify-step';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* What a service URL is called: every `TEST_*_URL` the suites read (`docker/test-services.env`) and
|
|
15
|
+
* `DATABASE_URL`, which `@ultimat3/testing`'s template database falls back to. A child of the lane
|
|
16
|
+
* sees none of them, so whatever it runs, it cannot reach the server `live` is using.
|
|
17
|
+
*/
|
|
18
|
+
export const isServiceUrlKey = (key: string): boolean =>
|
|
19
|
+
key === 'DATABASE_URL' || /^TEST_[A-Z0-9_]*_URL$/.test(key);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether `e2e` may run beside `live`. Both serial, and serial for reasons that do not meet: `live`
|
|
23
|
+
* holds a cluster-wide replication slot, `e2e` one browser profile and the built output. In a
|
|
24
|
+
* package monorepo the e2e suite boots throwaway apps on free ports and strips every service URL
|
|
25
|
+
* itself (`packages/cli/e2e/*`), and `besideLive` below makes that true of every file. An APP's
|
|
26
|
+
* e2e boots the app against the services its own live suite uses, so an app keeps them in order.
|
|
27
|
+
*/
|
|
28
|
+
export const e2eBesideLive = (ctx: Pick<VerifyContext, 'root'>): boolean =>
|
|
29
|
+
!existsSync(join(ctx.root, APP_CONFIG_FILE));
|
|
30
|
+
|
|
31
|
+
/** `ctx` with every child its runner starts stripped of each service URL in `env`. */
|
|
32
|
+
export function besideLive(ctx: VerifyContext): VerifyContext {
|
|
33
|
+
// Both: a child inherits the process environment, so a URL only `Bun.env` holds reaches it even
|
|
34
|
+
// when the caller handed `ctx.env`.
|
|
35
|
+
const keys = new Set([...Object.keys(Bun.env), ...Object.keys(ctx.env ?? {})]);
|
|
36
|
+
const strip: Record<string, undefined> = {};
|
|
37
|
+
for (const key of keys) if (isServiceUrlKey(key)) strip[key] = undefined;
|
|
38
|
+
const runner: Runner = (command, options) =>
|
|
39
|
+
ctx.runner(command, { ...options, env: { ...options.env, ...strip } });
|
|
40
|
+
return { ...ctx, runner };
|
|
41
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The `exercise` gate step: `x exercise --json` as a child under the step's deadline, its findings
|
|
2
|
+
// the step's. A child, never in process: the pass freezes the clock, seals the network and installs
|
|
3
|
+
// its own database, and the steps beside it in this process must see none of that.
|
|
4
|
+
|
|
5
|
+
// why: Bun has no directory probe for the app config, and no path joiner.
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { APP_CONFIG_FILE, ERROR_DOCS_URL, isCompiledBundle } from '@ultimat3/core';
|
|
9
|
+
import { t } from '@ultimat3/schema';
|
|
10
|
+
import { execOutput } from './exec';
|
|
11
|
+
import type { Finding } from './output';
|
|
12
|
+
import { documentIn } from './upgrade-gate';
|
|
13
|
+
import type { VerifyStep } from './verify-step';
|
|
14
|
+
|
|
15
|
+
/** `x exercise`'s argv, spawned as this process's own CLI: the source entry, or a compiled binary. */
|
|
16
|
+
export function verifyExerciseCommand(): readonly string[] {
|
|
17
|
+
const self = isCompiledBundle(import.meta.dir) ? [] : [join(import.meta.dir, 'bin.ts')];
|
|
18
|
+
return [process.execPath, ...self, 'exercise', '--json'];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const FINDING = t.object({
|
|
22
|
+
code: t.string,
|
|
23
|
+
cause: t.string,
|
|
24
|
+
fix: t.string,
|
|
25
|
+
docs: t.optional(t.string),
|
|
26
|
+
at: t.optional(t.string),
|
|
27
|
+
});
|
|
28
|
+
const DOCUMENT = t.object({ ok: t.boolean, findings: t.optional(t.array(FINDING)) });
|
|
29
|
+
|
|
30
|
+
interface ExerciseDocument {
|
|
31
|
+
readonly ok: boolean;
|
|
32
|
+
readonly findings?: readonly Finding[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const exerciseStep: VerifyStep = {
|
|
36
|
+
name: 'exercise',
|
|
37
|
+
summary: 'every action, query and job runs once against seeded rows, with no test written',
|
|
38
|
+
// A repo with no `app.config.ts` registers no primitive: SKIPPED, for the reason `policy` gives.
|
|
39
|
+
applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
|
|
40
|
+
async run(ctx) {
|
|
41
|
+
const result = await ctx.runner(verifyExerciseCommand(), { cwd: ctx.root });
|
|
42
|
+
const parsed = DOCUMENT.safeParse(documentIn(result.stdout));
|
|
43
|
+
const document = parsed.issues === undefined ? (parsed.value as ExerciseDocument) : undefined;
|
|
44
|
+
const reported = document?.findings ?? [];
|
|
45
|
+
// The exit code is the verdict, as `fromExec` reads every child's; the document adds the
|
|
46
|
+
// findings, and a document that says red is red whatever the exit. A red with nothing to say
|
|
47
|
+
// is still red, under the code for a CLI that failed without saying why.
|
|
48
|
+
if (result.ok && document?.ok !== false && reported.length === 0) {
|
|
49
|
+
return { ok: true, findings: [], output: execOutput(result) };
|
|
50
|
+
}
|
|
51
|
+
const findings: readonly Finding[] =
|
|
52
|
+
reported.length > 0
|
|
53
|
+
? reported
|
|
54
|
+
: [
|
|
55
|
+
{
|
|
56
|
+
code: 'X_CLI_UNEXPECTED',
|
|
57
|
+
cause: `x exercise --json exited ${String(result.code)} with no finding, so x verify could not say which primitive failed`,
|
|
58
|
+
fix: 'x exercise --json',
|
|
59
|
+
docs: ERROR_DOCS_URL,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
return { ok: false, findings, output: execOutput(result) };
|
|
63
|
+
},
|
|
64
|
+
};
|
package/src/verify-floor.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { COVERAGE_FIELD, readCoverageFloor } from './coverage-floor';
|
|
|
14
14
|
import type { Finding } from './output';
|
|
15
15
|
import type { StepTimeouts } from './verify-deadline';
|
|
16
16
|
import { readStepTimeouts, STEP_TIMEOUT_FIELD } from './verify-deadline';
|
|
17
|
+
import type { SkipCeiling } from './verify-skip-ceiling';
|
|
18
|
+
import { readSkipCeiling, SKIP_CEILING_FIELD } from './verify-skip-ceiling';
|
|
17
19
|
import { GATE_COMMAND, VERIFY_STEP_NAMES } from './verify-step';
|
|
18
20
|
|
|
19
21
|
/** Hand-written and committed, beside the generated `x.manifest.json` the same gate reads. */
|
|
@@ -62,6 +64,17 @@ export interface VerifyFloor {
|
|
|
62
64
|
* Absent in an app is `X_COVERAGE_FLOOR_UNSTATED`: no app is held to a number it did not write.
|
|
63
65
|
*/
|
|
64
66
|
readonly coverage?: CoverageFloor;
|
|
67
|
+
/**
|
|
68
|
+
* The migration id the rolling-deploy rail judges after (`db-rolling.ts`), exclusive: an applied
|
|
69
|
+
* migration cannot gain a marker, so what it already holds is not judged. `"0"` judges everything.
|
|
70
|
+
* Absent is off, and `x doctor` says which id turns it on.
|
|
71
|
+
*/
|
|
72
|
+
readonly rollingSince?: string;
|
|
73
|
+
/**
|
|
74
|
+
* The most tests a step may skip (`verify-skip-ceiling.ts`), judged by `x verify merge` and by a
|
|
75
|
+
* whole `x verify` under `CI`. Absent is no ceiling, and a CI run names the number to write.
|
|
76
|
+
*/
|
|
77
|
+
readonly skipCeiling?: SkipCeiling;
|
|
65
78
|
/** Why part of the file is not a floor. The `manifest` step reports these; nothing swallows them. */
|
|
66
79
|
readonly problems: readonly string[];
|
|
67
80
|
}
|
|
@@ -72,6 +85,9 @@ export const BUDGET_FIELD = 'agentsMdMaxBytes';
|
|
|
72
85
|
/** The floor's typecheck-binary key. Named once: the problem quotes it and the fix repairs it. */
|
|
73
86
|
export const TYPECHECK_BIN_FIELD = 'typecheckBin';
|
|
74
87
|
|
|
88
|
+
/** The floor's rolling-rail bound. Named once: the problem quotes it and the fix repairs it. */
|
|
89
|
+
export const ROLLING_SINCE_FIELD = 'rollingSince';
|
|
90
|
+
|
|
75
91
|
/** The floor's isolation key. */
|
|
76
92
|
export const ISOLATE_FIELD = 'isolate';
|
|
77
93
|
|
|
@@ -130,6 +146,19 @@ function readTypecheckBin(payload: Record<string, unknown> | undefined): {
|
|
|
130
146
|
return { bin: raw, problems: [] };
|
|
131
147
|
}
|
|
132
148
|
|
|
149
|
+
function readRollingSince(payload: Record<string, unknown> | undefined): {
|
|
150
|
+
since?: string;
|
|
151
|
+
problems: readonly string[];
|
|
152
|
+
} {
|
|
153
|
+
const raw = payload?.[ROLLING_SINCE_FIELD];
|
|
154
|
+
if (raw === undefined) return { problems: [] };
|
|
155
|
+
if (typeof raw !== 'string' || raw.trim().length === 0)
|
|
156
|
+
return {
|
|
157
|
+
problems: [`"${ROLLING_SINCE_FIELD}" is ${JSON.stringify(raw)}, which is not a migration id`],
|
|
158
|
+
};
|
|
159
|
+
return { since: raw, problems: [] };
|
|
160
|
+
}
|
|
161
|
+
|
|
133
162
|
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
134
163
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
135
164
|
? (value as Record<string, unknown>)
|
|
@@ -163,12 +192,16 @@ export function parseVerifyFloor(
|
|
|
163
192
|
const isolate = readIsolate(record);
|
|
164
193
|
const timeouts = readStepTimeouts(record, declared);
|
|
165
194
|
const coverage = readCoverageFloor(record);
|
|
195
|
+
const rolling = readRollingSince(record);
|
|
196
|
+
const skips = readSkipCeiling(record, declared);
|
|
166
197
|
const rest = {
|
|
167
198
|
...(budget.budget === undefined ? {} : { agentsMdMaxBytes: budget.budget }),
|
|
168
199
|
...(typecheckBin.bin === undefined ? {} : { typecheckBin: typecheckBin.bin }),
|
|
169
200
|
...(isolate.isolate === undefined ? {} : { isolate: isolate.isolate }),
|
|
170
201
|
...(timeouts.timeouts === undefined ? {} : { stepTimeoutMs: timeouts.timeouts }),
|
|
171
202
|
...(coverage.coverage === undefined ? {} : { coverage: coverage.coverage }),
|
|
203
|
+
...(rolling.since === undefined ? {} : { rollingSince: rolling.since }),
|
|
204
|
+
...(skips.ceiling === undefined ? {} : { skipCeiling: skips.ceiling }),
|
|
172
205
|
};
|
|
173
206
|
const steps = record?.['steps'];
|
|
174
207
|
if (!Array.isArray(steps)) {
|
|
@@ -182,6 +215,8 @@ export function parseVerifyFloor(
|
|
|
182
215
|
...isolate.problems,
|
|
183
216
|
...timeouts.problems,
|
|
184
217
|
...coverage.problems,
|
|
218
|
+
...rolling.problems,
|
|
219
|
+
...skips.problems,
|
|
185
220
|
],
|
|
186
221
|
};
|
|
187
222
|
}
|
|
@@ -200,6 +235,8 @@ export function parseVerifyFloor(
|
|
|
200
235
|
...budget.problems,
|
|
201
236
|
...timeouts.problems,
|
|
202
237
|
...coverage.problems,
|
|
238
|
+
...rolling.problems,
|
|
239
|
+
...skips.problems,
|
|
203
240
|
],
|
|
204
241
|
};
|
|
205
242
|
}
|
|
@@ -289,5 +326,9 @@ const fixFor = (problem: string): string => {
|
|
|
289
326
|
return `x verify --json # then write "${STEP_TIMEOUT_FIELD}" in ${VERIFY_FLOOR_FILE} as { "unit": 900000 } — a step it runs, a whole number of milliseconds`;
|
|
290
327
|
if (problem.includes(`"${COVERAGE_FIELD}`))
|
|
291
328
|
return `x verify --only unit --json # then write "${COVERAGE_FIELD}" in ${VERIFY_FLOOR_FILE} as { "lines": 95, "funcs": 95 }, every "exclude" entry as { "glob": "…", "why": "…" }`;
|
|
329
|
+
if (problem.includes(`"${ROLLING_SINCE_FIELD}"`))
|
|
330
|
+
return `x doctor --json # it names your newest migration id: set "${ROLLING_SINCE_FIELD}" in ${VERIFY_FLOOR_FILE} to that, "0" to judge them all, or drop the key`;
|
|
331
|
+
if (problem.includes(`"${SKIP_CEILING_FIELD}"`))
|
|
332
|
+
return `x verify --json # then write "${SKIP_CEILING_FIELD}" in ${VERIFY_FLOOR_FILE} as { "contract": 9 } — a step it runs, a whole number of tests`;
|
|
292
333
|
return `x verify --json # then write ${VERIFY_FLOOR_FILE} as {"steps":["unit","contract"]}, naming only steps it ran`;
|
|
293
334
|
};
|
package/src/verify-merge.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type { PartStep } from './verify-part-step';
|
|
|
29
29
|
import { readPartStep } from './verify-part-step';
|
|
30
30
|
import { verifySummary } from './verify-run';
|
|
31
31
|
import { corpusHash } from './verify-shard';
|
|
32
|
+
import { judgeSkips } from './verify-skip-ceiling';
|
|
32
33
|
import { GATE_COMMAND, VERIFY_STEP_NAMES } from './verify-step';
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -71,8 +72,8 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
71
72
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
|
-
* The LAST non-empty line, and only it:
|
|
75
|
-
* the gate's. Never "the last line that parses" — a part whose tail is not the gate's document is
|
|
75
|
+
* The LAST non-empty line, and only it: an app's own wrapper script (an older scaffold's
|
|
76
|
+
* `bin/check --json`) prints the build's document and then the gate's. Never "the last line that parses" — a part whose tail is not the gate's document is
|
|
76
77
|
* a truncated or appended upload, and falling back to an earlier line would believe it.
|
|
77
78
|
*/
|
|
78
79
|
export function parsePart(file: string, text: string, command: string = GATE_COMMAND): VerifyPart {
|
|
@@ -322,8 +323,19 @@ function mergeStep(
|
|
|
322
323
|
const nothingRan = sharded.length > 0 && tests !== undefined && tests.ran === 0;
|
|
323
324
|
const required = floorRequires(floor, name);
|
|
324
325
|
const floorGap = nothingRan && required ? [skippedSuiteFinding(name, tests.skipped)] : [];
|
|
326
|
+
// The ceiling on the SUM, always: a merge is the CI verdict, and one shard's skips say nothing.
|
|
327
|
+
const skips =
|
|
328
|
+
sharded.length > 0
|
|
329
|
+
? judgeSkips(name, tests, floor?.skipCeiling)
|
|
330
|
+
: { findings: [], warnings: [] };
|
|
325
331
|
const riding = rider?.findings ?? [];
|
|
326
|
-
const findings = [
|
|
332
|
+
const findings = [
|
|
333
|
+
...steps.flatMap((step) => step.findings),
|
|
334
|
+
...gaps,
|
|
335
|
+
...floorGap,
|
|
336
|
+
...skips.findings,
|
|
337
|
+
...riding,
|
|
338
|
+
];
|
|
327
339
|
const output = [
|
|
328
340
|
...steps
|
|
329
341
|
.filter((step) => !step.ok && step.output !== undefined && step.output.length > 0)
|
|
@@ -331,7 +343,9 @@ function mergeStep(
|
|
|
331
343
|
...(rider?.output === undefined ? [] : [rider.output]),
|
|
332
344
|
].join('\n');
|
|
333
345
|
// Every part's advice, once each and in the order the parts reported it.
|
|
334
|
-
const warnings = [
|
|
346
|
+
const warnings = [
|
|
347
|
+
...new Set([...steps.flatMap((step) => step.warnings ?? []), ...skips.warnings]),
|
|
348
|
+
];
|
|
335
349
|
const skipped =
|
|
336
350
|
sharded.length > 0 ? nothingRan && !required : steps.every((step) => step.skipped);
|
|
337
351
|
return {
|
|
@@ -340,6 +354,7 @@ function mergeStep(
|
|
|
340
354
|
steps.every((step) => step.ok) &&
|
|
341
355
|
gaps.length === 0 &&
|
|
342
356
|
floorGap.length === 0 &&
|
|
357
|
+
skips.findings.length === 0 &&
|
|
343
358
|
riding.length === 0,
|
|
344
359
|
// The slowest job: the parts ran side by side, and wall time is what a CI run waits on.
|
|
345
360
|
durationMs: Math.max(...steps.map((step) => step.durationMs)),
|
package/src/verify-role-load.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
// why: Bun ships no path API; the fix names the import relative to the API index.
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
|
+
import { gatedToolNames } from '@ultimat3/action';
|
|
9
10
|
import { ERROR_DOCS_URL, registeredServiceNames, singleLine } from '@ultimat3/core';
|
|
10
11
|
import { entityNames } from '@ultimat3/entity';
|
|
11
12
|
import { catalogDeclarationCount } from '@ultimat3/i18n';
|
|
@@ -67,6 +68,7 @@ export function registrations(): readonly Registration[] {
|
|
|
67
68
|
'channel',
|
|
68
69
|
registeredChannels().map((channel) => channel.name),
|
|
69
70
|
),
|
|
71
|
+
...of('tool gate', gatedToolNames()),
|
|
70
72
|
];
|
|
71
73
|
}
|
|
72
74
|
|
package/src/verify-run.ts
CHANGED
|
@@ -9,12 +9,14 @@ import { msg } from './messages';
|
|
|
9
9
|
import type { CommandResult, Finding, JsonValue, StepResult } from './output';
|
|
10
10
|
import type { StepDeadline, StepGuard } from './verify-deadline';
|
|
11
11
|
import { guardStep, raceDeadline, stepDeadline } from './verify-deadline';
|
|
12
|
+
import { besideLive, e2eBesideLive } from './verify-e2e-lane';
|
|
12
13
|
import {
|
|
13
14
|
floorRequires,
|
|
14
15
|
readVerifyFloor,
|
|
15
16
|
skippedSuiteFinding,
|
|
16
17
|
vanishedSuiteFinding,
|
|
17
18
|
} from './verify-floor';
|
|
19
|
+
import { inCi, judgeSkips } from './verify-skip-ceiling';
|
|
18
20
|
import { stalledOutput, stepTimeoutFinding } from './verify-stalled';
|
|
19
21
|
import type {
|
|
20
22
|
CoverageNumbers,
|
|
@@ -24,6 +26,7 @@ import type {
|
|
|
24
26
|
VerifyStepName,
|
|
25
27
|
} from './verify-step';
|
|
26
28
|
import { GATE_COMMAND } from './verify-step';
|
|
29
|
+
import { resetTestDiscovery } from './verify-tests';
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Run every step, never bailing early: an agent fixing three things at once needs all
|
|
@@ -38,6 +41,9 @@ export async function runVerify(
|
|
|
38
41
|
steps: readonly VerifyStep[],
|
|
39
42
|
ctx: VerifyContext,
|
|
40
43
|
): Promise<CommandResult> {
|
|
44
|
+
// The file list is one run's: the dev MCP host calls this again in one process, and a suite
|
|
45
|
+
// written or deleted since the last call must be what this one judges.
|
|
46
|
+
resetTestDiscovery();
|
|
41
47
|
const floor = await readVerifyFloor(ctx.root);
|
|
42
48
|
const only = onlyList(ctx.only);
|
|
43
49
|
const selected = only === undefined ? steps : steps.filter((step) => only.includes(step.name));
|
|
@@ -76,7 +82,14 @@ export async function runVerify(
|
|
|
76
82
|
// The static steps wait for the serial suites and then run BESIDE them — only when `live` is in
|
|
77
83
|
// the list, so a one-step run (`--only`) and a list with no serial suite keep today's order.
|
|
78
84
|
const overlapping = selected.some((step) => step.name === SERIAL_SUITES[0]);
|
|
79
|
-
|
|
85
|
+
// The second serial lane (`verify-e2e-lane.ts`): `e2e` beside `live`, its children stripped of
|
|
86
|
+
// every service URL, in a package monorepo only.
|
|
87
|
+
const e2eLane = overlapping && e2eBesideLive(ctx);
|
|
88
|
+
const beside = overlapping
|
|
89
|
+
? selected.filter(
|
|
90
|
+
(step) => BESIDE_SERIAL_SUITES.has(step.name) || (e2eLane && step.name === 'e2e'),
|
|
91
|
+
)
|
|
92
|
+
: [];
|
|
80
93
|
let pending: Promise<void> | undefined;
|
|
81
94
|
const join = async (): Promise<void> => {
|
|
82
95
|
await pending;
|
|
@@ -93,7 +106,9 @@ export async function runVerify(
|
|
|
93
106
|
for (const step of selected) {
|
|
94
107
|
if (beside.includes(step)) continue;
|
|
95
108
|
if (step.name === SERIAL_SUITES[0]) {
|
|
96
|
-
pending = Promise.all(
|
|
109
|
+
pending = Promise.all(
|
|
110
|
+
beside.map((other) => run(other, other.name === 'e2e' ? besideLive(base) : base)),
|
|
111
|
+
).then(() => undefined);
|
|
97
112
|
} else if (!SERIAL_SUITES.includes(step.name)) {
|
|
98
113
|
await join();
|
|
99
114
|
}
|
|
@@ -199,7 +214,7 @@ export const BESIDE_SERIAL_SUITES: ReadonlySet<VerifyStepName> = new Set<VerifyS
|
|
|
199
214
|
]);
|
|
200
215
|
|
|
201
216
|
/** The consecutive run of steps the static group overlaps, in the order `VERIFY_STEP_NAMES` holds. */
|
|
202
|
-
export const SERIAL_SUITES: readonly VerifyStepName[] = ['live', 'job', 'e2e', 'eval'];
|
|
217
|
+
export const SERIAL_SUITES: readonly VerifyStepName[] = ['live', 'job', 'exercise', 'e2e', 'eval'];
|
|
203
218
|
|
|
204
219
|
async function runStep(
|
|
205
220
|
step: VerifyStep,
|
|
@@ -279,20 +294,25 @@ async function runStep(
|
|
|
279
294
|
// with no files at all — the run just had to finish before it could be seen. Appended to the
|
|
280
295
|
// step's own findings so `data.failed`, the counts and every gate reading this table carry it.
|
|
281
296
|
const vanished = nothingRan && required ? [skippedSuiteFinding(step.name, tests.skipped)] : [];
|
|
297
|
+
// A mostly skipped suite, judged where the verdict is: CI, and only a whole run (a shard's
|
|
298
|
+
// share is judged by `x verify merge`, on the sum). A laptop without services stays a dev loop.
|
|
299
|
+
const skips =
|
|
300
|
+
outcome.shard === undefined && inCi(ctx.env ?? Bun.env)
|
|
301
|
+
? judgeSkips(step.name, tests, floor?.skipCeiling)
|
|
302
|
+
: { findings: [], warnings: [] };
|
|
303
|
+
const warnings = [...(outcome.warnings ?? []), ...skips.warnings];
|
|
282
304
|
return {
|
|
283
305
|
result: {
|
|
284
306
|
name: step.name,
|
|
285
|
-
ok: outcome.ok && vanished.length === 0,
|
|
307
|
+
ok: outcome.ok && vanished.length === 0 && skips.findings.length === 0,
|
|
286
308
|
durationMs: Math.round(performance.now() - started),
|
|
287
309
|
// Without a floor to require it, a suite that ran nothing is a SKIP and not a pass (#434):
|
|
288
310
|
// the `e2e` step printed `✓ e2e 46ms` over its one skipped test, which is the one thing a
|
|
289
311
|
// step table may never do — a reader cannot tell a lane that ran from a lane that did not.
|
|
290
312
|
skipped: nothingRan && !required,
|
|
291
|
-
findings: [...outcome.findings, ...vanished],
|
|
313
|
+
findings: [...outcome.findings, ...vanished, ...skips.findings],
|
|
292
314
|
...(outcome.output === undefined ? {} : { output: outcome.output }),
|
|
293
|
-
...(
|
|
294
|
-
? {}
|
|
295
|
-
: { warnings: outcome.warnings }),
|
|
315
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
296
316
|
...(outcome.workers === undefined ? {} : { workers: outcome.workers }),
|
|
297
317
|
...(outcome.widthReason === undefined ? {} : { widthReason: outcome.widthReason }),
|
|
298
318
|
...(outcome.slowest === undefined || outcome.slowest.length === 0
|
package/src/verify-shard.ts
CHANGED
|
@@ -23,8 +23,18 @@ export class VerifyShardInvalidError extends UltimateError {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The steps a shard may split: every suite that runs test files. `live` and `e2e` stay ONE process
|
|
28
|
+
* inside a shard — serial is about one machine's replication slot and browser, and each CI runner
|
|
29
|
+
* starts its own — so their slices split across runners, never across workers.
|
|
30
|
+
*/
|
|
31
|
+
export const SHARDABLE_STEPS: readonly VerifyStepName[] = [
|
|
32
|
+
'unit',
|
|
33
|
+
'contract',
|
|
34
|
+
'live',
|
|
35
|
+
'job',
|
|
36
|
+
'e2e',
|
|
37
|
+
];
|
|
28
38
|
|
|
29
39
|
export interface ShardSpec {
|
|
30
40
|
/** 1-based, as a CI matrix counts. */
|