@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// supervisor-seat.mjs — what the Supervisor seat (a bound caller of role supervisor) may run, from the `supervisor` table of
|
|
2
|
+
// modules/kernel/command-policy.yaml. The PreToolUse guard and the PATH shims evaluate it (command-policy.mjs policyVerdict): the
|
|
3
|
+
// seat reads, answers the items of its menu with `starci supervisor decide`, records a runtime defect for Debug and runs the
|
|
4
|
+
// collectors it owns. Every refusal names the status and decision verbs without running them.
|
|
5
|
+
import { plainWords } from '../lib/menu-parts.mjs';
|
|
6
|
+
import { refusal } from './rights.mjs';
|
|
7
|
+
import { runtimeChangeRefusal, RUNTIME_CHANGE_CODE } from '../machine/runtime-change.mjs';
|
|
8
|
+
|
|
9
|
+
const OPTION = /^--?([A-Za-z][\w-]*)(?:=(.*))?$/;
|
|
10
|
+
|
|
11
|
+
/** The option names and the values of the options that carry one (`--item x` or `--item=x`) of a call. */
|
|
12
|
+
function optionsOf(args) {
|
|
13
|
+
const names = new Set();
|
|
14
|
+
const values = new Map();
|
|
15
|
+
args.map(String).forEach((word, index, all) => {
|
|
16
|
+
const hit = OPTION.exec(word);
|
|
17
|
+
if (!hit) return;
|
|
18
|
+
names.add(hit[1]);
|
|
19
|
+
const next = all[index + 1];
|
|
20
|
+
const value = hit[2] ?? (next?.startsWith('-') === false ? next : undefined);
|
|
21
|
+
if (value !== undefined) values.set(hit[1], value);
|
|
22
|
+
});
|
|
23
|
+
return { names, values };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The first three non-option words of a starci call: its group, verb and action. */
|
|
27
|
+
const listed = (table, group, verb) => (table?.verbs?.[group] ?? []).includes(verb);
|
|
28
|
+
|
|
29
|
+
/** The reason a listed verb is still refused for its action or options, or null. */
|
|
30
|
+
function narrowReason(table, { group, verb, action }, options) {
|
|
31
|
+
const actions = table.actions?.[verb];
|
|
32
|
+
if (actions?.includes(action) === false) return `starci ${[group, verb, action].filter(Boolean).join(' ')} is not an action of the Supervisor seat (${actions.join(', ')})`;
|
|
33
|
+
const blocked = (table['read-flags']?.[verb] ?? []).find((flag) => options.names.has(flag));
|
|
34
|
+
if (blocked) return `starci ${group} ${verb} --${blocked} changes state: the menu's decision verb does it`;
|
|
35
|
+
if (verb === 'actions' && action === 'record' && !new RegExp(table['record-items']).test(options.values.get('item') ?? '')) {
|
|
36
|
+
return 'starci supervisor actions record is allowed for a runtime defect only: --item runtime-defect:<cause>';
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Whether the seat may run `starci <group> <verb> [action]` with these options: {allowed, reason?}. */
|
|
42
|
+
function supervisorSeatAllows(table, words, args = []) {
|
|
43
|
+
const [group, verb, action] = words;
|
|
44
|
+
if (!listed(table, group, verb)) return { allowed: false, reason: `starci ${group} ${verb} is not a verb of the Supervisor seat` };
|
|
45
|
+
const reason = narrowReason(table, { group, verb, action }, optionsOf(args));
|
|
46
|
+
return reason ? { allowed: false, reason } : { allowed: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const useOf = (table) => table.use;
|
|
50
|
+
|
|
51
|
+
/** A runtime-change verb the seat tried (land, a fix worker): the Debug code, with the menu as the way on. */
|
|
52
|
+
function runtimeChangeVerdict({ table, words, text }) {
|
|
53
|
+
const change = runtimeChangeRefusal(words);
|
|
54
|
+
if (!change) return null;
|
|
55
|
+
const use = useOf(table);
|
|
56
|
+
return refusal(RUNTIME_CHANGE_CODE, text, change.reason, `${change.remedy}\n${use}`, use);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The refusal of a call the Supervisor seat may not make, or null when it may run it. A program other than starci passes only as a
|
|
61
|
+
* pure read or as a program whose read forms the general policy judges (orca lifecycle, git, npm); the rest is refused.
|
|
62
|
+
*/
|
|
63
|
+
export function supervisorSeatVerdict({ policy, program, args, text }) {
|
|
64
|
+
const table = policy?.supervisor;
|
|
65
|
+
if (!table) return null;
|
|
66
|
+
if (program === 'starci') {
|
|
67
|
+
const words = plainWords(args, 3);
|
|
68
|
+
const changed = runtimeChangeVerdict({ table, words, text });
|
|
69
|
+
if (changed) return changed;
|
|
70
|
+
const verdict = supervisorSeatAllows(table, words, args);
|
|
71
|
+
if (verdict.allowed) return null;
|
|
72
|
+
const use = useOf(table);
|
|
73
|
+
return refusal('SUPERVISOR_USE_DECIDE', text, `${verdict.reason}: the runtime runs it, and the Supervisor answers the items of its menu`, use, use);
|
|
74
|
+
}
|
|
75
|
+
const reads = new Set([...(policy.read ?? []), ...Object.keys(policy['guarded-read'] ?? {}), ...(table.defer ?? [])].map(String));
|
|
76
|
+
const denied = new Set((table['deny-programs'] ?? []).map(String));
|
|
77
|
+
if (reads.has(program) && !denied.has(program)) return null;
|
|
78
|
+
const use = useOf(table);
|
|
79
|
+
return refusal('SUPERVISOR_STARCI_ONLY', text, `the Supervisor seat runs starci verbs and pure reads: ${program} is neither`, use, use);
|
|
80
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { spawnNode } from '../api/node/spawn-node.mjs';
|
|
3
|
+
import { killTree } from '../api/process/kill-tree.mjs';
|
|
4
|
+
import { HOOK_INPUT_LIMITS } from './hook-io.mjs';
|
|
5
|
+
|
|
6
|
+
const entry = fileURLToPath(new URL('./command-guard.mjs', import.meta.url));
|
|
7
|
+
|
|
8
|
+
/** Request forced teardown of this call's verifier tree; a failed tree stop is reported after the direct child is stopped. */
|
|
9
|
+
function stopVerifier(child, teardownTimeoutMs) {
|
|
10
|
+
if (!Number.isInteger(child?.pid) || child.pid <= 0 || child.exitCode !== null) return;
|
|
11
|
+
let failure;
|
|
12
|
+
if (process.platform === 'win32') {
|
|
13
|
+
const stopped = killTree(child.pid, { timeoutMs: teardownTimeoutMs });
|
|
14
|
+
if (!stopped.ok) {
|
|
15
|
+
child.kill('SIGKILL');
|
|
16
|
+
const detail = stopped.output || `exit ${stopped.status}`;
|
|
17
|
+
failure = new Error(`verifier tree stop failed (${detail})`);
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
try { process.kill(-child.pid, 'SIGKILL'); }
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code !== 'ESRCH') {
|
|
23
|
+
child.kill('SIGKILL');
|
|
24
|
+
failure = error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (child.connected) child.disconnect();
|
|
29
|
+
child.unref();
|
|
30
|
+
if (failure) throw failure;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate a hook in an owned child so synchronous policy work cannot block its caller's deadline.
|
|
35
|
+
* The promise resolves only after the child sends a decision and exits; startup, IPC and deadline failures reject.
|
|
36
|
+
* A deadline requests bounded forced teardown of that child's tree and never accepts a late decision.
|
|
37
|
+
*/
|
|
38
|
+
export function runHookVerdict({ input, root, bindings, env = process.env, start = spawnNode,
|
|
39
|
+
timeoutMs = HOOK_INPUT_LIMITS.verdictTimeoutMs, teardownTimeoutMs = HOOK_INPUT_LIMITS.teardownTimeoutMs } = {}) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const deadline = performance.now() + timeoutMs;
|
|
42
|
+
let child;
|
|
43
|
+
let settled = false;
|
|
44
|
+
let response;
|
|
45
|
+
const finish = (error, decision) => {
|
|
46
|
+
if (settled) return;
|
|
47
|
+
// Event-loop delays can deliver close before the expired timer; elapsed time still owns admission.
|
|
48
|
+
if (!error && performance.now() >= deadline) error = new Error(`hook verdict deadline exceeded (${timeoutMs} ms)`);
|
|
49
|
+
settled = true;
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
if (error) {
|
|
52
|
+
try { stopVerifier(child, teardownTimeoutMs); }
|
|
53
|
+
catch (cleanupError) {
|
|
54
|
+
child?.channel?.unref();
|
|
55
|
+
child?.unref();
|
|
56
|
+
reject(new Error(`${error.message}; verifier cleanup failed (${cleanupError.message})`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
reject(error);
|
|
60
|
+
} else resolve(decision);
|
|
61
|
+
};
|
|
62
|
+
const timer = setTimeout(() => finish(new Error(`hook verdict deadline exceeded (${timeoutMs} ms)`)), timeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
child = start([entry], { env, detached: process.platform !== 'win32', stdio: ['ignore', 'ignore', 'ignore', 'ipc'] });
|
|
65
|
+
child.once('error', (error) => finish(error));
|
|
66
|
+
child.on('message', (message) => {
|
|
67
|
+
if (settled || response) return;
|
|
68
|
+
if (message?.ok !== true || !Object.hasOwn(message, 'decision')) {
|
|
69
|
+
finish(new Error(String(message?.error ?? 'hook verifier returned an invalid decision')));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
response = message;
|
|
73
|
+
});
|
|
74
|
+
child.once('close', (code, signal) => {
|
|
75
|
+
if (response && code === 0 && !signal) finish(null, response.decision);
|
|
76
|
+
else finish(new Error(`hook verifier exited before its decision (exit ${code}, signal ${signal ?? 'none'})`));
|
|
77
|
+
});
|
|
78
|
+
child.send({ input, root, bindings }, (error) => { if (error) finish(error); });
|
|
79
|
+
} catch (error) { finish(error); }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* HFS_UNUSED_EXPORT an export of an owner's public entry that no production file outside the owner imports is dead.
|
|
4
4
|
* HFS_UNUSED_FILE a production source file that no root reaches is dead. Roots: the files an app slot requires
|
|
5
5
|
* (main.ts, app.module.ts), every route file (slot tier `route`), the public entry of every package,
|
|
6
|
-
*
|
|
6
|
+
* the declared API app's registered operation contract consumed by app emit,
|
|
7
7
|
* and a source file a framework config of the app names by a relative string literal
|
|
8
8
|
* (next.config.ts pointing next-intl at its request config). Specs and tests are not in the graph,
|
|
9
9
|
* so a file only a spec imports is dead, on purpose. Type-only imports reach a file.
|
|
@@ -42,14 +42,13 @@ import { treeOf } from './required-files.mjs';
|
|
|
42
42
|
import { globExpression } from '../../lib/glob.mjs';
|
|
43
43
|
import { stagedConsumerSources } from './staged-consumers.mjs';
|
|
44
44
|
import { bindingsOf } from './dead-exports-bindings.mjs';
|
|
45
|
+
import { operationContractRoots } from './operation-contract-roots.mjs';
|
|
45
46
|
|
|
46
47
|
export const DEAD_EXPORT_RULE_IDS = ['HFS_UNUSED_EXPORT', 'HFS_UNUSED_FILE'];
|
|
47
48
|
|
|
48
49
|
const PACKAGE_ENTRIES = ['src/index.ts', 'src/index.tsx', 'index.ts', 'index.tsx'];
|
|
49
50
|
const OWNER_ENTRIES = ['index.ts', 'index.tsx'];
|
|
50
51
|
const CHAIN_DEPTH = 6;
|
|
51
|
-
/** The file of an api app that the contract emit reads by path for the typed operation table (BE-OPERATIONS-1): no import reaches it, the emit does. */
|
|
52
|
-
const OPERATION_TABLE_FILE = "operations.ts";
|
|
53
52
|
/** Tiers whose files are not production source that an app serves: judged by no dead-file rule. */
|
|
54
53
|
const UNJUDGED_TIERS = new Set(['none', 'e2e', 'fixtures']);
|
|
55
54
|
const CONFIG_EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '/index.ts', '/index.tsx', '/index.js'];
|
|
@@ -139,15 +138,15 @@ const addFrameworkConfigRoots = (graph, config, roots) => {
|
|
|
139
138
|
for (const app of config.apps ?? []) addAppConfigRoots(graph, config, app, roots);
|
|
140
139
|
};
|
|
141
140
|
|
|
142
|
-
/**
|
|
143
|
-
function rootFiles(graph, config) {
|
|
141
|
+
/** Roots are required app files, registered operation contracts, routes, package entries and config-named files. */
|
|
142
|
+
function rootFiles(graph, config, context) {
|
|
144
143
|
const roots = new Set();
|
|
145
144
|
for (const [rel, node] of graph.files) {
|
|
146
145
|
if (node.tier === 'route') { roots.add(rel); continue; }
|
|
147
146
|
if (!node.owner || node.tier !== 'app') continue;
|
|
148
147
|
const slot = graph.resolver.slot(node.owner.slot);
|
|
149
148
|
const inside = rel.slice(withoutSlash(node.owner.root).length + 1);
|
|
150
|
-
if ((slot?.requires ?? []).includes(inside)
|
|
149
|
+
if ((slot?.requires ?? []).includes(inside)) roots.add(rel);
|
|
151
150
|
}
|
|
152
151
|
for (const [key, owner] of graph.ownerRoots) {
|
|
153
152
|
if (owner.tier !== 'package') continue;
|
|
@@ -156,6 +155,7 @@ function rootFiles(graph, config) {
|
|
|
156
155
|
}
|
|
157
156
|
// A framework config of an app (slot fe.app.next) names sources by string, never by import.
|
|
158
157
|
addFrameworkConfigRoots(graph, config, roots);
|
|
158
|
+
for (const entry of operationContractRoots(context, graph, config)) roots.add(entry);
|
|
159
159
|
return roots;
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -163,12 +163,12 @@ const unusedFileViolation = (rel, node) => ({
|
|
|
163
163
|
ruleId: 'HFS_UNUSED_FILE',
|
|
164
164
|
path: rel, line: 1, column: 1,
|
|
165
165
|
slot: node.slot, owner: node.owner?.root ?? null,
|
|
166
|
-
message: `${rel} is not reached from any root (an app main.ts or app.module.ts, a route file, a package entry): nothing
|
|
166
|
+
message: `${rel} is not reached from any root (an app main.ts or app.module.ts, a registered operation contract, a route file, a package entry): nothing consumes it. Delete the file, or import it where it is used (a spec alone does not count).`,
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
/** HFS_UNUSED_FILE: production files no root reaches through runtime or type-only imports and re-exports. */
|
|
170
|
-
function deadFiles(graph, config) {
|
|
171
|
-
const roots = rootFiles(graph, config);
|
|
170
|
+
function deadFiles(graph, config, context) {
|
|
171
|
+
const roots = rootFiles(graph, config, context);
|
|
172
172
|
const forward = Map.groupBy(graph.edges, edge => edge.from);
|
|
173
173
|
const reached = new Set(roots);
|
|
174
174
|
const queue = [...roots];
|
|
@@ -355,7 +355,7 @@ export function checkDeadExports({ context, graph, config }) {
|
|
|
355
355
|
const edgesTo = edgeIndex([...graph.edges, ...testConsumerEdges({ context, graph, config })]);
|
|
356
356
|
const analysis = { ...context, graph, edgesTo, namespaceCheckers: new WeakMap(), namespaceUses: new WeakMap() };
|
|
357
357
|
const exports = ownerExportFindings(analysis, config);
|
|
358
|
-
const files = deadFiles(graph, config);
|
|
358
|
+
const files = deadFiles(graph, config, context);
|
|
359
359
|
const dead = exports.violations.length;
|
|
360
360
|
exports.violations.push(...files.violations);
|
|
361
361
|
return { violations: exports.violations, coverage: { status: 'checked', owners: exports.owners, exports: exports.exports, dead, files: files.judged, roots: files.roots, unusedFiles: files.violations.length } };
|
|
@@ -51,7 +51,7 @@ export function valueObjectSubjects(ts, sourceFile) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** The value of one literal object property. */
|
|
54
|
-
const propertyValue = (ts, object, key) => {
|
|
54
|
+
export const propertyValue = (ts, object, key) => {
|
|
55
55
|
const property = object.properties.find(item => keyOf(ts, item) === key);
|
|
56
56
|
return property && ts.isPropertyAssignment(property) ? property.initializer : null;
|
|
57
57
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The app emitter consumes the API app's declared operation table without importing it from app.module.ts. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { hasAnyFlag } from '../../lib/ts-ast.mjs';
|
|
4
|
+
|
|
5
|
+
const actualSymbol = (ts, checker, symbol) => symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
|
|
6
|
+
|
|
7
|
+
/** Follow declaration aliases while retaining the actual expression rather than an asserted type. */
|
|
8
|
+
const initializerOf = (ts, checker, node, seen = new Set()) => {
|
|
9
|
+
if (!node || seen.has(node)) return null;
|
|
10
|
+
seen.add(node);
|
|
11
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
|
|
12
|
+
|| ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) return initializerOf(ts, checker, node.expression, seen);
|
|
13
|
+
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return node;
|
|
14
|
+
const declaration = actualSymbol(ts, checker, checker.getSymbolAtLocation(node))?.valueDeclaration;
|
|
15
|
+
return declaration?.initializer ? initializerOf(ts, checker, declaration.initializer, seen) : node;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** A same-named local function cannot declare the platform operation contract. */
|
|
19
|
+
const isOperationDefiner = (ts, checker, graph, config, expression, seen = new Set()) => {
|
|
20
|
+
const symbol = actualSymbol(ts, checker, checker.getSymbolAtLocation(expression));
|
|
21
|
+
if (!symbol || seen.has(symbol)) return false;
|
|
22
|
+
seen.add(symbol);
|
|
23
|
+
return (symbol?.declarations ?? []).some(declaration => {
|
|
24
|
+
const relative = path.relative(config.root, declaration.getSourceFile().fileName).split(path.sep).join('/');
|
|
25
|
+
const owner = graph.resolver.ownerOf(relative);
|
|
26
|
+
if (declaration.name?.text === 'defineOperations' && owner?.slot === 'be.platform' && path.posix.basename(owner.root) === 'operations') return true;
|
|
27
|
+
const alias = declaration.initializer;
|
|
28
|
+
return Boolean(alias && (ts.isIdentifier(alias) || ts.isPropertyAccessExpression(alias)) && isOperationDefiner(ts, checker, graph, config, alias, seen));
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** True only for an exported, nonempty registration by the canon platform capability. */
|
|
33
|
+
const declaresOperations = (context, graph, config, sourceFile) => {
|
|
34
|
+
const { ts } = context;
|
|
35
|
+
const checker = context.checkerFor?.(sourceFile.fileName);
|
|
36
|
+
const moduleSymbol = checker?.getSymbolAtLocation(sourceFile);
|
|
37
|
+
if (!moduleSymbol) return false;
|
|
38
|
+
const exported = checker.getExportsOfModule(moduleSymbol).find(symbol => symbol.getName() === 'OPERATIONS');
|
|
39
|
+
const declaration = actualSymbol(ts, checker, exported)?.valueDeclaration;
|
|
40
|
+
const registration = initializerOf(ts, checker, declaration?.initializer);
|
|
41
|
+
if (!registration || !ts.isCallExpression(registration) || !isOperationDefiner(ts, checker, graph, config, registration.expression)) return false;
|
|
42
|
+
const entries = initializerOf(ts, checker, registration.arguments[0]);
|
|
43
|
+
return entries !== null && ts.isObjectLiteralExpression(entries) && checker.getPropertiesOfType(checker.getTypeAtLocation(entries)).length > 0;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Only the emitter's exact entry of a declared API app is a contract root; workers and arbitrary app files remain judged. */
|
|
47
|
+
export function operationContractRoots(context, graph, config) {
|
|
48
|
+
return (config.apps ?? []).filter(app => app.kind === 'api').flatMap(app => {
|
|
49
|
+
const relative = `apps/${app.name}/src/operations.ts`;
|
|
50
|
+
const node = graph.files.get(relative);
|
|
51
|
+
return node?.tier === 'app' && node.slot === 'be.app.api' && declaresOperations(context, graph, config, node.sourceFile) ? [relative] : [];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -13,7 +13,7 @@ import { byCodeUnit } from '../../lib/list.mjs';
|
|
|
13
13
|
|
|
14
14
|
export const REACHABILITY_RULE_IDS = ['BE_FEATURE_NOT_COMPOSED', 'FE_OWNER_REACHABLE', 'FE_HREF_RESOLVES'];
|
|
15
15
|
|
|
16
|
-
const BE_CAPABILITY_SLOTS = new Set(['be.domain', 'be.integrations', 'be.platform']);
|
|
16
|
+
const BE_CAPABILITY_SLOTS = new Set(['be.domain', 'be.integrations', 'be.platform', 'be.platform.logging']);
|
|
17
17
|
const TEST_FILE = /(?:\.(?:spec|test|stories|e2e-spec)\.[cm]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)/;
|
|
18
18
|
const ROUTE_FILE = /^(?:page|route)\.(?:tsx|ts|jsx|js)$/;
|
|
19
19
|
const PLACEHOLDER = '\u0000';
|
|
@@ -3,6 +3,7 @@ import { analyzeSql, HOLE } from './sql-tokens.mjs';
|
|
|
3
3
|
import { machineKit } from './machine-ast.mjs';
|
|
4
4
|
import { contextModelOf } from './context-map.mjs';
|
|
5
5
|
import { supabaseTablesOf } from './supabase-tables.mjs';
|
|
6
|
+
import { supabaseUniqueSetsOf } from './supabase-keys.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* R86 `sql-owner` (BE_SQL_TABLE_OWNER). Every `sql` tagged template (the tag declared in platform/database) of a
|
|
@@ -14,10 +15,10 @@ import { supabaseTablesOf } from './supabase-tables.mjs';
|
|
|
14
15
|
* to the table owner's public entry); a table of another context is refused (cross-context data comes only by events);
|
|
15
16
|
* - every table it names is declared by some entity under TypeORM authority, or by the generated
|
|
16
17
|
* `Database['public']['Tables']` contract under Supabase authority;
|
|
17
|
-
* - a SELECT carries LIMIT, or
|
|
18
|
-
* aggregates without GROUP BY.
|
|
18
|
+
* - a SELECT carries LIMIT, or reads a single relation under a complete primary/unique-key equality to scalar values,
|
|
19
|
+
* or selects only proven scalar aggregates without GROUP BY. Inferred bounds reject multiplying projections and set operations.
|
|
19
20
|
* Table positions that hold a `${...}` substitution (SqlIdent) are dynamic; they are counted in the coverage and judged by
|
|
20
|
-
* nobody.
|
|
21
|
+
* nobody. A join needs an explicit result bound because key constraints on its first relation do not bound joined rows.
|
|
21
22
|
*/
|
|
22
23
|
export const SQL_OWNER_RULE_IDS = ['BE_SQL_TABLE_OWNER'];
|
|
23
24
|
|
|
@@ -132,7 +133,8 @@ const capabilityTable = root => path.posix.basename(root).replaceAll('-', '_').t
|
|
|
132
133
|
/** Supabase declarations, with an owner when the generated table name matches a capability's persistence folder. */
|
|
133
134
|
function supabaseDeclarations(graph, appRoot, ts) {
|
|
134
135
|
const generated = supabaseTablesOf(appRoot, ts);
|
|
135
|
-
const
|
|
136
|
+
const keys = supabaseUniqueSetsOf(appRoot);
|
|
137
|
+
const tables = new Map([...generated].map(table => [table, { table, owner: null, rel: null, uniqueSets: keys.get(table) ?? [] }]));
|
|
136
138
|
for (const file of graph.files.values()) {
|
|
137
139
|
if (file.slot !== 'be.persistence' || !file.owner) continue;
|
|
138
140
|
const key = capabilityTable(file.owner.root);
|
|
@@ -142,6 +144,12 @@ function supabaseDeclarations(graph, appRoot, ts) {
|
|
|
142
144
|
return tables;
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/** A capability owns the table named for it and the tables that start with its name and an underscore (`realtime`, `realtime_events`). */
|
|
148
|
+
const isOwnTable = (table, ownTable) => {
|
|
149
|
+
const name = table.toLowerCase();
|
|
150
|
+
return name === ownTable || name.startsWith(`${ownTable}_`);
|
|
151
|
+
};
|
|
152
|
+
|
|
145
153
|
function reportSqlWrite({ tables, supabase, file, ownTable, report }, write) {
|
|
146
154
|
const declaration = tables.get(write.table.toLowerCase());
|
|
147
155
|
if (!declaration) {
|
|
@@ -149,7 +157,7 @@ function reportSqlWrite({ tables, supabase, file, ownTable, report }, write) {
|
|
|
149
157
|
? `SQL writes table ${write.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
|
|
150
158
|
: `SQL writes table ${write.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: write.table });
|
|
151
159
|
} else if (supabase) {
|
|
152
|
-
if (write.table
|
|
160
|
+
if (isOwnTable(write.table, ownTable) && (declaration.owner === null || declaration.owner === file.owner.root)) return;
|
|
153
161
|
const ownerNote = declaration.owner ? `, owned by ${declaration.owner}` : '';
|
|
154
162
|
report(`SQL in ${file.owner.root} writes table ${write.table}${ownerNote}; under Supabase schema authority a capability writes only its own table (${ownTable}), named for that capability.`, { table: write.table, ...(declaration.owner ? { tableOwner: declaration.owner } : {}) });
|
|
155
163
|
} else if (declaration.owner !== file.owner.root) {
|
|
@@ -208,7 +216,10 @@ function inspectSqlTag(input, state, file, checker, node) {
|
|
|
208
216
|
seen.add(key);
|
|
209
217
|
state.violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
210
218
|
};
|
|
211
|
-
const result = analyzeSql(text, { uniqueSetsOf: table
|
|
219
|
+
const result = analyzeSql(text, { uniqueSetsOf: (table, schema) => {
|
|
220
|
+
if (supabase && ((schema && schema !== 'public') || table !== table.toLowerCase())) return null;
|
|
221
|
+
return tables.get(table.toLowerCase())?.uniqueSets ?? null;
|
|
222
|
+
} });
|
|
212
223
|
state.dynamic += result.dynamic;
|
|
213
224
|
const ownTable = capabilityTable(file.owner.root);
|
|
214
225
|
reportSqlWrites({ result, tables, supabase, file, ownTable, report });
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Token forms shared by SQL relation scanning and conservative singleton-value proofs. */
|
|
2
|
+
|
|
3
|
+
/** Whether a token can spell an SQL identifier. */
|
|
4
|
+
export const isName = token => token && (token.t === 'word' || token.t === 'ident');
|
|
5
|
+
/** The identifier spelling after SQL word folding. */
|
|
6
|
+
export const nameOf = token => (token.t === 'word' ? token.v.toLowerCase() : token.v);
|
|
7
|
+
/** Whether a token is the named SQL keyword. */
|
|
8
|
+
export const isWord = (token, up) => token?.t === 'word' && token.up === up;
|
|
9
|
+
|
|
10
|
+
// The scalar-cast grammar proves only these builtin targets, unqualified or explicitly under pg_catalog.
|
|
11
|
+
const SCALAR_CAST_TYPES = new Set(['uuid', 'text']);
|
|
12
|
+
|
|
13
|
+
/** Depth of each token (the depth before it when it is `(`, after it when it is `)`). */
|
|
14
|
+
export function depths(tokens) {
|
|
15
|
+
let depth = 0;
|
|
16
|
+
return tokens.map(token => {
|
|
17
|
+
if (token.v === ')' && token.t === 'punct') depth -= 1;
|
|
18
|
+
const here = depth;
|
|
19
|
+
if (token.v === '(' && token.t === 'punct') depth += 1;
|
|
20
|
+
return here;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The closing parenthesis of a token group, or the final token when unmatched. */
|
|
25
|
+
export function matching(tokens, open) {
|
|
26
|
+
let depth = 0;
|
|
27
|
+
for (let i = open; i < tokens.length; i += 1) {
|
|
28
|
+
if (tokens[i].t !== 'punct') continue;
|
|
29
|
+
if (tokens[i].v === '(') depth += 1;
|
|
30
|
+
else if (tokens[i].v === ')') { depth -= 1; if (depth === 0) return i; }
|
|
31
|
+
}
|
|
32
|
+
return tokens.length - 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function unparenthesized(tokens) {
|
|
36
|
+
let part = tokens;
|
|
37
|
+
while (part[0]?.v === '(' && part.at(-1)?.v === ')' && matching(part, 0) === part.length - 1) part = part.slice(1, -1);
|
|
38
|
+
return part;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function keyColumn(tokens, qualifiers) {
|
|
42
|
+
const part = unparenthesized(tokens);
|
|
43
|
+
if (singletonValue(part)) return null;
|
|
44
|
+
if (part.length === 1 && isName(part[0])) return nameOf(part[0]);
|
|
45
|
+
if (part.length === 3 && isName(part[0]) && part[1].v === '.' && isName(part[2]) && qualifiers.has(nameOf(part[0]))) return nameOf(part[2]);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scalarCastType(tokens) {
|
|
50
|
+
if (tokens.length === 1 && isName(tokens[0])) return SCALAR_CAST_TYPES.has(nameOf(tokens[0]));
|
|
51
|
+
return tokens.length === 3 && isName(tokens[0]) && nameOf(tokens[0]) === 'pg_catalog' && tokens[1].v === '.'
|
|
52
|
+
&& isName(tokens[2]) && SCALAR_CAST_TYPES.has(nameOf(tokens[2]));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** SQL CAST is a scalar wrapper; its operand is still inspected separately for row dependence or set-returning calls. */
|
|
56
|
+
function castParts(tokens, start = 0) {
|
|
57
|
+
if (!isWord(tokens[start], 'CAST') || tokens[start + 1]?.v !== '(') return null;
|
|
58
|
+
const end = matching(tokens, start + 1);
|
|
59
|
+
if (tokens[end]?.v !== ')') return null;
|
|
60
|
+
const inside = tokens.slice(start + 2, end);
|
|
61
|
+
const level = depths(inside);
|
|
62
|
+
const separator = inside.findIndex((token, index) => level[index] === 0 && isWord(token, 'AS'));
|
|
63
|
+
if (separator < 1 || separator === inside.length - 1) return null;
|
|
64
|
+
return { value: inside.slice(0, separator), type: inside.slice(separator + 1), next: end + 1 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A scalar atom, optionally cast through the supported builtin grammar, proves a row-independent equality value. */
|
|
68
|
+
function singletonValue(tokens) {
|
|
69
|
+
const part = unparenthesized(tokens);
|
|
70
|
+
if (part.length === 2 && ['+', '-'].includes(part[0].v) && part[1].t === 'number') return true;
|
|
71
|
+
if (part.length === 1) {
|
|
72
|
+
const [token] = part;
|
|
73
|
+
return ['param', 'string', 'number'].includes(token.t) || isWord(token, 'NULL') || isWord(token, 'TRUE') || isWord(token, 'FALSE');
|
|
74
|
+
}
|
|
75
|
+
const cast = castParts(part);
|
|
76
|
+
if (cast?.next === part.length && scalarCastType(cast.type)) return singletonValue(cast.value);
|
|
77
|
+
const level = depths(part);
|
|
78
|
+
for (let index = part.length - 1; index > 0; index -= 1) {
|
|
79
|
+
if (level[index] === 0 && part[index].v === '::') return scalarCastType(part.slice(index + 1)) && singletonValue(part.slice(0, index));
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Key columns equated to proven row-independent singleton values in either operand order. */
|
|
85
|
+
export function equalityColumns(conjunct, qualifiers) {
|
|
86
|
+
const found = new Set();
|
|
87
|
+
const part = unparenthesized(conjunct);
|
|
88
|
+
const level = depths(part);
|
|
89
|
+
const equals = part.findIndex((token, index) => level[index] === 0 && token.t === 'punct' && token.v === '=');
|
|
90
|
+
if (equals < 0) return found;
|
|
91
|
+
const left = part.slice(0, equals);
|
|
92
|
+
const right = part.slice(equals + 1);
|
|
93
|
+
const leftColumn = keyColumn(left, qualifiers);
|
|
94
|
+
const rightColumn = keyColumn(right, qualifiers);
|
|
95
|
+
if (leftColumn && singletonValue(right)) found.add(leftColumn);
|
|
96
|
+
if (rightColumn && singletonValue(left)) found.add(rightColumn);
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Whether a call position is SQL CAST through the supported scalar type grammar. */
|
|
101
|
+
export function scalarCastAt(tokens, index) {
|
|
102
|
+
if (tokens[index - 1]?.v === '.') return false;
|
|
103
|
+
const cast = castParts(tokens, index);
|
|
104
|
+
return Boolean(cast && scalarCastType(cast.type));
|
|
105
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* A substitution of the `sql` tag (a `${...}` hole, the SqlIdent form) is an opaque token: a table position that holds
|
|
6
6
|
* one is counted as dynamic and judged by nobody.
|
|
7
7
|
*/
|
|
8
|
+
import { depths, equalityColumns, isName, isWord, matching, nameOf, scalarCastAt } from './sql-token-forms.mjs';
|
|
9
|
+
|
|
8
10
|
export const HOLE = '';
|
|
9
11
|
|
|
10
12
|
const isSpace = char => /\s/u.test(char);
|
|
@@ -129,10 +131,6 @@ const NOT_A_WRITE_BEFORE_UPDATE = new Set(['FOR', 'DO', 'ON', 'KEY', 'SHARE', 'O
|
|
|
129
131
|
const WHERE_END = new Set(['GROUP', 'ORDER', 'HAVING', 'LIMIT', 'OFFSET', 'UNION', 'INTERSECT', 'EXCEPT', 'FOR', 'WINDOW', 'RETURNING', 'FETCH']);
|
|
130
132
|
const SYSTEM_SCHEMAS = new Set(['pg_catalog', 'information_schema']);
|
|
131
133
|
|
|
132
|
-
const isName = token => token && (token.t === 'word' || token.t === 'ident');
|
|
133
|
-
const nameOf = token => (token.t === 'word' ? token.v.toLowerCase() : token.v);
|
|
134
|
-
const isWord = (token, up) => token?.t === 'word' && token.up === up;
|
|
135
|
-
|
|
136
134
|
/**
|
|
137
135
|
* The table reference at `start`: {parts, table, alias, next}, {dynamic: true}, or null (a subquery, a function, nothing).
|
|
138
136
|
* `next` is the index after the reference and its alias.
|
|
@@ -223,27 +221,6 @@ function cteNames(tokens) {
|
|
|
223
221
|
return names;
|
|
224
222
|
}
|
|
225
223
|
|
|
226
|
-
/** Depth of each token (the depth before it when it is `(`, after it when it is `)`). */
|
|
227
|
-
function depths(tokens) {
|
|
228
|
-
let depth = 0;
|
|
229
|
-
return tokens.map(token => {
|
|
230
|
-
if (token.v === ')' && token.t === 'punct') depth -= 1;
|
|
231
|
-
const here = depth;
|
|
232
|
-
if (token.v === '(' && token.t === 'punct') depth += 1;
|
|
233
|
-
return here;
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function matching(tokens, open) {
|
|
238
|
-
let depth = 0;
|
|
239
|
-
for (let i = open; i < tokens.length; i += 1) {
|
|
240
|
-
if (tokens[i].t !== 'punct') continue;
|
|
241
|
-
if (tokens[i].v === '(') depth += 1;
|
|
242
|
-
else if (tokens[i].v === ')') { depth -= 1; if (depth === 0) return i; }
|
|
243
|
-
}
|
|
244
|
-
return tokens.length - 1;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
224
|
function isAggregateItem(item) {
|
|
248
225
|
for (let i = 0; i < item.length; i += 1) {
|
|
249
226
|
if (item[i].t === 'word' && AGGREGATES.has(item[i].up) && item[i + 1]?.v === '(') {
|
|
@@ -257,28 +234,11 @@ function isAggregateItem(item) {
|
|
|
257
234
|
/** The unique-key columns `columns` (lowercase Set of `=`-constrained column names) satisfy for one of `uniqueSets`. */
|
|
258
235
|
const covers = (uniqueSets, columns) => uniqueSets.some(set => set.length > 0 && set.every(column => columns.has(column)));
|
|
259
236
|
|
|
260
|
-
function equalityColumns(conjunct, qualifiers) {
|
|
261
|
-
const found = new Set();
|
|
262
|
-
const name = (from, to) => {
|
|
263
|
-
const part = conjunct.slice(from, to);
|
|
264
|
-
if (part.length === 1 && isName(part[0])) return nameOf(part[0]);
|
|
265
|
-
if (part.length === 3 && isName(part[0]) && part[1].v === '.' && isName(part[2]) && qualifiers.has(nameOf(part[0]))) return nameOf(part[2]);
|
|
266
|
-
return null;
|
|
267
|
-
};
|
|
268
|
-
const equals = conjunct.findIndex(token => token.t === 'punct' && token.v === '=');
|
|
269
|
-
if (equals < 0) return found;
|
|
270
|
-
const left = name(0, equals);
|
|
271
|
-
const rightStart = conjunct[equals + 1];
|
|
272
|
-
if (left && !(rightStart?.t === 'word' && ['ANY', 'ALL', 'SOME'].includes(rightStart.up))) found.add(left);
|
|
273
|
-
const right = name(equals + 1, conjunct.length);
|
|
274
|
-
if (right) found.add(right);
|
|
275
|
-
return found;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
237
|
/**
|
|
279
238
|
* Judge one statement that reads rows (no INSERT/UPDATE/DELETE/MERGE at its own depth): `{bounded, basis}`. Bounded is
|
|
280
239
|
* a LIMIT/FETCH at the statement depth, no FROM at all (one row), only aggregates without GROUP BY, or an equality on
|
|
281
|
-
* every column of the primary key or of one unique key of
|
|
240
|
+
* every column of the primary key or of one unique key of a single FROM relation. Unknown projection calls can
|
|
241
|
+
* multiply rows even when that relation contributes at most one, so they need an explicit statement bound.
|
|
282
242
|
*/
|
|
283
243
|
function aggregateSelectItems(tokens, level, select, from) {
|
|
284
244
|
const items = [];
|
|
@@ -338,16 +298,42 @@ function isUniqueBounded(tokens, level, from, primary, uniqueSets) {
|
|
|
338
298
|
return Boolean(columns) && covers(uniqueSets, columns);
|
|
339
299
|
}
|
|
340
300
|
|
|
301
|
+
function singleFromRelation(tokens, level, from) {
|
|
302
|
+
for (let index = from + 1; index < tokens.length; index += 1) {
|
|
303
|
+
if (level[index] !== 0) continue;
|
|
304
|
+
if (isWord(tokens[index], 'WHERE') || (tokens[index].t === 'word' && WHERE_END.has(tokens[index].up))) break;
|
|
305
|
+
if (isWord(tokens[index], 'JOIN') || (tokens[index].t === 'punct' && tokens[index].v === ',')) return false;
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Plain known aggregate calls are scalar; other calls, including wrappers around aggregates, may return sets. */
|
|
311
|
+
function unprovenProjectionCall(tokens, level, select, from) {
|
|
312
|
+
const end = from < 0 ? tokens.length : from;
|
|
313
|
+
for (let index = select + 1; index < end; index += 1) {
|
|
314
|
+
const token = tokens[index];
|
|
315
|
+
if (level[index] === 0 && (isWord(token, 'WHERE') || (token.t === 'word' && WHERE_END.has(token.up)))) break;
|
|
316
|
+
if (!isName(token) || tokens[index + 1]?.v !== '(') continue;
|
|
317
|
+
if (scalarCastAt(tokens, index)) continue;
|
|
318
|
+
if (token.t !== 'word' || !AGGREGATES.has(token.up) || tokens[index - 1]?.v === '.') return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
341
323
|
function selectBound(tokens, level, uniqueSetsOf, cte) {
|
|
342
324
|
if (findTopWord(tokens, level, 'LIMIT') >= 0 || findTopWord(tokens, level, 'FETCH') >= 0) return { bounded: true, basis: 'limit' };
|
|
343
325
|
const select = findTopWord(tokens, level, 'SELECT');
|
|
344
326
|
const from = findTopWord(tokens, level, 'FROM', select);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
if (
|
|
350
|
-
return { bounded:
|
|
327
|
+
const primary = from < 0 ? null : tableRef(tokens, from + 1);
|
|
328
|
+
const unbounded = { bounded: false, basis: 'unbounded', table: primary?.table ?? null };
|
|
329
|
+
const grouped = hasTopGroupBy(tokens, level);
|
|
330
|
+
if (tokens.some((token, index) => level[index] === 0 && (isWord(token, 'UNION') || isWord(token, 'INTERSECT') || isWord(token, 'EXCEPT')))) return unbounded;
|
|
331
|
+
if (unprovenProjectionCall(tokens, level, select, from)) return unbounded;
|
|
332
|
+
if (from < 0) return grouped ? unbounded : { bounded: true, basis: 'no from' };
|
|
333
|
+
if (!grouped && aggregateSelectItems(tokens, level, select, from).every(isAggregateItem)) return { bounded: true, basis: 'aggregate' };
|
|
334
|
+
if (grouped || !primary || primary.dynamic || cte.has(primary.table) || !singleFromRelation(tokens, level, from)) return unbounded;
|
|
335
|
+
if (isUniqueBounded(tokens, level, from, primary, uniqueSetsOf(primary.table, primary.schema))) return { bounded: true, basis: 'unique' };
|
|
336
|
+
return unbounded;
|
|
351
337
|
}
|
|
352
338
|
|
|
353
339
|
function recordSqlReference(list, ref, reads, cte, state) {
|
|
@@ -437,8 +423,8 @@ function statementAnalysis(tokens, uniqueSetsOf) {
|
|
|
437
423
|
}
|
|
438
424
|
|
|
439
425
|
/**
|
|
440
|
-
* Read one SQL text. `uniqueSetsOf(table)` returns the unique-key column sets (each a lowercase column list)
|
|
441
|
-
*
|
|
426
|
+
* Read one SQL text. `uniqueSetsOf(table, schema)` returns the unique-key column sets (each a lowercase column list)
|
|
427
|
+
* proven for that relation by its declared schema authority, or null when no key is known.
|
|
442
428
|
* Returns {writes: [{table, schema}], reads: [{table, schema}], selects: [{bounded, basis, table}], dynamic}.
|
|
443
429
|
*/
|
|
444
430
|
export function analyzeSql(text, { uniqueSetsOf = () => null } = {}) {
|