@starci/hfs 4.7.32 → 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 +9 -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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { lsFiles } from '../../api/git/ls-files.mjs';
|
|
3
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
4
|
+
import { MIGRATIONS_DIR } from '../rules/database-constants.mjs';
|
|
5
|
+
import { databaseSqlStatements, databaseSqlTokens, isMigrationName, migrationStamp } from '../rules/database-migrations.mjs';
|
|
6
|
+
import { readText } from '../rules/read.mjs';
|
|
7
|
+
|
|
8
|
+
const word = (token, value) => token?.t === 'word' && token.v === value;
|
|
9
|
+
const identifier = (token) => ['word', 'ident'].includes(token?.t) && token.v === token.v.toLowerCase() ? token.v : null;
|
|
10
|
+
const OPAQUE_STATEMENTS = new Set(['do', 'call', 'select', 'with', 'insert', 'update', 'delete', 'merge', 'truncate', 'execute', 'rollback', 'abort', 'prepare', 'discard', 'refresh', 'copy']);
|
|
11
|
+
const settingName = (token, value) => ['word', 'ident'].includes(token?.t) && token.v.toLowerCase() === value;
|
|
12
|
+
|
|
13
|
+
/** A migration relation is public only when its qualification or the current search path proves it. */
|
|
14
|
+
function relationAt(tokens, start, schema) {
|
|
15
|
+
const first = identifier(tokens[start]);
|
|
16
|
+
if (first === null) return null;
|
|
17
|
+
if (tokens[start + 1]?.v !== '.') return { schema, table: first, next: start + 1 };
|
|
18
|
+
const table = identifier(tokens[start + 2]);
|
|
19
|
+
if (table === null || tokens[start + 3]?.v === '.') return null;
|
|
20
|
+
return { schema: first, table, next: start + 3 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Split declarations at their own commas, keeping type arguments and constraint column lists together. */
|
|
24
|
+
function clausesOf(tokens) {
|
|
25
|
+
const clauses = [[]];
|
|
26
|
+
let depth = 0;
|
|
27
|
+
for (const token of tokens) {
|
|
28
|
+
if (token.v === '(') depth += 1;
|
|
29
|
+
if (token.v === ')') depth -= 1;
|
|
30
|
+
if (depth < 0) return null;
|
|
31
|
+
if (token.v === ',' && depth === 0) clauses.push([]);
|
|
32
|
+
else clauses.at(-1).push(token);
|
|
33
|
+
}
|
|
34
|
+
return depth === 0 ? clauses : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parenthesized(tokens, start) {
|
|
38
|
+
if (tokens[start]?.v !== '(') return null;
|
|
39
|
+
let depth = 1;
|
|
40
|
+
for (let index = start + 1; index < tokens.length; index += 1) {
|
|
41
|
+
if (tokens[index].v === '(') depth += 1;
|
|
42
|
+
if (tokens[index].v === ')') depth -= 1;
|
|
43
|
+
if (depth === 0) return { body: tokens.slice(start + 1, index), next: index + 1 };
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isDeferred = (tokens) => tokens.some((token, index) => word(token, 'deferrable') && !word(tokens[index - 1], 'not'));
|
|
49
|
+
|
|
50
|
+
/** A declared key proves only its complete, plain column list; index expressions are never column names. */
|
|
51
|
+
function tableKey(tokens) {
|
|
52
|
+
if (isDeferred(tokens)) return null;
|
|
53
|
+
let start = word(tokens[0], 'constraint') ? 2 : 0;
|
|
54
|
+
if (word(tokens[start], 'primary') && word(tokens[start + 1], 'key')) start += 2;
|
|
55
|
+
else if (word(tokens[start], 'unique')) start += 1;
|
|
56
|
+
else return null;
|
|
57
|
+
const enclosed = parenthesized(tokens, start);
|
|
58
|
+
const clauses = enclosed && clausesOf(enclosed.body);
|
|
59
|
+
if (!clauses?.length || clauses.some((clause) => clause.length !== 1 || identifier(clause[0]) === null)) return null;
|
|
60
|
+
return clauses.map((clause) => identifier(clause[0]));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Inline key clauses are read at column depth, outside defaults, generated expressions and type parameters. */
|
|
64
|
+
function columnKey(tokens) {
|
|
65
|
+
const column = identifier(tokens[0]);
|
|
66
|
+
if (column === null || isDeferred(tokens)) return null;
|
|
67
|
+
let depth = 0;
|
|
68
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
69
|
+
if (tokens[index].v === '(') depth += 1;
|
|
70
|
+
if (tokens[index].v === ')') depth -= 1;
|
|
71
|
+
if (depth === 0 && (word(tokens[index], 'unique') || (word(tokens[index], 'primary') && word(tokens[index + 1], 'key')))) return [column];
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function declarationKey(tokens) {
|
|
77
|
+
const tableConstraint = ['constraint', 'primary', 'unique', 'check', 'foreign', 'exclude'].some((value) => word(tokens[0], value));
|
|
78
|
+
return tableConstraint ? tableKey(tokens) : columnKey(tokens);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function addKey(keys, columns) {
|
|
82
|
+
if (columns && !keys.some((key) => key.length === columns.length && key.every((column, index) => column === columns[index]))) keys.push(columns);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function clearKeys(state, uncertain = false) {
|
|
86
|
+
for (const table of state.tables.keys()) state.tables.set(table, []);
|
|
87
|
+
if (uncertain) state.safe.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function opaqueExecution(state) {
|
|
91
|
+
clearKeys(state, true);
|
|
92
|
+
state.schema = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function forgetRelation(state, relation) {
|
|
96
|
+
if (!relation || relation.schema === null) clearKeys(state, true);
|
|
97
|
+
else if (relation.schema === 'public') {
|
|
98
|
+
state.tables.set(relation.table, []);
|
|
99
|
+
state.safe.delete(relation.table);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Conditional table creation cannot establish what an unknown pre-existing table actually constrains. */
|
|
104
|
+
function createTable(state, tokens, start) {
|
|
105
|
+
const conditional = word(tokens[start], 'if') && word(tokens[start + 1], 'not') && word(tokens[start + 2], 'exists');
|
|
106
|
+
if (conditional) start += 3;
|
|
107
|
+
const relation = relationAt(tokens, start, state.schema);
|
|
108
|
+
if (!relation || relation.schema !== 'public') return;
|
|
109
|
+
if (conditional) {
|
|
110
|
+
if (!state.tables.has(relation.table)) state.tables.set(relation.table, []);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const enclosed = parenthesized(tokens, relation.next);
|
|
114
|
+
const clauses = enclosed && clausesOf(enclosed.body);
|
|
115
|
+
const keys = [];
|
|
116
|
+
state.safe.delete(relation.table);
|
|
117
|
+
state.tables.set(relation.table, keys);
|
|
118
|
+
if (!clauses) return;
|
|
119
|
+
state.safe.add(relation.table);
|
|
120
|
+
for (const clause of clauses) addKey(keys, declarationKey(clause));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function safeAlter(tokens) {
|
|
124
|
+
if (word(tokens[0], 'owner') || word(tokens[0], 'cluster')) return true;
|
|
125
|
+
const security = tokens.some((token) => word(token, 'security'));
|
|
126
|
+
if (security && ['enable', 'disable', 'force', 'no'].some((value) => word(tokens[0], value))) return true;
|
|
127
|
+
return (word(tokens[0], 'set') || word(tokens[0], 'reset')) && tokens[1]?.v === '(';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** ADD can establish a new key; drops, renames and unreadable alterations discard the affected proof. */
|
|
131
|
+
function alterClause(state, relation, tokens) {
|
|
132
|
+
if (tokens.some((token) => word(token, 'inherit') || word(token, 'partition'))) { clearKeys(state, true); return; }
|
|
133
|
+
if (word(tokens[0], 'validate') || tokens.some((token) => word(token, 'default') || word(token, 'check') || word(token, 'generated') || word(token, 'using'))) {
|
|
134
|
+
opaqueExecution(state);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (safeAlter(tokens)) return;
|
|
138
|
+
if (word(tokens[0], 'drop') && (word(tokens[1], 'column') || word(tokens[1], 'constraint'))) {
|
|
139
|
+
if (relation.schema === 'public') state.tables.set(relation.table, []);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!word(tokens[0], 'add')) { forgetRelation(state, relation); return; }
|
|
143
|
+
let start = word(tokens[1], 'column') ? 2 : 1;
|
|
144
|
+
if (word(tokens[start], 'if')) return;
|
|
145
|
+
if (relation.schema !== 'public' || !state.safe.has(relation.table)) return;
|
|
146
|
+
const keys = state.tables.get(relation.table) ?? [];
|
|
147
|
+
state.tables.set(relation.table, keys);
|
|
148
|
+
addKey(keys, declarationKey(tokens.slice(start)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function alterTable(state, tokens, start) {
|
|
152
|
+
if (word(tokens[start], 'if') && word(tokens[start + 1], 'exists')) start += 2;
|
|
153
|
+
if (word(tokens[start], 'only')) start += 1;
|
|
154
|
+
const relation = relationAt(tokens, start, state.schema);
|
|
155
|
+
if (!relation || relation.schema === null) { clearKeys(state, true); return; }
|
|
156
|
+
const clauses = clausesOf(tokens.slice(relation.next));
|
|
157
|
+
if (!clauses) { forgetRelation(state, relation); return; }
|
|
158
|
+
for (const clause of clauses) alterClause(state, relation, clause);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function dropTable(state, tokens, start) {
|
|
162
|
+
if (word(tokens[start], 'if') && word(tokens[start + 1], 'exists')) start += 2;
|
|
163
|
+
for (const clause of clausesOf(tokens.slice(start)) ?? []) forgetRelation(state, relationAt(clause, 0, state.schema));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function searchPath(state, tokens) {
|
|
167
|
+
const local = word(tokens[1], 'local');
|
|
168
|
+
const start = local || word(tokens[1], 'session') ? 2 : 1;
|
|
169
|
+
if (word(tokens[start], 'schema')) {
|
|
170
|
+
state.schema = !local && tokens[start + 1]?.t === 'string' ? tokens[start + 1].v : null;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!settingName(tokens[start], 'search_path')) {
|
|
174
|
+
if (tokens.some((token) => settingName(token, 'search_path')) || word(tokens[start], 'all')) state.schema = null;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (word(tokens[0], 'reset')) { state.schema = 'public'; return; }
|
|
178
|
+
if (!word(tokens[start + 1], 'to') && tokens[start + 1]?.v !== '=') { state.schema = null; return; }
|
|
179
|
+
const value = tokens[start + 2];
|
|
180
|
+
const declared = identifier(value) ?? (value?.t === 'string' ? value.v : null);
|
|
181
|
+
state.schema = local ? null : declared;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createObject(tokens) {
|
|
185
|
+
let start = 1;
|
|
186
|
+
if (word(tokens[start], 'or') && word(tokens[start + 1], 'replace')) start += 2;
|
|
187
|
+
if (word(tokens[start], 'global') || word(tokens[start], 'local')) start += 1;
|
|
188
|
+
const temporary = word(tokens[start], 'temp') || word(tokens[start], 'temporary');
|
|
189
|
+
if (temporary || word(tokens[start], 'unlogged')) start += 1;
|
|
190
|
+
if (word(tokens[start], 'unique')) start += 1;
|
|
191
|
+
return { start, temporary };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** CTAS, materialization and index building can call user code; event triggers also affect later DDL. */
|
|
195
|
+
function executingCreate(state, tokens, start) {
|
|
196
|
+
if (word(tokens[start], 'event') && word(tokens[start + 1], 'trigger')) {
|
|
197
|
+
state.tainted = true;
|
|
198
|
+
opaqueExecution(state);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (word(tokens[start], 'index') || (word(tokens[start], 'materialized') && word(tokens[start + 1], 'view'))) {
|
|
202
|
+
opaqueExecution(state);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (!word(tokens[start], 'table')) return false;
|
|
206
|
+
let depth = 0;
|
|
207
|
+
for (const token of tokens.slice(start + 1)) {
|
|
208
|
+
if (token.v === '(') depth += 1;
|
|
209
|
+
if (token.v === ')') depth -= 1;
|
|
210
|
+
if (depth === 0 && word(token, 'as')) { opaqueExecution(state); return true; }
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function applyCreate(state, tokens) {
|
|
216
|
+
// A private or temporary child also contributes rows to a public parent's ordinary SELECT.
|
|
217
|
+
if (tokens.some((token) => word(token, 'inherits'))) { clearKeys(state, true); return; }
|
|
218
|
+
const { start, temporary } = createObject(tokens);
|
|
219
|
+
if (executingCreate(state, tokens, start)) return;
|
|
220
|
+
if (temporary) { opaqueExecution(state); return; }
|
|
221
|
+
if (word(tokens[start], 'table')) createTable(state, tokens, start + 1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function applyTableCommand(state, tokens, command) {
|
|
225
|
+
if (word(tokens[1], 'table')) command(state, tokens, 2);
|
|
226
|
+
else clearKeys(state, true);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Reduce only unconditional schema declarations; executable bodies may remove facts but never manufacture them. */
|
|
230
|
+
function applyStatement(state, tokens) {
|
|
231
|
+
if (state.tainted) return;
|
|
232
|
+
if (word(tokens[0], 'set') || word(tokens[0], 'reset')) { searchPath(state, tokens); return; }
|
|
233
|
+
if (tokens[0]?.t === 'word' && OPAQUE_STATEMENTS.has(tokens[0].v)) { opaqueExecution(state); return; }
|
|
234
|
+
if (word(tokens[0], 'commit') && word(tokens[1], 'prepared')) { opaqueExecution(state); return; }
|
|
235
|
+
if (word(tokens[0], 'create')) { applyCreate(state, tokens); return; }
|
|
236
|
+
if (word(tokens[0], 'alter')) { applyTableCommand(state, tokens, alterTable); return; }
|
|
237
|
+
if (word(tokens[0], 'drop')) applyTableCommand(state, tokens, dropTable);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function activeMigrations(root) {
|
|
241
|
+
const listed = lsFiles(['-z', '--cached', '--exclude-standard', '--', MIGRATIONS_DIR], { dir: root, maxBuffer: 256 * 1024 * 1024 });
|
|
242
|
+
if (listed.error || listed.status !== 0) return null;
|
|
243
|
+
return listed.stdout.split('\0').filter((file) => path.posix.dirname(file) === MIGRATIONS_DIR
|
|
244
|
+
&& isMigrationName(path.posix.basename(file)) && migrationStamp(path.posix.basename(file).slice(0, 14)) !== null).sort(byCodeUnit);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Public-table key sets proven by ordered, tracked active migrations. Direct event triggers taint later DDL proof.
|
|
249
|
+
* Opaque bodies discard current facts; persistent schema machinery hidden inside them is outside static authority.
|
|
250
|
+
*/
|
|
251
|
+
export function supabaseUniqueSetsOf(appRoot) {
|
|
252
|
+
const root = path.resolve(appRoot);
|
|
253
|
+
const files = activeMigrations(root);
|
|
254
|
+
const state = { tables: new Map(), safe: new Set(), schema: 'public', tainted: false };
|
|
255
|
+
for (const file of files ?? []) {
|
|
256
|
+
const text = readText(root, file);
|
|
257
|
+
if (text === null) { clearKeys(state, true); state.schema = null; continue; }
|
|
258
|
+
for (const statement of databaseSqlStatements(databaseSqlTokens(text))) applyStatement(state, statement);
|
|
259
|
+
}
|
|
260
|
+
return state.tainted ? new Map() : state.tables;
|
|
261
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { machineKit } from './machine-ast.mjs';
|
|
3
|
+
import { propertyValue } from './function-spec-providers.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* R50 `transport-owner` (FE_TRANSPORT_OWNER), the machine half of the eslint transport rules. ESLint judges one file at a time by
|
|
@@ -14,14 +15,16 @@ import { machineKit } from './machine-ast.mjs';
|
|
|
14
15
|
* - two or more client folders, or two or more unions, in the repository (two apps each keeping one, or a package one plus an
|
|
15
16
|
* app one) are findings on every copy;
|
|
16
17
|
* - an app client or app union in a repository where more than one app calls the back end is a finding (it belongs to the package);
|
|
17
|
-
* - no file outside the client folder references the global `fetch`: a call, `globalThis.fetch`, `window.fetch`, or a
|
|
18
|
+
* - no file outside the client folder and the stream reader folder (`fe.transport.stream`, `modules/api/sse/clients/`) references the global `fetch`: a call, `globalThis.fetch`, `window.fetch`, or a
|
|
18
19
|
* reference passed as a value (`useSWR(key, fetch)`, `const send = fetch`), all of which a spelling-based rule misses; with no
|
|
19
20
|
* client at all every such reference is a finding that says the repository has no client;
|
|
20
21
|
* - axios is imported only by the api owner (modules/api, the api package) for its rest calls; every other HTTP library is a
|
|
21
22
|
* second transport wherever it is imported, by static import, `import()` or `require()`;
|
|
22
23
|
* - an `ApolloClient` is constructed only inside the client folder;
|
|
23
|
-
* - every GraphQL operation (`graphql/queries/query-*.ts`, `graphql/mutations/mutation-*.ts`) imports the client (or the api
|
|
24
|
+
* - every GraphQL operation (`graphql/queries/query-*.ts`, `graphql/mutations/mutation-*.ts`, `graphql/subscriptions/subscription-*.ts`) imports the client (or the api
|
|
24
25
|
* package that holds it), so an operation is a caller of the one transport and not a second one.
|
|
26
|
+
* - every stream operation (`sse/<area>/stream-*.ts`) imports the stream reader, so a server-sent-events stream is read by the
|
|
27
|
+
* transport client layer and not by a fetch of its own.
|
|
25
28
|
*/
|
|
26
29
|
export const TRANSPORT_OWNER_RULE_IDS = ['FE_TRANSPORT_OWNER'];
|
|
27
30
|
|
|
@@ -34,11 +37,14 @@ const APP_CLIENT_SLOT = 'fe.transport.client';
|
|
|
34
37
|
const APP_OUTCOME_SLOT = 'fe.transport.outcome';
|
|
35
38
|
const PACKAGE_API_SLOT = 'fe.package.api';
|
|
36
39
|
const CLIENT_SLOTS = new Set([APP_CLIENT_SLOT, 'fe.package.api.client']);
|
|
40
|
+
/** The stream reader (server-sent events) is the second half of the transport client layer: it may call `fetch`, and it is no second Apollo client folder. */
|
|
41
|
+
const STREAM_SLOT = 'fe.transport.stream';
|
|
37
42
|
/** The api owner: the slots whose files may import axios (the rest calls and the client's own links). */
|
|
38
43
|
const API_OWNER_SLOTS = new Set(['fe.modules.api', PACKAGE_API_SLOT, ...CLIENT_SLOTS]);
|
|
39
44
|
/** The slots that hold the GraphQL operations of an app or of the api package. */
|
|
40
45
|
const OPERATION_SLOTS = new Set(['fe.modules.api', PACKAGE_API_SLOT]);
|
|
41
|
-
const OPERATION_FILE = /^(?:query|mutation)-[^/]+\.ts$/u;
|
|
46
|
+
const OPERATION_FILE = /^(?:query|mutation|subscription)-[^/]+\.ts$/u;
|
|
47
|
+
const STREAM_OPERATION_FILE = /^stream-[^/]+\.ts$/u;
|
|
42
48
|
const NO_CLIENT = "the repository has no transport client. Create the one Apollo client folder (packages/<family>-api/src/graphql/clients/, or the only app's modules/api/graphql/clients/) and call the back end through it.";
|
|
43
49
|
|
|
44
50
|
/** True when `node` names the global fetch: no import, and no declaration outside a lib declaration file. */
|
|
@@ -108,11 +114,25 @@ function inspectLibraryImport({ ts, file, node, transport, report }) {
|
|
|
108
114
|
report(file, node, `${specifier.text} is a second HTTP transport in ${file.rel}; ${where}.`, { library: specifier.text });
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
/** True when a call of fetch declares, in its init object literal, the request header accept: "text/event-stream". */
|
|
118
|
+
function asksForEventStream(ts, node) {
|
|
119
|
+
const call = node.parent && ts.isCallExpression(node.parent) && node.parent.expression === node ? node.parent : null;
|
|
120
|
+
const init = call?.arguments[1];
|
|
121
|
+
if (!init || !ts.isObjectLiteralExpression(init)) return false;
|
|
122
|
+
const headers = propertyValue(ts, init, 'headers');
|
|
123
|
+
const accept = headers && ts.isObjectLiteralExpression(headers) ? propertyValue(ts, headers, 'accept') : null;
|
|
124
|
+
return Boolean(accept && ts.isStringLiteralLike(accept) && accept.text === 'text/event-stream');
|
|
125
|
+
}
|
|
126
|
+
|
|
111
127
|
function inspectTransportNode(scope, node) {
|
|
112
128
|
const { ts, kit, checker, file, isClient, owned, report } = scope;
|
|
113
129
|
const fetchUse = globalFetchUse(ts, kit, checker, node);
|
|
114
130
|
if (fetchUse !== null) {
|
|
115
131
|
if (isClient) return false;
|
|
132
|
+
if (file.slot === STREAM_SLOT) {
|
|
133
|
+
if (!asksForEventStream(ts, node)) report(file, node, `${file.rel} fetches something other than an event stream; the stream reader folder reads server-sent events only (accept: "text/event-stream"), every other request is an operation of modules/api run by Apollo or axios.`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
116
136
|
if (fetchUse === 'property') report(file, node, `${node.getText(file.sourceFile)} reaches the global fetch outside the transport client; ${owned}`);
|
|
117
137
|
else report(file, node, `fetch is used outside the transport client (called, aliased or passed as a value); ${owned}`);
|
|
118
138
|
return false;
|
|
@@ -131,6 +151,27 @@ function reachesClient(graph, file, clientRels) {
|
|
|
131
151
|
});
|
|
132
152
|
}
|
|
133
153
|
|
|
154
|
+
/** True for a stream operation of the app's api layer: `modules/api/sse/<area>/stream-<channel>.ts`. */
|
|
155
|
+
function isStreamOperation(file) {
|
|
156
|
+
return file.slot === 'fe.modules.api' && STREAM_OPERATION_FILE.test(path.posix.basename(file.rel)) && path.posix.basename(path.posix.dirname(path.posix.dirname(file.rel))) === 'sse';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** True when the stream operation imports a file of the stream reader folder. */
|
|
160
|
+
function reachesStreamReader(graph, file) {
|
|
161
|
+
return graph.edges.some(edge => edge.from === file.rel && edge.runtime && graph.files.get(edge.to)?.slot === STREAM_SLOT);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Only a stream operation (or the reader folder itself) imports the stream reader: a REST call, a GraphQL operation or a hook never does. */
|
|
165
|
+
function inspectReaderImporters({ files, graph, report }) {
|
|
166
|
+
const byRel = new Map(files.map(file => [file.rel, file]));
|
|
167
|
+
for (const edge of graph.edges) {
|
|
168
|
+
if (!edge.runtime || graph.files.get(edge.to)?.slot !== STREAM_SLOT) continue;
|
|
169
|
+
const from = byRel.get(edge.from);
|
|
170
|
+
if (!from || from.slot === STREAM_SLOT || isStreamOperation(from)) continue;
|
|
171
|
+
report(from, from.sourceFile, `${from.rel} imports the stream reader (modules/api/sse/clients); only a stream operation (sse/<area>/stream-<channel>.ts) reads a server-sent-events stream.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
134
175
|
function inspectTransportFiles({ files, graph, kit, ts, clientRels, transport, owned, report }) {
|
|
135
176
|
const appsWithFiles = new Set();
|
|
136
177
|
let operations = 0;
|
|
@@ -142,7 +183,12 @@ function inspectTransportFiles({ files, graph, kit, ts, clientRels, transport, o
|
|
|
142
183
|
operations += 1;
|
|
143
184
|
if (!reachesClient(graph, file, clientRels)) report(file, file.sourceFile, `${file.rel} is a GraphQL operation that does not import the transport client; an operation runs through the repository's one Apollo client, never through its own transport.`);
|
|
144
185
|
}
|
|
186
|
+
if (isStreamOperation(file)) {
|
|
187
|
+
operations += 1;
|
|
188
|
+
if (!reachesStreamReader(graph, file)) report(file, file.sourceFile, `${file.rel} is a stream operation that does not import the stream reader (modules/api/sse/clients); a stream operation runs on the transport client layer's reader, never on a fetch of its own.`);
|
|
189
|
+
}
|
|
145
190
|
}
|
|
191
|
+
inspectReaderImporters({ files, graph, report });
|
|
146
192
|
return { appsWithFiles, operations };
|
|
147
193
|
}
|
|
148
194
|
|
|
@@ -311,7 +311,7 @@ const packageIndexSource = workspace => {
|
|
|
311
311
|
};
|
|
312
312
|
|
|
313
313
|
/** The source entry a workspace import resolves to without a build: its export targets mapped back to source, `src/index.ts` for the package root. */
|
|
314
|
-
function workspaceSourceEntry(ts, workspace, specifier) {
|
|
314
|
+
export function workspaceSourceEntry(ts, workspace, specifier) {
|
|
315
315
|
if (!workspace.workspace || !workspace.name || !sameOrUnder(specifier, workspace.name)) return null;
|
|
316
316
|
const isRoot = specifier === workspace.name;
|
|
317
317
|
const candidates = isRoot && !workspace.exports ? manifestEntryTargets(workspace.manifest) : exportCandidates(workspace, specifier);
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
// declares a package knowledge/hfs/fe-stack.yaml forbids
|
|
28
28
|
// HFS_LITE_SECRET_CUSTODY (R220, rules/supabase-secrets.mjs) under edition lite: secret material or a forbidden custody path; the R06 scan itself runs for every edition
|
|
29
29
|
// HFS_EDITION_FORBIDDEN_PRESENT (R212, rules/edition.mjs) under edition lite: an e2e, contract, test-stack or browser script, or a
|
|
30
|
-
// dependency of
|
|
30
|
+
// dependency of infrastructure lite does not run (test containers, fault proxy, Kafka)
|
|
31
|
+
// HFS_BACKING_UNDECLARED, HFS_BACKING_UNUSED (R212, rules/edition.mjs) under edition lite: a client library or a pattern of a backing
|
|
32
|
+
// service hfs.json sides.be.backing does not declare or the edition does not admit, or a declared service no dependency consumes
|
|
31
33
|
// DB_MIGRATION_SHAPE ... DB_TYPES_DRIFT (R213-R216, rules/database.mjs, async: passed in as extraFindings) the Supabase migrations, policies, definer functions, buckets, config.toml and generated types
|
|
32
34
|
// BE_SPEC_PLACEMENT (R102, rules/spec-placement.mjs) a spec or test file outside the four test layers, scripts/ and tools/ included
|
|
33
35
|
// HFS_REPO_LOCAL_CHECK (R103, rules/repo-local-checks.mjs) a local eslint rule or plugin, a `check-*` script, a relative import in eslint.config
|
|
@@ -108,7 +110,7 @@ export const CHECK_CODES = Object.freeze([
|
|
|
108
110
|
'HFS_MANAGED_FILE_DRIFT', 'HFS_TOOL_CONFIG_LOCAL', 'HFS_RULE_OFF_WITHOUT_REPLACEMENT', 'HFS_TS_STRICT',
|
|
109
111
|
'HFS_PLAINTEXT_SECRET', 'HFS_STACKS_SHAPE', 'HFS_CI_MISSING_CANON', 'HFS_DEP_VERSION_SKEW', 'HFS_CONTRACT_SNAPSHOT_DRIFT', 'HFS_MCP_TOOLS_SNAPSHOT',
|
|
110
112
|
'BE_TEST_TOPOLOGY', 'BE_SPEC_PLACEMENT', 'HFS_REPO_LOCAL_CHECK', 'HFS_LINT_SUPPRESSION_FILE', 'HFS_BYPASS_MARKER', 'HFS_PROOF_COMMAND_FILE_MISSING', 'HFS_PEER_INTEGRATION_MISSING', 'BE_INTEGRATION_SPEC_MISSING', 'FE_GRAPHQL_CONTRACT', 'FE_NO_TESTS', 'HFS_MONO_WORKSPACES', 'HFS_MONO_FE_WORKSPACE', 'HFS_MONO_NEST_PROJECTS', 'HFS_MONO_WORKSPACE_DEP', 'BE_CLI_REQUIRED', 'FE_WIRE_GENERATED', 'FE_I18N_PLACEMENT', 'FE_I18N_CATALOG', 'HFS_SERVICE_PLACEMENT', 'HFS_IMAGE_UNPINNED', 'HFS_SERVICE_STACK_DECLARATION', 'HFS_EVENT_CONTRACT', 'BE_ASYNC_SPEC_MISSING', 'BE_SAGA_STEP_COMPENSATION', 'BE_SAGA_STATE_VERSIONED', 'BE_SAGA_EVENT_CONTRACT', 'BE_SAGA_CONSUMER_DEDUPE', 'BE_EVENT_CLASS_CONTRACT', 'BE_PATTERN_SPEC_MISSING', 'BE_CONTRACT_BREAKING', 'BE_KIND_DECLARATION', 'BE_KIND_EMPTY', 'HFS_DOCKER_BUILD_CONTEXT', 'HFS_DOCKER_STAGES', 'HFS_DOCKER_ENTRY', 'HFS_DOCKER_BASE_PIN', 'HFS_DOCKER_SECRETS', 'BE_SAGA_E2E_MISSING',
|
|
111
|
-
'FE_STACK_FORBIDDEN', 'HFS_EDITION_FORBIDDEN_PRESENT', 'HFS_LITE_SECRET_CUSTODY', 'HFS_STARCIWORK_LITE_FORBIDDEN',
|
|
113
|
+
'FE_STACK_FORBIDDEN', 'HFS_EDITION_FORBIDDEN_PRESENT', 'HFS_BACKING_UNDECLARED', 'HFS_BACKING_UNUSED', 'HFS_LITE_SECRET_CUSTODY', 'HFS_STARCIWORK_LITE_FORBIDDEN',
|
|
112
114
|
'DB_MIGRATION_SHAPE', 'DB_RLS_REQUIRED', 'DB_DYNAMIC_DDL', 'DB_POLICY_SHAPE', 'DB_DEFINER_SAFE', 'DB_STORAGE_POLICY', 'DB_CONFIG_POLICY', 'DB_TYPES_DRIFT',
|
|
113
115
|
'HFS_DOCKER_HEALTHCHECK_FAKE', 'DB_BE_ONLY_ACCESS', 'DB_REALTIME_UNLISTED', 'DB_APP_ROLE', 'DB_FORCE_RLS_DEFAULT',
|
|
114
116
|
'DB_POLICY_TAUTOLOGY', 'DB_VIEW_UNSAFE', 'DB_STORAGE_PREFIX', 'DB_DEFINER_REVOKE',
|
|
@@ -397,11 +399,11 @@ export function checkRepository({ repoRoot, root = skillRoot, fast = false, base
|
|
|
397
399
|
return withCheckProofs(result, { repoRoot, root, manifest, wiring, coverageFrom });
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
/** Optional CLI proofs run over the whole tracked tree
|
|
402
|
+
/** Optional CLI proofs run over the whole tracked tree. */
|
|
401
403
|
export function withCheckProofs(result, { repoRoot, root = skillRoot, manifest = loadSlotManifest({ root }), wiring = false, coverageFrom, edition } = {}) {
|
|
402
404
|
if ((!wiring && coverageFrom === undefined) || result.profile === null) return result;
|
|
403
405
|
const declared = readRepoDeclaration(manifest, repoRoot);
|
|
404
|
-
const repo = { ...declared, edition:
|
|
406
|
+
const repo = { ...declared, edition: edition ?? declared.edition };
|
|
405
407
|
if (repo.sides) repo.sides = Object.fromEntries(Object.entries(repo.sides).map(([side, declaration]) => [side, { ...declaration, edition: repo.edition }]));
|
|
406
408
|
const context = { repoRoot, repo, manifest, files: trackedFiles(repoRoot) };
|
|
407
409
|
const reports = {};
|
|
@@ -75,6 +75,33 @@ function addEntryPatterns(sonar, slot, entry, isRole) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Whether `text` fits `pattern`, where each `*` stands for any run of characters. */
|
|
79
|
+
function wildcardMatches(pattern, text) {
|
|
80
|
+
const parts = pattern.split('*');
|
|
81
|
+
if (!text.startsWith(parts[0])) return false;
|
|
82
|
+
let at = parts[0].length;
|
|
83
|
+
for (const part of parts.slice(1, -1)) {
|
|
84
|
+
const found = text.indexOf(part, at);
|
|
85
|
+
if (found < 0) return false;
|
|
86
|
+
at = found + part.length;
|
|
87
|
+
}
|
|
88
|
+
const last = parts.at(-1);
|
|
89
|
+
return parts.length === 1 ? text === pattern : text.length - last.length >= at && text.endsWith(last);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Whether glob segments `glob` match path-or-glob segments `entry` (`**` crosses directories, `*` stays inside one segment). */
|
|
93
|
+
function segmentsCover(glob, entry) {
|
|
94
|
+
if (glob.length === 0) return entry.length === 0;
|
|
95
|
+
const [head, ...rest] = glob;
|
|
96
|
+
if (head === '**') return Array.from({ length: entry.length + 1 }, (_, skipped) => skipped).some((skipped) => segmentsCover(rest, entry.slice(skipped)));
|
|
97
|
+
if (entry.length === 0 || entry[0].includes('**')) return false;
|
|
98
|
+
const matches = head.includes('*') ? wildcardMatches(head, entry[0]) : head === entry[0];
|
|
99
|
+
return matches && segmentsCover(rest, entry.slice(1));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The exclusion entries that no other entry already matches: a glob covered by a broader one adds nothing to the scope. */
|
|
103
|
+
export const withoutCovered = (entries) => entries.filter((entry) => !entries.some((other) => other !== entry && segmentsCover(other.split('/'), entry.split('/'))));
|
|
104
|
+
|
|
78
105
|
/** The directory a path pattern lives in (itself when it is one). */
|
|
79
106
|
const dirOf = (path) => (isDir(path) ? path : path.slice(0, path.lastIndexOf('/') + 1));
|
|
80
107
|
|
|
@@ -114,7 +141,7 @@ export function coverageScope(manifest, providers = []) {
|
|
|
114
141
|
addRequiredPatterns(sonar, required, suffixes);
|
|
115
142
|
sonar.add('fe/**');
|
|
116
143
|
noneMinimal.sort(byCodeUnit);
|
|
117
|
-
return { roles, roots, excludes, none: noneMinimal, sonar: [...sonar].sort(byCodeUnit), codecovPaths: roots.map((root) => `be/${root}**`) };
|
|
144
|
+
return { roles, roots, excludes, none: noneMinimal, sonar: withoutCovered([...sonar]).sort(byCodeUnit), codecovPaths: roots.map((root) => `be/${root}**`) };
|
|
118
145
|
}
|
|
119
146
|
|
|
120
147
|
/** The glob a root measures: every file of a logic role below it (`src/modules/domain/*\/**\/*.{service,policy}.ts`). */
|
|
@@ -7,8 +7,48 @@ import { migrationStamp } from './rules/database-migrations.mjs';
|
|
|
7
7
|
import { MIGRATIONS_DIR, MIGRATIONS_ARCHIVE_DIR } from './rules/database-constants.mjs';
|
|
8
8
|
import { DbVerbError, dbOptions, dbAppRoot, dbSqlFiles, runDbSupabase, repairArgs, dbFailure } from './db-baseline-verb.mjs';
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
function
|
|
10
|
+
/** The map value of one old version: a new version, or { file, to, applied } for the colliding-versions case. */
|
|
11
|
+
function mapEntry(old, raw) {
|
|
12
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return { old, file: null, to: String(raw), applied: undefined };
|
|
13
|
+
const known = Object.keys(raw).every((key) => ['file', 'to', 'applied'].includes(key));
|
|
14
|
+
if (!known || (raw.applied !== undefined && typeof raw.applied !== 'boolean') || (raw.file !== undefined && typeof raw.file !== 'string')) throw new DbVerbError('a restamp entry is a new version or { file, to, applied } with a string file and a boolean applied');
|
|
15
|
+
return { old, file: raw.file ?? null, to: raw.to === undefined ? null : String(raw.to), applied: raw.applied };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The first unoccupied version after `old`, one second at a time. */
|
|
19
|
+
function nextFreeVersion(old, occupied) {
|
|
20
|
+
let moment = migrationStamp(old);
|
|
21
|
+
for (;;) {
|
|
22
|
+
moment += 1000;
|
|
23
|
+
const candidate = new Date(moment).toISOString().replace(/\D/g, '').slice(0, 14);
|
|
24
|
+
if (!occupied.has(candidate)) return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The active migration that moves: the only file of its version, or the one an entry names among files that share it. */
|
|
29
|
+
function sourceOf(entry, active) {
|
|
30
|
+
const sharing = active.filter((file) => file.stamp === entry.old);
|
|
31
|
+
const named = entry.file === null ? sharing : sharing.filter((file) => [file.name, file.name.slice(15), file.name.slice(15).replace(/\.sql$/, '')].includes(entry.file));
|
|
32
|
+
if (named.length === 1 && (sharing.length === 1 || entry.file !== null)) return named[0];
|
|
33
|
+
if (sharing.length > 1) throw new DbVerbError(`version ${entry.old} is shared by ${sharing.map((file) => file.name).join(', ')}; name the file to move: "${entry.old}": { file: <name> }`);
|
|
34
|
+
throw new DbVerbError('each restamp source must identify exactly one active migration; archives never change');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether the history records the file that moves under the shared version `old` (SELECT only: supabase_migrations.schema_migrations keeps the name
|
|
39
|
+
* of the file that ran). No row means nothing ran; a row named for the other file means the one that stays ran. A history that cannot be read is
|
|
40
|
+
* not guessed: the entry has to say `applied`.
|
|
41
|
+
*/
|
|
42
|
+
function recordedAsApplied(root, source, old, local, ports) {
|
|
43
|
+
const unreadable = new DbVerbError(`version ${old} is shared by several migrations and the history could not be read; say which one ran with "${old}": { file: <name>, applied: true|false }`);
|
|
44
|
+
let rows;
|
|
45
|
+
try { rows = JSON.parse(runDbSupabase(root, ['db', 'query', `select name from supabase_migrations.schema_migrations where version = '${old}'`, local ? '--local' : '--linked', '--output', 'json', '--agent', 'no'], ports)); } catch { throw unreadable; }
|
|
46
|
+
if (!Array.isArray(rows) || (rows.length > 0 && typeof rows[0]?.name !== 'string')) throw unreadable;
|
|
47
|
+
return rows.length > 0 && rows[0].name === source.name.slice(15).replace(/\.sql$/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** YAML maps use quoted UTC stamps as keys and a new version, or { file, to?, applied? }, as values (or a versions: mapping). */
|
|
51
|
+
function restampPlan(root, mapFile, { local = false, ports = {} } = {}) {
|
|
12
52
|
if (!mapFile) throw new DbVerbError('restamp requires --map <restamp.yaml>');
|
|
13
53
|
let document;
|
|
14
54
|
try { document = parseYaml(fs.readFileSync(path.resolve(root, mapFile), 'utf8')); }
|
|
@@ -16,22 +56,22 @@ function restampPlan(root, mapFile) {
|
|
|
16
56
|
const mapping = document?.versions ?? document;
|
|
17
57
|
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
58
|
const active = dbSqlFiles(root);
|
|
19
|
-
const
|
|
20
|
-
const occupied = new Set([...active, ...archived].map((entry) => entry.stamp));
|
|
59
|
+
const occupied = new Set([...active, ...dbSqlFiles(root, MIGRATIONS_ARCHIVE_DIR)].map((entry) => entry.stamp));
|
|
21
60
|
const targets = new Set();
|
|
22
61
|
const steps = Object.entries(mapping).map(([old, raw]) => {
|
|
23
|
-
const
|
|
62
|
+
const entry = mapEntry(old, raw);
|
|
63
|
+
const source = sourceOf(entry, active);
|
|
64
|
+
const shared = active.filter((file) => file.stamp === old).length > 1;
|
|
65
|
+
const next = entry.to ?? nextFreeVersion(old, new Set([...occupied, ...targets]));
|
|
24
66
|
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
67
|
if (occupied.has(next) || targets.has(next)) throw new DbVerbError('restamp targets must be unique unoccupied versions');
|
|
28
68
|
targets.add(next);
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
return { old, next, source: source.file, target: `${MIGRATIONS_DIR}/${name}
|
|
69
|
+
// a version two files share ran for at most one of them: the history says which, unless the entry does
|
|
70
|
+
const applied = entry.applied ?? (shared ? recordedAsApplied(root, source, old, local, ports) : true);
|
|
71
|
+
return { old, next, source: source.file, target: `${MIGRATIONS_DIR}/${source.name.replace(old, next)}`, applied };
|
|
32
72
|
});
|
|
33
|
-
const
|
|
34
|
-
const resulting = active.map((entry) =>
|
|
73
|
+
const moved = new Map(steps.map((step) => [step.source, step.next]));
|
|
74
|
+
const resulting = active.map((entry) => moved.get(entry.file) ?? entry.stamp);
|
|
35
75
|
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
76
|
return steps.sort((left, right) => left.old < right.old ? -1 : 1);
|
|
37
77
|
}
|
|
@@ -41,8 +81,8 @@ export function dbRestampMain(argv, { cwd = invocationDir(), stdout = (text) =>
|
|
|
41
81
|
try {
|
|
42
82
|
const opts = dbOptions(argv, ['cwd', 'map', 'apply', 'local']);
|
|
43
83
|
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)]);
|
|
84
|
+
const steps = restampPlan(root, opts.map, { local: opts.local, ports });
|
|
85
|
+
const repairs = steps.filter((step) => step.applied).flatMap((step) => [repairArgs(step.old, 'reverted', opts.local), repairArgs(step.next, 'applied', opts.local)]);
|
|
46
86
|
stdout(`${opts.apply ? 'apply' : 'preview'} restamp ${steps.length} migrations\n`);
|
|
47
87
|
for (const step of steps) stdout(`${step.source} -> ${step.target}\n`);
|
|
48
88
|
for (const args of repairs) stdout(`supabase ${args.join(' ')}\n`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// declaration-shape.mjs - the dependency-free hfs.json shape validation used by the HFS slot loader. Semantic checks
|
|
2
2
|
// against a loaded manifest remain in slots.mjs; this module mirrors modules/schemas/hfs-repo.schema.yaml only.
|
|
3
3
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
4
|
-
import { APP_KIND, connectionShapeProblems, NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
|
|
4
|
+
import { APP_KIND, connectionShapeProblems, NAME, PACKAGE_NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
|
|
5
5
|
import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs';
|
|
6
6
|
import { byCodeUnit } from '../lib/list.mjs';
|
|
7
7
|
|
|
@@ -60,8 +60,6 @@ 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
63
|
function validExtraPackage(entry) {
|
|
66
64
|
return isPlainObject(entry) && Object.keys(entry).every(key => key === 'name' || key === 'reason')
|
|
67
65
|
&& typeof entry.name === 'string' && entry.name.length <= 214 && PACKAGE_NAME.test(entry.name)
|
|
@@ -79,9 +77,11 @@ function declarationSideProblems(side, value, names) {
|
|
|
79
77
|
const bad = [];
|
|
80
78
|
const at = `sides.${side}`;
|
|
81
79
|
if (!isPlainObject(value)) return [`${at} must be an object`];
|
|
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}`);
|
|
80
|
+
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads', 'extraPackages', 'backing'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
83
81
|
bad.push(...appShapeProblems(value.apps, at, side, names));
|
|
84
82
|
if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
83
|
+
if (value.backing !== undefined && side !== 'be') bad.push('backing belongs to the be side');
|
|
84
|
+
else if (isBadUniqueList(value.backing, (name) => NAME.test(String(name)))) bad.push(`${at}.backing must be a unique list of backing service names`);
|
|
85
85
|
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
86
86
|
if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
87
87
|
if (value.connections !== undefined) bad.push(...connectionProblems(side, value));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// declaration-slots.mjs - how an app declaration (hfs.json) switches on the opt-in slots of a side: by name (`optionalSlots`), by app kind
|
|
2
2
|
// (a slot with `appKind`) and by declared pattern (a slot with `pattern: <name>` is enabled by `sides.<side>.patterns: [<name>]`, one
|
|
3
|
-
// mechanism for
|
|
4
|
-
// slot is enabled for a repository.
|
|
3
|
+
// mechanism for pattern slots). A cross-cutting pattern can instead be owned by the side's canonical proof scenario catalog.
|
|
4
|
+
// slots.mjs asks this file to judge a side's lists and to say whether a slot is enabled for a repository.
|
|
5
|
+
import { judgedInEdition } from './edition-slots.mjs';
|
|
5
6
|
|
|
6
7
|
/** The problems of a side's `optionalSlots` and `patterns` against the manifest. */
|
|
7
8
|
export function optionalSlotProblems(manifest, side, s) {
|
|
@@ -13,7 +14,9 @@ export function optionalSlotProblems(manifest, side, s) {
|
|
|
13
14
|
else if (slot.appKind !== undefined) bad.push(`sides.${side}.optionalSlots names ${id}; an app of kind ${slot.appKind} enables it`);
|
|
14
15
|
}
|
|
15
16
|
for (const name of s.patterns ?? []) {
|
|
16
|
-
|
|
17
|
+
const ownsSlot = manifest.slots.some((slot) => slot.profiles.includes(side) && slot.pattern === name);
|
|
18
|
+
const ownsProof = Object.hasOwn(manifest.ruleParams?.[side]?.patternScenarios ?? {}, name);
|
|
19
|
+
if (!ownsSlot && !ownsProof) bad.push(`sides.${side}.patterns names ${name}, which neither a ${side} slot nor its proof scenario catalog declares`);
|
|
17
20
|
}
|
|
18
21
|
if (s.kinds !== undefined && side !== 'be') bad.push(`sides.${side}.kinds belongs to the be side`);
|
|
19
22
|
for (const kind of s.kinds ?? []) {
|
|
@@ -22,6 +25,14 @@ export function optionalSlotProblems(manifest, side, s) {
|
|
|
22
25
|
return bad;
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
/** The problems of a side's `backing` list: every name is admitted by ruleParams.be.backing in this edition. */
|
|
29
|
+
export function backingProblems(manifest, side, s, edition = 'full') {
|
|
30
|
+
const services = Object.entries(manifest.ruleParams?.be?.backing ?? {})
|
|
31
|
+
.filter(([, entry]) => judgedInEdition(entry, edition)).map(([name]) => name);
|
|
32
|
+
return (s.backing ?? []).filter((name) => !services.includes(name))
|
|
33
|
+
.map((name) => `sides.${side}.backing names ${name}, which is not admitted in edition ${edition} by ruleParams.be.backing (${services.join(', ')})`);
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
/** The problems of the shape of a side's `patterns` list (a unique list of pattern names). */
|
|
26
37
|
export function patternShapeProblems(s, at, namePattern) {
|
|
27
38
|
const list = s.patterns;
|