@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
|
@@ -7,16 +7,22 @@
|
|
|
7
7
|
// - inside a package.json: an e2e, stack or browser script (`test:e2e`, `test:stack`, `test:browser`,
|
|
8
8
|
// with a pre/post prefix too) or a dependency of the infrastructure lite does not run - a test-world container or proxy
|
|
9
9
|
// (testcontainers, toxiproxy) or a browser runner (playwright, cypress), and every library the lite infraOwners own nowhere
|
|
10
|
-
// while full owns them (
|
|
10
|
+
// while full owns them (Kafka: the lite event bus runs pgmq);
|
|
11
11
|
// - a declaration of what lite does not have: an app kind, pattern or trigger kind every slot of which is forbidden under
|
|
12
12
|
// lite. The forbidden names are DERIVED from the manifest, so adding or restoring a kind is a manifest edit, not an edit here
|
|
13
|
-
// (under the 2026-10-13 ruling no pattern, trigger or app kind is gone)
|
|
13
|
+
// (under the 2026-10-13 ruling no pattern, trigger or app kind is gone);
|
|
14
|
+
// - a backing service that is not declared or not used (ruling:R212/case-10): a client library of a service of
|
|
15
|
+
// ruleParams.be.backing (bullmq for redis) or a declared pattern that needs one (queue) while hfs.json sides.be.backing does
|
|
16
|
+
// not declare it is HFS_BACKING_UNDECLARED, as is a service the catalog's editions excludes (owner:lite-vector-is-pgvector),
|
|
17
|
+
// and a declared service no dependency of the app consumes is HFS_BACKING_UNUSED.
|
|
14
18
|
// A full app answers []: the rule tests the edition once and a full view forbids none of these names.
|
|
15
19
|
import { found, readJson } from './read.mjs';
|
|
16
20
|
import { DEPENDENCY_SECTIONS } from './fe-no-tests.mjs';
|
|
17
|
-
import { editionRuleParams } from '../edition-slots.mjs';
|
|
21
|
+
import { editionRuleParams, judgedInEdition } from '../edition-slots.mjs';
|
|
18
22
|
|
|
19
23
|
export const HFS_EDITION_FORBIDDEN_PRESENT = 'HFS_EDITION_FORBIDDEN_PRESENT';
|
|
24
|
+
export const HFS_BACKING_UNDECLARED = 'HFS_BACKING_UNDECLARED';
|
|
25
|
+
export const HFS_BACKING_UNUSED = 'HFS_BACKING_UNUSED';
|
|
20
26
|
|
|
21
27
|
const PACKAGE_JSON = /(?:^|\/)package\.json$/;
|
|
22
28
|
/** The scripts of a world lite does not have: the e2e layer, the self-hosted stack and the browser journey. */
|
|
@@ -54,8 +60,8 @@ function editionGone(resolver) {
|
|
|
54
60
|
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
55
61
|
|
|
56
62
|
/**
|
|
57
|
-
* The specifiers a lite back end may import nowhere although full gives them an owner: the
|
|
58
|
-
*
|
|
63
|
+
* The specifiers a lite back end may import nowhere although full gives them an owner: the infrastructure libraries the lite
|
|
64
|
+
* infraOwners override removes (kafkajs). Read from the two rule-parameter views, never listed here.
|
|
59
65
|
*/
|
|
60
66
|
function liteRemovedLibraries(manifest) {
|
|
61
67
|
const be = manifest?.ruleParams?.be;
|
|
@@ -72,24 +78,60 @@ function scriptFindings(rel, pkg) {
|
|
|
72
78
|
.map((name) => found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: a lite app has unit specs, optional integration specs on the local Supabase stack and optional contract specs, never an e2e, stack or browser run`, { name }));
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
/** The backing services of the manifest (ruleParams.be.backing): name -> {clients, patterns, images, editions?}. */
|
|
82
|
+
const backingCatalog = (manifest) => manifest?.ruleParams?.be?.backing ?? {};
|
|
83
|
+
|
|
84
|
+
/** The backing service `name` is a client library of, or null. */
|
|
85
|
+
const serviceOfClient = (catalog, name) => Object.keys(catalog).find((service) => catalog[service].clients.includes(name)) ?? null;
|
|
86
|
+
|
|
87
|
+
/** The dependency names of a package.json, every section. */
|
|
88
|
+
const dependencyNames = (pkg) => DEPENDENCY_SECTIONS.flatMap((section) => (isPlainObject(pkg?.[section]) ? Object.keys(pkg[section]) : []));
|
|
89
|
+
|
|
90
|
+
function dependencyFindings(rel, pkg, { removed, catalog, declared }) {
|
|
76
91
|
const findings = [];
|
|
77
92
|
for (const section of DEPENDENCY_SECTIONS) {
|
|
78
93
|
const deps = pkg[section];
|
|
79
94
|
if (!isPlainObject(deps)) continue;
|
|
80
95
|
for (const name of Object.keys(deps)) {
|
|
96
|
+
const service = serviceOfClient(catalog, name);
|
|
81
97
|
if (LITE_FORBIDDEN_TEST_INFRA.test(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: a lite app runs no test containers, fault proxy or browser journey - its integration specs run against \`supabase start\``, { section, name }));
|
|
82
|
-
else if (removed.has(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: lite
|
|
98
|
+
else if (removed.has(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: lite runs no self-hosted Kafka - the event bus port is implemented on Supabase (pgmq) or in process`, { section, name }));
|
|
99
|
+
else if (service !== null && !judgedInEdition(catalog[service], 'lite')) findings.push(found(HFS_BACKING_UNDECLARED, rel, `${rel} ${section} has ${name}, a client of ${service}, which the lite edition does not admit: remove the dependency`, { section, name, service }));
|
|
100
|
+
else if (service !== null && !declared.has(service)) findings.push(found(HFS_BACKING_UNDECLARED, rel, `${rel} ${section} has ${name}, a client of ${service}, which hfs.json sides.be.backing does not declare: declare ${service} for the capability that needs it, or remove the dependency`, { section, name, service }));
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
103
|
return findings;
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
/** The findings one package.json's scripts and dependencies produce under lite. */
|
|
89
|
-
function packageFindings(repoRoot, rel,
|
|
107
|
+
function packageFindings(repoRoot, rel, context) {
|
|
90
108
|
const pkg = readJson(repoRoot, rel);
|
|
91
109
|
if (!isPlainObject(pkg)) return [];
|
|
92
|
-
return [...scriptFindings(rel, pkg), ...dependencyFindings(rel, pkg,
|
|
110
|
+
return [...scriptFindings(rel, pkg), ...dependencyFindings(rel, pkg, context)];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The backing findings of the be declaration: a declared pattern whose service is undeclared, and (at the app root or a
|
|
115
|
+
* standalone back end, whose package.json carries the back end's dependencies) a declared service no dependency consumes.
|
|
116
|
+
*/
|
|
117
|
+
function backingFindings({ repoRoot, beView, catalog, ownsPackage }) {
|
|
118
|
+
const declared = new Set(beView.backing ?? []);
|
|
119
|
+
const findings = [];
|
|
120
|
+
for (const service of declared) {
|
|
121
|
+
if (catalog[service] && !judgedInEdition(catalog[service], 'lite')) findings.push(found(HFS_BACKING_UNDECLARED, 'hfs.json', `hfs.json sides.be.backing declares ${service}, which the lite edition does not admit: remove the declaration`, { service }));
|
|
122
|
+
}
|
|
123
|
+
for (const [service, entry] of Object.entries(catalog)) {
|
|
124
|
+
const needing = (beView.patterns ?? []).filter((pattern) => entry.patterns.includes(pattern));
|
|
125
|
+
if (needing.length && !declared.has(service)) findings.push(found(HFS_BACKING_UNDECLARED, 'hfs.json', `hfs.json declares the ${needing.join(', ')} pattern, which runs on ${service}: declare ${service} in sides.be.backing`, { service, patterns: needing }));
|
|
126
|
+
}
|
|
127
|
+
if (!ownsPackage) return findings;
|
|
128
|
+
const dependencies = new Set(dependencyNames(readJson(repoRoot, 'package.json')));
|
|
129
|
+
for (const service of declared) {
|
|
130
|
+
if (!judgedInEdition(catalog[service], 'lite')) continue;
|
|
131
|
+
const clients = catalog[service]?.clients ?? [];
|
|
132
|
+
if (!clients.some((name) => dependencies.has(name))) findings.push(found(HFS_BACKING_UNUSED, 'hfs.json', `hfs.json sides.be.backing declares ${service}, but no dependency consumes it (${clients.join(', ')}): remove the declaration, or add the capability that needs it`, { service }));
|
|
133
|
+
}
|
|
134
|
+
return findings;
|
|
93
135
|
}
|
|
94
136
|
|
|
95
137
|
const goneAppFindings = (repo, gone) => (repo.apps ?? [])
|
|
@@ -121,11 +163,13 @@ export function editionFindings({ repoRoot, files, repo, resolver, manifest, wit
|
|
|
121
163
|
let beView = null;
|
|
122
164
|
if (withDeclaration && repo.profile === 'app') beView = repo.sides?.be;
|
|
123
165
|
else if (withDeclaration && repo.profile === 'be') beView = repo;
|
|
124
|
-
const
|
|
125
|
-
const
|
|
166
|
+
const catalog = backingCatalog(manifest);
|
|
167
|
+
const findings = beView ? [...declarationFindings(beView, gone), ...backingFindings({ repoRoot, beView, catalog, ownsPackage: true })] : [];
|
|
168
|
+
const declared = new Set((repo.profile === 'app' ? repo.sides?.be?.backing : repo.backing) ?? []);
|
|
169
|
+
const context = { removed: liteRemovedLibraries(manifest), catalog, declared };
|
|
126
170
|
for (const file of files) {
|
|
127
171
|
if (file.includes('node_modules/') || !PACKAGE_JSON.test(file)) continue;
|
|
128
|
-
findings.push(...packageFindings(repoRoot, file,
|
|
172
|
+
findings.push(...packageFindings(repoRoot, file, context));
|
|
129
173
|
}
|
|
130
174
|
return findings;
|
|
131
175
|
}
|
|
@@ -11,6 +11,7 @@ import { byCodeUnit } from '../../lib/list.mjs';
|
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
13
13
|
import { isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
14
|
+
import { lexicalChecker } from '../../lib/ts-analysis.mjs';
|
|
14
15
|
|
|
15
16
|
export const FE_GRAPHQL_CONTRACT = 'FE_GRAPHQL_CONTRACT';
|
|
16
17
|
|
|
@@ -61,16 +62,6 @@ function templateText(ts, node, checker, names, visiting = new Set()) {
|
|
|
61
62
|
return text;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
/** Bind this file in isolation: the compiler resolves lexical names, but imports and library files are never loaded. */
|
|
65
|
-
function lexicalChecker(ts, source) {
|
|
66
|
-
const options = { noLib: true, noResolve: true, allowJs: true };
|
|
67
|
-
const host = ts.createCompilerHost(options);
|
|
68
|
-
host.getSourceFile = (file) => file === source.fileName ? source : undefined;
|
|
69
|
-
host.fileExists = () => false;
|
|
70
|
-
host.readFile = () => undefined;
|
|
71
|
-
return ts.createProgram([source.fileName], options, host).getTypeChecker();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
65
|
/** Actual tagged-template AST nodes: comments, quoted examples and other template tags are not operations. */
|
|
75
66
|
function gqlDocuments(ts, file, text) {
|
|
76
67
|
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
@@ -11,6 +11,7 @@ import { parseYaml } from '../../../engine/yaml.mjs';
|
|
|
11
11
|
import { found, readJson } from './read.mjs';
|
|
12
12
|
import { DEPENDENCY_SECTIONS } from './fe-no-tests.mjs';
|
|
13
13
|
import { editionRuleParams } from '../edition-slots.mjs';
|
|
14
|
+
import { globExpression } from '../../lib/glob.mjs';
|
|
14
15
|
|
|
15
16
|
export const FE_STACK_FORBIDDEN = 'FE_STACK_FORBIDDEN';
|
|
16
17
|
export const FE_STACK_UNLISTED = 'FE_STACK_UNLISTED';
|
|
@@ -21,18 +22,23 @@ const APP_ROOT_MANIFEST = 'package.json';
|
|
|
21
22
|
|
|
22
23
|
let forbidden = null;
|
|
23
24
|
|
|
24
|
-
/** name
|
|
25
|
+
/** Package-name patterns and their concern, canonical packages and guidance, read once. */
|
|
25
26
|
function forbiddenPackages() {
|
|
26
27
|
if (forbidden) return forbidden;
|
|
27
28
|
const catalog = parseYaml(fs.readFileSync(CATALOG_FILE, 'utf8'));
|
|
28
29
|
if (catalog?.schema !== 'starci/fe-stack@1' || !Array.isArray(catalog.concerns)) throw new Error('knowledge/hfs/fe-stack.yaml is not a starci/fe-stack@1 catalog');
|
|
29
|
-
forbidden =
|
|
30
|
+
forbidden = [];
|
|
30
31
|
for (const concern of catalog.concerns) {
|
|
31
|
-
for (const name of concern.forbids ?? []) forbidden.
|
|
32
|
+
for (const name of concern.forbids ?? []) forbidden.push({ matches: globExpression(name), concern: concern.id, canonical: concern.packages ?? [], why: concern.why });
|
|
32
33
|
}
|
|
33
34
|
return forbidden;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/** A forbidden package or npm scope is judged before the closed dependency catalog. */
|
|
38
|
+
function forbiddingPackage(name) {
|
|
39
|
+
return forbiddenPackages().find(entry => entry.matches.test(name));
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* The packages the back end owns (be infraOwners with at least one owner, full's view: a library lite removes is the edition rule's
|
|
38
44
|
* finding, not a front-end one): a root dependency of the back end.
|
|
@@ -47,14 +53,13 @@ function backendOwned(manifest) {
|
|
|
47
53
|
function manifestFindings(repoRoot, rel, skip) {
|
|
48
54
|
const pkg = readJson(repoRoot, rel);
|
|
49
55
|
if (!pkg || typeof pkg !== 'object') return [];
|
|
50
|
-
const catalog = forbiddenPackages();
|
|
51
56
|
const findings = [];
|
|
52
57
|
for (const section of DEPENDENCY_SECTIONS) {
|
|
53
58
|
for (const name of Object.keys(pkg[section] ?? {})) {
|
|
54
|
-
const entry =
|
|
59
|
+
const entry = forbiddingPackage(name);
|
|
55
60
|
if (!entry || skip.has(name)) continue;
|
|
56
61
|
const canonical = entry.canonical.length ? entry.canonical.join(', ') : 'the canonical library of the concern';
|
|
57
|
-
findings.push(found(FE_STACK_FORBIDDEN, rel, `${rel} ${section} declares ${name}, which the front-end stack forbids for the ${entry.concern} concern (knowledge/hfs/fe-stack.yaml); use ${canonical} and delete ${name}`, { section, dependency: name, concern: entry.concern }));
|
|
62
|
+
findings.push(found(FE_STACK_FORBIDDEN, rel, `${rel} ${section} declares ${name}, which the front-end stack forbids for the ${entry.concern} concern (knowledge/hfs/fe-stack.yaml); use ${canonical} and delete ${name}. ${entry.why}`, { section, dependency: name, concern: entry.concern }));
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
return findings;
|
|
@@ -98,7 +103,7 @@ function extraPackages(declaration) {
|
|
|
98
103
|
function unlistedFindings(rel, pkg, allowed, extras, workspaces) {
|
|
99
104
|
const findings = [];
|
|
100
105
|
for (const section of DEPENDENCY_SECTIONS) for (const name of Object.keys(pkg?.[section] ?? {})) {
|
|
101
|
-
if (allowed.has(name) || extras.has(name) || workspaces.has(name) ||
|
|
106
|
+
if (allowed.has(name) || extras.has(name) || workspaces.has(name) || forbiddingPackage(name)) continue;
|
|
102
107
|
findings.push(found(FE_STACK_UNLISTED, rel, `${rel} ${section} declares ${name}, absent from knowledge/hfs/fe-stack.yaml; add a canonical concern or declare sides.fe.extraPackages with its name and a nonempty reason in hfs.json`, { section, dependency: name }));
|
|
103
108
|
}
|
|
104
109
|
return findings;
|
|
@@ -11,7 +11,7 @@ const LITE_PROOF_LANE_MISSING = 'BE_LITE_PROOF_LANE_MISSING';
|
|
|
11
11
|
const PATTERN_SPEC_MISSING = 'BE_PATTERN_SPEC_MISSING';
|
|
12
12
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const INTEGRATION_SPEC = /^be\/src\/tests\/integration\/.+\.integration-spec\.ts$/;
|
|
14
|
-
const PATTERNS = new Set(['event-bus', 'queue', 'webhooks', 'realtime', 'fenced-job', 'projection', 'saga']);
|
|
14
|
+
const PATTERNS = new Set(['logging', 'event-bus', 'queue', 'webhooks', 'realtime', 'fenced-job', 'projection', 'saga', 'idempotency']);
|
|
15
15
|
// Fallback saga scenarios until the shared manifest provides its scenario table.
|
|
16
16
|
const SAGA_SCENARIOS = ['compensation-on-failure', 'step-order', 'state-version-fences', 'consumer-dedupes'];
|
|
17
17
|
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
// R276: lite
|
|
1
|
+
// R276: lite keeps Postgres and pgvector on managed Supabase, admits only the backing services available to its edition,
|
|
2
|
+
// and runs a declared Redis as one service with AOF fsynced at least every second on a named volume and no eviction
|
|
3
|
+
// (ruling:R212/case-10, owner:lite-vector-is-pgvector); sidecar SQLite persists on declared named volumes.
|
|
2
4
|
import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
|
|
3
5
|
import { composeEntries } from '../../lib/compose-values.mjs';
|
|
4
6
|
import { repositoryOf } from '../../lib/stack-services.mjs';
|
|
7
|
+
import { loadSlotManifest } from '../slots.mjs';
|
|
8
|
+
import { judgedInEdition } from '../edition-slots.mjs';
|
|
9
|
+
import { readJson } from './read.mjs';
|
|
5
10
|
import { stacksDeploymentFindings } from './stacks-runtime.mjs';
|
|
6
11
|
|
|
7
|
-
const
|
|
12
|
+
const DATABASE_IMAGE = /^(?:postgres(?:ql)?|postgresql-repmgr|mongo(?:db)?|mongodb-community-server)$/i;
|
|
13
|
+
const APPEND_ONLY = /(?:^|\s)--appendonly\s+yes(?:\s|$)/;
|
|
14
|
+
const EVICTION_POLICY = /(?:^|\s)--maxmemory-policy\s+(\S+)/;
|
|
15
|
+
const FSYNC_POLICY = /(?:^|\s)--appendfsync\s+(\S+)/;
|
|
16
|
+
const BOUNDED_FSYNC = new Set(['everysec', 'always']);
|
|
17
|
+
const REDIS_DATA_DIR = '/data';
|
|
8
18
|
const DATABASE_ADAPTER_KEYS = new Set(['db_type', 'database_type', 'db_provider', 'database_provider', 'db_client', 'database_client', 'db_adapter', 'database_adapter']);
|
|
9
19
|
const SQLITE_ADAPTERS = new Set(['sqlite', 'sqlite3', 'better-sqlite3', 'sql.js']);
|
|
10
20
|
const DATABASE_URL_KEY = /DB|DATABASE|POSTGRES|REDIS|MONGO/i;
|
|
@@ -14,18 +24,72 @@ const NAMED_VOLUME = /^[a-z0-9][a-z0-9_.-]*$/i;
|
|
|
14
24
|
const mapping = value => plain(value) ? value : {};
|
|
15
25
|
const present = value => typeof value === 'string' && Boolean(value.trim());
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
/** The backing service (a key of ruleParams.be.backing) whose image repository `image` names, or null. */
|
|
28
|
+
function serviceOfImage(catalog, image) {
|
|
29
|
+
if (!present(image)) return null;
|
|
30
|
+
const repository = repositoryOf(image).toLowerCase();
|
|
31
|
+
return Object.keys(catalog).find(service => catalog[service].images.includes(repository)) ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The services hfs.json sides.be.backing declares: from the resolved declaration, else from hfs.json at the app root. */
|
|
35
|
+
function declaredBacking(repo, repoRoot) {
|
|
36
|
+
const declared = repo?.sides?.be?.backing ?? repo?.backing ?? (present(repoRoot) ? readJson(repoRoot, 'hfs.json')?.sides?.be?.backing : undefined);
|
|
37
|
+
return new Set(Array.isArray(declared) ? declared : []);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function imageIssues(components, services, { catalog, declared }) {
|
|
18
41
|
const errors = [];
|
|
19
42
|
for (const [prefix, inventory] of [['components', components], ['services', services]]) {
|
|
20
43
|
for (const [id, entry] of Object.entries(inventory)) {
|
|
21
|
-
if (present(entry?.image)
|
|
44
|
+
if (!present(entry?.image)) continue;
|
|
45
|
+
const service = serviceOfImage(catalog, entry.image);
|
|
46
|
+
if (DATABASE_IMAGE.test(repositoryOf(entry.image)))
|
|
22
47
|
errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
|
|
23
|
-
message: 'lite must not run a self-hosted Postgres
|
|
48
|
+
message: 'lite must not run a self-hosted Postgres or Mongo database image; use managed Supabase backing' });
|
|
49
|
+
else if (service !== null && !judgedInEdition(catalog[service], 'lite'))
|
|
50
|
+
errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
|
|
51
|
+
message: `lite must not run ${service}; vector search uses Supabase pgvector (owner:lite-vector-is-pgvector)` });
|
|
52
|
+
else if (service !== null && !declared.has(service))
|
|
53
|
+
errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
|
|
54
|
+
message: `lite runs ${service} only when hfs.json sides.be.backing declares it for the capability that needs it (ruling:R212/case-10)` });
|
|
24
55
|
}
|
|
25
56
|
}
|
|
26
57
|
return errors;
|
|
27
58
|
}
|
|
28
59
|
|
|
60
|
+
const commandText = service => (Array.isArray(service?.command) ? service.command.map(String).join(' ') : String(service?.command ?? ''));
|
|
61
|
+
|
|
62
|
+
function persistentDataMount(entry, volumes) {
|
|
63
|
+
if (typeof entry === 'string') {
|
|
64
|
+
const [name, target, options = ''] = entry.split(':');
|
|
65
|
+
return NAMED_VOLUME.test(name) && Object.hasOwn(volumes, name) && target === REDIS_DATA_DIR && !options.split(',').includes('ro');
|
|
66
|
+
}
|
|
67
|
+
return plain(entry) && entry.type === 'volume' && Object.hasOwn(volumes, entry.source) && entry.target === REDIS_DATA_DIR && entry.read_only !== true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* AOF on a writable named volume at the data directory, fsynced at least every second (everysec, the Redis default, or always),
|
|
72
|
+
* and no eviction policy but noeviction (the Redis default).
|
|
73
|
+
*/
|
|
74
|
+
function persisted(service, volumes) {
|
|
75
|
+
const command = commandText(service);
|
|
76
|
+
const policy = EVICTION_POLICY.exec(command)?.[1].replaceAll(/["']/g, '');
|
|
77
|
+
const fsync = FSYNC_POLICY.exec(command)?.[1].replaceAll(/["']/g, '');
|
|
78
|
+
return APPEND_ONLY.test(command) && (policy === undefined || policy === 'noeviction') && (fsync === undefined || BOUNDED_FSYNC.has(fsync))
|
|
79
|
+
&& (Array.isArray(service.volumes) ? service.volumes : []).some(entry => persistentDataMount(entry, volumes));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Queue jobs live in Redis, so a declared Redis runs as exactly one stack service that persists every job and evicts none. */
|
|
83
|
+
function redisPersistenceIssues(services, volumes, { catalog, declared }) {
|
|
84
|
+
if (!declared.has('redis')) return [];
|
|
85
|
+
const redis = Object.entries(services).filter(([, service]) => serviceOfImage(catalog, service?.image) === 'redis');
|
|
86
|
+
if (redis.length !== 1) return [{ code: 'STACKS_LITE_REDIS_PERSISTENCE', at: 'services',
|
|
87
|
+
message: `hfs.json sides.be.backing declares redis, so the lite stack runs exactly one Redis service; it runs ${redis.length}` }];
|
|
88
|
+
return redis.filter(([, service]) => !persisted(service, volumes))
|
|
89
|
+
.map(([id]) => ({ code: 'STACKS_LITE_REDIS_PERSISTENCE', at: `services.${id}`,
|
|
90
|
+
message: `the lite Redis runs redis-server --appendonly yes with appendfsync everysec (the default) or always, a writable named volume mounted at ${REDIS_DATA_DIR} and maxmemory-policy noeviction (the default), so queue jobs survive a restart and are never evicted` }));
|
|
91
|
+
}
|
|
92
|
+
|
|
29
93
|
function unsupportedAdapter(service) {
|
|
30
94
|
for (const [key, value] of composeEntries(service?.environment, { stringsOnly: true })) {
|
|
31
95
|
const adapterKey = DATABASE_ADAPTER_KEYS.has(key.toLowerCase());
|
|
@@ -94,13 +158,15 @@ function sidecarIssues({ components, services, bindings, volumes }) {
|
|
|
94
158
|
}
|
|
95
159
|
|
|
96
160
|
/** The lite backing contract; full keeps its existing backing choices. */
|
|
97
|
-
export function liteBackingIssues({ doc, environment, spec = doc?.environments?.[environment], model, repo, edition = repo?.edition }) {
|
|
161
|
+
export function liteBackingIssues({ doc, environment, spec = doc?.environments?.[environment], model, repo, repoRoot, edition = repo?.edition }) {
|
|
98
162
|
if (edition !== 'lite' || spec?.runtime !== 'docker-swarm') return [];
|
|
99
|
-
const components = mapping(doc?.components), services = mapping(model?.services);
|
|
163
|
+
const components = mapping(doc?.components), services = mapping(model?.services), volumes = mapping(model?.volumes);
|
|
164
|
+
const backing = { catalog: mapping(loadSlotManifest().ruleParams?.be?.backing), declared: declaredBacking(repo, repoRoot) };
|
|
100
165
|
return [
|
|
101
|
-
...imageIssues(components, services),
|
|
166
|
+
...imageIssues(components, services, backing),
|
|
167
|
+
...redisPersistenceIssues(services, volumes, backing),
|
|
102
168
|
...renderedAdapterIssues(components, services),
|
|
103
|
-
...sidecarIssues({ components, services, bindings: mapping(spec.components), volumes
|
|
169
|
+
...sidecarIssues({ components, services, bindings: mapping(spec.components), volumes }),
|
|
104
170
|
];
|
|
105
171
|
}
|
|
106
172
|
|
|
@@ -23,7 +23,7 @@ export function hostCrontabFindings({ repoRoot, files }) {
|
|
|
23
23
|
if (text === null) continue;
|
|
24
24
|
for (const [index, line] of text.split(/\r?\n/).entries()) {
|
|
25
25
|
if (HOST_CRON.test(line)) findings.push(found(CODE, file,
|
|
26
|
-
`${file}:${index + 1}: host crontab installation is forbidden; recurring work must use an app scheduler,
|
|
26
|
+
`${file}:${index + 1}: host crontab installation is forbidden; recurring work must use an app queue scheduler, a pg_cron tick enqueueing through the queue outbox, or a declared Swarm scheduler and replicated-job`, { line: index + 1 }));
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
return findings;
|
|
@@ -381,14 +381,25 @@ function appComposesRealtime(repoRoot, app, files) {
|
|
|
381
381
|
return false;
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
const SHARED_HUBS = new Set(['postgres', 'redis']);
|
|
385
|
+
|
|
386
|
+
/** The hub a service selects with REALTIME_HUB, or null when it declares none. */
|
|
387
|
+
function realtimeHub(service) {
|
|
388
|
+
const entry = composeEntries(service.environment, { missingValue: null }).find(([key]) => key === 'REALTIME_HUB');
|
|
389
|
+
return entry && present(entry[1]) ? String(entry[1]).trim() : null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** A service runs on several nodes: more than one replica, or the global mode (one task per node). */
|
|
393
|
+
const scaledOut = service => service.deploy?.mode === 'global' || Number(service.deploy?.replicas ?? 1) > 1;
|
|
394
|
+
|
|
395
|
+
/** Realtime scale (knowledge/patterns/be/realtime.yaml BE-REALTIME-7): an API with several replicas selects a shared hub. */
|
|
384
396
|
function realtimeIssues(input, add) {
|
|
385
|
-
if (input.repo?.edition !== 'lite') return;
|
|
386
397
|
const apis = new Set(list(input.repo?.sides?.be?.apps).filter(app => app.kind === 'api').map(app => app.name));
|
|
387
398
|
for (const [id, service] of Object.entries(mapping(input.model.services))) {
|
|
388
399
|
const component = mapping(input.doc.components)[id], app = component?.app ?? id;
|
|
389
|
-
if (!apis.has(app) || service.
|
|
390
|
-
if (
|
|
391
|
-
add('STACKS_REALTIME_SCALED', `services.${id}.
|
|
400
|
+
if (!apis.has(app) || !scaledOut(service) || !appComposesRealtime(input.repoRoot, app, list(input.files))) continue;
|
|
401
|
+
if (!SHARED_HUBS.has(realtimeHub(service) ?? ''))
|
|
402
|
+
add('STACKS_REALTIME_SCALED', `services.${id}.environment.REALTIME_HUB`, 'A realtime API with more than one replica must select a shared hub (REALTIME_HUB postgres or redis): the memory hub serves one process only');
|
|
392
403
|
}
|
|
393
404
|
}
|
|
394
405
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// R311: the Vietnamese copy of an app reaches users as written. A message catalog (`<owner>.messages.ts` or a `vi.json` locale file)
|
|
2
|
+
// is valid UTF-8, and no `vi` string carries the mark of a lossy re-encode: a diacritic letter replaced by a literal `?`
|
|
3
|
+
// (so a word reads as letters around a question mark) or by the U+FFFD replacement character.
|
|
4
|
+
import { found, readText } from './read.mjs';
|
|
5
|
+
|
|
6
|
+
export const HFS_TEXT_CORRUPT = 'HFS_TEXT_CORRUPT';
|
|
7
|
+
const CATALOG_TS_SUFFIX = '.messages.ts';
|
|
8
|
+
const CATALOG_JSON = /(?:^|\/)vi\.json$/;
|
|
9
|
+
const OUTSIDE = /(?:^|\/)(?:node_modules|\.git|dist|build)\//;
|
|
10
|
+
const REPLACEMENT = '\uFFFD';
|
|
11
|
+
// A `?` between letters, doubled, or closing a word before punctuation is a lost diacritic; a sentence-final "?" and a lone "?" are not.
|
|
12
|
+
const LOST_LETTER = /\p{L}\?(?=[\p{L}?])|\?\?|\p{L}\?(?=[.,;:)])|(?:^|[\s("])\?\p{L}/u;
|
|
13
|
+
const STRING_LITERAL = /"((?:[^"\\\n]|\\.)*)"/g;
|
|
14
|
+
|
|
15
|
+
function lineOf(text, index) {
|
|
16
|
+
return text.slice(0, index).split('\n').length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The text of the `vi:` map of a catalog bundle: from `vi:` to the first closing brace at the bundle's own indent.
|
|
20
|
+
function viBlocks(text) {
|
|
21
|
+
const blocks = [];
|
|
22
|
+
for (const start of text.matchAll(/\bvi:\s*\{/g)) {
|
|
23
|
+
const from = start.index + start[0].length;
|
|
24
|
+
const end = text.indexOf('\n }', from);
|
|
25
|
+
blocks.push({ from, to: end < 0 ? text.length : end });
|
|
26
|
+
}
|
|
27
|
+
return blocks;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function jsonStrings(node, out) {
|
|
31
|
+
if (typeof node === 'string') out.push(node);
|
|
32
|
+
else if (node && typeof node === 'object') for (const value of Object.values(node)) jsonStrings(value, out);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function corruptLine(file, line, value) {
|
|
36
|
+
return found(HFS_TEXT_CORRUPT, file, `${file}:${line} Vietnamese text ${JSON.stringify(value)} lost its diacritics (a "?" or U+FFFD stands for a letter); rewrite the file as UTF-8 with an editor or Node, never a shell pipe`, { line });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function tsFindings(file, text) {
|
|
40
|
+
const findings = [];
|
|
41
|
+
for (const { from, to } of viBlocks(text)) {
|
|
42
|
+
for (const literal of text.slice(from, to).matchAll(STRING_LITERAL)) {
|
|
43
|
+
if (LOST_LETTER.test(literal[1])) findings.push(corruptLine(file, lineOf(text, from + literal.index), literal[1]));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return findings;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function jsonFindings(file, text) {
|
|
50
|
+
let doc;
|
|
51
|
+
try { doc = JSON.parse(text); } catch { return []; }
|
|
52
|
+
const strings = [];
|
|
53
|
+
jsonStrings(doc, strings);
|
|
54
|
+
return strings.filter((value) => LOST_LETTER.test(value)).map((value) => corruptLine(file, lineOf(text, text.indexOf(value)), value));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Corrupted Vietnamese copy in the message catalogs of this root/side scope. */
|
|
58
|
+
export function textIntegrityFindings({ repoRoot, files, repo, inScope = () => true }) {
|
|
59
|
+
if (repo.kind === 'runtime' || !['lite', 'full'].includes(repo.edition ?? 'full')) return [];
|
|
60
|
+
const findings = [];
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const ts = file.endsWith(CATALOG_TS_SUFFIX);
|
|
63
|
+
if (!inScope(file) || OUTSIDE.test(file) || !(ts || CATALOG_JSON.test(file))) continue;
|
|
64
|
+
const text = readText(repoRoot, file);
|
|
65
|
+
if (text === null) continue;
|
|
66
|
+
if (text.includes(REPLACEMENT)) {
|
|
67
|
+
findings.push(found(HFS_TEXT_CORRUPT, file, `${file}:${lineOf(text, text.indexOf(REPLACEMENT))} is not valid UTF-8 (it holds U+FFFD); rewrite the file as UTF-8 with an editor or Node`, { line: lineOf(text, text.indexOf(REPLACEMENT)) }));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
findings.push(...(ts ? tsFindings(file, text) : jsonFindings(file, text)));
|
|
71
|
+
}
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// hook-shape.mjs - RT_HOOK_SHAPE (knowledge/hfs/rules.yaml, gate runtime): the app hook templates keep the gate model.
|
|
2
|
+
// pre-commit L0 only: no typecheck and no test run (`test:` scripts, npm test, jest, vitest, a spec). Types and the targeted specs
|
|
3
|
+
// belong to the op gate.
|
|
4
|
+
// pre-push a release gate check, never a build or test run: no npm test, jest, vitest, typecheck or lint, and it carries the
|
|
5
|
+
// release gate markers: `starci-release` (the L4 record directory), `refs/backup/` (the backup namespace) and `v[0-9]`
|
|
6
|
+
// (the release tag pattern). A push of anything but the release cut and refs/backup/* is refused there.
|
|
7
|
+
// Guard hooks keep their host timeout above the runtime's own input, verdict, cleanup and output budgets.
|
|
8
|
+
// Comments are not judged (the templates explain what they do not run). Pure apart from ctx.read.
|
|
9
|
+
import { shellCommandSource } from '../../lib/shell-source.mjs';
|
|
10
|
+
import { HOOK_INPUT_LIMITS } from '../../lib/hook-limits.mjs';
|
|
11
|
+
|
|
12
|
+
export const CODE = 'RT_HOOK_SHAPE';
|
|
13
|
+
export const TEMPLATE_DIR = 'packages/hfs/templates/app/hooks/husky';
|
|
14
|
+
const PRE_COMMIT = `${TEMPLATE_DIR}/pre-commit`;
|
|
15
|
+
const PRE_PUSH = `${TEMPLATE_DIR}/pre-push`;
|
|
16
|
+
const HOST_SETTINGS = '.claude/settings.json';
|
|
17
|
+
const GUARD_LIMITS = 'modules/kernel/hook-input.yaml';
|
|
18
|
+
|
|
19
|
+
const PRE_COMMIT_FORBIDDEN = [
|
|
20
|
+
[/\btypecheck\b/, 'a typecheck'],
|
|
21
|
+
[/\btest:|\bnpm\s+(?:run\s+)?test\b|\bjest\b|\bvitest\b|\bspecs?\b/, 'a test run'],
|
|
22
|
+
];
|
|
23
|
+
const PRE_PUSH_FORBIDDEN = [
|
|
24
|
+
[/\bnpm\s+(?:run\s+)?test\b|\btest:|\bjest\b|\bvitest\b/, 'a test run'],
|
|
25
|
+
[/\btypecheck\b/, 'a typecheck'],
|
|
26
|
+
[/\blint\b/, 'a lint run'],
|
|
27
|
+
];
|
|
28
|
+
const PRE_PUSH_MARKERS = ['starci-release', 'refs/backup/', 'v[0-9]', 'RIGHTS_PUSH_NOT_RELEASE'];
|
|
29
|
+
|
|
30
|
+
const finding = (file, message) => ({ code: CODE, level: 'error', path: file, message: `${CODE} ${file}: ${message}` });
|
|
31
|
+
|
|
32
|
+
/** The RT_HOOK_SHAPE findings of one hook template: `name` is 'pre-commit' or 'pre-push', `text` its content. */
|
|
33
|
+
function fileHookShapeFindings({ path: file, name, text }) {
|
|
34
|
+
const body = shellCommandSource(text);
|
|
35
|
+
const found = [];
|
|
36
|
+
const forbidden = name === 'pre-commit' ? PRE_COMMIT_FORBIDDEN : PRE_PUSH_FORBIDDEN;
|
|
37
|
+
for (const [pattern, what] of forbidden) {
|
|
38
|
+
if (pattern.test(body)) found.push(finding(file, name === 'pre-commit' ? `the commit gate is L0 only and runs ${what}; types and targeted specs belong to the op gate` : `the push gate checks the release gate and runs ${what}; only the release cut tests`));
|
|
39
|
+
}
|
|
40
|
+
if (name === 'pre-push') {
|
|
41
|
+
const missing = PRE_PUSH_MARKERS.filter((marker) => !body.includes(marker));
|
|
42
|
+
if (missing.length) {
|
|
43
|
+
const markers = missing.map((m) => `\`${m}\``).join(', ');
|
|
44
|
+
found.push(finding(file, `the push gate lacks the release gate marker${missing.length === 1 ? '' : 's'} ${markers}; it must check the L4 record of HEAD and allow only the release cut and refs/backup/*`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Guard hooks finish inside the host timeout even when input, verdict, tree cleanup and both writes exhaust their budgets. */
|
|
51
|
+
function guardTimeoutFindings(ctx) {
|
|
52
|
+
if (!ctx.fileSet.has(HOST_SETTINGS)) return [];
|
|
53
|
+
const limits = HOOK_INPUT_LIMITS;
|
|
54
|
+
const budget = limits.minInputWindowMs + limits.verdictTimeoutMs + limits.teardownTimeoutMs + 2 * limits.outputTimeoutMs;
|
|
55
|
+
const found = budget < limits.hookTimeoutSeconds * 1000 ? [] : [finding(GUARD_LIMITS, 'the complete guard lifetime must be shorter than the host hook timeout')];
|
|
56
|
+
let settings;
|
|
57
|
+
try { settings = JSON.parse(ctx.read(HOST_SETTINGS)); }
|
|
58
|
+
catch { return [...found, finding(HOST_SETTINGS, 'the guard hook settings must be readable JSON')]; }
|
|
59
|
+
const hooks = (settings?.hooks?.PreToolUse ?? []).flatMap((entry) => entry.hooks ?? []);
|
|
60
|
+
for (const hook of hooks) {
|
|
61
|
+
if (!/\bguard\s+(?:command|seat-tools)\b/.test(String(hook.command ?? ''))) continue;
|
|
62
|
+
if (hook.timeout !== limits.hookTimeoutSeconds) found.push(finding(HOST_SETTINGS, `the guard hook timeout must match ${GUARD_LIMITS} hookTimeoutSeconds`));
|
|
63
|
+
}
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** RT_HOOK_SHAPE over the app hook templates and tracked host guard settings (ctx of scripts/hfs/runtime-check.mjs). */
|
|
68
|
+
export function hookShapeFindings(ctx) {
|
|
69
|
+
return [...guardTimeoutFindings(ctx), ...[['pre-commit', PRE_COMMIT], ['pre-push', PRE_PUSH]].flatMap(([name, file]) => {
|
|
70
|
+
if (!ctx.fileSet.has(file)) return [];
|
|
71
|
+
const text = ctx.read(file);
|
|
72
|
+
return text === null || text === undefined ? [] : fileHookShapeFindings({ path: file, name, text });
|
|
73
|
+
})];
|
|
74
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// release-notes.mjs - RELEASE_NOTES (knowledge/hfs/rules.yaml, gate runtime): a release tag `v<version>` on HEAD has its CHANGELOG.md section
|
|
2
|
+
// `## [<version>]` with no unfinished markers left in it and no `in preparation` mark. The tag message and the GitHub Release are that section
|
|
3
|
+
// (docs/git-governance.md), so a tag cut over an unfinished section would publish a half-written release. Only the tags that point at HEAD are
|
|
4
|
+
// judged: the release being cut. Pure apart from ctx.read and the git read (ctx.tagsAtHead overrides it).
|
|
5
|
+
import { tag } from '../../api/git/tag.mjs';
|
|
6
|
+
|
|
7
|
+
export const CODE = 'RELEASE_NOTES';
|
|
8
|
+
const RELEASE_TAG = /^v\d[\w.+-]*$/;
|
|
9
|
+
const UNFINISHED = /\b(?:TODO|PENDING|TBD)\b|in preparation/i;
|
|
10
|
+
|
|
11
|
+
/** The CHANGELOG section of `version`: {found, body}. Pure. */
|
|
12
|
+
export function changelogSection(changelog, version) {
|
|
13
|
+
const lines = String(changelog ?? '').split(/\r?\n/);
|
|
14
|
+
const start = lines.findIndex((line) => { const m = /^## \[([^\]]+)\]/.exec(line); return m && m[1] === version; });
|
|
15
|
+
if (start < 0) return { found: false, body: '' };
|
|
16
|
+
const end = lines.findIndex((line, i) => i > start && line.startsWith('## ['));
|
|
17
|
+
return { found: true, body: lines.slice(start, end < 0 ? undefined : end).join('\n') };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The RELEASE_NOTES findings of the release tags `tags` against `changelog` text. Pure. */
|
|
21
|
+
export function releaseNotesFindings({ tags, changelog, file = 'CHANGELOG.md' }) {
|
|
22
|
+
const found = [];
|
|
23
|
+
for (const name of tags.filter((t) => RELEASE_TAG.test(t))) {
|
|
24
|
+
const section = changelogSection(changelog, name.slice(1));
|
|
25
|
+
if (!section.found) { found.push({ code: CODE, level: 'error', path: file, message: `${CODE} ${file}: the release tag ${name} has no \`## [${name.slice(1)}]\` section: write the release notes before the tag` }); continue; }
|
|
26
|
+
const unfinished = new RegExp(UNFINISHED.source, 'gi');
|
|
27
|
+
const marks = [...new Set([...section.body.matchAll(unfinished)].map(([mark]) => mark))];
|
|
28
|
+
if (marks.length) {
|
|
29
|
+
const listedMarks = marks.map((m) => `\`${m}\``).join(', ');
|
|
30
|
+
found.push({ code: CODE, level: 'error', path: file, message: `${CODE} ${file}: the section of ${name} still holds ${listedMarks}: finish it before the tag` });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** RELEASE_NOTES over the checkout (ctx of scripts/hfs/runtime-check.mjs): the release tags at HEAD against the CHANGELOG at HEAD. */
|
|
37
|
+
export function releaseNotesRepoFindings(ctx) {
|
|
38
|
+
let tags = ctx.tagsAtHead;
|
|
39
|
+
if (!tags) {
|
|
40
|
+
try { const r = tag(['--points-at', 'HEAD'], { dir: ctx.repoRoot }); tags = r.status === 0 ? String(r.stdout).split(/\r?\n/).map((t) => t.trim()).filter(Boolean) : []; } catch { tags = []; }
|
|
41
|
+
}
|
|
42
|
+
const changelog = ctx.read('CHANGELOG.md');
|
|
43
|
+
return tags.some((t) => RELEASE_TAG.test(t)) ? releaseNotesFindings({ tags, changelog }) : [];
|
|
44
|
+
}
|
|
@@ -80,10 +80,10 @@ function placeholderValue(key) {
|
|
|
80
80
|
return generated;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/** Create missing members for this app's recipient, preserving owner-provisioned members. */
|
|
84
|
-
export function materializeDeclaredPlaceholders(repoRoot, { recipient, recipientsForFile, processEnv = process.env, sops, write } = {}) {
|
|
83
|
+
/** Create missing members (those `only` selects) for this app's recipient, preserving owner-provisioned members. */
|
|
84
|
+
export function materializeDeclaredPlaceholders(repoRoot, { recipient, recipientsForFile, processEnv = process.env, sops, write, only = () => true } = {}) {
|
|
85
85
|
const members = [];
|
|
86
|
-
for (const member of declaredSealedMembers(repoRoot)) {
|
|
86
|
+
for (const member of declaredSealedMembers(repoRoot).filter((declared) => only(declared))) {
|
|
87
87
|
const relative = `.starcistacks/${member.env}/secrets/${member.slug}.enc`;
|
|
88
88
|
if (fs.existsSync(path.join(repoRoot, relative))) continue;
|
|
89
89
|
const age = recipientsForFile?.(path.join(repoRoot, relative)) ?? [recipient];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// slot-side-problems.mjs - declaration checks for one app side against the HFS manifest.
|
|
2
|
-
import { optionalSlotProblems } from './declaration-slots.mjs';
|
|
2
|
+
import { backingProblems, optionalSlotProblems } from './declaration-slots.mjs';
|
|
3
3
|
|
|
4
4
|
function sideAppKindProblems(manifest, side, apps) {
|
|
5
5
|
const bad = [];
|
|
@@ -19,9 +19,9 @@ function requiredAppKindProblems(manifest, side, declaration) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** Problems for one side declaration, preserving the manifest's checks and their order. */
|
|
22
|
-
export function sideProblems(manifest, side, declaration) {
|
|
22
|
+
export function sideProblems(manifest, side, declaration, edition = 'full') {
|
|
23
23
|
const bad = sideAppKindProblems(manifest, side, declaration.apps);
|
|
24
|
-
bad.push(...optionalSlotProblems(manifest, side, declaration));
|
|
24
|
+
bad.push(...optionalSlotProblems(manifest, side, declaration), ...backingProblems(manifest, side, declaration, edition));
|
|
25
25
|
for (const read of declaration.reads ?? []) if (!manifest.sides[side].reads.includes(read)) bad.push(`sides.${side}.reads names ${read}; ${side} may read only ${manifest.sides[side].reads.join(', ') || 'nothing of the other side'}`);
|
|
26
26
|
bad.push(...requiredAppKindProblems(manifest, side, declaration));
|
|
27
27
|
return bad;
|
|
@@ -116,6 +116,7 @@ function sideDeclaration(manifest, declaration, name, edition, providers) {
|
|
|
116
116
|
apps: Object.freeze(s.apps.map((a) => Object.freeze({ name: a.name, kind: a.kind }))),
|
|
117
117
|
optionalSlots: Object.freeze([...(s.optionalSlots ?? [])]),
|
|
118
118
|
patterns: Object.freeze([...(s.patterns ?? [])]), kinds: Object.freeze([...(s.kinds ?? [])]),
|
|
119
|
+
backing: Object.freeze([...(s.backing ?? [])]),
|
|
119
120
|
connections: Object.freeze((s.connections ?? []).map(connectionView)),
|
|
120
121
|
providers,
|
|
121
122
|
reads: Object.freeze([...(s.reads ?? [])]),
|
|
@@ -139,7 +140,7 @@ export function resolveRepoDeclaration(manifest, declaration, { file = HFS_DECLA
|
|
|
139
140
|
if (declaration.kind === RUNTIME_KIND) return runtimeDeclaration(manifest, declaration, file, side);
|
|
140
141
|
const { edition, known, valid } = declarationEdition(manifest, declaration);
|
|
141
142
|
if (!valid) fail('HFS_EDITION_INVALID', `hfs.json edition is ${JSON.stringify(declaration.edition)}; the editions this manifest knows are ${known.join(', ')} (absent means full)`, { file, edition: declaration.edition });
|
|
142
|
-
const bad = PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name]));
|
|
143
|
+
const bad = PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name], edition));
|
|
143
144
|
if (bad.length) declarationInvalid(bad, file);
|
|
144
145
|
if (side !== null && !PROFILES.includes(side)) fail('HFS_DECLARATION_INVALID', `${side} is not a side of an app (be, fe)`, { file, side });
|
|
145
146
|
// A provider declared on any connection enables the provider slots of every profile (a fe side declares none): each view carries the union.
|