@starci/hfs 4.7.31 → 4.7.33
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 +12 -0
- package/README.md +13 -6
- package/emit/contracts.mjs +4 -3
- package/emit/form-bounds-reader.ts.partial +58 -0
- package/emit/form-bounds.mjs +155 -0
- package/emit/graphql-input-bounds.mjs +97 -0
- package/emit/input-bounds.mjs +44 -0
- package/emit/operation-schemas.mjs +137 -0
- package/emit/operations-worker.mjs +2 -2
- package/emit/operations.mjs +52 -16
- package/emit/schema-worker.mjs +17 -4
- package/emit/type-schema.mjs +17 -5
- package/package.json +9 -2
- package/release/gate.mjs +18 -23
- package/release/index.mjs +1 -1
- package/release/push.mjs +4 -5
- package/runtime/engine/admission.mjs +331 -0
- package/runtime/engine/canonical-json.mjs +14 -0
- package/runtime/engine/db/blob.mjs +380 -0
- package/runtime/engine/db/event-compact.mjs +73 -0
- package/runtime/engine/db/ledger-open.mjs +38 -0
- package/runtime/engine/db/ledger-paths.mjs +87 -0
- package/runtime/engine/db/ledger.mjs +1068 -0
- package/runtime/engine/db/machine-connection.mjs +150 -0
- package/runtime/engine/db/machine-open.mjs +60 -0
- package/runtime/engine/db/machine-schema.mjs +105 -0
- package/runtime/engine/db/machine.mjs +1317 -0
- package/runtime/engine/db/provider-reservations.mjs +127 -0
- package/runtime/engine/db/provision-authority.mjs +76 -0
- package/runtime/engine/db/provision-lock.mjs +29 -0
- package/runtime/engine/db/provision-operations.mjs +146 -0
- package/runtime/engine/db/ref-value.mjs +67 -0
- package/runtime/engine/db/schema/machine.sql +920 -0
- package/runtime/engine/db/schema/runtime.sql +1113 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
- package/runtime/knowledge/hfs/design-truths.yaml +149 -0
- package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
- package/runtime/knowledge/hfs/rules.yaml +269 -48
- package/runtime/knowledge/hfs/slots.yaml +113 -53
- package/runtime/knowledge/patterns/be/api.yaml +46 -20
- package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
- package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
- package/runtime/knowledge/patterns/be/projections.yaml +2 -2
- package/runtime/knowledge/patterns/be/queues.yaml +57 -22
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
- package/runtime/knowledge/patterns/be/saga.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +249 -0
- package/runtime/modules/kernel/failure-codes.yaml +211 -52
- package/runtime/modules/kernel/hook-input.yaml +6 -0
- package/runtime/modules/kernel/protected-zone.yaml +81 -0
- package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
- package/runtime/modules/kernel/roles.yaml +405 -0
- package/runtime/modules/models/registry.yaml +3 -1
- package/runtime/modules/models/runtimes.yaml +2 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
- package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
- package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
- package/runtime/scripts/agent/codex-app-server.mjs +79 -0
- package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
- package/runtime/scripts/agent/host-agents.mjs +80 -0
- package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
- package/runtime/scripts/agent/trust-launch.mjs +191 -0
- package/runtime/scripts/agent/trust.mjs +523 -0
- package/runtime/scripts/api/fs/rename-over.mjs +21 -0
- package/runtime/scripts/api/fs/temp-path.mjs +8 -0
- package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
- package/runtime/scripts/api/git/diff-names.mjs +8 -0
- package/runtime/scripts/api/git/head-time.mjs +10 -0
- package/runtime/scripts/api/git/index-lock.mjs +78 -0
- package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
- package/runtime/scripts/api/git/merge-tree.mjs +5 -0
- package/runtime/scripts/api/git/rev-list.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse.mjs +13 -0
- package/runtime/scripts/api/git/status-query.mjs +5 -0
- package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
- package/runtime/scripts/api/npm/lib.mjs +32 -0
- package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
- package/runtime/scripts/api/npm/run-npm.mjs +6 -0
- package/runtime/scripts/api/npm/view.mjs +30 -0
- package/runtime/scripts/api/npm/whoami.mjs +8 -0
- package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
- package/runtime/scripts/api/process/kill-tree.mjs +22 -0
- package/runtime/scripts/api/process/lib.mjs +125 -0
- package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
- package/runtime/scripts/api/process/process-list.mjs +28 -0
- package/runtime/scripts/api/process/system-tool.mjs +37 -0
- package/runtime/scripts/gates/canon-digest.mjs +81 -0
- package/runtime/scripts/gates/canon-pins.mjs +16 -0
- package/runtime/scripts/gates/commit-proof.mjs +53 -0
- package/runtime/scripts/gates/gate-input.mjs +98 -0
- package/runtime/scripts/gates/gate.mjs +669 -0
- package/runtime/scripts/gates/grammar-dist.mjs +206 -0
- package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
- package/runtime/scripts/gates/test-world-run.mjs +254 -0
- package/runtime/scripts/gates/type-impact.mjs +277 -0
- package/runtime/scripts/gates/type-program.mjs +113 -0
- package/runtime/scripts/guards/assigned-command.mjs +12 -0
- package/runtime/scripts/guards/call-rights.mjs +47 -0
- package/runtime/scripts/guards/command-guard.mjs +468 -0
- package/runtime/scripts/guards/command-policy.mjs +342 -0
- package/runtime/scripts/guards/critic-reach.mjs +82 -0
- package/runtime/scripts/guards/deps-guard.mjs +150 -0
- package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
- package/runtime/scripts/guards/git-pathspec.mjs +225 -0
- package/runtime/scripts/guards/git-policy.mjs +355 -0
- package/runtime/scripts/guards/guards-root.mjs +27 -0
- package/runtime/scripts/guards/hook-install.mjs +343 -0
- package/runtime/scripts/guards/hook-io.mjs +76 -0
- package/runtime/scripts/guards/hook-verdict.mjs +12 -0
- package/runtime/scripts/guards/install-verdict.mjs +41 -0
- package/runtime/scripts/guards/kernel-seat.mjs +63 -0
- package/runtime/scripts/guards/launch-verdict.mjs +74 -0
- package/runtime/scripts/guards/op-starci.mjs +16 -0
- package/runtime/scripts/guards/op-write-scope.mjs +27 -0
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
- package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
- package/runtime/scripts/guards/protected-zone.mjs +56 -0
- package/runtime/scripts/guards/refusals.mjs +27 -0
- package/runtime/scripts/guards/release-definition.mjs +185 -0
- package/runtime/scripts/guards/release-push-gate.mjs +72 -0
- package/runtime/scripts/guards/release-record.mjs +58 -0
- package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
- package/runtime/scripts/guards/rights.mjs +268 -0
- package/runtime/scripts/guards/seat-tools.mjs +47 -0
- package/runtime/scripts/guards/shell-commands.mjs +328 -0
- package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
- package/runtime/scripts/guards/verdict-process.mjs +81 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
- package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +6 -4
- package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
- package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
- package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
- package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
- package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
- package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
- package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
- package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
- package/runtime/scripts/hfs/rules/edition.mjs +56 -12
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
- package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
- package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
- package/runtime/scripts/hfs/sealed-members.mjs +3 -3
- package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
- package/runtime/scripts/hfs/slots.mjs +2 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
- package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
- package/runtime/scripts/hfs/view.mjs +2 -1
- package/runtime/scripts/lib/app-data.mjs +11 -0
- package/runtime/scripts/lib/bypass-markers.mjs +272 -75
- package/runtime/scripts/lib/check-format.mjs +7 -0
- package/runtime/scripts/lib/cli-arg.mjs +120 -0
- package/runtime/scripts/lib/css-rules.mjs +220 -0
- package/runtime/scripts/lib/dom-tree.mjs +4 -0
- package/runtime/scripts/lib/escape.mjs +16 -0
- package/runtime/scripts/lib/event-items.mjs +22 -0
- package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
- package/runtime/scripts/lib/git-dir.mjs +120 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
- package/runtime/scripts/lib/guard-command.mjs +24 -0
- package/runtime/scripts/lib/hook-limits.mjs +5 -0
- package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
- package/runtime/scripts/lib/js-lex.mjs +200 -0
- package/runtime/scripts/lib/json-formats.mjs +0 -1
- package/runtime/scripts/lib/language.mjs +94 -1
- package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
- package/runtime/scripts/lib/loosening-config.mjs +361 -0
- package/runtime/scripts/lib/loosening-state.mjs +223 -0
- package/runtime/scripts/lib/menu-parts.mjs +11 -0
- package/runtime/scripts/lib/normalize.mjs +25 -0
- package/runtime/scripts/lib/op-gate.mjs +15 -0
- package/runtime/scripts/lib/redact.mjs +208 -0
- package/runtime/scripts/lib/runtime-location.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
- package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
- package/runtime/scripts/lib/seat-env.mjs +43 -0
- package/runtime/scripts/lib/secret-patterns.mjs +58 -9
- package/runtime/scripts/lib/shell-source.mjs +4 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/swarm-stack.mjs +2 -1
- package/runtime/scripts/lib/tar-files.mjs +39 -0
- package/runtime/scripts/lib/ts-analysis.mjs +10 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
- package/runtime/scripts/lib/worktree-record.mjs +14 -0
- package/runtime/scripts/machine/artifact-hold.mjs +52 -0
- package/runtime/scripts/machine/ask-receipts.mjs +113 -0
- package/runtime/scripts/machine/evidence-store.mjs +223 -0
- package/runtime/scripts/machine/home.mjs +216 -0
- package/runtime/scripts/machine/host-lock.mjs +184 -0
- package/runtime/scripts/machine/lock-recovery.mjs +55 -0
- package/runtime/scripts/machine/roles-contract.mjs +90 -0
- package/runtime/scripts/machine/runtime-change.mjs +20 -0
- package/runtime/scripts/machine/workflow-tree.mjs +122 -0
- package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
- package/runtime/scripts/reconciler/guard-row.mjs +25 -0
- package/runtime/scripts/work/asset-slot.mjs +284 -0
- package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
- package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
- package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
- package/runtime/scripts/work/brand/brand.mjs +892 -0
- package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
- package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
- package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
- package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
- package/runtime/scripts/work/draw/draw-source.mjs +412 -0
- package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
- package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
- package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
- package/runtime/scripts/work/draw-grammar.mjs +175 -0
- package/runtime/scripts/work/draw-render-page.mjs +188 -0
- package/runtime/scripts/work/draw-render.mjs +626 -0
- package/runtime/scripts/work/grammar-proposal.mjs +279 -0
- package/runtime/scripts/work/png.mjs +237 -0
- package/runtime/scripts/work/render-tools.mjs +64 -0
- package/runtime/scripts/work/work-io.mjs +119 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-plan.mjs +40 -13
- package/scaffold/add-queue-lite.mjs +372 -0
- package/scaffold/add.mjs +5 -2
- package/scaffold/app.mjs +1 -2
- package/scaffold/edition-gate.mjs +11 -7
- package/scaffold/lite-exports.mjs +10 -0
- package/scaffold/pattern-file-slots.mjs +33 -0
- package/scaffold/test-world.mjs +1 -1
- package/scaffold/yaml-block-edit.mjs +117 -0
- package/src/main.mjs +5 -4
- package/stack/deploy.mjs +7 -3
- package/stack/post-deploy.mjs +48 -0
- package/sync/index.mjs +10 -0
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/codegen-graphql.mjs.partial +59 -12
- package/templates/app/hooks/husky/pre-commit +2 -2
- package/templates/app/hooks/husky/pre-push +10 -3
- package/templates/app/package-scripts/redis-dev.json +3 -0
- package/templates/app/package-scripts-lite/package.json +1 -0
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
- package/templates/app/skeleton/README.md +2 -0
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/README.md +1 -1
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
- package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
- package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
- package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/inbox/platform/migration.sql.tpl +1 -0
- package/templates/be/inbox/service.spec.ts.tpl +4 -4
- package/templates/be/inbox/service.ts.tpl +3 -1
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
- package/templates/be/inbox/webhook-door.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
- package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
- package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
- package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
- package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
- package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
- package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
- package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
- package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
- package/upgrade/check.mjs +12 -135
- package/upgrade/file-state.mjs +1 -16
- package/upgrade/index.mjs +34 -361
- package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
- package/runtime/scripts/lib/gate-loosening.mjs +0 -292
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
- package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
- package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
|
@@ -8,6 +8,8 @@ import { stringList, byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
|
|
|
8
8
|
|
|
9
9
|
export const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
10
10
|
export const NAME = /^[a-z][a-z0-9-]*$/;
|
|
11
|
+
/** An npm package name, scoped or not. */
|
|
12
|
+
export const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
11
13
|
const ENV_PREFIX =/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/;
|
|
12
14
|
/** How a bounded context (a connection) is isolated: its own logical database, or its own schema of a shared one. */
|
|
13
15
|
const CONTEXT_ISOLATIONS = Object.freeze(["database", "schema"]);
|
|
@@ -66,14 +68,15 @@ export function unitRolesProblems(be) {
|
|
|
66
68
|
return [];
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
/** Shape problems of ruleParams.be.logicRoles and thinRoles: each a non-empty list of unique roles of the suffix vocabulary; every unit role is a logic role. */
|
|
71
|
+
/** Shape problems of ruleParams.be.logicRoles, valueRoles and thinRoles: each a non-empty list of unique roles of the suffix vocabulary; every unit role is a logic role. */
|
|
70
72
|
export function roleListProblems(be) {
|
|
71
73
|
const bad = [];
|
|
72
|
-
for (const key of ['logicRoles', 'thinRoles']) {
|
|
74
|
+
for (const key of ['logicRoles', 'valueRoles', 'thinRoles']) {
|
|
73
75
|
const list = be[key];
|
|
74
76
|
if (!strList(list) || !list.length || new Set(list).size !== list.length || !list.every((role) => NAME.test(role))) bad.push(`ruleParams.be.${key} must be a non-empty list of unique kebab-case roles`);
|
|
75
77
|
else if (Array.isArray(be.suffixes) && list.some((role) => !be.suffixes.includes(role))) bad.push(`ruleParams.be.${key} names a role that is not in ruleParams.be.suffixes`);
|
|
76
78
|
}
|
|
79
|
+
if (strList(be.valueRoles) && strList(be.logicRoles) && be.valueRoles.some((role) => !be.logicRoles.includes(role))) bad.push('every ruleParams.be.valueRoles role is a logic role (ruleParams.be.logicRoles)');
|
|
77
80
|
if (strList(be.logicRoles) && Array.isArray(be.unitRoles) && be.unitRoles.some((r) => r?.slot === undefined && !be.logicRoles.includes(r?.role))) bad.push('every ruleParams.be.unitRoles role that is not tied to a slot is a logic role (ruleParams.be.logicRoles)');
|
|
78
81
|
return bad;
|
|
79
82
|
}
|
|
@@ -359,13 +362,14 @@ function runtimeParamProblems(rp) {
|
|
|
359
362
|
const folderKindsValid = list => strList(list) && list.length > 0 && new Set(list).size === list.length && list.every(kind => NAME.test(kind));
|
|
360
363
|
const kindPairValid = pair => isPlainObject(pair) && Object.keys(pair).sort(byCodeUnit).join() === 'from,to'
|
|
361
364
|
&& ['from', 'to'].every(side => folderKindsValid(pair[side]));
|
|
362
|
-
|
|
363
|
-
|
|
365
|
+
/** A non-empty map whose every key is a name and every value satisfies `valid`. */
|
|
366
|
+
export const namedMapValid = (map, valid) => isPlainObject(map) && Object.keys(map).length > 0
|
|
367
|
+
&& Object.entries(map).every(([name, entry]) => NAME.test(name) && valid(entry));
|
|
368
|
+
const kindRestrictionValid = kinds => namedMapValid(kinds, kindPairValid);
|
|
364
369
|
const slotIdsValid = list => strList(list) && list.length > 0 && new Set(list).size === list.length
|
|
365
370
|
&& list.every(id => SLOT_ID.test(id) || RUNTIME_SLOT_ID.test(id));
|
|
366
371
|
const slotRestrictionPairValid = pair => isPlainObject(pair) && Object.keys(pair).join() === 'from' && slotIdsValid(pair.from);
|
|
367
|
-
const slotRestrictionValid = slots =>
|
|
368
|
-
&& Object.entries(slots).every(([target, pair]) => NAME.test(target) && slotRestrictionPairValid(pair));
|
|
372
|
+
const slotRestrictionValid = slots => namedMapValid(slots, slotRestrictionPairValid);
|
|
369
373
|
|
|
370
374
|
function tierDefinitionProblems(profile, tier, def) {
|
|
371
375
|
const bad = [];
|
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
5
5
|
import { paramNamesOk } from './param-names.mjs';
|
|
6
6
|
import { kindParamProblems, scenarioProblem } from './declaration-slots.mjs';
|
|
7
|
-
import { NAME, roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
|
|
7
|
+
import { NAME, namedMapValid, PACKAGE_NAME, roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
|
|
8
8
|
import { byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
|
|
9
|
+
import { ruleEditionProblems } from './edition-slots.mjs';
|
|
9
10
|
|
|
10
11
|
const PROFILES = ['be', 'fe'];
|
|
11
|
-
const BE_OWN = ['infraOwners', 'eventBus', 'specDoubles', 'paramNames', 'suffixes', 'bannedSuffixes', 'contractShape', 'unitRoles', 'logicRoles', 'thinRoles', 'patternScenarios', 'kindPatterns', 'addKinds'];
|
|
12
|
+
const BE_OWN = ['infraOwners', 'eventBus', 'specDoubles', 'paramNames', 'suffixes', 'bannedSuffixes', 'contractShape', 'unitRoles', 'logicRoles', 'valueRoles', 'thinRoles', 'patternScenarios', 'kindPatterns', 'addKinds', 'backing'];
|
|
13
|
+
const backingEntryOk = (entry) => isPlainObject(entry) && Object.keys(entry).every((key) => ['clients', 'images', 'patterns', 'editions'].includes(key))
|
|
14
|
+
&& nonemptyUniqueList(entry.clients, (name) => PACKAGE_NAME.test(String(name)))
|
|
15
|
+
&& Array.isArray(entry.patterns) && entry.patterns.every((name) => NAME.test(String(name))) && new Set(entry.patterns).size === entry.patterns.length
|
|
16
|
+
&& nonemptyUniqueList(entry.images, (name) => PACKAGE_NAME.test(String(name)))
|
|
17
|
+
&& ruleEditionProblems(entry, 'backing service').length === 0;
|
|
12
18
|
const FORM_NAMES = new Set(['call', 'new', 'curried', 'object', 'primitive', 'array']);
|
|
13
19
|
const blockOk = (v) => isPlainObject(v) && Number.isInteger(v.lines) && v.lines >= 2 && Number.isInteger(v.tokens) && v.tokens >= 1 && Object.keys(v).length === 2;
|
|
14
20
|
const fileLinesOk = (v) => isPlainObject(v) && Number.isInteger(v.soft) && v.soft >= 1 && typeof v.hardGrowth === 'boolean' && Object.keys(v).length === 2;
|
|
@@ -30,7 +36,12 @@ const formsOk = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => FORM_N
|
|
|
30
36
|
const doubleOk = (v) => isPlainObject(v) && /^[A-Za-z][A-Za-z0-9]*$/.test(String(v.double)) && formsOk(v.forms);
|
|
31
37
|
const regexOk = (v) => { try { return typeof v === 'string' && v.length > 0 && Boolean(new RegExp(v)); } catch { return false; } };
|
|
32
38
|
const dbExtensionsOk = (value) => Array.isArray(value) && value.every((name) => typeof name === 'string' && name.length > 0) && new Set(value).size === value.length;
|
|
33
|
-
const
|
|
39
|
+
const SQL_NAME = /^[a-z_][a-z0-9_]*$/;
|
|
40
|
+
const queueOutboxOk = (value) => isPlainObject(value) && Object.keys(value).sort(byCodeUnit).join() === 'columns,table'
|
|
41
|
+
&& typeof value.table === 'string' && value.table.split('.').length === 2 && value.table.split('.').every((part) => SQL_NAME.test(part))
|
|
42
|
+
&& nonemptyUniqueList(value.columns, (column) => SQL_NAME.test(String(column))) && value.columns.includes('queue');
|
|
43
|
+
const DB_PARAMS = { extensions: dbExtensionsOk, queueOutbox: queueOutboxOk };
|
|
44
|
+
const dbParamsOk = (db) => db === undefined || (isPlainObject(db) && Object.entries(db).every(([key, value]) => Object.hasOwn(DB_PARAMS, key) && DB_PARAMS[key](value)));
|
|
34
45
|
|
|
35
46
|
function beSpecificProblems(be, manifest) {
|
|
36
47
|
const bad = [];
|
|
@@ -42,11 +53,12 @@ function beSpecificProblems(be, manifest) {
|
|
|
42
53
|
const eventBus = be.eventBus;
|
|
43
54
|
if (!isPlainObject(eventBus) || Object.keys(eventBus).sort(byCodeUnit).join() !== 'classes,imports' || !memberMap(eventBus.imports, true) || !memberMap(eventBus.classes, false)) bad.push('ruleParams.be.eventBus must be {imports: {<module>: [unique PascalCase members]}, classes: {<module>: [non-empty unique PascalCase classes]}}');
|
|
44
55
|
if (!paramNamesOk(be.paramNames)) bad.push('ruleParams.be.paramNames must be a non-empty list of unique {type | typeSuffix, names, nameSuffix?} entries');
|
|
56
|
+
if (!namedMapValid(be.backing, backingEntryOk)) bad.push('ruleParams.be.backing must map a backing service name to {clients: [unique package names], patterns: [unique pattern names], images: [unique image repositories], editions?: [unique known editions]}');
|
|
45
57
|
if (!roleList(be.suffixes)) bad.push('ruleParams.be.suffixes must be a non-empty list of unique kebab-case role suffixes');
|
|
46
58
|
if (!roleList(be.bannedSuffixes)) bad.push('ruleParams.be.bannedSuffixes must be a non-empty list of unique kebab-case suffixes');
|
|
47
59
|
else if (roleList(be.suffixes) && be.suffixes.some((x) => be.bannedSuffixes.includes(x))) bad.push('ruleParams.be.suffixes and bannedSuffixes must be disjoint');
|
|
48
60
|
const sd = be.specDoubles;
|
|
49
|
-
if (!isPlainObject(sd) || Object.keys(sd).sort(byCodeUnit).join() !== 'doubles,fallback,kit' || typeof sd.kit !== 'string' || !sd.kit || !Array.isArray(sd.doubles) || !sd.doubles.length || !sd.doubles.every((e) => doubleOk(e) && regexOk(e.token) && Object.keys(e).length === 3) || !doubleOk(sd.fallback) || Object.keys(sd.fallback).length !== 2) bad.push('ruleParams.be.specDoubles must be {kit, doubles: [{token: regex, double, forms}], fallback: {double, forms}} with forms drawn from call, new, curried, object, primitive, array');
|
|
61
|
+
if (!isPlainObject(sd) || Object.keys(sd).sort(byCodeUnit).join() !== 'doubles,fallback,kit' || typeof sd.kit !== 'string' || !sd.kit || !Array.isArray(sd.doubles) || !sd.doubles.length || !sd.doubles.every((e) => doubleOk(e) && regexOk(e.token) && (e.from === undefined || regexOk(e.from)) && Object.keys(e).length === (e.from === undefined ? 3 : 4)) || !doubleOk(sd.fallback) || Object.keys(sd.fallback).length !== 2) bad.push('ruleParams.be.specDoubles must be {kit, doubles: [{token: regex, double, forms}], fallback: {double, forms}} with forms drawn from call, new, curried, object, primitive, array');
|
|
50
62
|
return bad;
|
|
51
63
|
}
|
|
52
64
|
|
|
@@ -59,10 +71,10 @@ export function ruleParamsProblems(m) {
|
|
|
59
71
|
// common holds the parameters both sides share; a side may restate one of them (a valid value) as an override, nothing else.
|
|
60
72
|
// Edition lite: a side's optional `lite` map overrides keys of the same side (never `lite` itself) when the app declares edition lite.
|
|
61
73
|
if (!sideOk(rp.common, []) || !fileLinesOk(rp.common.fileLines) || !blockOk(rp.common.duplicateBlock)) bad.push('ruleParams.common needs fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}; optional functionLines and featureFiles must be positive integers');
|
|
62
|
-
if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns and
|
|
74
|
+
if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns, addKinds and backing (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + ') and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
|
|
63
75
|
bad.push(...beSpecificProblems(rp.be, m));
|
|
64
76
|
if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite, SHARED_KEYS))) bad.push('ruleParams.fe may only restate the shared parameters of ruleParams.common, and carry a lite override of them');
|
|
65
|
-
if (!dbParamsOk(rp.db)) bad.push('ruleParams.db may contain only extensions
|
|
77
|
+
if (!dbParamsOk(rp.db)) bad.push('ruleParams.db may contain only extensions (a list of unique non-empty extension names) and queueOutbox ({table: <schema>.<table>, columns: [unique column names including queue]})');
|
|
66
78
|
}
|
|
67
79
|
return bad;
|
|
68
80
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// finders-import-begin
|
|
6
6
|
import { featureFileFindings } from './feature-files.mjs';
|
|
7
7
|
import { lexiconFindings } from './lexicon.mjs';
|
|
8
|
+
import { textIntegrityFindings } from './text-integrity.mjs';
|
|
8
9
|
import { assetsFindings } from './assets.mjs';
|
|
9
10
|
import { unusedSymbolFindings } from './unused-symbol.mjs';
|
|
10
11
|
import { databaseBaselineFindings } from './database-baseline.mjs';
|
|
@@ -41,11 +42,12 @@ import { feBuildArgsFindings } from './fe-build-args.mjs';
|
|
|
41
42
|
/** The failure codes the finders above emit, so `starci app check` ships their labels (check.mjs adds them to CHECK_CODES). Append-only; the coordinator regenerates it at merge time. */
|
|
42
43
|
export const A15_CODES = [
|
|
43
44
|
// codes-begin
|
|
45
|
+
'HFS_TEXT_CORRUPT',
|
|
44
46
|
'FE_PUBLIC_BUILD_ARG_MISSING',
|
|
45
47
|
'HFS_WORKFLOW_CONTEXT',
|
|
46
48
|
'CI_STANDARD',
|
|
47
49
|
'CI_SECRET_SEALED', 'APP_SECRET_IN_CI', 'CI_DECLARATION_DRIFT',
|
|
48
|
-
'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_API_EXPOSED', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_CRON_OWNED', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_SERVICE_ROLE_SCOPE', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
|
|
50
|
+
'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_API_EXPOSED', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_CRON_OWNED', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_SERVICE_ROLE_SCOPE', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_REDIS_PERSISTENCE', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
|
|
49
51
|
// codes-end
|
|
50
52
|
];
|
|
51
53
|
|
|
@@ -56,6 +58,7 @@ export const FINDERS = [
|
|
|
56
58
|
ciStandardAppFindings,
|
|
57
59
|
featureFileFindings,
|
|
58
60
|
lexiconFindings,
|
|
61
|
+
textIntegrityFindings,
|
|
59
62
|
assetsFindings,
|
|
60
63
|
unusedSymbolFindings,
|
|
61
64
|
databaseBaselineFindings,
|
|
@@ -4,21 +4,36 @@
|
|
|
4
4
|
// ts-nocheck directives), a Sonar exclusion comment, an istanbul / c8 / v8 coverage ignore;
|
|
5
5
|
// - a skipped, focused or todo test, or a skip option (a front end has no specs, but its source is still judged for the inline disables);
|
|
6
6
|
// - a failure swallowed in a hook, a script or a workflow: `|| true`, `continue-on-error`, `set +e`, `HUSKY=0`, `--no-verify`,
|
|
7
|
-
// `fail_ci_if_error: false
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// `fail_ci_if_error: false`, `--passWithNoTests`;
|
|
8
|
+
// - a gate configuration relaxed: a coverage target or threshold below the canon, a relaxing switch, a disabled ESLint rule, a strictness option
|
|
9
|
+
// turned off, an exception file the design-truths manifest does not list.
|
|
10
|
+
// The tree is read as it stands, by the library of the runtime's own loosening check (scripts/lib/loosening-state.mjs, knowledge/hfs/design-truths.yaml);
|
|
11
|
+
// `starci app hygiene` (the pre-commit hook) reads the staged bypass markers with the same table (scripts/lib/bypass-markers.mjs). The ignore and
|
|
12
|
+
// exclude lists of a managed config are the managed-file drift's (HFS_MANAGED_FILE_DRIFT).
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
10
15
|
import { found, readText } from './read.mjs';
|
|
11
16
|
import { bypassesIn } from '../../lib/bypass-markers.mjs';
|
|
17
|
+
import { findingLine, stateFindings } from '../../lib/loosening-state.mjs';
|
|
12
18
|
|
|
13
19
|
export const HFS_BYPASS_MARKER = 'HFS_BYPASS_MARKER';
|
|
20
|
+
const TRUTHS_FILE = new URL('../../../knowledge/hfs/design-truths.yaml', import.meta.url);
|
|
14
21
|
|
|
15
22
|
/** The finding message of one marker of `file`. */
|
|
16
23
|
export const bypassMessage = (file, hit) => `${file}:${hit.line} spells ${hit.label} (${hit.text.slice(0, 80)}); a check is satisfied, never bypassed: fix the cause and delete the marker`;
|
|
17
24
|
|
|
25
|
+
const markerFindings = ({ repoRoot, files }) => files.flatMap((file) => {
|
|
26
|
+
const text = readText(repoRoot, file);
|
|
27
|
+
return text === null ? [] : bypassesIn(file, text).map((hit) => found(HFS_BYPASS_MARKER, file, bypassMessage(file, hit), { line: hit.line }));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** The relaxed gate configuration of `files`: the library's findings other than the markers and the lists. */
|
|
31
|
+
const configFindings = ({ repoRoot, files }) => stateFindings({ files, read: (file) => readText(repoRoot, file), truths: parseYaml(fs.readFileSync(TRUTHS_FILE, 'utf8')), lists: false })
|
|
32
|
+
.filter((entry) => entry.kind !== 'bypass' && entry.kind !== 'truths')
|
|
33
|
+
.map((entry) => found(HFS_BYPASS_MARKER, entry.file, `${entry.file}:${entry.line} relaxes a gate (${findingLine(entry)}); a check is satisfied, never relaxed: restore the canon value`, { line: entry.line }));
|
|
34
|
+
|
|
18
35
|
/** The findings of R303 over the tracked paths `files` of the repository at `repoRoot`. */
|
|
19
36
|
export function bypassMarkerFindings({ repoRoot, files }) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return text === null ? [] : bypassesIn(file, text).map((hit) => found(HFS_BYPASS_MARKER, file, bypassMessage(file, hit), { line: hit.line }));
|
|
23
|
-
});
|
|
37
|
+
const markers = markerFindings({ repoRoot, files });
|
|
38
|
+
return markers.concat(configFindings({ repoRoot, files }));
|
|
24
39
|
}
|
|
@@ -11,6 +11,8 @@ import { DB_MIGRATION_DESTRUCTIVE, DB_MIGRATION_SHAPE, isActiveMigration, MIGRAT
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
14
|
+
import { isConstVariable } from '../../lib/ts-ast.mjs';
|
|
15
|
+
import { lexicalChecker, unwrapExpression } from '../../lib/ts-analysis.mjs';
|
|
14
16
|
|
|
15
17
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
18
|
const MIGRATION_PLAIN = /[a-z0-9]+(?:-[a-z0-9]+)*/;
|
|
@@ -332,10 +334,11 @@ function alteredColumn(table, column, line) {
|
|
|
332
334
|
|
|
333
335
|
function tableActionObject(body, index, table, line) {
|
|
334
336
|
if (sqlWord(body[index], 'drop')) {
|
|
337
|
+
const constraint = sqlWord(body[index + 1], 'constraint');
|
|
335
338
|
const at = skipWords(body, index + 1, ['column', 'constraint', 'if', 'exists']);
|
|
336
339
|
if (sqlWord(body[at], 'default') || sqlWord(body[at], 'not')) return null;
|
|
337
340
|
const column = nameAt(body, at);
|
|
338
|
-
return column ? alteredColumn(table, column, line) : null;
|
|
341
|
+
return column ? { ...alteredColumn(table, column, line), kind: constraint ? 'constraint' : 'column' } : null;
|
|
339
342
|
}
|
|
340
343
|
if (sqlWord(body[index], 'rename')) {
|
|
341
344
|
const column = sqlWord(body[index + 1], 'column') ? nameAt(body, index + 2) : null;
|
|
@@ -378,29 +381,81 @@ function destructiveObjects(statement) {
|
|
|
378
381
|
return objects;
|
|
379
382
|
}
|
|
380
383
|
|
|
381
|
-
|
|
382
|
-
|
|
384
|
+
/** Keep the static parts of one SQL expression together across interpolation and string concatenation. */
|
|
385
|
+
function sourceSqlText(ts, expression, checker, visiting = new Set()) {
|
|
386
|
+
const node = unwrapExpression(ts, expression);
|
|
387
|
+
if (ts.isStringLiteralLike(node)) return node.text;
|
|
388
|
+
if (ts.isTaggedTemplateExpression(node)) return sourceSqlText(ts, node.template, checker, visiting);
|
|
389
|
+
if (ts.isIdentifier(node)) {
|
|
390
|
+
const declaration = checker.getSymbolAtLocation(node)?.valueDeclaration;
|
|
391
|
+
if (!declaration || !ts.isVariableDeclaration(declaration) || !isConstVariable(ts, declaration)
|
|
392
|
+
|| !declaration.initializer || visiting.has(declaration)) return null;
|
|
393
|
+
return sourceSqlText(ts, declaration.initializer, checker, new Set([...visiting, declaration]));
|
|
394
|
+
}
|
|
395
|
+
if (ts.isTemplateExpression(node)) return node.head.text + node.templateSpans.map((span) =>
|
|
396
|
+
`${sourceSqlText(ts, span.expression, checker, visiting) ?? ' '}${span.literal.text}`).join('');
|
|
397
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
398
|
+
return `${sourceSqlText(ts, node.left, checker, visiting) ?? ' '}${sourceSqlText(ts, node.right, checker, visiting) ?? ' '}`;
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** SET CONSTRAINTS names have no table qualifier; every matching table's constraint is affected. */
|
|
404
|
+
function globalConstraintNames(statement) {
|
|
383
405
|
const names = new Set();
|
|
384
|
-
|
|
406
|
+
for (let index = 0; index < statement.length; index += 1) {
|
|
407
|
+
if (!sqlWord(statement[index], 'set') || !sqlWord(statement[index + 1], 'constraints')) continue;
|
|
408
|
+
let at = index + 2;
|
|
409
|
+
while (at < statement.length && !sqlWord(statement[at], 'all')) {
|
|
410
|
+
const constraint = nameAt(statement, at);
|
|
411
|
+
if (!constraint) break;
|
|
412
|
+
names.add(constraint.leaf);
|
|
413
|
+
if (statement[constraint.next]?.v !== ',') break;
|
|
414
|
+
at = constraint.next + 1;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return names;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function sourceNameScopes(ts, text, file) {
|
|
421
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
422
|
+
const checker = lexicalChecker(ts, source);
|
|
423
|
+
const scopes = [];
|
|
424
|
+
const entityNames = /\.entity\.[cm]?tsx?$/.test(file) ? new Set() : null;
|
|
425
|
+
const addSql = (sql) => {
|
|
426
|
+
for (const statement of databaseSqlStatements(databaseSqlTokens(sql))) {
|
|
427
|
+
const names = new Set(statement.filter((token) => sqlName(token) !== null).map((token) => token.v));
|
|
428
|
+
if (entityNames) for (const name of names) entityNames.add(name);
|
|
429
|
+
else {
|
|
430
|
+
const constraints = new Set(statement.flatMap((token, index) =>
|
|
431
|
+
sqlWord(token, 'constraint') && sqlName(statement[index + 1]) !== null ? [statement[index + 1].v] : []));
|
|
432
|
+
scopes.push({ names, constraints, globalConstraints: globalConstraintNames(statement) });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
385
436
|
const visit = (node) => {
|
|
386
|
-
|
|
387
|
-
if (
|
|
388
|
-
if (
|
|
437
|
+
const sql = sourceSqlText(ts, node, checker);
|
|
438
|
+
if (sql !== null) addSql(sql);
|
|
439
|
+
if (entityNames && ts.isIdentifier(node)) entityNames.add(node.text);
|
|
389
440
|
ts.forEachChild(node, visit);
|
|
390
441
|
};
|
|
391
442
|
visit(source);
|
|
392
|
-
return names;
|
|
443
|
+
return entityNames ? [{ names: entityNames, constraints: entityNames, globalConstraints: new Set() }] : scopes;
|
|
393
444
|
}
|
|
394
445
|
|
|
395
446
|
/**
|
|
396
447
|
* Whether a be/src source references a destroyed object. A column (`<schema>.<table>.<column>`) is referenced only by a source that
|
|
397
|
-
* names both its table and the column
|
|
398
|
-
*
|
|
448
|
+
* names both its table and the column in one SQL statement or entity, so separate queries cannot supply each other's names.
|
|
449
|
+
* SQL constraints require explicit constraint names, while entities retain their table/decorator association.
|
|
450
|
+
* Other object kinds match their leaf name conservatively.
|
|
399
451
|
*/
|
|
400
|
-
const referencesObject = (
|
|
452
|
+
const referencesObject = (scopes, object) => scopes.some(({ names, constraints, globalConstraints }) =>
|
|
453
|
+
(object.kind === 'constraint' && globalConstraints.has(object.leaf))
|
|
454
|
+
|| ((object.kind === 'constraint' ? constraints : names).has(object.leaf)
|
|
455
|
+
&& (!['column', 'constraint'].includes(object.kind) || !object.table || names.has(object.table))));
|
|
401
456
|
|
|
402
457
|
function destructiveObjectFinding(file, object, contract, references) {
|
|
403
|
-
const reference = references.find(({
|
|
458
|
+
const reference = references.find(({ scopes }) => referencesObject(scopes, object));
|
|
404
459
|
if (contract && !reference) return null;
|
|
405
460
|
const reason = reference && contract ? `${reference.file} still references this object; remove the reference before the contract migration`
|
|
406
461
|
: 'use a *_contract_* migration after the application stops using this object';
|
|
@@ -413,7 +468,7 @@ function destructiveFileFindings(file, text, references) {
|
|
|
413
468
|
const seen = new Set();
|
|
414
469
|
const objects = databaseSqlStatements(databaseExecutableTokens(text)).flatMap(destructiveObjects);
|
|
415
470
|
return objects.flatMap((object) => {
|
|
416
|
-
const key = `${object.line}:${object.name}`;
|
|
471
|
+
const key = `${object.line}:${object.kind}:${object.name}`;
|
|
417
472
|
if (seen.has(key)) return [];
|
|
418
473
|
seen.add(key);
|
|
419
474
|
const finding = destructiveObjectFinding(file, object, contract, references);
|
|
@@ -426,7 +481,7 @@ export function destructiveMigrationFindings({ repoRoot, migrations, files }) {
|
|
|
426
481
|
const sources = files.filter((file) => /^be\/src\/.+\.(?:sql|entity)\.[cm]?tsx?$/.test(file));
|
|
427
482
|
const ts = sources.length ? loadTypescript(repoRoot, HERE) : null;
|
|
428
483
|
if (sources.length && !ts) return [found(DB_MIGRATION_DESTRUCTIVE, 'hfs.json', 'destructive migration checks require the TypeScript parser to read be/src references')];
|
|
429
|
-
const references = sources.map((file) => ({ file,
|
|
484
|
+
const references = sources.map((file) => ({ file, scopes: sourceNameScopes(ts, readText(repoRoot, file) ?? '', file) }));
|
|
430
485
|
return migrations.flatMap((file) => {
|
|
431
486
|
const text = readText(repoRoot, file);
|
|
432
487
|
return text === null ? [] : destructiveFileFindings(file, text, references);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// TypeScript queue constants and scheduler references for migration parity.
|
|
1
|
+
// TypeScript queue constants, event-bus topic queue constants and scheduler references for migration parity.
|
|
2
2
|
import pg from 'libpg-query';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -10,6 +10,7 @@ import { DB_PGMQ_DECLARED } from './database-constants.mjs';
|
|
|
10
10
|
await pg.loadModule();
|
|
11
11
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const QUEUE_FILE = /^(?:be\/)?src\/modules\/queues\/.*\.ts$/;
|
|
13
|
+
const TOPIC_FILE = /^(?:be\/)?src\/modules\/platform\/event-bus\/.*\.ts$/;
|
|
13
14
|
const SOURCE_FILE = /^(?:be\/)?(?:src|apps)\/.*\.[cm]?[jt]s$/;
|
|
14
15
|
const TEST_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]s$|\/(?:tests|__tests__)\//;
|
|
15
16
|
const sqlTree = sqlParser(pg);
|
|
@@ -64,7 +65,7 @@ export function queueSourceFacts(state, files, inspectSql) {
|
|
|
64
65
|
const ts = loadTypescript(state.repoRoot, HERE);
|
|
65
66
|
const sources = new Map();
|
|
66
67
|
if (!ts) {
|
|
67
|
-
if (files.some((file) => QUEUE_FILE.test(file))) queueProblem(state, DB_PGMQ_DECLARED, 'hfs.json', 'queue declarations require the TypeScript parser');
|
|
68
|
+
if (files.some((file) => QUEUE_FILE.test(file) || TOPIC_FILE.test(file))) queueProblem(state, DB_PGMQ_DECLARED, 'hfs.json', 'queue declarations require the TypeScript parser');
|
|
68
69
|
return { ts, sources };
|
|
69
70
|
}
|
|
70
71
|
for (const file of files.filter((item) => SOURCE_FILE.test(item) && !TEST_FILE.test(item))) {
|
|
@@ -72,18 +73,19 @@ export function queueSourceFacts(state, files, inspectSql) {
|
|
|
72
73
|
if (text === null) continue;
|
|
73
74
|
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
74
75
|
sources.set(file, sourceBindings(ts, source));
|
|
75
|
-
if (QUEUE_FILE.test(file))
|
|
76
|
+
if (QUEUE_FILE.test(file)) addLiteralConstants(state, state.constants, ts, source, file, /(?:^QUEUE$|_QUEUE(?:_NAME)?$|^QUEUE_)/, 'queue names');
|
|
77
|
+
if (TOPIC_FILE.test(file)) addLiteralConstants(state, state.topics, ts, source, file, /_TOPIC_QUEUE$/, 'event-bus topic queue names');
|
|
76
78
|
addSourceSql(state, ts, source, file, inspectSql);
|
|
77
79
|
}
|
|
78
80
|
return { ts, sources };
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
function
|
|
83
|
+
function addLiteralConstants(state, into, ts, source, file, name, what) {
|
|
82
84
|
const visit = (node) => {
|
|
83
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) &&
|
|
85
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && name.test(node.name.text)) {
|
|
84
86
|
const value = sourceValue(ts, node.initializer);
|
|
85
|
-
if (value && ts.isStringLiteralLike(value))
|
|
86
|
-
else queueProblem(state, DB_PGMQ_DECLARED, file,
|
|
87
|
+
if (value && ts.isStringLiteralLike(value)) into.set(value.text, file);
|
|
88
|
+
else queueProblem(state, DB_PGMQ_DECLARED, file, `${what} must be literal constants so the migrations can be checked against them`);
|
|
87
89
|
}
|
|
88
90
|
ts.forEachChild(node, visit);
|
|
89
91
|
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
// R258: queue
|
|
1
|
+
// R258: the queue is BullMQ on Redis (ruling:R212/case-10), so the active migrations leave no pgmq queue for a queue constant, and
|
|
2
|
+
// every pgmq queue they leave is an event-bus topic queue named by a literal *_TOPIC_QUEUE constant under platform/event-bus. A
|
|
3
|
+
// pg_cron tick has one migration owner and only enqueues: it inserts one row of a queue constant into the queue outbox table of
|
|
4
|
+
// ruleParams.db.queueOutbox. A pgmq.send to a queue that existed when the job was scheduled is the historical form an older
|
|
5
|
+
// migration may still hold; the live job never enqueues that way.
|
|
2
6
|
// Initialize PostgreSQL's WASM parser at module load; the exported finder itself is synchronous.
|
|
3
7
|
// SQL and TypeScript ASTs keep comments, quoted identifiers and inert string literals out of the inventory.
|
|
4
8
|
import pg from 'libpg-query';
|
|
@@ -6,6 +10,7 @@ import { sqlStringParts as strings, walkSqlAst as walk, sqlParser, sqlCallArgume
|
|
|
6
10
|
import { queueSourceFacts, queueSchedulers, queueProblem as problem } from './database-queue-sources.mjs';
|
|
7
11
|
import { readJson, readText } from './read.mjs';
|
|
8
12
|
import { DB_CRON_OWNED, DB_PGMQ_DECLARED, isActiveMigration } from './database-constants.mjs';
|
|
13
|
+
import { loadSlotManifest } from '../slots.mjs';
|
|
9
14
|
|
|
10
15
|
await pg.loadModule();
|
|
11
16
|
|
|
@@ -192,6 +197,7 @@ function singleCall(tree) {
|
|
|
192
197
|
return select.targetList[0].ResTarget?.val?.FuncCall ?? null;
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
/** The historical pgmq.send of an existing queue; only a job's original body may hold it. */
|
|
195
201
|
function enqueue(state, call) {
|
|
196
202
|
if (callName(call) !== 'pgmq.send') return null;
|
|
197
203
|
const queue = sqlLiteral(argument(call, 0, ['queue_name']));
|
|
@@ -200,13 +206,52 @@ function enqueue(state, call) {
|
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
function pureArguments(call) {
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
return pureExpressions(call.args);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function pureExpressions(nodes) {
|
|
213
|
+
let safe = callsOf(nodes).every((nested) => uuidConstructor(nested) || PURE_CALLS.has(callName(nested).replace(/^pg_catalog\./, '')));
|
|
214
|
+
walk(nodes, (node) => {
|
|
205
215
|
if (['SubLink', 'SelectStmt', 'InsertStmt', 'UpdateStmt', 'DeleteStmt', 'MergeStmt'].some((key) => node[key])) safe = false;
|
|
206
216
|
});
|
|
207
217
|
return safe;
|
|
208
218
|
}
|
|
209
219
|
|
|
220
|
+
/** The queue outbox table and the columns an enqueue may write, from ruleParams.db.queueOutbox; null refuses every outbox enqueue. */
|
|
221
|
+
function queueOutbox(manifest) {
|
|
222
|
+
const declared = manifest?.ruleParams?.db?.queueOutbox;
|
|
223
|
+
return declared ? { table: declared.table, columns: new Set(declared.columns) } : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The queue constant one outbox row enqueues: `insert into <outbox> (queue, ...) values ('<queue constant>', ...)` with pure values. */
|
|
227
|
+
function outboxEnqueue(state, insert) {
|
|
228
|
+
if (!state.outbox || insert.returningClause || insert.returningList || insert.onConflictClause || insert.withClause) return null;
|
|
229
|
+
if (`${insert.relation?.schemaname || 'public'}.${insert.relation?.relname}` !== state.outbox.table) return null;
|
|
230
|
+
const columns = (insert.cols ?? []).map((item) => item.ResTarget?.name);
|
|
231
|
+
if (new Set(columns).size !== columns.length || columns.some((column) => !state.outbox.columns.has(column))) return null;
|
|
232
|
+
const rows = insert.selectStmt?.SelectStmt?.valuesLists ?? [];
|
|
233
|
+
const values = rows.length === 1 ? rows[0].List?.items ?? [] : [];
|
|
234
|
+
const at = columns.indexOf('queue');
|
|
235
|
+
if (at < 0 || values.length !== columns.length) return null;
|
|
236
|
+
const queue = sqlLiteral(values[at]);
|
|
237
|
+
return queue !== null && state.constants.has(queue) && pureExpressions(values) ? queue : null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The queue one statement enqueues to (an outbox row of a queue constant; a pgmq.send only when `legacy`), or null. */
|
|
241
|
+
function singleEnqueue(state, tree, legacy) {
|
|
242
|
+
const insert = tree?.stmts?.length === 1 ? tree.stmts[0].stmt?.InsertStmt : undefined;
|
|
243
|
+
if (insert) return outboxEnqueue(state, insert);
|
|
244
|
+
const call = singleCall(tree);
|
|
245
|
+
return legacy && call ? enqueue(state, call) : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** PostgreSQL's catalog-owned scalar UUID constructor adds an envelope identity without doing work in the tick. */
|
|
249
|
+
function uuidConstructor(call) {
|
|
250
|
+
return callName(call) === 'pg_catalog.gen_random_uuid' && (call.args?.length ?? 0) === 0
|
|
251
|
+
&& (call.agg_order?.length ?? 0) === 0 && !call.agg_filter && !call.over && !call.agg_star
|
|
252
|
+
&& !call.agg_distinct && !call.agg_within_group && !call.func_variadic;
|
|
253
|
+
}
|
|
254
|
+
|
|
210
255
|
function functionBody(fn) {
|
|
211
256
|
const option = (name) => (fn.options ?? []).find((item) => item.DefElem?.defname === name)?.DefElem?.arg;
|
|
212
257
|
return { body: strings(option('as')?.List?.items).join(''), language: option('language')?.String?.sval ?? 'sql' };
|
|
@@ -239,7 +284,7 @@ function plpgsqlTickTrees(fn, body) {
|
|
|
239
284
|
let safe = true;
|
|
240
285
|
walk(tree, (item) => {
|
|
241
286
|
for (const key of Object.keys(item)) {
|
|
242
|
-
if (key.startsWith('PLpgSQL_stmt_') && !['PLpgSQL_stmt_block', 'PLpgSQL_stmt_perform', 'PLpgSQL_stmt_return'].includes(key)) safe = false;
|
|
287
|
+
if (key.startsWith('PLpgSQL_stmt_') && !['PLpgSQL_stmt_block', 'PLpgSQL_stmt_perform', 'PLpgSQL_stmt_execsql', 'PLpgSQL_stmt_return'].includes(key)) safe = false;
|
|
243
288
|
if (key === 'PLpgSQL_expr') trees.push(sqlTree(item[key].query));
|
|
244
289
|
}
|
|
245
290
|
});
|
|
@@ -254,20 +299,22 @@ function tickTrees(fn) {
|
|
|
254
299
|
return sqlTree(body)?.stmts?.map((entry) => ({ stmts: [entry] })) ?? null;
|
|
255
300
|
}
|
|
256
301
|
|
|
257
|
-
function tickQueues(state, fn) {
|
|
302
|
+
function tickQueues(state, fn, legacy) {
|
|
258
303
|
const trees = tickTrees(fn.node);
|
|
259
304
|
if (!trees) return null;
|
|
260
|
-
const queues = trees.map((tree) =>
|
|
305
|
+
const queues = trees.map((tree) => singleEnqueue(state, tree, legacy));
|
|
261
306
|
return queues.length && queues.every(Boolean) ? queues : null;
|
|
262
307
|
}
|
|
263
308
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
309
|
+
/** What a cron command enqueues; `legacy` judges a job's original body, which may still pgmq.send to a queue that existed then. */
|
|
310
|
+
function scheduleTarget(state, command, legacy) {
|
|
311
|
+
const tree = sqlTree(command);
|
|
312
|
+
const target = singleCall(tree);
|
|
313
|
+
if (!target) return { tick: null, queues: singleEnqueue(state, tree, legacy) };
|
|
314
|
+
if (!callName(target).endsWith('_tick')) return { tick: null, queues: legacy ? enqueue(state, target) : null };
|
|
268
315
|
const tick = qualified(callName(target));
|
|
269
316
|
const fn = state.functions.get(tick);
|
|
270
|
-
const queues = fn && pureArguments(target) && tickQueues(state, fn);
|
|
317
|
+
const queues = fn && pureArguments(target) && tickQueues(state, fn, legacy);
|
|
271
318
|
return { tick, queues };
|
|
272
319
|
}
|
|
273
320
|
|
|
@@ -289,9 +336,9 @@ function scheduleCheck(state, schedule, names, schedulers, ticks) {
|
|
|
289
336
|
if (names.has(name)) problem(state, DB_CRON_OWNED, file, 'cron job names must be unique across active migrations');
|
|
290
337
|
names.add(name);
|
|
291
338
|
const historical = { ...state, created: schedule.created, functions: schedule.functions };
|
|
292
|
-
const { tick, queues } = scheduleTarget(historical, command);
|
|
293
|
-
if (!queues) problem(state, DB_CRON_OWNED, file, 'a cron command
|
|
294
|
-
if (state.liveSchedules.get(name) === schedule && !scheduleTarget(state, command).queues) problem(state, DB_CRON_OWNED, file, 'the live cron job targets a missing queue or tick, or its current tick no longer only
|
|
339
|
+
const { tick, queues } = scheduleTarget(historical, command, true);
|
|
340
|
+
if (!queues) problem(state, DB_CRON_OWNED, file, 'a cron command inserts one row of a queue constant into the queue outbox, directly or through one existing *_tick function that only does that; business DML, dynamic SQL and network calls are refused');
|
|
341
|
+
if (state.liveSchedules.get(name) === schedule && !scheduleTarget(state, command, false).queues) problem(state, DB_CRON_OWNED, file, 'the live cron job must insert one outbox row of a queue constant (ruling:R212/case-10): it targets a missing queue or tick, still enqueues with pgmq.send, or its current tick no longer only does that');
|
|
295
342
|
tickOwner(state, tick, file, ticks);
|
|
296
343
|
const owners = [name, ...(tick ? [tick, tick.split('.').at(-1)] : []), ...[queues ?? []].flat()];
|
|
297
344
|
if (owners.some((owner) => schedulers.has(owner))) problem(state, DB_CRON_OWNED, file, 'the tick is also declared in an application scheduler list; give it one owner');
|
|
@@ -304,20 +351,23 @@ function tickOwner(state, tick, file, ticks) {
|
|
|
304
351
|
ticks.add(tick);
|
|
305
352
|
}
|
|
306
353
|
|
|
307
|
-
function queueState({ repoRoot, files = [], all = files, repo = {} }) {
|
|
354
|
+
function queueState({ repoRoot, files = [], all = files, repo = {}, manifest }) {
|
|
308
355
|
const paths = [...new Set(all)];
|
|
309
356
|
const declaration = readJson(repoRoot, 'hfs.json') ?? repo;
|
|
310
357
|
const connected = ['be', 'fe'].some((side) => (declaration.sides?.[side]?.connections ?? []).some((item) => item.provider === 'supabase'));
|
|
311
358
|
if (repo.edition !== 'lite' && declaration.edition !== 'lite' && !declaration.supabase && !connected && !paths.some((file) => file.startsWith('supabase/'))) return null;
|
|
312
|
-
const state = { repoRoot, appRole: declaration.supabase?.appRole ?? 'app_be', findings: [], constants: new Map(), created: new Map(), functions: new Map(), schedules: [], activeCron: new Map(), liveSchedules: new Map(), unschedules: [], drops: [], grants: [], queueAcls: new Map() };
|
|
359
|
+
const state = { repoRoot, appRole: declaration.supabase?.appRole ?? 'app_be', findings: [], outbox: queueOutbox(manifest ?? loadSlotManifest()), constants: new Map(), topics: new Map(), created: new Map(), functions: new Map(), schedules: [], activeCron: new Map(), liveSchedules: new Map(), unschedules: [], drops: [], grants: [], queueAcls: new Map() };
|
|
313
360
|
state.sources = queueSourceFacts(state, paths, inspectCalls);
|
|
314
361
|
migrationFacts(state, paths);
|
|
315
362
|
return state;
|
|
316
363
|
}
|
|
317
364
|
|
|
318
365
|
function validateQueues(state, inScope) {
|
|
319
|
-
for (const [queue, file] of state.
|
|
320
|
-
|
|
366
|
+
for (const [queue, file] of state.created) {
|
|
367
|
+
if (state.constants.has(queue)) problem(state, DB_PGMQ_DECLARED, file, `pgmq queue ${queue} is the queue constant of ${state.constants.get(queue)}, and the queue is BullMQ on Redis (ruling:R212/case-10): drop it with pgmq.drop_queue in a later migration`);
|
|
368
|
+
else if (!state.topics.has(queue)) problem(state, DB_PGMQ_DECLARED, file, `pgmq queue ${queue} has no event-bus owner: name it in a literal *_TOPIC_QUEUE constant under platform/event-bus, or drop it with pgmq.drop_queue in a later migration`);
|
|
369
|
+
}
|
|
370
|
+
for (const [topic, file] of state.topics) if (!state.created.has(topic)) problem(state, DB_PGMQ_DECLARED, file, `event-bus topic queue ${topic} has no pgmq.create in an active migration`);
|
|
321
371
|
for (const { node, file, created, executed } of state.grants) grantCheck({ ...state, created: executed ? created : state.created }, node, file);
|
|
322
372
|
const names = new Set();
|
|
323
373
|
const ticks = new Set();
|