@starci/hfs 4.7.32 → 4.7.34
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 +10 -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 +56 -16
- 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
|
@@ -5,7 +5,7 @@ purpose: |-
|
|
|
5
5
|
Given a fact another context or a background process must react to, how does it travel? Work inside ONE context stays
|
|
6
6
|
synchronous: the handler calls that context's domain service in one transaction and no event exists. An event exists only to
|
|
7
7
|
cross contexts or to trigger asynchronous work (jobs, sagas, reactors, projections). It is a typed event class, published with
|
|
8
|
-
`eventBus.publish(event, tx)` by a domain service inside its transaction, and travels one durable path: transactional outbox,
|
|
8
|
+
`eventBus.publish(event, tx, correlationId)` by a domain service inside its transaction, and travels one durable path: transactional outbox,
|
|
9
9
|
relay, Kafka, consumer with retry and dead letter. `platform/event-bus` is the one owner and the only importer of `kafkajs`.
|
|
10
10
|
appliesTo:
|
|
11
11
|
- backend
|
|
@@ -15,12 +15,12 @@ family: be
|
|
|
15
15
|
slots: [be.transport.message, be.feature.reactors, be.events, be.platform.event-bus]
|
|
16
16
|
pattern: event-bus
|
|
17
17
|
provenance:
|
|
18
|
-
reviewedAt: 2026-10-
|
|
18
|
+
reviewedAt: 2026-10-24
|
|
19
19
|
sources:
|
|
20
20
|
- knowledge/hfs/README.md
|
|
21
21
|
- knowledge/hfs/slots.yaml
|
|
22
22
|
- knowledge/hfs/rules.yaml
|
|
23
|
-
- owner decision 2026-10-02 (Kafka only, transactional outbox, typed event classes
|
|
23
|
+
- owner decision 2026-10-02 (Kafka only, transactional outbox, typed event classes)
|
|
24
24
|
- https://microservices.io/patterns/data/transactional-outbox.html
|
|
25
25
|
limitations: |-
|
|
26
26
|
Static checks prove who may publish, with which transaction, and that a consumer is a named door that forwards the event
|
|
@@ -85,19 +85,24 @@ rules:
|
|
|
85
85
|
kind: mandatory
|
|
86
86
|
hfsRules: [R152, R90, R87]
|
|
87
87
|
requirement: |-
|
|
88
|
-
`eventBus.publish(event, tx)` is called only in `modules/domain`, with the manager of the transaction that makes the
|
|
89
|
-
domain change (`this.entityManager.transaction(async (manager) => { ...; await this.eventBus.publish(event, manager) })`).
|
|
88
|
+
`eventBus.publish(event, tx, correlationId)` is called only in `modules/domain`, with the manager of the transaction that makes the
|
|
89
|
+
domain change (`this.entityManager.transaction(async (manager) => { ...; await this.eventBus.publish(event, manager, request.correlationId) })`).
|
|
90
90
|
It writes a row in the outbox table of that transaction's connection; the relay hands committed rows to Kafka (topic
|
|
91
91
|
`events.<service>`, key = aggregate id) and marks them sent. `kafkajs` is imported only by `platform/event-bus`. Nest's
|
|
92
92
|
`EventBus`, `@EventsHandler`, `@OnEvent` and `EventEmitter2` do not exist, and nothing publishes to Kafka directly.
|
|
93
93
|
There is no in-process or local transport: unit specs use a recording fake of the `EventBus` port, allowed in `*.spec.ts`.
|
|
94
|
+
The explicit id is validated before SQL and stored in the outbox's `correlation_id` column and envelope
|
|
95
|
+
`metadata.correlationId`, following BE-LOGGING-4. The relay makes the row's `correlation_id` authoritative in outbound metadata.
|
|
96
|
+
A broker envelope whose metadata is genuinely absent may derive its root from a bounded, safe stable event id only
|
|
97
|
+
when its event name and payload have the basic envelope shape. That delivery is promoted to explicit metadata before
|
|
98
|
+
consumer execution, retry or dead-letter publication. Present malformed metadata is rejected.
|
|
94
99
|
rationale: |-
|
|
95
100
|
The outbox row commits with the domain change, so a rolled-back change never emits and a committed one is never lost;
|
|
96
101
|
Kafka then gives ordered, replayable, cross-service delivery.
|
|
97
102
|
cases:
|
|
98
103
|
- id: case-1
|
|
99
104
|
when: A domain service places an order
|
|
100
|
-
write: 'await this.entityManager.transaction(async (manager) => { await manager.save(...); await this.eventBus.publish(OrderPlacedEvent.create({ ... }), manager) })'
|
|
105
|
+
write: 'await this.entityManager.transaction(async (manager) => { await manager.save(...); await this.eventBus.publish(OrderPlacedEvent.create({ ... }), manager, request.correlationId) })'
|
|
101
106
|
verification:
|
|
102
107
|
automated:
|
|
103
108
|
- BE_OUTBOX_WRITE_TX
|
|
@@ -112,7 +117,7 @@ rules:
|
|
|
112
117
|
hfsRules: [R153, R80]
|
|
113
118
|
requirement: |-
|
|
114
119
|
A consumer is `transport/message/<event>.consumer.ts` implementing `EventConsumer<E>` with `readonly event = <Event>Event`;
|
|
115
|
-
`handle(delivery)` dispatches one command and forwards `delivery.eventId`. Delivery is at least once: the domain service the
|
|
120
|
+
`handle(delivery)` dispatches one command and forwards `delivery.eventId` and `delivery.correlationId`. Delivery is at least once: the domain service the
|
|
116
121
|
command reaches claims the event id in `platform/inbox` as its first awaited expression, inside its own transaction, so a
|
|
117
122
|
redelivery is a no-op. A throw schedules a retry with exponential backoff on the retry topic (`events.<service>.retry`);
|
|
118
123
|
after the event's `attempts` the message goes to the dead-letter topic (`events.<service>.dlq`), which an operator reads
|
|
@@ -124,7 +129,7 @@ rules:
|
|
|
124
129
|
cases:
|
|
125
130
|
- id: case-1
|
|
126
131
|
when: A consumer of order.placed
|
|
127
|
-
write: 'order-placed.consumer.ts: handle(delivery) { await this.commandBus.execute(new IssueInvoiceCommand({ request: { eventId: delivery.eventId, ... } })) }'
|
|
132
|
+
write: 'order-placed.consumer.ts: handle(delivery) { await this.commandBus.execute(new IssueInvoiceCommand({ request: { eventId: delivery.eventId, correlationId: delivery.correlationId, ... } })) }'
|
|
128
133
|
verification:
|
|
129
134
|
automated:
|
|
130
135
|
- BE_EVENT_CONSUMER_SHAPE
|
|
@@ -283,12 +288,15 @@ files:
|
|
|
283
288
|
role: "The consumed event's e2e flow; titles event-bus/<scenario>: ... prove the pattern."
|
|
284
289
|
spec: event-bus/atomic-with-transaction, event-bus/relay-only-after-commit, event-bus/redelivery-then-dead-letter, event-bus/duplicate-delivery-is-a-noop
|
|
285
290
|
rules: [BE_PATTERN_SPEC_MISSING]
|
|
286
|
-
# Edition lite (R212
|
|
291
|
+
# Edition lite (R212): the same ports and rules, a Supabase adapter behind them. ruling:R212/case-10 moved the lite queue to Redis
|
|
292
|
+
# because full's queue is Redis; full's event bus is Kafka, so the lite event-bus adapter below is unchanged by that ruling.
|
|
287
293
|
liteAdapter:
|
|
288
294
|
summary: >-
|
|
289
|
-
Under lite the event bus keeps typed events, the vendored contract, publish(event, tx) in the domain transaction, the outbox,
|
|
295
|
+
Under lite the event bus keeps typed events, the vendored contract, publish(event, tx, correlationId) in the domain transaction, the outbox,
|
|
290
296
|
the inbox and thin consumers; Kafka is replaced by pgmq-backed topics, or by in-process delivery after commit for a back end
|
|
291
|
-
with a single api app.
|
|
297
|
+
with a single api app. Each consumer group of a topic has its own pgmq queue `<service>__<event>__<group>`, created by a
|
|
298
|
+
supabase migration, named by a literal `<GROUP>_TOPIC_QUEUE` constant under platform/event-bus (its owner, R258) and
|
|
299
|
+
granted only to the application role, so each group keeps its own cursor and dead letter.
|
|
292
300
|
ports:
|
|
293
301
|
- {port: EventPublisher (platform/event-bus), adapter: the same outbox row in the domain transaction}
|
|
294
302
|
- {port: relay, adapter: "fans each committed event out with pgmq.send to one queue per consumer group of its topic, marking the row in the same transaction"}
|
|
@@ -297,4 +305,3 @@ liteAdapter:
|
|
|
297
305
|
proof: >-
|
|
298
306
|
Unit specs as full; declaring event-bus requires integration specs titled `event-bus/<scenario>: ...` for every declared
|
|
299
307
|
scenario on `starci supabase start` (R284). Lite has no e2e world.
|
|
300
|
-
see: [knowledge/patterns/be/supabase-queues.yaml]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
schema: starci/knowledge-source@1
|
|
2
|
+
id: be.idempotency
|
|
3
|
+
title: Inbox dedupe and client operation receipts
|
|
4
|
+
purpose: |-
|
|
5
|
+
Given a door a delivery can repeat through - a signed webhook, a queue message, a relayed outbox row - how does the receiver
|
|
6
|
+
make a repeat a no-op instead of a second charge, a second email or a second state transition? One inbox table keyed by
|
|
7
|
+
(source, event id) sits in front of every such door, reached through `InjectInbox()`. Effectful client commands use the
|
|
8
|
+
same owner to bind a key to the request fingerprint and replay the committed outcome.
|
|
9
|
+
appliesTo:
|
|
10
|
+
- backend
|
|
11
|
+
family: be
|
|
12
|
+
# The slot ids this topic teaches (knowledge/hfs/slots.yaml); scripts/gates/read-digest.mjs inverts these
|
|
13
|
+
# into the READ-step slot -> pattern-topic map (knowledge/op-gate.yaml kinds), each slot's topics in index.yaml order.
|
|
14
|
+
slots: [be.transport.message, be.feature.reactors, be.integrations, be.feature.application, be.transport.graphql, be.transport.http, be.platform]
|
|
15
|
+
pattern: idempotency
|
|
16
|
+
provenance:
|
|
17
|
+
reviewedAt: 2026-10-24
|
|
18
|
+
sources:
|
|
19
|
+
- knowledge/hfs/README.md
|
|
20
|
+
- knowledge/hfs/slots.yaml
|
|
21
|
+
- knowledge/hfs/rules.yaml
|
|
22
|
+
- owner-locked BE convention 2026-09-30
|
|
23
|
+
- knowledge/patterns/be/persistence.yaml
|
|
24
|
+
- owner decision 2026-10-24 (effectful client commands require marker and lint)
|
|
25
|
+
- https://www.postgresql.org/docs/16/sql-insert.html (read 2026-10-24)
|
|
26
|
+
- https://www.postgresql.org/docs/16/transaction-iso.html (read 2026-10-24)
|
|
27
|
+
- https://raw.githubusercontent.com/typeorm/typeorm/0.3.31/src/driver/postgres/PostgresQueryRunner.ts (read 2026-10-24)
|
|
28
|
+
limitations: |-
|
|
29
|
+
Static checks judge the delivery entry of the service a thin door calls: the genuine Inbox receiver, the first await,
|
|
30
|
+
and the return on false. For a sole returned or awaited EntityManager transaction, they also inspect its actual inline
|
|
31
|
+
callback, require the claim in its first statement, and bind the claim's manager to that callback's parameter.
|
|
32
|
+
They do not prove getter effects, called helpers, database rollback or broker delivery; service specs and the real test world
|
|
33
|
+
prove failure and redelivery behaviour. Non-inline callbacks and dynamic isolation expressions are not static proof here.
|
|
34
|
+
The client rule checks the marked command contract, transport key validation and the handler's resolved domain delegation
|
|
35
|
+
into a transaction-owned runClient call. Whether an operation has effects, its fingerprint covers all request meaning,
|
|
36
|
+
and its remote effects are separately protected remain design review and behavioral proof obligations.
|
|
37
|
+
rules:
|
|
38
|
+
- id: BE-IDEMPOTENCY-1
|
|
39
|
+
title: The delivery service claims the event before it acts
|
|
40
|
+
kind: mandatory
|
|
41
|
+
hfsRules: [R80, R85]
|
|
42
|
+
requirement: |-
|
|
43
|
+
A consumer (`transport/message/<event>.consumer.ts`) is a thin door that forwards the delivery's `eventId` through one
|
|
44
|
+
command to the domain service. A signed webhook verifies first and calls one domain method that durably records the
|
|
45
|
+
inbox receipt and outbox event; only a reactor or job dispatches its processing command after commit (webhooks.yaml).
|
|
46
|
+
A public delivery method
|
|
47
|
+
of that service (its first parameter carries `eventId`) takes `@InjectInbox() private readonly inbox: Inbox`, claims
|
|
48
|
+
`(source, eventId)` first and returns when the claim answers `false`. When the service owns an EntityManager transaction,
|
|
49
|
+
the first awaited work in the transaction callback is `this.inbox.claim(source, eventId, manager)`, with that callback's
|
|
50
|
+
own manager. The claim, domain writes and outbox rows commit or roll back together; never move the claim to the shared
|
|
51
|
+
manager outside the transaction to satisfy a static check. A duplicate return ends the callback and no outer delivery
|
|
52
|
+
work follows it. `source` names the provider or publisher; `eventId` comes from the delivery and is never generated by
|
|
53
|
+
the receiver. The rule identifies the Inbox and EntityManager by their declared type origins, not variable names.
|
|
54
|
+
An outbox relay that only produces is not a delivery service and is not asked for a claim.
|
|
55
|
+
rationale: |-
|
|
56
|
+
A sender that gets no timely reply resends: a payment provider retries a webhook, a queue redelivers a message it never
|
|
57
|
+
saw acknowledged. Identifying the receiver by type removes the keyword match a renamed variable could satisfy.
|
|
58
|
+
cases:
|
|
59
|
+
- id: case-1
|
|
60
|
+
when: A signed webhook controller
|
|
61
|
+
write: 'the webhook door (webhooks.yaml) verifies the signature and calls one domain recording intake; that method claims (PAYOS_SOURCE, eventId) first in its transaction, returns when the claim answers false, and saves the durable receipt with its outbox event. A reactor or job consumes the committed receipt and dispatches the processing command; the webhook door, HTTP module and called intake never dispatch on a command bus.'
|
|
62
|
+
- id: case-2
|
|
63
|
+
when: A consumer
|
|
64
|
+
write: 'be/src/features/api/billing/transport/message/payment-captured.consumer.ts forwards delivery.eventId in one command; its domain service claims first inside its own transaction with the callback manager, then writes and publishes through that manager.'
|
|
65
|
+
verification:
|
|
66
|
+
automated:
|
|
67
|
+
- BE_INBOX_DEDUPE_MISSING
|
|
68
|
+
- BE_RAW_INJECT
|
|
69
|
+
manual:
|
|
70
|
+
- Trace that the claim executes on every path before the side effect, including an early return added later.
|
|
71
|
+
relatedExamples: []
|
|
72
|
+
- id: BE-IDEMPOTENCY-2
|
|
73
|
+
title: The inbox is one table, owned by platform, shared by every door
|
|
74
|
+
kind: mandatory
|
|
75
|
+
hfsRules: [R80, R35, R34]
|
|
76
|
+
requirement: |-
|
|
77
|
+
`platform/inbox` owns one table, `inbox_claims(source varchar(200), event_id varchar(200),
|
|
78
|
+
claimed_at timestamptz not null, request_fingerprint varchar(64) null, outcome jsonb null)`, with a composite primary key on `(source, event_id)`, created by a migration under
|
|
79
|
+
`platform/inbox/persistence/migrations/`. `Inbox.claim(source, eventId, tx?)` inserts atomically on the supplied
|
|
80
|
+
transaction manager, or on the configured manager when no caller transaction is supplied. A conflict-safe insert
|
|
81
|
+
(`ON CONFLICT (source, event_id) DO NOTHING RETURNING event_id`) returns `true` for a new row and `false` for a
|
|
82
|
+
duplicate without aborting the caller's transaction. It never reads first and writes after, which races two concurrent
|
|
83
|
+
deliveries of the same event. Lite uses its existing `public.inbox` table with the same client receipt columns,
|
|
84
|
+
owned by a forward-only Supabase migration. A client receipt extends this table; no capability creates another
|
|
85
|
+
dedupe table or unique request-key constraint to compete with it.
|
|
86
|
+
rationale: |-
|
|
87
|
+
One shared table with one owner keeps the guarantee "this (source, event id) claims at most once" in one place, and
|
|
88
|
+
the atomic conflict-safe insert makes the claim safe under concurrent delivery and keeps a duplicate transaction usable.
|
|
89
|
+
cases:
|
|
90
|
+
- id: case-1
|
|
91
|
+
when: The migration
|
|
92
|
+
write: 'The platform inbox migration creates inbox_claims with PRIMARY KEY (source, event_id); its client-operation migration adds request_fingerprint and outcome to that same table.'
|
|
93
|
+
- id: case-2
|
|
94
|
+
when: The adapter
|
|
95
|
+
write: 'async claim(source, eventId, tx?) { const manager = tx ?? this.managers[0]; const rows = await manager.query(CLAIM_EVENT, [source, eventId, this.clock.now()]); return rows.length > 0 } // CLAIM_EVENT is the conflict-safe insert in the inbox owner persistence file.'
|
|
96
|
+
verification:
|
|
97
|
+
automated:
|
|
98
|
+
- BE_INBOX_DEDUPE_MISSING
|
|
99
|
+
- BE_SCHEMA_OWNER
|
|
100
|
+
- BE_SCHEMA_AUTHORITY
|
|
101
|
+
manual:
|
|
102
|
+
- Confirm no capability declares a second inbox-shaped table instead of using this one.
|
|
103
|
+
relatedExamples: [idempotency]
|
|
104
|
+
- id: BE-IDEMPOTENCY-3
|
|
105
|
+
title: Effectful client commands require a key and replay their committed outcome
|
|
106
|
+
kind: mandatory
|
|
107
|
+
hfsRules: [R310, R155, R83, R87]
|
|
108
|
+
requirement: |-
|
|
109
|
+
Mark every command that initiates an effectful client operation with the `Effectful()` decorator exported by
|
|
110
|
+
`platform/inbox` (deciding that a command is effectful is a design-review judgment: R310 judges the commands that carry
|
|
111
|
+
the marker, it cannot find one that lacks it, and an app that never marks a command declares no idempotency
|
|
112
|
+
pattern). Its request carries a required string `idempotencyKey`. GraphQL accepts a required input field;
|
|
113
|
+
REST maps `Idempotency-Key` through a validated input. Validate the key with `IsUUID()` or with `IsString()`,
|
|
114
|
+
`MinLength(1)` and `MaxLength(64)`. The client retains one key across retries of one intended operation and uses a new
|
|
115
|
+
key for a new intent. Internal delivery commands retain the delivery identity of BE-IDEMPOTENCY-1.
|
|
116
|
+
The thin handler forwards the key to one domain service. That service returns its EntityManager transaction,
|
|
117
|
+
whose callback returns `inbox.runClient({ principalId, operation, idempotencyKey, request }, manager, effect)`.
|
|
118
|
+
The principal comes from verified authentication; the operation is a stable operation name. `request` contains every
|
|
119
|
+
business input that changes the meaning of the operation, excluding the key. Inbox canonicalizes the JSON object
|
|
120
|
+
keys and hashes the request, then atomically claims `(source = client:<principalId>:<operation>, event_id = key)`
|
|
121
|
+
through that callback manager. A matching repeat returns the stored complete outcome, including a domain refusal,
|
|
122
|
+
without invoking `effect`; a different fingerprint throws `IDEMPOTENCY_KEY_REUSED`, mapped to a conflict.
|
|
123
|
+
The claim, effect's local writes and outbox rows, and JSON outcome commit together. An exception rolls them all back.
|
|
124
|
+
The outcome is JSON data; undefined, non-finite numbers and non-JSON objects are refused. A concurrent claimant waits
|
|
125
|
+
for the first transaction and replays its committed outcome or claims after its rollback. External calls use an
|
|
126
|
+
outbox or their provider's supported idempotency contract; the local receipt does not prove remote deduplication.
|
|
127
|
+
The domain owns the transaction (BE-PERSISTENCE-10); no interceptor owns a claim or transaction.
|
|
128
|
+
A scheduled fenced job sweeps completed client receipts in bounded batches after the configured retention window
|
|
129
|
+
measured from their first claim.
|
|
130
|
+
Delivery claims are preserved. A key may identify a new intent after its receipt is swept, so clients must keep
|
|
131
|
+
retries within the advertised retention window. Declare `idempotency` in `hfs.json` `sides.be.patterns` and prove
|
|
132
|
+
`idempotency/repeat-returns-same-outcome` and `idempotency/reused-key-other-body-is-refused` on real Postgres through
|
|
133
|
+
the full test world or lite's local integration lane.
|
|
134
|
+
rationale: |-
|
|
135
|
+
A timeout leaves a client unsure whether a mutation committed. The receipt binds the caller's intent to its durable
|
|
136
|
+
result while a request fingerprint prevents the same key from silently authorizing different work.
|
|
137
|
+
cases:
|
|
138
|
+
- id: case-1
|
|
139
|
+
when: Checkout adds a cart item
|
|
140
|
+
write: 'Mark AddCartItemCommand with Effectful(); forward its validated key and fingerprint { productId, quantity } to runClient inside the checkout transaction, and return the stored Outcome on retry.'
|
|
141
|
+
- id: case-2
|
|
142
|
+
when: A transaction fails before commit
|
|
143
|
+
write: 'Let the exception escape the transaction; the receipt and mutation roll back together so the same key can retry.'
|
|
144
|
+
- id: case-3
|
|
145
|
+
when: Retention expires
|
|
146
|
+
write: 'A FencedProcessor delegates to the inbox retention domain service; the inbox owner deletes only completed client receipts older than the configured window in a bounded batch.'
|
|
147
|
+
verification:
|
|
148
|
+
automated:
|
|
149
|
+
- BE_MUTATION_IDEMPOTENT
|
|
150
|
+
- BE_PATTERN_SPEC_MISSING
|
|
151
|
+
- BE_UNNAMED_DATA_ACCESS
|
|
152
|
+
- BE_CQRS_SHAPE
|
|
153
|
+
- BE_READONLY_BOUNDARY
|
|
154
|
+
manual:
|
|
155
|
+
- Review the effectful designation, authenticated principal, operation identity, complete request meaning and JSON outcome.
|
|
156
|
+
- Exercise simultaneous identical requests, rollback and a changed body under the same key on the real database.
|
|
157
|
+
- Confirm retention and provider recovery behavior match the advertised client contract.
|
|
158
|
+
relatedExamples: [idempotency]
|
|
159
|
+
files:
|
|
160
|
+
- path: src/modules/platform/idempotency/index.ts
|
|
161
|
+
slot: be.platform
|
|
162
|
+
template: idempotency/platform/idempotency/index.ts.tpl
|
|
163
|
+
- path: src/modules/platform/idempotency/errors/idempotency.error.ts
|
|
164
|
+
slot: be.platform
|
|
165
|
+
template: idempotency/platform/idempotency/errors/idempotency.error.ts.tpl
|
|
166
|
+
- path: src/modules/platform/idempotency/messages/idempotency.messages.ts
|
|
167
|
+
slot: be.domain.messages
|
|
168
|
+
template: idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl
|
|
169
|
+
- path: src/modules/platform/inbox/index.ts
|
|
170
|
+
slot: be.platform
|
|
171
|
+
template: idempotency/platform/inbox/index.ts.tpl
|
|
172
|
+
- path: src/modules/platform/inbox/inbox.port.ts
|
|
173
|
+
slot: be.platform
|
|
174
|
+
template: idempotency/platform/inbox/inbox.port.ts.tpl
|
|
175
|
+
- path: src/modules/platform/inbox/inbox.decorators.ts
|
|
176
|
+
slot: be.platform
|
|
177
|
+
template: idempotency/platform/inbox/inbox.decorators.ts.tpl
|
|
178
|
+
- path: src/modules/platform/inbox/effectful.decorators.ts
|
|
179
|
+
slot: be.platform
|
|
180
|
+
template: idempotency/platform/inbox/effectful.decorators.ts.tpl
|
|
181
|
+
- path: src/modules/platform/inbox/inbox.options.ts
|
|
182
|
+
slot: be.platform
|
|
183
|
+
template: idempotency/platform/inbox/inbox.options.ts.tpl
|
|
184
|
+
- path: src/modules/platform/inbox/inbox.module-definition.ts
|
|
185
|
+
slot: be.platform
|
|
186
|
+
template: idempotency/platform/inbox/inbox.module-definition.ts.tpl
|
|
187
|
+
- path: src/modules/platform/inbox/inbox.module.ts
|
|
188
|
+
slot: be.platform
|
|
189
|
+
template: idempotency/platform/inbox/inbox.module.ts.tpl
|
|
190
|
+
- path: src/modules/platform/inbox/inbox.service.ts
|
|
191
|
+
slot: be.platform
|
|
192
|
+
template: idempotency/platform/inbox/inbox.service.ts.tpl
|
|
193
|
+
- path: src/modules/platform/inbox/inbox.service.spec.ts
|
|
194
|
+
slot: be.platform
|
|
195
|
+
template: idempotency/platform/inbox/inbox.service.spec.ts.tpl
|
|
196
|
+
- path: src/modules/platform/inbox/request-fingerprint.policy.ts
|
|
197
|
+
slot: be.platform
|
|
198
|
+
template: idempotency/platform/inbox/request-fingerprint.policy.ts.tpl
|
|
199
|
+
- path: src/modules/platform/inbox/request-fingerprint.policy.spec.ts
|
|
200
|
+
slot: be.platform
|
|
201
|
+
template: idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl
|
|
202
|
+
- path: src/modules/platform/inbox/client-operation.policy.ts
|
|
203
|
+
slot: be.platform
|
|
204
|
+
template: idempotency/platform/inbox/client-operation.policy.ts.tpl
|
|
205
|
+
- path: src/modules/platform/inbox/client-operation.policy.spec.ts
|
|
206
|
+
slot: be.platform
|
|
207
|
+
template: idempotency/platform/inbox/client-operation.policy.spec.ts.tpl
|
|
208
|
+
- path: src/modules/platform/inbox/errors/inbox.error.ts
|
|
209
|
+
slot: be.platform
|
|
210
|
+
template: idempotency/platform/inbox/errors/inbox.error.ts.tpl
|
|
211
|
+
- path: src/modules/platform/inbox/messages/inbox.messages.ts
|
|
212
|
+
slot: be.domain.messages
|
|
213
|
+
template: idempotency/platform/inbox/messages/inbox.messages.ts.tpl
|
|
214
|
+
- path: src/modules/platform/inbox/persistence/connection.ts
|
|
215
|
+
slot: be.persistence
|
|
216
|
+
template: idempotency/platform/inbox/persistence/connection.ts.tpl
|
|
217
|
+
- path: src/modules/platform/inbox/persistence/inbox.sql.ts
|
|
218
|
+
slot: be.persistence
|
|
219
|
+
template: idempotency/platform/inbox/persistence/inbox.sql.ts.tpl
|
|
220
|
+
- path: src/modules/platform/inbox/persistence/inbox.rows.ts
|
|
221
|
+
slot: be.persistence
|
|
222
|
+
template: idempotency/platform/inbox/persistence/inbox.rows.ts.tpl
|
|
223
|
+
- path: src/modules/platform/inbox/client-outcome-backfill.client.ts
|
|
224
|
+
slot: be.platform
|
|
225
|
+
template: idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl
|
|
226
|
+
- path: src/modules/platform/inbox/client-outcome-backfill.client.spec.ts
|
|
227
|
+
slot: be.platform
|
|
228
|
+
template: idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl
|
|
229
|
+
- path: src/modules/platform/inbox/delivery-claim-backfill.client.ts
|
|
230
|
+
slot: be.platform
|
|
231
|
+
template: idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl
|
|
232
|
+
- path: src/modules/platform/inbox/delivery-claim-backfill.client.spec.ts
|
|
233
|
+
slot: be.platform
|
|
234
|
+
template: idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl
|
|
235
|
+
- path: src/modules/platform/inbox/delivery-claim-release.client.ts
|
|
236
|
+
slot: be.platform
|
|
237
|
+
template: idempotency/platform/inbox/delivery-claim-release.client.ts.tpl
|
|
238
|
+
- path: src/modules/platform/inbox/delivery-claim-release.client.spec.ts
|
|
239
|
+
slot: be.platform
|
|
240
|
+
template: idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl
|
|
241
|
+
- path: src/modules/platform/inbox/persistence/entities/inbox-claim.entity.ts
|
|
242
|
+
slot: be.persistence
|
|
243
|
+
template: idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl
|
|
244
|
+
- path: src/modules/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts
|
|
245
|
+
slot: be.persistence
|
|
246
|
+
template: idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl
|
|
247
|
+
- path: src/modules/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts
|
|
248
|
+
slot: be.persistence
|
|
249
|
+
template: idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl
|
|
250
|
+
liteAdapter:
|
|
251
|
+
summary: >-
|
|
252
|
+
Lite keeps the Inbox port and transaction-owned client receipt API. Its existing public.inbox
|
|
253
|
+
table uses Supabase migration authority; the be/inbox platform templates carry the lite adapter and the
|
|
254
|
+
platform/client-operation-outcomes.sql.tpl forward migration. The files tree above describes the full TypeORM adapter.
|
|
255
|
+
Delivery-only webhook scaffolds do not install a client marker. When adopting this client pattern, add the shared
|
|
256
|
+
idempotency/platform/inbox/effectful.decorators.ts.tpl as src/modules/platform/inbox/effectful.decorators.ts and
|
|
257
|
+
export Effectful from the inbox barrel. Every effectful client command then uses that marker and declares the
|
|
258
|
+
idempotency pattern with its required proof scenarios.
|
|
259
|
+
ports:
|
|
260
|
+
- {port: Inbox, adapter: PostgresInbox through the caller's active EntityManager transaction}
|
|
261
|
+
- {port: receipt retention, adapter: a fenced job delivered by the BullMQ queue on the declared Redis and scheduled by a pg_cron tick that enqueues through the queue outbox}
|
|
262
|
+
proof: >-
|
|
263
|
+
The same inbox and fingerprint unit specs apply; declaring idempotency requires both client receipt scenarios in
|
|
264
|
+
lite's local Supabase integration lane.
|
|
265
|
+
see: [knowledge/patterns/be/queues.yaml]
|
|
@@ -14,7 +14,7 @@ family: be
|
|
|
14
14
|
slots: [be.feature.jobs, be.feature.jobs.steps, be.platform.jobs]
|
|
15
15
|
pattern: fenced-job
|
|
16
16
|
provenance:
|
|
17
|
-
reviewedAt: 2026-10-
|
|
17
|
+
reviewedAt: 2026-10-24
|
|
18
18
|
sources:
|
|
19
19
|
- knowledge/hfs/README.md
|
|
20
20
|
- knowledge/hfs/slots.yaml
|
|
@@ -40,13 +40,15 @@ rules:
|
|
|
40
40
|
delivery (the job key is the BullMQ job id), then `process`, then the guarded `complete`. Nothing else writes the row: no
|
|
41
41
|
`manager.update`, `increment`, `save`, `insert`, `delete` or query-builder write on the job entity outside `platform/jobs`. The table
|
|
42
42
|
exists on every connection that runs jobs.
|
|
43
|
+
`ClaimParams.correlationId` comes from the queue delivery and is carried into `ClaimedJob.correlationId` for the processor
|
|
44
|
+
and steps. It keeps the delivery identity across claims, independently of the fencing token and provider run key.
|
|
43
45
|
rationale: |-
|
|
44
46
|
A read-modify-write claim races, and a second writer defeats the fence. One owner and one atomic statement keep the token
|
|
45
47
|
monotonic.
|
|
46
48
|
cases:
|
|
47
49
|
- id: case-1
|
|
48
50
|
when: A worker picks up a job
|
|
49
|
-
write: 'const job = await this.claims.claim({ kind: "send-receipt", jobKey: delivery.id, payload, workerId, leaseMs }) // token = previous + 1'
|
|
51
|
+
write: 'const job = await this.claims.claim({ kind: "send-receipt", jobKey: delivery.id, correlationId: delivery.correlationId, payload: delivery.payload, workerId, leaseMs }) // token = previous + 1'
|
|
50
52
|
verification:
|
|
51
53
|
automated:
|
|
52
54
|
- BE_JOB_WRITE_OUTSIDE_OWNER
|
|
@@ -81,7 +83,7 @@ rules:
|
|
|
81
83
|
hfsRules: [R159]
|
|
82
84
|
requirement: |-
|
|
83
85
|
`JobClaims.runKey(job, step)` returns a `RunKey` (`<jobId>:<step>:<fencingToken>`); it is the only way to make one. The step's
|
|
84
|
-
one delegating call hands the payload as delivered
|
|
86
|
+
one delegating call hands the payload as delivered, the run key and `job.correlationId` to the module service that owns the effect, and every
|
|
85
87
|
call that reaches an integration passes the key as the provider's idempotency key, so a repeated effect of a stale
|
|
86
88
|
worker is dropped by the provider. A read from an integration belongs in the application layer, not in a step. When the
|
|
87
89
|
provider cannot enforce idempotency, constrain overlap and reconcile before a retry.
|
|
@@ -89,8 +91,8 @@ rules:
|
|
|
89
91
|
Fencing the local row does not fence the remote effect. The key ties the effect to one claim.
|
|
90
92
|
cases:
|
|
91
93
|
- id: case-1
|
|
92
|
-
when: A step
|
|
93
|
-
write: 'await this.
|
|
94
|
+
when: A step archives a receipt
|
|
95
|
+
write: 'await this.receipts.archiveReceipt(job.payload, this.claims.runKey(job, "store"), job.correlationId)'
|
|
94
96
|
verification:
|
|
95
97
|
automated:
|
|
96
98
|
- BE_JOB_RUN_KEY
|
|
@@ -106,7 +108,7 @@ rules:
|
|
|
106
108
|
settles through the guarded writes); `steps/<step>.step.ts` declares a class implementing `JobStep`; `application/` holds
|
|
107
109
|
plain CQRS. The processor and every step are thin feature files (R203 `BE_FEATURE_THIN`): they map the claimed job and
|
|
108
110
|
delegate into `modules/`; the claim, the fence, the settle and the retry live in `platform/jobs`. A domain service starts
|
|
109
|
-
the job with a typed queue producer `enqueueX(payload, tx)` inside its transaction
|
|
111
|
+
the job with a typed queue producer `enqueueX(payload, tx, correlationId)` inside its transaction
|
|
110
112
|
(queues.yaml). The jobs kind imports only `modules/*`: it never imports an api, saga or reactor feature, and it talks to
|
|
111
113
|
them through events. An app of kind `worker` or `api` composes the job queue module.
|
|
112
114
|
rationale: |-
|
|
@@ -169,7 +171,7 @@ files:
|
|
|
169
171
|
template: jobs/platform/jobs.decorators.ts.tpl
|
|
170
172
|
- path: src/modules/platform/jobs/jobs.contracts.ts
|
|
171
173
|
slot: be.platform.jobs
|
|
172
|
-
role: "ClaimedJob, GuardedWrite (the token is required), RunKey."
|
|
174
|
+
role: "ClaimedJob with the delivery correlationId, GuardedWrite (the token is required), RunKey."
|
|
173
175
|
template: jobs/platform/jobs.contracts.ts.tpl
|
|
174
176
|
- path: src/modules/platform/jobs/jobs.port.ts
|
|
175
177
|
slot: be.platform.jobs
|
|
@@ -259,7 +261,7 @@ files:
|
|
|
259
261
|
- path: src/features/jobs/<job>/steps/<step>.step.ts
|
|
260
262
|
slot: be.feature.jobs.steps
|
|
261
263
|
template: jobs/job.step.ts.tpl
|
|
262
|
-
role: 'One external effect; implements JobStep — thin (R203 `BE_FEATURE_THIN`): the fence call first, then one delegating call that hands the payload
|
|
264
|
+
role: 'One external effect; implements JobStep — thin (R203 `BE_FEATURE_THIN`): the fence call first, then one delegating call that hands the payload, the run key and correlationId to a service of the modules; the payload guards live there.'
|
|
263
265
|
rules: [BE_JOB_SHAPE, BE_JOB_RUN_KEY]
|
|
264
266
|
- path: src/features/jobs/<job>/application/<action>.command.ts
|
|
265
267
|
slot: be.feature.application
|
|
@@ -279,11 +281,14 @@ files:
|
|
|
279
281
|
liteAdapter:
|
|
280
282
|
summary: >-
|
|
281
283
|
Under lite a fenced job is unchanged: the job row with its fencing token, the atomic claim, guarded writes and run keys. The
|
|
282
|
-
row's table is a supabase migration, and the processor is fed by the lite
|
|
284
|
+
row's table is a supabase migration, and the processor is fed by full's BullMQ queue on the declared Redis, so a lite app
|
|
285
|
+
that declares fenced-job declares `backing: [redis]`. Without a declared Redis a lite app has no fenced job; a long task is
|
|
286
|
+
a declared replicated-job running a cli command.
|
|
283
287
|
ports:
|
|
284
288
|
- {port: job claim (platform/jobs), adapter: "the same claim SQL through the shared EntityManager; the table comes from supabase/migrations"}
|
|
285
|
-
- {port: processor delivery, adapter: "
|
|
289
|
+
- {port: processor delivery, adapter: "full's BullMQ worker (attempts and exponential backoff from the queue policy, failed jobs kept as the dead letter)"}
|
|
286
290
|
proof: >-
|
|
287
291
|
Unit specs as full (job-claim.service.spec.ts); declaring fenced-job requires integration specs titled
|
|
288
|
-
`fenced-job/<scenario>: ...` for every declared scenario on
|
|
289
|
-
|
|
292
|
+
`fenced-job/<scenario>: ...` for every declared scenario on the local Supabase stack (R284, which runs no container of its
|
|
293
|
+
own): the claim, the fencing token and the guarded writes are Postgres facts, driven there by the real runner without BullMQ.
|
|
294
|
+
see: [knowledge/patterns/be/queues.yaml]
|
|
@@ -181,10 +181,10 @@ liteAdapter:
|
|
|
181
181
|
summary: >-
|
|
182
182
|
Under lite a projection is unchanged in behaviour (only it writes its table, recompute is idempotent and replayable, readers
|
|
183
183
|
use get*); its table is a supabase migration with a rows type, so the slot carries no projection entity (be.projections lite
|
|
184
|
-
overlay), and it is kept current by a reactor or a job on the
|
|
184
|
+
overlay), and it is kept current by a reactor on the lite event bus or by a job on the queue (BullMQ on the declared Redis).
|
|
185
185
|
ports:
|
|
186
186
|
- {port: read model table, adapter: "a supabase migration plus <name>.sql.ts and <name>.rows.ts under persistence/"}
|
|
187
187
|
proof: >-
|
|
188
188
|
Unit specs as full (the projection is a logic role at 100 per file); declaring projection requires integration specs titled
|
|
189
189
|
`projection/<scenario>: ...` for every declared scenario on `starci supabase start` (R284).
|
|
190
|
-
see: [knowledge/patterns/be/
|
|
190
|
+
see: [knowledge/patterns/be/event-bus.yaml, knowledge/patterns/be/queues.yaml]
|
|
@@ -3,7 +3,7 @@ id: be.queues
|
|
|
3
3
|
title: Queues and schedulers
|
|
4
4
|
purpose: |-
|
|
5
5
|
Given work that must run later, in the background or on a schedule, how is it started? It is a job of a typed queue:
|
|
6
|
-
`modules/queues/<queue>/<queue>.queue.ts` is the producer (`enqueueX(payload, tx)` writes the outbox in the caller's
|
|
6
|
+
`modules/queues/<queue>/<queue>.queue.ts` is the producer (`enqueueX(payload, tx, correlationId)` writes the outbox in the caller's
|
|
7
7
|
transaction), `platform/queue` relays committed rows to BullMQ (the job id is the outbox id, so a relay retry never duplicates
|
|
8
8
|
a job), and a `features/jobs/<job>/<job>.processor.ts` runs it. A schedule is a BullMQ job scheduler declared beside the
|
|
9
9
|
producer; `@Cron`, `@Interval` and `@Timeout` do not exist.
|
|
@@ -15,12 +15,13 @@ family: be
|
|
|
15
15
|
slots: [be.feature.jobs, be.queues, be.platform.queue]
|
|
16
16
|
pattern: queue
|
|
17
17
|
provenance:
|
|
18
|
-
reviewedAt: 2026-10-
|
|
18
|
+
reviewedAt: 2026-10-24
|
|
19
19
|
sources:
|
|
20
20
|
- knowledge/hfs/README.md
|
|
21
21
|
- knowledge/hfs/slots.yaml
|
|
22
22
|
- knowledge/hfs/rules.yaml
|
|
23
23
|
- owner decision 2026-10-02 (BullMQ queues with a transactional outbox, schedulers replace cron)
|
|
24
|
+
- ruling:R212/case-10 (2026-10-24; the queue is Redis in both editions, lite declares its Redis)
|
|
24
25
|
- https://docs.bullmq.io/guide/job-schedulers
|
|
25
26
|
limitations: |-
|
|
26
27
|
Static checks prove who enqueues, with which transaction, and that nothing imports BullMQ or Nest scheduling outside the
|
|
@@ -33,17 +34,18 @@ rules:
|
|
|
33
34
|
hfsRules: [R152, R154]
|
|
34
35
|
requirement: |-
|
|
35
36
|
Each queue is a folder `be/src/modules/queues/<queue>/` with `<queue>.queue.ts` (a class whose methods are the typed
|
|
36
|
-
producers `enqueueX(payload, tx: EntityManager)`), optional `<queue>.contracts.ts` (payload types and the queue name) and
|
|
37
|
-
`index.ts`. A producer
|
|
37
|
+
producers `enqueueX(payload, tx: EntityManager, correlationId: string)`), optional `<queue>.contracts.ts` (payload types and the queue name) and
|
|
38
|
+
`index.ts`. A producer calls `QueueOutbox.write(tx, queue, payload, correlationId)` in `platform/queue`. A producer
|
|
38
39
|
is called only by a domain service, inside its transaction (the same rule as `eventBus.publish`). Work inside one context
|
|
39
40
|
that must happen now is a direct call, not a job.
|
|
41
|
+
Current outbox writes validate the explicit correlation id before issuing SQL.
|
|
40
42
|
rationale: |-
|
|
41
43
|
The job row commits with the change that caused it, so a rolled-back change never starts work and a committed one is never
|
|
42
44
|
lost.
|
|
43
45
|
cases:
|
|
44
46
|
- id: case-1
|
|
45
47
|
when: A domain service must send a receipt after payment
|
|
46
|
-
write: 'await this.entityManager.transaction(async (manager) => { await manager.save(...); await this.receiptQueue.enqueueSendReceipt({ orderId }, manager) })'
|
|
48
|
+
write: 'await this.entityManager.transaction(async (manager) => { await manager.save(...); await this.receiptQueue.enqueueSendReceipt({ orderId }, manager, request.correlationId) })'
|
|
47
49
|
verification:
|
|
48
50
|
automated:
|
|
49
51
|
- BE_OUTBOX_WRITE_TX
|
|
@@ -60,34 +62,47 @@ rules:
|
|
|
60
62
|
registered connection and adds each job with `jobId = <outbox id>`; BullMQ ignores a second add of the same id, so a relay
|
|
61
63
|
that crashes after adding and before marking the row sent re-adds harmlessly. Nothing else constructs a `Queue`, `Worker`
|
|
62
64
|
or `FlowProducer`.
|
|
65
|
+
The row's explicit `correlationId` travels in the job envelope and `QueueDelivery`, following BE-LOGGING-4.
|
|
66
|
+
BullMQ envelopes carry the owned `schema: "starci/queue@1"` marker; enqueued jobs wrap the payload and explicit id.
|
|
67
|
+
Only the owned marker family is interpreted as transport data. Unmarked legacy objects remain whole business payloads,
|
|
68
|
+
including their `kind`, `schema` and `correlationId` fields.
|
|
63
69
|
rationale: |-
|
|
64
70
|
The relay is at least once; a deterministic job id turns it into effectively once.
|
|
65
71
|
cases:
|
|
66
72
|
- id: case-1
|
|
67
73
|
when: The relay re-reads a row it already added
|
|
68
|
-
write: '
|
|
74
|
+
write: 'transport.add(row.queue, row.id, row.payload, row.correlationId) preserves the outbox id and correlation id; the BullMQ adapter adds the envelope with jobId = row.id.'
|
|
69
75
|
verification:
|
|
70
76
|
automated:
|
|
71
77
|
- BE_INFRA_OWNER
|
|
72
78
|
manual: []
|
|
73
79
|
relatedExamples: [queue]
|
|
74
80
|
- id: BE-QUEUES-3
|
|
75
|
-
title: A schedule is a BullMQ job scheduler declared with its queue
|
|
81
|
+
title: A schedule is a BullMQ job scheduler declared with its queue, or a pg_cron tick that writes the outbox
|
|
76
82
|
kind: mandatory
|
|
77
|
-
hfsRules: [R90]
|
|
83
|
+
hfsRules: [R90, R258]
|
|
78
84
|
requirement: |-
|
|
79
85
|
A recurring task is a job scheduler (`upsertJobScheduler`) that the queue module declares in its `schedulers` list and
|
|
80
|
-
`platform/queue` registers at boot
|
|
81
|
-
|
|
86
|
+
`platform/queue` registers at boot, or, in an app on Supabase, a uniquely named pg_cron job of a migration whose command
|
|
87
|
+
(directly or through one `*_tick` function) only inserts one row of a queue constant into the queue outbox table of
|
|
88
|
+
`ruleParams.db.queueOutbox` (`public.queue`), which the relay hands to BullMQ; a tick has exactly one of the two owners. Its job is processed by the same
|
|
89
|
+
`features/jobs/<job>/<job>.processor.ts` as any enqueued job. `@nestjs/schedule`, `@Cron`, `@Interval`, `@Timeout` and
|
|
90
|
+
`setInterval` for work are not used anywhere.
|
|
82
91
|
rationale: |-
|
|
83
|
-
One scheduler in Redis fires once for the whole deployment, where cron in a replicated
|
|
92
|
+
One scheduler in Redis, or one pg_cron job in the database, fires once for the whole deployment, where cron in a replicated
|
|
93
|
+
process fires once per replica.
|
|
84
94
|
cases:
|
|
85
95
|
- id: case-1
|
|
86
96
|
when: Expire unpaid orders every five minutes
|
|
87
97
|
write: 'order-expiry.queue.ts declares scheduler { every: 5 minutes } for the expire-orders job; the processor dispatches ExpireOrdersCommand.'
|
|
98
|
+
- id: case-2
|
|
99
|
+
when: A lite app on Supabase sends booking reminders every fifteen minutes
|
|
100
|
+
write: "A migration declares private.booking_reminder_tick() as one insert into public.queue (queue, payload, created_at) values ('booking_reminder', jsonb_build_object('tick', true), now()) and schedules it with cron.schedule('booking_reminder_tick', '*/15 * * * *', ...); the send-reminder processor is the queue's."
|
|
88
101
|
verification:
|
|
89
102
|
automated:
|
|
90
103
|
- BE_INFRA_OWNER
|
|
104
|
+
- DB_PGMQ_DECLARED
|
|
105
|
+
- DB_CRON_OWNED
|
|
91
106
|
manual:
|
|
92
107
|
- Confirm the cadence and the job's idempotence.
|
|
93
108
|
relatedExamples: [queue]
|
|
@@ -169,6 +184,13 @@ files:
|
|
|
169
184
|
- path: src/modules/platform/queue/queue.log-events.ts
|
|
170
185
|
slot: be.platform.queue
|
|
171
186
|
template: queues/platform/queue.log-events.ts.tpl
|
|
187
|
+
- path: src/modules/platform/queue/errors/queue.error.ts
|
|
188
|
+
slot: be.platform.queue
|
|
189
|
+
role: "Malformed stored queue envelope failures."
|
|
190
|
+
template: queues/platform/errors/queue.error.ts.tpl
|
|
191
|
+
- path: src/modules/platform/queue/messages/queue.messages.ts
|
|
192
|
+
slot: be.domain.messages
|
|
193
|
+
template: queues/platform/messages/queue.messages.ts.tpl
|
|
172
194
|
- path: src/modules/platform/queue/bullmq-queue-transport.client.ts
|
|
173
195
|
slot: be.platform.queue
|
|
174
196
|
role: "The one bullmq importer."
|
|
@@ -242,18 +264,31 @@ files:
|
|
|
242
264
|
role: "Titles queue/<scenario>: ... prove the pattern."
|
|
243
265
|
spec: queue/enqueue-atomic-with-transaction, queue/relay-job-id-is-outbox-id, queue/scheduler-fires-once
|
|
244
266
|
rules: [BE_PATTERN_SPEC_MISSING]
|
|
245
|
-
# Edition lite (R212
|
|
267
|
+
# Edition lite (ruling:R212/case-10): the queue is BullMQ on Redis in both editions; only the outbox table's schema authority differs.
|
|
246
268
|
liteAdapter:
|
|
247
269
|
summary: >-
|
|
248
|
-
Under lite the queue pattern is
|
|
249
|
-
|
|
270
|
+
Under lite the queue pattern is full's, adapter included: producers write the outbox in the caller's transaction, the relay
|
|
271
|
+
adds each committed row to BullMQ with the outbox id as the job id, BullMQ workers run the processors with the queue policy's
|
|
272
|
+
attempts and exponential backoff, and a job that exhausts its attempts stays in BullMQ's failed set (the dead letter). The
|
|
273
|
+
queue needs Redis: a lite app that declares queue or fenced-job declares `backing: [redis]` in hfs.json sides.be, and its
|
|
274
|
+
stack runs one Redis with AOF persistence (appendonly yes, appendfsync everysec) on a named volume. One difference from
|
|
275
|
+
full's adapter: the relay marks an outbox row sent only after the transport's persist step confirms Redis wrote the job to
|
|
276
|
+
its append-only file (WAITAOF) and still holds it, so a job Redis loses inside the at most one second fsync window leaves
|
|
277
|
+
its row waiting and is added again; that is why a lite queue does not lose a job. Without a declared Redis there is no queue
|
|
278
|
+
pattern: the work is a direct call inside the request, or a declared replicated-job running a cli command. Supabase Queues
|
|
279
|
+
(pgmq) is not a queue transport. `starci app add queue|job` writes this adapter in a lite app, with the outbox and job
|
|
280
|
+
tables as one supabase migration, the Redis declaration and the persisted Redis of every docker-swarm environment.
|
|
250
281
|
ports:
|
|
251
|
-
- {port: QueueOutbox (platform/queue), adapter:
|
|
252
|
-
- {port: queue transport (queue-transport.port.ts), adapter: "
|
|
253
|
-
- {port:
|
|
254
|
-
- {port:
|
|
255
|
-
|
|
282
|
+
- {port: QueueOutbox (platform/queue), adapter: "full's outbox row written through the caller's tx; the table comes from a supabase migration (no TypeORM entity under lite)"}
|
|
283
|
+
- {port: queue transport (queue-transport.port.ts), adapter: "full's bullmq-queue-transport.client.ts over the declared Redis (host, port, db and the password read from its sealed secret file), plus persist(jobs): WAITAOF 1 0 on each queue's connection, then each job's state, refusing with QUEUE_NOT_PERSISTED or QUEUE_JOB_LOST (errors/queue.error.ts)"}
|
|
284
|
+
- {port: outbox relay (queue-relay.service.ts), adapter: "full's relay, which calls persist after the adds and before it marks the rows sent, so a refusal rolls the mark back and the next pass adds the rows again"}
|
|
285
|
+
- {port: QueueWorkerRegistry (platform/queue), adapter: "full's BullMQ worker per queue: attempts and exponential backoff from queue.policy.ts, failed jobs kept as the dead letter"}
|
|
286
|
+
- {port: schedulers, adapter: "a BullMQ job scheduler declared with its queue, as full, or a pg_cron job in a supabase migration whose tick inserts one row of the queue constant into the queue outbox"}
|
|
287
|
+
forbids: [kafkajs, "@nestjs/schedule"]
|
|
256
288
|
proof: >-
|
|
257
|
-
Unit specs as full (queue-outbox, relay and worker services, the
|
|
258
|
-
requires integration specs titled `queue/<scenario>: ...` for every declared scenario on
|
|
259
|
-
|
|
289
|
+
Unit specs as full (queue-outbox, relay and worker services, the BullMQ client against a mocked factory); declaring queue
|
|
290
|
+
requires integration specs titled `queue/<scenario>: ...` for every declared scenario on the local Supabase stack (R284,
|
|
291
|
+
which runs no container of its own): the outbox, its commit boundary, the relay identity and the pg_cron tick are Postgres
|
|
292
|
+
facts proved there with the real outbox writer and relay, and the queue transport records what the relay hands to BullMQ
|
|
293
|
+
(the job id is the outbox id). BullMQ itself is covered by the unit spec of the BullMQ client, which also proves the
|
|
294
|
+
persist refusals; the relay's unit spec proves a refused persist leaves the rows waiting.
|