@starci/hfs 4.3.0 → 4.4.1
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/contracts.mjs +2 -2
- package/emit/db-types.mjs +12 -3
- package/lint/run.mjs +3 -3
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/report/sonar.mjs +1 -1
- package/runtime/config.example.yaml +7 -6
- package/runtime/engine/config.mjs +10 -18
- package/runtime/engine/removed-vocabulary.mjs +0 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +2855 -163
- package/runtime/knowledge/hfs/slots.yaml +333 -62
- package/runtime/knowledge/patterns/be/api.yaml +32 -5
- package/runtime/knowledge/patterns/be/cli.yaml +14 -4
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3091 -43
- package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
- package/runtime/modules/models/runtimes.yaml +5 -7
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +13769 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +4 -1
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +42 -1
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
- package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/config.mjs +1 -1
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/index.mjs +1 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +4 -4
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- package/runtime/scripts/hfs/architecture.mjs +1 -1
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +46 -47
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +69 -9
- package/runtime/scripts/hfs/param-names.mjs +1 -1
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +158 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract.mjs +67 -6
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +9 -0
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +9 -8
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +2 -2
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-init.mjs +127 -19
- package/runtime/scripts/hfs/secret-sops.mjs +38 -6
- package/runtime/scripts/hfs/secret-store.mjs +56 -54
- package/runtime/scripts/hfs/secret.mjs +34 -5
- package/runtime/scripts/hfs/slot-imports.mjs +18 -5
- package/runtime/scripts/hfs/slots.mjs +3 -2
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -8
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/env.mjs +2 -2
- package/runtime/scripts/lib/git.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +0 -2
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/language.mjs +1 -1
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +2 -4
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +5 -18
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +5 -2
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +14 -6
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +1 -0
- package/scaffold/add-fe-page.mjs +1 -0
- package/scaffold/add-inbox-lite.mjs +74 -42
- package/scaffold/add-table.mjs +110 -87
- package/scaffold/add.mjs +159 -44
- package/scaffold/app.mjs +124 -39
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/scaffold/supabase-ports.mjs +1 -1
- package/src/main.mjs +229 -116
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +110 -54
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +3 -0
- package/templates/app/gitignore-lite +3 -0
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/README.md +5 -3
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
- package/templates/be/inbox/database-client.ts.tpl +12 -11
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +42 -9
- package/templates/be/inbox/service.ts.tpl +23 -8
- package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/index.mjs +54 -41
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// app db plan: compare migration-owned state without fetching commands or Vault values.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import pg from 'libpg-query';
|
|
6
|
+
import { parse as parseToml } from 'smol-toml';
|
|
7
|
+
import { invocationDir } from '../lib/roots.mjs';
|
|
8
|
+
import { sqlStringParts, walkSqlAst as walk, sqlParser, sqlCallArgument as argument, sqlStringLiteral as literal } from '../lib/sql-ast.mjs';
|
|
9
|
+
import { DbVerbError, dbOptions, dbAppRoot, dbSqlFiles, runDbSupabase, dbFailure } from './db-baseline-verb.mjs';
|
|
10
|
+
|
|
11
|
+
await pg.loadModule();
|
|
12
|
+
|
|
13
|
+
// The pinned Supabase CLI uses positional SQL, --output json and --agent no for a plain rows array.
|
|
14
|
+
// Contract: supabase/cli apps/cli/src/commands/db/query/{query.command,query.handler}.ts at the pinned CLI tag.
|
|
15
|
+
const PRESENCE_SQL = `select
|
|
16
|
+
pg_catalog.to_regclass('supabase_migrations.schema_migrations') is not null as migrations,
|
|
17
|
+
pg_catalog.to_regclass('cron.job') is not null as cron,
|
|
18
|
+
pg_catalog.to_regclass('pgmq.meta') is not null as queues,
|
|
19
|
+
pg_catalog.to_regclass('vault.secrets') is not null as vault;`;
|
|
20
|
+
const INVENTORIES = {
|
|
21
|
+
migrations: 'select version from supabase_migrations.schema_migrations order by version;',
|
|
22
|
+
cron: 'select jobname as name, schedule, pg_catalog.md5(command) as "commandHash", active from cron.job order by jobid;',
|
|
23
|
+
queues: 'select queue_name as name from pgmq.meta order by queue_name;',
|
|
24
|
+
vault: 'select name from vault.secrets order by name;',
|
|
25
|
+
};
|
|
26
|
+
const names = (nodes) => sqlStringParts(nodes).join('.');
|
|
27
|
+
const hash = (command) => createHash('md5').update(command).digest('hex');
|
|
28
|
+
const OWNERSHIP_CALLS = new Set(['pgmq.create', 'pgmq.drop_queue', 'cron.schedule', 'cron.unschedule', 'cron.schedule_in_database', 'cron.alter_job']);
|
|
29
|
+
|
|
30
|
+
const parseSql = sqlParser(pg, () => { throw new DbVerbError('migration SQL could not be inventoried safely'); });
|
|
31
|
+
|
|
32
|
+
function requiredLiteral(call, index, aliases) {
|
|
33
|
+
const value = literal(argument(call, index, aliases));
|
|
34
|
+
if (value === null || value.length === 0) throw new DbVerbError('queue and cron ownership requires literal migration declarations');
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function inventoryCall(state, call) {
|
|
39
|
+
const name = names(call.funcname);
|
|
40
|
+
if (name === 'pgmq.create') {
|
|
41
|
+
state.queues.set(requiredLiteral(call, 0, ['queue_name']), null);
|
|
42
|
+
} else if (name === 'pgmq.drop_queue') {
|
|
43
|
+
state.queues.delete(requiredLiteral(call, 0, ['queue_name']));
|
|
44
|
+
} else if (name === 'cron.schedule') {
|
|
45
|
+
const job = requiredLiteral(call, 0, ['job_name']);
|
|
46
|
+
const schedule = requiredLiteral(call, 1, ['schedule']);
|
|
47
|
+
const command = requiredLiteral(call, 2, ['command']);
|
|
48
|
+
state.cron.set(job, { schedule, commandHash: hash(command), active: true });
|
|
49
|
+
} else if (name === 'cron.unschedule') {
|
|
50
|
+
state.cron.delete(requiredLiteral(call, 0, ['job_name']));
|
|
51
|
+
} else if (name === 'cron.schedule_in_database' || name === 'cron.alter_job') {
|
|
52
|
+
throw new DbVerbError('database plan requires cron.schedule and named cron.unschedule ownership');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ownershipCalls(tree) {
|
|
57
|
+
const calls = [];
|
|
58
|
+
walk(tree, (node) => { if (node.FuncCall && OWNERSHIP_CALLS.has(names(node.FuncCall.funcname))) calls.push(node.FuncCall); });
|
|
59
|
+
return calls;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function flatDeclaration(tree, call) {
|
|
63
|
+
const node = tree.SelectStmt ?? (tree.stmts?.length === 1 && tree.stmts[0].stmt.SelectStmt);
|
|
64
|
+
const constrained = ['fromClause', 'withClause', 'whereClause', 'intoClause', 'limitCount', 'limitOffset', 'groupClause', 'havingClause', 'distinctClause', 'windowClause'];
|
|
65
|
+
return node && /^SETOP_NONE$/.test(node.op) && node.targetList?.length === 1
|
|
66
|
+
&& !constrained.some((key) => node[key]) && node.targetList[0].ResTarget?.val?.FuncCall === call;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function inventoryTree(state, tree) {
|
|
70
|
+
const calls = ownershipCalls(tree);
|
|
71
|
+
if (!calls.length) return;
|
|
72
|
+
if (calls.length !== 1 || !flatDeclaration(tree, calls[0])) throw new DbVerbError('database ownership requires one unconditional migration SELECT per declaration');
|
|
73
|
+
inventoryCall(state, calls[0]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function conditionalBlock(item) {
|
|
77
|
+
if (item.PLpgSQL_stmt_block?.exceptions) return true;
|
|
78
|
+
return Object.keys(item).some((key) => key.startsWith('PLpgSQL_stmt_')
|
|
79
|
+
&& !['PLpgSQL_stmt_block', 'PLpgSQL_stmt_perform', 'PLpgSQL_stmt_execsql'].includes(key)
|
|
80
|
+
&& !(key === 'PLpgSQL_stmt_return' && item[key].lineno === undefined));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inventoryDo(state, node) {
|
|
84
|
+
const option = (key) => (node.args ?? []).find((item) => item.DefElem?.defname === key)?.DefElem?.arg?.String?.sval;
|
|
85
|
+
if (option('language') && option('language') !== 'plpgsql') throw new DbVerbError('database plan requires statically readable migration blocks');
|
|
86
|
+
const body = option('as') ?? '';
|
|
87
|
+
let delimiter = '$starci_plan$';
|
|
88
|
+
while (body.includes(delimiter)) delimiter = `${delimiter.slice(0, -1)}_$`;
|
|
89
|
+
let tree;
|
|
90
|
+
try { tree = pg.parsePlPgSQLSync(`create function starci_plan_probe() returns void language plpgsql as ${delimiter}${body}${delimiter};`); }
|
|
91
|
+
catch { throw new DbVerbError('migration block could not be inventoried safely'); }
|
|
92
|
+
const statements = [];
|
|
93
|
+
let conditional = false;
|
|
94
|
+
walk(tree, (item) => {
|
|
95
|
+
conditional ||= conditionalBlock(item);
|
|
96
|
+
if (item.PLpgSQL_stmt_dynexecute) throw new DbVerbError('dynamic migration blocks cannot prove database inventory');
|
|
97
|
+
if (item.PLpgSQL_expr?.parseMode === 0) statements.push(parseSql(item.PLpgSQL_expr.query));
|
|
98
|
+
});
|
|
99
|
+
if (conditional && statements.some((statement) => ownershipCalls(statement).length)) throw new DbVerbError('conditional migration blocks cannot prove database inventory');
|
|
100
|
+
for (const statement of statements) inventoryTree(state, statement);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function quotedEnd(text, start) {
|
|
104
|
+
const quote = text[start];
|
|
105
|
+
const delimiter = text.startsWith(quote.repeat(3), start) ? quote.repeat(3) : quote;
|
|
106
|
+
let cursor = start + delimiter.length;
|
|
107
|
+
while (cursor < text.length) {
|
|
108
|
+
if (quote === '"' && text[cursor] === '\\') { cursor += 2; continue; }
|
|
109
|
+
if (text.startsWith(delimiter, cursor)) {
|
|
110
|
+
let end = cursor + delimiter.length;
|
|
111
|
+
if (delimiter.length === 3) while (text[end] === quote) end += 1;
|
|
112
|
+
return end;
|
|
113
|
+
}
|
|
114
|
+
cursor += 1;
|
|
115
|
+
}
|
|
116
|
+
throw new DbVerbError('Supabase config contains an unterminated string');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Advance over an assignment value without collecting, decoding or resolving its content.
|
|
120
|
+
function valueEnd(text, start) {
|
|
121
|
+
let cursor = start;
|
|
122
|
+
let depth = 0;
|
|
123
|
+
while (cursor < text.length) {
|
|
124
|
+
const character = text[cursor];
|
|
125
|
+
if (character === '"' || character === "'") { cursor = quotedEnd(text, cursor); continue; }
|
|
126
|
+
if (character === '#') {
|
|
127
|
+
cursor = text.indexOf('\n', cursor);
|
|
128
|
+
if (cursor < 0) return text.length;
|
|
129
|
+
if (depth === 0) return cursor + 1;
|
|
130
|
+
} else if (character === '[' || character === '{') depth += 1;
|
|
131
|
+
else if (character === ']' || character === '}') depth -= 1;
|
|
132
|
+
else if (character === '\n' && depth === 0) return cursor + 1;
|
|
133
|
+
cursor += 1;
|
|
134
|
+
}
|
|
135
|
+
return cursor;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function assignmentEnd(text, start) {
|
|
139
|
+
let cursor = start;
|
|
140
|
+
while (cursor < text.length && !['=', '\n', '#'].includes(text[cursor])) {
|
|
141
|
+
if (text[cursor] === '"' || text[cursor] === "'") cursor = quotedEnd(text, cursor);
|
|
142
|
+
else cursor += 1;
|
|
143
|
+
}
|
|
144
|
+
return cursor;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function vaultKeys(table, key) {
|
|
148
|
+
// The TOML parser sees only the table/key syntax and an invented empty value.
|
|
149
|
+
let metadata;
|
|
150
|
+
try { metadata = parseToml(`${table}\n${key} = ""`); }
|
|
151
|
+
catch { throw new DbVerbError('Supabase config keys could not be inventoried safely'); }
|
|
152
|
+
const vault = metadata.db?.vault ?? {};
|
|
153
|
+
if (typeof vault !== 'object' || Object.values(vault).some((value) => typeof value !== 'string')) throw new DbVerbError('Vault config requires individual named keys');
|
|
154
|
+
return Object.keys(vault);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function configToken(text, cursor, table) {
|
|
158
|
+
if (/\s/.test(text[cursor])) return { cursor: cursor + 1, table, keys: [] };
|
|
159
|
+
if (text[cursor] === '#' || text[cursor] === '[') {
|
|
160
|
+
const end = text.indexOf('\n', cursor);
|
|
161
|
+
const line = text.slice(cursor, end < 0 ? undefined : end);
|
|
162
|
+
return { cursor: end < 0 ? text.length : end + 1, table: text[cursor] === '[' ? line : table, keys: [] };
|
|
163
|
+
}
|
|
164
|
+
const end = assignmentEnd(text, cursor);
|
|
165
|
+
if (text[end] !== '=') throw new DbVerbError('Supabase config keys could not be inventoried safely');
|
|
166
|
+
return { cursor: valueEnd(text, end + 1), table, keys: vaultKeys(table, text.slice(cursor, end)) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function configVaultNames(root) {
|
|
170
|
+
const file = path.join(root, 'supabase/config.toml');
|
|
171
|
+
if (!fs.existsSync(file)) return new Map();
|
|
172
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
173
|
+
const configured = new Map();
|
|
174
|
+
let token = { cursor: 0, table: '' };
|
|
175
|
+
while (token.cursor < text.length) {
|
|
176
|
+
token = configToken(text, token.cursor, token.table);
|
|
177
|
+
for (const name of token.keys) {
|
|
178
|
+
if (configured.has(name)) throw new DbVerbError('Vault config names must be unique');
|
|
179
|
+
configured.set(name, null);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return configured;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function repositoryState(root) {
|
|
186
|
+
const migrations = dbSqlFiles(root);
|
|
187
|
+
const state = { migrations: new Map(migrations.map((entry) => [entry.stamp, null])), cron: new Map(), queues: new Map(), vault: configVaultNames(root) };
|
|
188
|
+
if (state.migrations.size !== migrations.length) throw new DbVerbError('active migrations must have unique versions');
|
|
189
|
+
for (const entry of migrations) {
|
|
190
|
+
const tree = parseSql(fs.readFileSync(path.join(root, entry.file), 'utf8'));
|
|
191
|
+
for (const statement of tree.stmts ?? []) {
|
|
192
|
+
// Creating a function does not execute its body. Inert SQL strings never declare remote state.
|
|
193
|
+
if (statement.stmt.CreateFunctionStmt) continue;
|
|
194
|
+
if (statement.stmt.DoStmt) inventoryDo(state, statement.stmt.DoStmt);
|
|
195
|
+
else inventoryTree(state, statement.stmt);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return state;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function queryRows(root, sql, local, ports) {
|
|
202
|
+
// The pinned CLI otherwise creates this cache in its linked-query finalizer.
|
|
203
|
+
// Existing metadata is sufficient; never inspect the cache or credential files.
|
|
204
|
+
if (!local && !ports.runSupabase && !ports.run && !fs.existsSync(path.join(root, 'supabase/.temp/linked-project.json'))) throw new DbVerbError('link the existing project before running a read-only database plan');
|
|
205
|
+
const output = runDbSupabase(root, ['db', 'query', sql, local ? '--local' : '--linked', '--output', 'json', '--agent', 'no'], ports);
|
|
206
|
+
let rows;
|
|
207
|
+
try { rows = JSON.parse(output); } catch { throw new DbVerbError('Supabase returned unreadable inventory; database output was withheld'); }
|
|
208
|
+
if (!Array.isArray(rows) || rows.some((row) => !row || typeof row !== 'object' || Array.isArray(row))) throw new DbVerbError('Supabase returned an invalid inventory; database output was withheld');
|
|
209
|
+
return rows;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function remoteState(root, local, ports) {
|
|
213
|
+
const rows = queryRows(root, PRESENCE_SQL, local, ports);
|
|
214
|
+
if (rows.length !== 1 || Object.keys(INVENTORIES).some((key) => typeof rows[0][key] !== 'boolean')) throw new DbVerbError('Supabase returned invalid inventory availability');
|
|
215
|
+
const state = {};
|
|
216
|
+
for (const [key, sql] of Object.entries(INVENTORIES)) state[key] = rows[0][key] ? queryRows(root, sql, local, ports) : [];
|
|
217
|
+
return state;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function compare(expected, actual, kind) {
|
|
221
|
+
const unmatched = new Set(expected.keys());
|
|
222
|
+
let unexpected = 0;
|
|
223
|
+
let changed = 0;
|
|
224
|
+
for (const row of actual) {
|
|
225
|
+
const key = kind === 'migrations' ? row.version : row.name;
|
|
226
|
+
if (key !== null && (typeof key !== 'string' || key.length === 0)) throw new DbVerbError('Supabase returned an invalid inventory identifier');
|
|
227
|
+
if (kind === 'cron' && (typeof row.schedule !== 'string' || !/^[a-f0-9]{32}$/.test(row.commandHash ?? '') || typeof row.active !== 'boolean')) throw new DbVerbError('Supabase returned invalid cron metadata');
|
|
228
|
+
if (!unmatched.delete(key)) unexpected += 1;
|
|
229
|
+
else if (kind === 'cron') {
|
|
230
|
+
const desired = expected.get(key);
|
|
231
|
+
if (desired.schedule !== row.schedule || desired.commandHash !== row.commandHash || desired.active !== row.active) changed += 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { expected: expected.size, remote: actual.length, missing: unmatched.size, unexpected, changed };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Numeric exit codes: 0 equal, 1 drift, 2 invalid/failed; all IO and CLI calls are injectable. */
|
|
238
|
+
export function dbPlanMain(argv, { cwd = invocationDir(), stdout = (text) => process.stdout.write(text), stderr = (text) => process.stderr.write(text), ...ports } = {}) {
|
|
239
|
+
try {
|
|
240
|
+
const opts = dbOptions(argv, ['cwd', 'local', 'json', 'apply']);
|
|
241
|
+
const root = dbAppRoot(cwd, opts);
|
|
242
|
+
const expected = repositoryState(root);
|
|
243
|
+
const actual = remoteState(root, opts.local, ports);
|
|
244
|
+
const summaries = Object.fromEntries(Object.keys(INVENTORIES).map((kind) => [kind, compare(expected[kind], actual[kind], kind)]));
|
|
245
|
+
const drift = Object.values(summaries).some((item) => item.missing || item.unexpected || item.changed);
|
|
246
|
+
// --apply is accepted for shared verb ergonomics; a plan never mutates history, queues or jobs.
|
|
247
|
+
if (opts.json) stdout(`${JSON.stringify({ readOnly: true, drift, ...summaries })}\n`);
|
|
248
|
+
else {
|
|
249
|
+
stdout(`read-only database plan: ${drift ? 'drift detected' : 'repo and database agree'}\n`);
|
|
250
|
+
for (const [kind, item] of Object.entries(summaries)) stdout(`${kind}: repo ${item.expected}, remote ${item.remote}, missing ${item.missing}, unexpected ${item.unexpected}, changed ${item.changed}\n`);
|
|
251
|
+
}
|
|
252
|
+
return drift ? 1 : 0;
|
|
253
|
+
} catch (error) { return dbFailure('plan', error, stderr); }
|
|
254
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// app db restamp: an explicit version map previews file renames and history repairs.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
5
|
+
import { invocationDir } from '../lib/roots.mjs';
|
|
6
|
+
import { migrationStamp } from './rules/database-migrations.mjs';
|
|
7
|
+
import { MIGRATIONS_DIR, MIGRATIONS_ARCHIVE_DIR } from './rules/database-constants.mjs';
|
|
8
|
+
import { DbVerbError, dbOptions, dbAppRoot, dbSqlFiles, runDbSupabase, repairArgs, dbFailure } from './db-baseline-verb.mjs';
|
|
9
|
+
|
|
10
|
+
/** YAML maps use quoted UTC stamps as keys and values (or a versions: mapping). */
|
|
11
|
+
function restampPlan(root, mapFile) {
|
|
12
|
+
if (!mapFile) throw new DbVerbError('restamp requires --map <restamp.yaml>');
|
|
13
|
+
let document;
|
|
14
|
+
try { document = parseYaml(fs.readFileSync(path.resolve(root, mapFile), 'utf8')); }
|
|
15
|
+
catch { throw new DbVerbError('restamp map must be readable YAML with unique version keys'); }
|
|
16
|
+
const mapping = document?.versions ?? document;
|
|
17
|
+
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping) || !Object.keys(mapping).length) throw new DbVerbError('restamp map must contain old-to-new version pairs');
|
|
18
|
+
const active = dbSqlFiles(root);
|
|
19
|
+
const archived = dbSqlFiles(root, MIGRATIONS_ARCHIVE_DIR);
|
|
20
|
+
const occupied = new Set([...active, ...archived].map((entry) => entry.stamp));
|
|
21
|
+
const targets = new Set();
|
|
22
|
+
const steps = Object.entries(mapping).map(([old, raw]) => {
|
|
23
|
+
const next = String(raw);
|
|
24
|
+
if (![old, next].every((stamp) => /^\d{14}$/.test(stamp) && migrationStamp(stamp) !== null) || old === next) throw new DbVerbError('restamp pairs require distinct valid UTC versions');
|
|
25
|
+
const sources = active.filter((entry) => entry.stamp === old);
|
|
26
|
+
if (sources.length !== 1) throw new DbVerbError('each restamp source must identify exactly one active migration; archives never change');
|
|
27
|
+
if (occupied.has(next) || targets.has(next)) throw new DbVerbError('restamp targets must be unique unoccupied versions');
|
|
28
|
+
targets.add(next);
|
|
29
|
+
const source = sources[0];
|
|
30
|
+
const name = source.name.replace(old, next);
|
|
31
|
+
return { old, next, source: source.file, target: `${MIGRATIONS_DIR}/${name}` };
|
|
32
|
+
});
|
|
33
|
+
const replacements = new Map(steps.map((step) => [step.old, step.next]));
|
|
34
|
+
const resulting = active.map((entry) => replacements.get(entry.stamp) ?? entry.stamp);
|
|
35
|
+
if (resulting.some((stamp, index) => index > 0 && stamp <= resulting[index - 1])) throw new DbVerbError('the map must cover every version needed to preserve migration order');
|
|
36
|
+
return steps.sort((left, right) => left.old < right.old ? -1 : 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The complete map is validated before any repair or file mutation. */
|
|
40
|
+
export function dbRestampMain(argv, { cwd = invocationDir(), stdout = (text) => process.stdout.write(text), stderr = (text) => process.stderr.write(text), ...ports } = {}) {
|
|
41
|
+
try {
|
|
42
|
+
const opts = dbOptions(argv, ['cwd', 'map', 'apply', 'local']);
|
|
43
|
+
const root = dbAppRoot(cwd, opts);
|
|
44
|
+
const steps = restampPlan(root, opts.map);
|
|
45
|
+
const repairs = steps.flatMap((step) => [repairArgs(step.old, 'reverted', opts.local), repairArgs(step.next, 'applied', opts.local)]);
|
|
46
|
+
stdout(`${opts.apply ? 'apply' : 'preview'} restamp ${steps.length} migrations\n`);
|
|
47
|
+
for (const step of steps) stdout(`${step.source} -> ${step.target}\n`);
|
|
48
|
+
for (const args of repairs) stdout(`supabase ${args.join(' ')}\n`);
|
|
49
|
+
if (!opts.apply) return 0;
|
|
50
|
+
// CLI failures never echo credentials and leave the local files intact for an explicit retry.
|
|
51
|
+
for (const args of repairs) runDbSupabase(root, args, ports);
|
|
52
|
+
for (const step of steps) {
|
|
53
|
+
const source = path.join(root, step.source);
|
|
54
|
+
const text = fs.readFileSync(source, 'utf8').replace(/^\s*--\s*starci:restamp\s+\d{14}\s+\d{14}\s*\r?\n/, '');
|
|
55
|
+
fs.writeFileSync(path.join(root, step.target), `-- starci:restamp ${step.old} ${step.next}\n${text}`, { flag: 'wx' });
|
|
56
|
+
fs.unlinkSync(source);
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
} catch (error) { return dbFailure('restamp', error, stderr); }
|
|
60
|
+
}
|
|
@@ -60,26 +60,62 @@ function connectionProblems(side, value) {
|
|
|
60
60
|
/** Whether a declared list is present but not a list of distinct entries that all satisfy `valid`. */
|
|
61
61
|
const isBadUniqueList = (list, valid) => list !== undefined && (!Array.isArray(list) || !list.every((entry) => valid(entry)) || new Set(list).size !== list.length);
|
|
62
62
|
|
|
63
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
64
|
+
|
|
65
|
+
function validExtraPackage(entry) {
|
|
66
|
+
return isPlainObject(entry) && Object.keys(entry).every(key => key === 'name' || key === 'reason')
|
|
67
|
+
&& typeof entry.name === 'string' && entry.name.length <= 214 && PACKAGE_NAME.test(entry.name)
|
|
68
|
+
&& typeof entry.reason === 'string' && entry.reason.trim().length > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extraPackageProblems(side, entries) {
|
|
72
|
+
if (entries === undefined) return [];
|
|
73
|
+
if (side !== 'fe') return ['extraPackages belong to the fe side'];
|
|
74
|
+
if (!Array.isArray(entries) || !entries.every(validExtraPackage)) return ['sides.fe.extraPackages must be a list of {name, reason} with a package name and a nonempty reason'];
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
function declarationSideProblems(side, value, names) {
|
|
64
79
|
const bad = [];
|
|
65
80
|
const at = `sides.${side}`;
|
|
66
81
|
if (!isPlainObject(value)) return [`${at} must be an object`];
|
|
67
|
-
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
82
|
+
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads', 'extraPackages'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
68
83
|
bad.push(...appShapeProblems(value.apps, at, side, names));
|
|
69
84
|
if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
70
85
|
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
71
86
|
if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
72
87
|
if (value.connections !== undefined) bad.push(...connectionProblems(side, value));
|
|
88
|
+
bad.push(...extraPackageProblems(side, value.extraPackages));
|
|
89
|
+
return bad;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Product vocabulary is a declaration, never a switch for a rule. */
|
|
93
|
+
function lexiconBlockProblems(lexicon) {
|
|
94
|
+
if (lexicon === undefined) return [];
|
|
95
|
+
if (!isPlainObject(lexicon)) return ['lexicon must be an object'];
|
|
96
|
+
const bad = [];
|
|
97
|
+
for (const key of Object.keys(lexicon)) if (key !== 'removed') bad.push(`lexicon has unknown key ${key}`);
|
|
98
|
+
if (isBadUniqueList(lexicon.removed, (word) => typeof word === 'string' && word.trim().length > 0)) bad.push('lexicon.removed must be a unique list of nonempty words');
|
|
99
|
+
return bad;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Front-end error reporting remains optional in both editions. */
|
|
103
|
+
function observeBlockProblems(observe) {
|
|
104
|
+
if (observe === undefined) return [];
|
|
105
|
+
if (!isPlainObject(observe)) return ['observe must be an object'];
|
|
106
|
+
const bad = [];
|
|
107
|
+
for (const key of Object.keys(observe)) if (key !== 'reporter') bad.push(`observe has unknown key ${key}`);
|
|
108
|
+
if (observe.reporter !== undefined && (typeof observe.reporter !== 'string' || !observe.reporter.trim())) bad.push('observe.reporter must name the optional error reporter');
|
|
73
109
|
return bad;
|
|
74
110
|
}
|
|
75
111
|
|
|
76
|
-
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser']);
|
|
112
|
+
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser', 'lexicon', 'observe']);
|
|
77
113
|
const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
|
|
78
114
|
|
|
79
115
|
function productDeclarationProblems(declaration, profiles) {
|
|
80
116
|
const bad = [];
|
|
81
117
|
for (const key of Object.keys(declaration)) if (!DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key}`);
|
|
82
|
-
bad.push(...supabaseBlockProblems(declaration.supabase));
|
|
118
|
+
bad.push(...supabaseBlockProblems(declaration.supabase), ...lexiconBlockProblems(declaration.lexicon), ...observeBlockProblems(declaration.observe));
|
|
83
119
|
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
84
120
|
if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
|
|
85
121
|
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|