@starci/hfs 4.0.8 → 4.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +81 -39
- package/emit/contracts.mjs +56 -9
- package/emit/db-types.mjs +169 -0
- package/emit/events.mjs +41 -0
- package/emit/operations-worker.mjs +1 -1
- package/emit/operations.mjs +2 -2
- package/emit/schema-worker.mjs +1 -1
- package/lint/bound-sys.cjs +50 -0
- package/lint/run.mjs +131 -24
- package/package.json +16 -10
- package/report/sonar.mjs +8 -8
- package/runtime/engine/admission.mjs +308 -0
- package/runtime/engine/canonical-json.mjs +10 -0
- package/runtime/engine/config.mjs +486 -0
- package/runtime/engine/db/blob.mjs +315 -0
- package/runtime/engine/db/ledger-paths.mjs +83 -0
- package/runtime/engine/db/ledger.mjs +1118 -0
- package/runtime/engine/db/machine-connection.mjs +135 -0
- package/runtime/engine/db/machine-schema.mjs +84 -0
- package/runtime/engine/db/machine.mjs +1367 -0
- package/runtime/engine/db/migrations/machine/0001-init.sql +924 -0
- package/runtime/engine/db/migrations/runtime/0001-init.sql +1108 -0
- package/runtime/engine/db/provider-reservations.mjs +101 -0
- package/runtime/engine/digest.mjs +16 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/engine/secrets.mjs +130 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +162 -31
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +912 -120
- package/runtime/knowledge/patterns/be/api.yaml +369 -0
- package/runtime/knowledge/patterns/be/cli.yaml +297 -0
- package/runtime/knowledge/patterns/be/domain.yaml +113 -0
- package/runtime/knowledge/patterns/be/event-bus.yaml +285 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +261 -0
- package/runtime/knowledge/patterns/be/projections.yaml +178 -0
- package/runtime/knowledge/patterns/be/queues.yaml +236 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +140 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +222 -0
- package/runtime/knowledge/patterns/be/saga.yaml +248 -0
- package/runtime/knowledge/patterns/be/webhooks.yaml +221 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
- package/runtime/knowledge/sonar-gate.yaml +52 -18
- package/runtime/modules/kernel/failure-codes.yaml +611 -51
- package/runtime/modules/models/registry.yaml +553 -0
- package/runtime/modules/models/runtimes.yaml +695 -0
- package/runtime/modules/ops/_labels.yaml +53 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +51 -0
- package/runtime/scripts/api/fs/is-link-like.mjs +25 -0
- package/runtime/scripts/api/fs/lib.mjs +16 -2
- package/runtime/scripts/api/fs/safe-remove.mjs +12 -132
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/api/git/lib.mjs +18 -26
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/ls-tree.mjs +5 -0
- package/runtime/scripts/api/git/merge-base.mjs +9 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/api/git/show.mjs +6 -0
- package/runtime/scripts/api/node/lib.mjs +14 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/process/lib.mjs +111 -0
- package/runtime/scripts/api/process/owned-process.mjs +78 -0
- package/runtime/scripts/api/process/resolve-real-tool.mjs +46 -0
- package/runtime/scripts/api/process/run-program.mjs +7 -0
- package/runtime/scripts/api/process/stop-owned-process.mjs +9 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -12
- package/runtime/scripts/api/sops/lib.mjs +255 -0
- package/runtime/scripts/api/sops/seal.mjs +22 -0
- package/runtime/scripts/connectors/lib.mjs +488 -0
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
- package/runtime/scripts/hfs/architecture/config.mjs +9 -6
- package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
- package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
- package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +17 -4
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
- package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
- package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
- package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
- package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
- package/runtime/scripts/hfs/architecture.mjs +4 -4
- package/runtime/scripts/hfs/check.mjs +75 -72
- package/runtime/scripts/hfs/coverage-scope.mjs +162 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/path-findings.mjs +1 -1
- package/runtime/scripts/hfs/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +51 -0
- package/runtime/scripts/hfs/rules/cli.mjs +37 -0
- package/runtime/scripts/hfs/rules/contract-compat.mjs +71 -0
- package/runtime/scripts/hfs/rules/contract.mjs +10 -9
- package/runtime/scripts/hfs/rules/database-config.mjs +102 -0
- package/runtime/scripts/hfs/rules/database-constants.mjs +13 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +111 -0
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +233 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +336 -0
- package/runtime/scripts/hfs/rules/database.mjs +68 -0
- package/runtime/scripts/hfs/rules/deps.mjs +1 -1
- package/runtime/scripts/hfs/rules/docker.mjs +259 -0
- package/runtime/scripts/hfs/rules/edition.mjs +111 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +153 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -1
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +9 -9
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -6
- package/runtime/scripts/hfs/rules/kinds.mjs +84 -0
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +1 -1
- package/runtime/scripts/hfs/rules/monorepo.mjs +179 -0
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +3 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +11 -21
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -2
- package/runtime/scripts/hfs/rules/read.mjs +1 -1
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +2 -2
- package/runtime/scripts/hfs/rules/saga.mjs +170 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/services.mjs +220 -0
- package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks.mjs +3 -3
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +89 -0
- package/runtime/scripts/hfs/rules/test-topology.mjs +2 -2
- package/runtime/scripts/hfs/secret.mjs +209 -0
- package/runtime/scripts/hfs/slots.mjs +78 -108
- package/runtime/scripts/hfs/sql/pg-parse.mjs +46 -0
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/hfs/tree.mjs +5 -4
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
- package/runtime/scripts/hfs/view.mjs +3 -2
- package/runtime/scripts/lib/clip.mjs +19 -0
- package/runtime/scripts/lib/display-names.mjs +259 -0
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +103 -0
- package/runtime/scripts/lib/example-refs.mjs +158 -0
- package/runtime/scripts/lib/fs-kind.mjs +14 -4
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/graphql-contract.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json-schema.mjs +52 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +24 -5
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/package-at.mjs +37 -0
- package/runtime/scripts/lib/path-key.mjs +57 -2
- package/runtime/scripts/lib/process-identity.mjs +6 -0
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/read-yaml.mjs +18 -0
- package/runtime/scripts/lib/redact.mjs +161 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/same-text.mjs +5 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sleep-sync.mjs +2 -1
- package/runtime/scripts/lib/sleep.mjs +7 -0
- package/runtime/scripts/lib/sops-envelope.mjs +112 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/lib/walk.mjs +4 -3
- package/scaffold/add-app.mjs +174 -0
- package/scaffold/add-cli-lite.mjs +257 -0
- package/scaffold/add-inbox-lite.mjs +314 -0
- package/scaffold/add-table.mjs +344 -0
- package/scaffold/add.mjs +299 -0
- package/scaffold/app.mjs +358 -69
- package/scaffold/edition-gate.mjs +25 -0
- package/scaffold/image.mjs +19 -0
- package/scaffold/lite-exports.mjs +186 -0
- package/scaffold/service.mjs +32 -26
- package/scaffold/supabase-ports.mjs +26 -0
- package/src/main.mjs +327 -0
- package/sync/cli.mjs +6 -9
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +10 -10
- package/sync/index.mjs +167 -104
- package/sync/managed.mjs +19 -11
- package/sync/sonar-key.mjs +5 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +14 -14
- package/templates/app/ci-workflows/github/workflows/images.yml +37 -0
- package/templates/app/ci-workflows/sonar-steps.yml +9 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +66 -0
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +31 -0
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +37 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +32 -0
- package/templates/app/docker-ignore/dockerignore +50 -0
- package/templates/app/gitignore-lite +35 -0
- package/templates/app/hooks/husky/pre-commit +3 -8
- package/templates/app/hooks/husky/pre-push +45 -6
- package/templates/app/package-scripts/core.json +8 -0
- package/templates/app/package-scripts/package.json +3 -10
- package/templates/app/package-scripts-lite/package.json +15 -0
- package/templates/app/quality-config/codecov.yml +15 -3
- package/templates/app/quality-config/sonar-base.properties +4 -0
- package/templates/app/quality-config/sonar-project.properties +1 -4
- package/templates/app/quality-config-lite/sonar-project.properties +5 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/primary-notes.sql +5 -0
- package/templates/app/skeleton/.starciwork/features/system-health/index.yaml +4 -0
- package/templates/app/skeleton/.starciwork/{features/index.yaml → index.yaml} +2 -2
- package/templates/app/skeleton/README.md +7 -5
- package/templates/app/skeleton-lite/.sops.yaml +3 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/secrets/.gitkeep +1 -0
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +55 -0
- package/templates/app/skeleton-lite/README.md +40 -0
- package/templates/app/skeleton-lite/supabase/config.toml +41 -0
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +56 -0
- package/templates/app/skeleton-lite/supabase/seed.sql +0 -0
- package/templates/app/task-graph/turbo.json +23 -0
- package/templates/app/tool-config-lite/prettierignore +2 -0
- package/templates/app/tool-config-lite/prettierrc +1 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +38 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +10 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +13 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +3 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +12 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +7 -0
- package/templates/be/common/main-listen.ts.partial +14 -0
- package/templates/be/common/sql-ident.ts.partial +7 -0
- package/templates/be/common/sql-tag.ts.partial +5 -0
- package/templates/be/image/api/Dockerfile +38 -0
- package/templates/be/image/cli/Dockerfile +38 -0
- package/templates/be/image/worker/Dockerfile +36 -0
- package/templates/be/inbox/database-client.ts.tpl +50 -0
- package/templates/be/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/migration.sql.tpl +29 -0
- package/templates/be/inbox/module-definition.ts.tpl +6 -0
- package/templates/be/inbox/module.ts.tpl +14 -0
- package/templates/be/inbox/options.ts.tpl +2 -0
- package/templates/be/inbox/service.ts.tpl +33 -0
- package/templates/be/inbox/sql.ts.tpl +16 -0
- package/templates/be/inbox/webhook-door.ts.tpl +36 -0
- package/templates/be/patterns/api/application.command.http.ts.tpl +10 -0
- package/templates/be/patterns/api/application.command.ts.tpl +10 -0
- package/templates/be/patterns/api/application.contracts.ts.tpl +11 -0
- package/templates/be/patterns/api/application.handler.http.ts.tpl +22 -0
- package/templates/be/patterns/api/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/api/feature.index.http.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.index.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.input.ts.tpl +13 -0
- package/templates/be/patterns/api/graphql.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +7 -0
- package/templates/be/patterns/api/graphql.resolver.ts.tpl +27 -0
- package/templates/be/patterns/api/graphql.type.ts.tpl +9 -0
- package/templates/be/patterns/api/http.controller.ts.tpl +27 -0
- package/templates/be/patterns/api/http.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/http.module.ts.tpl +7 -0
- package/templates/be/patterns/api/http.request.ts.tpl +10 -0
- package/templates/be/patterns/api/http.response.ts.tpl +5 -0
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +15 -0
- package/templates/be/patterns/cli/group.cli.ts.tpl +11 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +7 -0
- package/templates/be/patterns/cli/sub.run.cli.spec.ts.tpl +17 -0
- package/templates/be/patterns/cli/sub.run.cli.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +22 -0
- package/templates/be/patterns/event-bus/platform/event-bus.config.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-bus.decorators.ts.tpl +46 -0
- package/templates/be/patterns/event-bus/platform/event-bus.log-events.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module.ts.tpl +52 -0
- package/templates/be/patterns/event-bus/platform/event-bus.options.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +28 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +165 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +93 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +90 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +136 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +56 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +272 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +128 -0
- package/templates/be/patterns/event-bus/platform/event-transport.port.ts.tpl +180 -0
- package/templates/be/patterns/event-bus/platform/event.policy.spec.ts.tpl +67 -0
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +38 -0
- package/templates/be/patterns/event-bus/platform/index.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.spec.ts.tpl +348 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +241 -0
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +8 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +37 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +26 -0
- package/templates/be/patterns/jobs/job.index.ts.tpl +1 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.processor.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.step.ts.tpl +19 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +150 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +100 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +138 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +69 -0
- package/templates/be/patterns/jobs/platform/jobs.config.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +62 -0
- package/templates/be/patterns/jobs/platform/jobs.decorators.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/jobs.error.ts.tpl +22 -0
- package/templates/be/patterns/jobs/platform/jobs.index.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +9 -0
- package/templates/be/patterns/jobs/platform/jobs.messages.ts.tpl +15 -0
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform/jobs.module.ts.tpl +32 -0
- package/templates/be/patterns/jobs/platform/jobs.options.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.port.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/jobs/platform/persistence.job.entity.ts.tpl +53 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.rows.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/jobs/platform/persistence.migration.ts.tpl +29 -0
- package/templates/be/patterns/outbox/platform/index.ts.tpl +2 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +96 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +78 -0
- package/templates/be/patterns/outbox/platform/outbox.log-events.ts.tpl +5 -0
- package/templates/be/patterns/projections/migration.ts.tpl +18 -0
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/projections/projection.entity.ts.tpl +9 -0
- package/templates/be/patterns/projections/projection.index.ts.tpl +3 -0
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/projections/projection.module.ts.tpl +18 -0
- package/templates/be/patterns/projections/projection.options.ts.tpl +2 -0
- package/templates/be/patterns/projections/projection.projection.spec.ts.tpl +55 -0
- package/templates/be/patterns/projections/projection.projection.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +142 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +81 -0
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +9 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +128 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +115 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +91 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +15 -0
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +31 -0
- package/templates/be/patterns/queues/platform/queue.decorators.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +7 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +22 -0
- package/templates/be/patterns/queues/platform/queue.policy.spec.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/queue.policy.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +14 -0
- package/templates/be/patterns/queues/queue.index.ts.tpl +3 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +23 -0
- package/templates/be/patterns/reactors/application.command.ts.tpl +10 -0
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +12 -0
- package/templates/be/patterns/reactors/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +19 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +20 -0
- package/templates/be/patterns/reactors/reactor.index.ts.tpl +1 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.index.ts.tpl +1 -0
- package/templates/be/patterns/realtime/channel.input.ts.tpl +12 -0
- package/templates/be/patterns/realtime/channel.subscription.spec.ts.tpl +51 -0
- package/templates/be/patterns/realtime/channel.subscription.ts.tpl +32 -0
- package/templates/be/patterns/realtime/channel.type.ts.tpl +9 -0
- package/templates/be/patterns/saga/saga.compensate.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.compensate.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.compensate.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.compensation.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.complete.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.complete.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.complete.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.index.ts.tpl +1 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +23 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +20 -0
- package/templates/be/patterns/saga/saga.service.spec.ts.tpl +70 -0
- package/templates/be/patterns/saga/saga.service.ts.tpl +44 -0
- package/templates/be/patterns/saga/saga.state.ts.tpl +14 -0
- package/templates/be/patterns/saga/saga.step.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.undo.command.ts.tpl +10 -0
- package/templates/be/patterns/saga/saga.undo.contracts.ts.tpl +5 -0
- package/templates/be/patterns/saga/saga.undo.handler.ts.tpl +21 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +72 -0
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +36 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.index.ts.tpl +1 -0
- package/templates/be/patterns/webhooks/webhook.request.ts.tpl +9 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -2
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -15
- package/templates/be/skeleton/apps/cli/src/app.module.ts +30 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +22 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +24 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/index.ts +1 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.ts +18 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +33 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +34 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +13 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/note/errors/note.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/note/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/note/note.contracts.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.spec.ts +76 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.ts +37 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/entities/note.entity.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/migrations/1790000000000-init-note.ts +22 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.rows.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +41 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +77 -0
- package/templates/be/skeleton/src/modules/platform/database/database.options.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/database.port.ts +65 -0
- package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/database/index.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.spec.ts +84 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.config.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.connection.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +75 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +88 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +29 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +10 -2
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +63 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +172 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +52 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +37 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +81 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +50 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +7 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +8 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +17 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +54 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +49 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +28 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +5 -0
- package/templates/be/table/error.ts.tpl +16 -0
- package/templates/be/table/index.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +51 -0
- package/templates/be/table/module-definition.ts.tpl +7 -0
- package/templates/be/table/module.ts.tpl +14 -0
- package/templates/be/table/options.ts.tpl +2 -0
- package/templates/be/table/rows.ts.tpl +4 -0
- package/templates/be/table/service.ts.tpl +23 -0
- package/templates/be/table/sql.ts.tpl +4 -0
- package/templates/be/tool-config/jest.config.js +3 -1
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +14 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +26 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +6 -0
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +23 -0
- package/templates/common/gitignore.base +1 -2
- package/templates/fe/app/package.json.tpl +1 -0
- package/templates/fe/app/tsconfig.json +12 -0
- package/templates/fe/common/app-layout.tsx.partial +8 -0
- package/templates/fe/common/site-shell.tsx.partial +9 -0
- package/templates/fe/image/next/Dockerfile +42 -0
- package/templates/fe/skeleton/apps/app/next.config.ts +23 -0
- package/templates/fe/skeleton/apps/app/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/loading.tsx +2 -2
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/not-found.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/global-error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +18 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/component.tsx +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +28 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/proxy.ts +1 -0
- package/templates/fe/skeleton/apps/{__app__ → landing}/next.config.ts +4 -2
- package/templates/fe/skeleton/apps/landing/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/app/global-error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/component.tsx +18 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/component.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +7 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/vi.json +31 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/proxy.ts +1 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +49 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +24 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +20 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +23 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/ErrorNotice/index.tsx +14 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/GlobalErrorNotice/index.tsx +25 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LoadingNotice/index.tsx +9 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +21 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +6 -0
- package/templates/fe/skeleton-lite/apps/web/next.config.ts +22 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +37 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/global-error.tsx +10 -0
- package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/app/globals.css +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/health/live/route.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/component.tsx +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +41 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/component.tsx +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +38 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/ErrorNotice/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/GlobalErrorNotice/index.tsx +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/LoadingNotice/index.tsx +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +18 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/component.tsx +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/index.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppGlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +30 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +3 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +59 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +70 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +50 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +48 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +16 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +43 -0
- package/templates/fe/table/read.ts.tpl +16 -0
- package/templates/fe/table/write.ts.tpl +16 -0
- package/upgrade/check.mjs +137 -0
- package/upgrade/index.mjs +524 -0
- package/bin/hfs.mjs +0 -233
- package/runtime/scripts/api/fs/rmdir-link.mjs +0 -12
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +0 -20
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +0 -9
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +0 -35
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -19
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +0 -11
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -24
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -14
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -21
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +0 -15
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -23
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +0 -2
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +0 -25
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +0 -19
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +0 -9
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +0 -4
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +0 -10
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.contracts.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.handler.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.query.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/index.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/system-health.module.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/live.controller.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/system-health-http.module.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/app/health/live/route.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/modules/config/index.ts +0 -0
- /package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/postcss.config.mjs +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// kinds.mjs - the trigger kinds a back end uses are declared, whole and generated only when used (hfs.json sides.be.kinds).
|
|
2
|
+
// BE_KIND_DECLARATION `sides.be.kinds` lists exactly the kinds the features hold: a kind in use that is not declared, a declared kind with no
|
|
3
|
+
// feature, a declared kind whose patterns (ruleParams.be.kindPatterns) are not all in `sides.be.patterns`, and a declared
|
|
4
|
+
// kind whose platform capabilities (the tier-platform slots that name those patterns) are not tracked are each a finding;
|
|
5
|
+
// BE_KIND_EMPTY a kind folder `be/src/features/<kind>/` holds only instance folders `<kind>/<x>/`, and each instance holds source: a file
|
|
6
|
+
// directly in the kind folder (a `.gitkeep`) or an instance with no TypeScript file is a finding.
|
|
7
|
+
// A kind is a folder name of `be/src/features/` read from `triggerKinds` of the slot manifest (api features live in `features/api/<feature>/`);
|
|
8
|
+
// `features/cli/` is the one cli feature root (its module and group folders), so it is in use as soon as it holds a file.
|
|
9
|
+
import { loadSlotManifest, ruleParams } from '../slots.mjs';
|
|
10
|
+
import { found } from './read.mjs';
|
|
11
|
+
|
|
12
|
+
const KIND_DECLARATION = 'BE_KIND_DECLARATION';
|
|
13
|
+
const KIND_EMPTY = 'BE_KIND_EMPTY';
|
|
14
|
+
|
|
15
|
+
const FEATURES = 'be/src/features/';
|
|
16
|
+
const CLI = 'cli';
|
|
17
|
+
|
|
18
|
+
let memo;
|
|
19
|
+
/** The manifest facts the rules read: the trigger kinds, the patterns each kind needs, and the platform capability folders of each pattern. */
|
|
20
|
+
const facts = () => {
|
|
21
|
+
if (memo) return memo;
|
|
22
|
+
const manifest = loadSlotManifest();
|
|
23
|
+
const platform = new Map();
|
|
24
|
+
for (const slot of manifest.slots) {
|
|
25
|
+
if (slot.pattern === undefined || slot.tier !== 'platform' || !slot.profiles.includes('be')) continue;
|
|
26
|
+
const folder = /^src\/modules\/platform\/([a-z][a-z0-9-]*)\/$/.exec(slot.path);
|
|
27
|
+
if (!folder) continue;
|
|
28
|
+
if (!platform.has(slot.pattern)) platform.set(slot.pattern, []);
|
|
29
|
+
platform.get(slot.pattern).push(folder[1]);
|
|
30
|
+
}
|
|
31
|
+
memo = { triggerKinds: manifest.triggerKinds ?? [], kindPatterns: ruleParams(manifest, 'be').kindPatterns, platform };
|
|
32
|
+
return memo;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Findings of BE_KIND_DECLARATION and BE_KIND_EMPTY. */
|
|
36
|
+
export function kindFindings({ files, repo }) {
|
|
37
|
+
const be = repo.sides?.be;
|
|
38
|
+
if (!be) return [];
|
|
39
|
+
const { triggerKinds, kindPatterns, platform } = facts();
|
|
40
|
+
const kindFolders = new Set(triggerKinds);
|
|
41
|
+
const findings = [];
|
|
42
|
+
const instances = new Map();
|
|
43
|
+
const sources = new Map();
|
|
44
|
+
const roots = new Set();
|
|
45
|
+
const tracked = new Set(files);
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
if (!file.startsWith(FEATURES)) continue;
|
|
48
|
+
const rest = file.slice(FEATURES.length).split('/');
|
|
49
|
+
if (rest.length < 2) continue;
|
|
50
|
+
const [first, second] = rest;
|
|
51
|
+
if (!kindFolders.has(first)) continue;
|
|
52
|
+
if (first === CLI) { roots.add(CLI); continue; }
|
|
53
|
+
if (rest.length === 2) {
|
|
54
|
+
findings.push(found(KIND_EMPTY, file, `${file} sits directly in the kind folder ${FEATURES}${first}/, which holds only instance folders (${first}/<name>/); an empty kind folder is never kept: run \`starci app add\` to create the first member.`, { kind: first }));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!instances.has(first)) instances.set(first, new Set());
|
|
58
|
+
instances.get(first).add(second);
|
|
59
|
+
const key = `${first}/${second}`;
|
|
60
|
+
sources.set(key, (sources.get(key) ?? 0) + (/\.[cm]?tsx?$/.test(file) ? 1 : 0));
|
|
61
|
+
}
|
|
62
|
+
for (const [key, count] of sources) {
|
|
63
|
+
if (count === 0) findings.push(found(KIND_EMPTY, `${FEATURES}${key}/`, `${FEATURES}${key}/ holds no TypeScript source: a kind member without code is an empty folder; add its files with \`starci app add\` or remove the folder.`, { kind: key.split('/')[0] }));
|
|
64
|
+
}
|
|
65
|
+
const present = new Set([...instances.keys(), ...roots]);
|
|
66
|
+
const declared = new Set(be.kinds ?? []);
|
|
67
|
+
const patterns = new Set(be.patterns ?? []);
|
|
68
|
+
for (const kind of [...present].sort()) {
|
|
69
|
+
if (!declared.has(kind)) findings.push(found(KIND_DECLARATION, 'hfs.json', `hfs.json sides.be.kinds does not declare the kind \`${kind}\`, which ${FEATURES}${kind}/ uses; declare every kind in use (\`starci app add\` registers it).`, { kind }));
|
|
70
|
+
}
|
|
71
|
+
for (const kind of [...declared].sort()) {
|
|
72
|
+
if (!present.has(kind)) {
|
|
73
|
+
findings.push(found(KIND_DECLARATION, 'hfs.json', `hfs.json sides.be.kinds declares \`${kind}\` but no feature of that kind exists; a kind is declared only when it is used, so remove it or add its first member with \`starci app add\`.`, { kind }));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const pattern of kindPatterns[kind] ?? []) {
|
|
77
|
+
if (!patterns.has(pattern)) findings.push(found(KIND_DECLARATION, 'hfs.json', `the kind \`${kind}\` needs the pattern \`${pattern}\`, which hfs.json sides.be.patterns does not declare.`, { kind, pattern }));
|
|
78
|
+
for (const capability of platform.get(pattern) ?? []) {
|
|
79
|
+
if (!tracked.has(`be/src/modules/platform/${capability}/index.ts`)) findings.push(found(KIND_DECLARATION, `be/src/modules/platform/${capability}/`, `the kind \`${kind}\` needs the platform capability \`${capability}\` (pattern ${pattern}), which is not tracked at be/src/modules/platform/${capability}/; \`starci app add\` generates it with the first member of the kind.`, { kind, pattern, capability }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return findings;
|
|
84
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Comment-level suppression (`eslint-disable`) is the lint rule `no-inline-suppression`'s (R18).
|
|
8
8
|
import { found, readJson, readText } from './read.mjs';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const LINT_SUPPRESSION_FILE = 'HFS_LINT_SUPPRESSION_FILE';
|
|
11
11
|
const SUPPRESSION_FILE = /(?:^|\/)eslint[.-]suppressions[^/]*$/;
|
|
12
12
|
const SUPPRESSION_SCRIPT_NAME = /(?:^|:)suppressions$/;
|
|
13
13
|
const SUPPRESSION_FLAG = /--(?:suppressions-location|suppress-all|suppress-rule|prune-suppressions)\b/;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// monorepo.mjs - every product is a monorepo, even a single app: the app root holds one package.json and lockfile whose npm
|
|
2
|
+
// workspaces are the fe apps and packages (turbo runs their tasks), and the back end is a Nest monorepo of be/apps/<app>.
|
|
3
|
+
// HFS_MONO_WORKSPACES the root package.json: `workspaces` exactly fe/apps/* and fe/packages/*, `packageManager` npm at an
|
|
4
|
+
// exact version, turbo a root devDependency (its version is the canon pin); turbo.json is app.task-graph
|
|
5
|
+
// HFS_MONO_FE_WORKSPACE every fe app's package.json is @<project>/<app>, private, with exactly the workspace scripts; every
|
|
6
|
+
// fe package's package.json is private with build and typecheck scripts and the workspace lint
|
|
7
|
+
// HFS_MONO_NEST_PROJECTS be/nest-cli.json is `monorepo: true`, its root an api app, and one project per declared be app
|
|
8
|
+
// HFS_MONO_WORKSPACE_DEP a package an fe workspace imports is declared in that workspace's package.json (a sibling workspace
|
|
9
|
+
// package at "*"); the root package.json never declares a workspace package
|
|
10
|
+
// The imports are read with TypeScript's own pre-processor (import, export-from, require and import() specifiers), and a
|
|
11
|
+
// workspace's path aliases come from its tsconfig.json as TypeScript resolves it: no specifier is matched by a regular expression.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import { isBuiltin } from 'node:module';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
17
|
+
import { found, readJson, readText } from './read.mjs';
|
|
18
|
+
|
|
19
|
+
const MONO_WORKSPACES = 'HFS_MONO_WORKSPACES';
|
|
20
|
+
const MONO_FE_WORKSPACE = 'HFS_MONO_FE_WORKSPACE';
|
|
21
|
+
const MONO_NEST_PROJECTS = 'HFS_MONO_NEST_PROJECTS';
|
|
22
|
+
const MONO_WORKSPACE_DEP = 'HFS_MONO_WORKSPACE_DEP';
|
|
23
|
+
|
|
24
|
+
/** The npm workspaces of every app, in this order. */
|
|
25
|
+
export const WORKSPACES = Object.freeze(['fe/apps/*', 'fe/packages/*']);
|
|
26
|
+
/** The lint script of every fe workspace: the one lint of the app, scoped to the workspace. */
|
|
27
|
+
export const WORKSPACE_LINT = 'starci app lint --workspace .';
|
|
28
|
+
/** The scripts of every fe app workspace, exactly: turbo runs build, dev, lint and typecheck; start serves a build. */
|
|
29
|
+
export const FE_APP_SCRIPTS = Object.freeze({ build: 'next build', dev: 'next dev', lint: WORKSPACE_LINT, start: 'next start', typecheck: 'tsc --noEmit' });
|
|
30
|
+
/** The scripts every fe package workspace has (its build and typecheck commands are its own; its lint is the workspace lint). */
|
|
31
|
+
const FE_PACKAGE_SCRIPTS = Object.freeze(['build', 'typecheck', 'lint']);
|
|
32
|
+
/** The package manager every app root declares (turbo reads it to parse the lockfile); the starci app scaffold writes it. */
|
|
33
|
+
export const PACKAGE_MANAGER = 'npm@11.6.2';
|
|
34
|
+
/** The npm name of an fe app workspace. */
|
|
35
|
+
export const feAppPackageName = (project, app) => `@${project}/${app}`;
|
|
36
|
+
|
|
37
|
+
const MANIFEST = 'package.json';
|
|
38
|
+
const NEST_CLI = 'be/nest-cli.json';
|
|
39
|
+
const PACKAGE_MANAGER_FORM = /^npm@\d+\.\d+\.\d+$/;
|
|
40
|
+
const DEP_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
41
|
+
const SOURCE = /\.(?:[cm]?[jt]sx?)$/;
|
|
42
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
|
|
44
|
+
const declaredIn = (pkg) => new Set(DEP_SECTIONS.flatMap((section) => Object.keys(pkg?.[section] ?? {})));
|
|
45
|
+
const sameList = (a, b) => Array.isArray(a) && a.length === b.length && a.every((item, i) => item === b[i]);
|
|
46
|
+
|
|
47
|
+
/** The workspace folders the tracked tree holds: fe/apps/<x> and fe/packages/<x> that track a package.json. */
|
|
48
|
+
function workspacesOf(files) {
|
|
49
|
+
const folders = new Set();
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const parts = file.split('/');
|
|
52
|
+
if (parts.length === 4 && parts[0] === 'fe' && (parts[1] === 'apps' || parts[1] === 'packages') && parts[3] === MANIFEST) folders.add(parts.slice(0, 3).join('/'));
|
|
53
|
+
}
|
|
54
|
+
return [...folders].sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** R143: the root package.json declares the workspaces, the package manager and turbo. */
|
|
58
|
+
function rootFindings(repoRoot) {
|
|
59
|
+
const pkg = readJson(repoRoot, MANIFEST);
|
|
60
|
+
if (!pkg) return [];
|
|
61
|
+
const findings = [];
|
|
62
|
+
if (!sameList(pkg.workspaces, WORKSPACES)) findings.push(found(MONO_WORKSPACES, MANIFEST, `${MANIFEST} declares workspaces ${JSON.stringify(pkg.workspaces ?? null)}; every app is a monorepo whose npm workspaces are exactly ${JSON.stringify(WORKSPACES)} (each fe app and package has its own package.json; be has none).`, { workspaces: pkg.workspaces ?? null }));
|
|
63
|
+
if (!PACKAGE_MANAGER_FORM.test(String(pkg.packageManager ?? ''))) findings.push(found(MONO_WORKSPACES, MANIFEST, `${MANIFEST} declares packageManager ${JSON.stringify(pkg.packageManager ?? null)}; turbo needs \`npm@<exact version>\` there.`, { packageManager: pkg.packageManager ?? null }));
|
|
64
|
+
if (typeof pkg.devDependencies?.turbo !== 'string') findings.push(found(MONO_WORKSPACES, MANIFEST, `${MANIFEST} does not declare turbo in its devDependencies; the turbo task graph (turbo.json) runs build, dev, lint and typecheck over the fe workspaces.`, { missing: 'turbo' }));
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** R144: the manifest of each fe workspace. */
|
|
69
|
+
function workspaceManifestFindings(repoRoot, project, workspaces) {
|
|
70
|
+
const findings = [];
|
|
71
|
+
for (const folder of workspaces) {
|
|
72
|
+
const file = `${folder}/${MANIFEST}`;
|
|
73
|
+
const pkg = readJson(repoRoot, file);
|
|
74
|
+
if (!pkg) { findings.push(found(MONO_FE_WORKSPACE, file, `${file} is not readable JSON; an fe workspace has a package.json.`)); continue; }
|
|
75
|
+
const name = folder.split('/')[2];
|
|
76
|
+
if (pkg.private !== true) findings.push(found(MONO_FE_WORKSPACE, file, `${file} is not \`"private": true\`; a workspace of the app is never published.`));
|
|
77
|
+
const scripts = pkg.scripts ?? {};
|
|
78
|
+
if (folder.startsWith('fe/apps/')) {
|
|
79
|
+
const expected = feAppPackageName(project, name);
|
|
80
|
+
if (pkg.name !== expected) findings.push(found(MONO_FE_WORKSPACE, file, `${file} is named ${JSON.stringify(pkg.name ?? null)}; the fe app ${name} is the workspace ${expected}.`, { expected }));
|
|
81
|
+
const want = Object.keys(FE_APP_SCRIPTS).sort();
|
|
82
|
+
const off = [...new Set([...want, ...Object.keys(scripts)])].sort().filter((key) => scripts[key] !== FE_APP_SCRIPTS[key]);
|
|
83
|
+
if (off.length) findings.push(found(MONO_FE_WORKSPACE, file, `${file} has scripts ${JSON.stringify(scripts)}; an fe app workspace has exactly ${JSON.stringify(FE_APP_SCRIPTS)} (turbo runs build, dev, lint and typecheck; differs at ${off.join(', ')}).`, { scripts: off }));
|
|
84
|
+
} else {
|
|
85
|
+
const missing = FE_PACKAGE_SCRIPTS.filter((key) => typeof scripts[key] !== 'string' || !scripts[key]);
|
|
86
|
+
if (missing.length) findings.push(found(MONO_FE_WORKSPACE, file, `${file} has no ${missing.join(', ')} script; an fe package workspace builds, type-checks and lints (turbo builds it before the apps that import it).`, { scripts: missing }));
|
|
87
|
+
else if (scripts.lint !== WORKSPACE_LINT) findings.push(found(MONO_FE_WORKSPACE, file, `${file} lints with ${JSON.stringify(scripts.lint)}; a workspace lints with ${JSON.stringify(WORKSPACE_LINT)}, the one lint of the app scoped to it.`, { scripts: ['lint'] }));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return findings;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** R145: be/nest-cli.json is the Nest monorepo of exactly the declared be apps. */
|
|
94
|
+
function nestFindings(repoRoot, files, beApps) {
|
|
95
|
+
if (!files.includes(NEST_CLI)) return [];
|
|
96
|
+
const cli = readJson(repoRoot, NEST_CLI);
|
|
97
|
+
if (!cli) return [found(MONO_NEST_PROJECTS, NEST_CLI, `${NEST_CLI} is not readable JSON.`)];
|
|
98
|
+
const findings = [];
|
|
99
|
+
if (cli.monorepo !== true) findings.push(found(MONO_NEST_PROJECTS, NEST_CLI, `${NEST_CLI} is not \`"monorepo": true\`; the back end is a Nest monorepo of be/apps/<app>, even with one service.`));
|
|
100
|
+
const declared = beApps.map((app) => app.name).sort();
|
|
101
|
+
const projects = Object.keys(cli.projects ?? {}).sort();
|
|
102
|
+
if (!sameList(projects, declared)) findings.push(found(MONO_NEST_PROJECTS, NEST_CLI, `${NEST_CLI} has projects ${JSON.stringify(projects)}, but hfs.json declares the be apps ${JSON.stringify(declared)}; one project per declared app, no other.`, { projects, declared }));
|
|
103
|
+
for (const name of projects.filter((project) => declared.includes(project))) {
|
|
104
|
+
const project = cli.projects[name] ?? {};
|
|
105
|
+
if (project.type !== 'application' || project.root !== `apps/${name}` || project.sourceRoot !== `apps/${name}/src`) findings.push(found(MONO_NEST_PROJECTS, NEST_CLI, `${NEST_CLI} project ${name} is ${JSON.stringify({ type: project.type, root: project.root, sourceRoot: project.sourceRoot })}; it is {"type":"application","root":"apps/${name}","sourceRoot":"apps/${name}/src"}.`, { project: name }));
|
|
106
|
+
}
|
|
107
|
+
const apis = beApps.filter((app) => app.kind === 'api').map((app) => app.name);
|
|
108
|
+
const rootApp = /^apps\/([^/]+)$/.exec(String(cli.root ?? ''))?.[1];
|
|
109
|
+
if (!apis.includes(rootApp) || cli.sourceRoot !== `apps/${rootApp}/src`) findings.push(found(MONO_NEST_PROJECTS, NEST_CLI, `${NEST_CLI} has root ${JSON.stringify(cli.root ?? null)} and sourceRoot ${JSON.stringify(cli.sourceRoot ?? null)}; the default project is an api app (${apis.join(', ') || 'none declared'}): root apps/<api>, sourceRoot apps/<api>/src.`, { root: cli.root ?? null }));
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The TypeScript compiler of the app (else of the runtime), or null. */
|
|
114
|
+
const typescriptFor = (repoRoot) => loadTypescript(repoRoot, HERE);
|
|
115
|
+
|
|
116
|
+
/** The path-alias patterns of a workspace (its tsconfig.json as TypeScript resolves it, `extends` included): [{ prefix, exact }]. */
|
|
117
|
+
function aliasesOf(ts, dir) {
|
|
118
|
+
const file = path.join(dir, 'tsconfig.json');
|
|
119
|
+
if (!fs.existsSync(file)) return [];
|
|
120
|
+
const read = ts.readConfigFile(file, ts.sys.readFile);
|
|
121
|
+
if (read.error) return [];
|
|
122
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dir);
|
|
123
|
+
return Object.keys(parsed.options.paths ?? {}).map((key) => (key.endsWith('*') ? { prefix: key.slice(0, -1), exact: false } : { prefix: key, exact: true }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The npm package a bare module specifier names (`next/link` is next, `@scope/name/sub` is @scope/name). */
|
|
127
|
+
const packageOf = (specifier) => {
|
|
128
|
+
const parts = specifier.split('/');
|
|
129
|
+
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** R146: what each fe workspace imports, it declares; the root declares no workspace package. */
|
|
133
|
+
function workspaceDepFindings(repoRoot, files, workspaces) {
|
|
134
|
+
const findings = [];
|
|
135
|
+
const names = new Map(workspaces.map((folder) => [readJson(repoRoot, `${folder}/${MANIFEST}`)?.name, folder]).filter(([name]) => typeof name === 'string'));
|
|
136
|
+
const root = readJson(repoRoot, MANIFEST);
|
|
137
|
+
for (const name of declaredIn(root)) if (names.has(name)) findings.push(found(MONO_WORKSPACE_DEP, MANIFEST, `${MANIFEST} declares the workspace package ${name} (${names.get(name)}); the root never depends on a workspace: the fe workspace that imports it declares it.`, { dependency: name }));
|
|
138
|
+
if (!workspaces.length) return findings;
|
|
139
|
+
const ts = typescriptFor(repoRoot);
|
|
140
|
+
if (!ts) return [...findings, found(MONO_WORKSPACE_DEP, MANIFEST, 'the fe workspaces cannot be judged: no TypeScript resolves from the app; install the app\'s dependencies.', { missing: ['typescript'] })];
|
|
141
|
+
for (const folder of workspaces) {
|
|
142
|
+
const manifest = `${folder}/${MANIFEST}`;
|
|
143
|
+
const pkg = readJson(repoRoot, manifest);
|
|
144
|
+
if (!pkg) continue;
|
|
145
|
+
const declared = declaredIn(pkg);
|
|
146
|
+
const aliases = aliasesOf(ts, path.join(repoRoot, folder));
|
|
147
|
+
const isAlias = (specifier) => aliases.some((alias) => (alias.exact ? specifier === alias.prefix : specifier.startsWith(alias.prefix)));
|
|
148
|
+
const missing = new Map();
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
if (!file.startsWith(`${folder}/`) || !SOURCE.test(file) || file.endsWith('.d.ts')) continue;
|
|
151
|
+
const text = readText(repoRoot, file);
|
|
152
|
+
if (text === null) continue;
|
|
153
|
+
for (const { fileName: specifier } of ts.preProcessFile(text, true, true).importedFiles) {
|
|
154
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || isBuiltin(specifier) || isAlias(specifier)) continue;
|
|
155
|
+
const name = packageOf(specifier);
|
|
156
|
+
if (declared.has(name) || name === pkg.name) continue;
|
|
157
|
+
if (!missing.has(name)) missing.set(name, file);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [name, file] of [...missing].sort()) {
|
|
161
|
+
const sibling = names.get(name);
|
|
162
|
+
findings.push(found(MONO_WORKSPACE_DEP, manifest, `${file} imports ${name}, which ${manifest} does not declare; ${sibling ? `declare the workspace package ${name} as "*"` : `declare ${name} in the dependencies of ${manifest} (one version across the workspace, R14)`}.`, { dependency: name, importedBy: file }));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return findings;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The findings of the monorepo rules over the tracked paths `files` (app-relative) of the app at `repoRoot`. */
|
|
169
|
+
export function monorepoFindings({ repoRoot, files, repo }) {
|
|
170
|
+
if (!repo?.sides) return [];
|
|
171
|
+
const workspaces = workspacesOf(files);
|
|
172
|
+
const beApps = repo.sides.be.apps;
|
|
173
|
+
return [
|
|
174
|
+
...rootFindings(repoRoot),
|
|
175
|
+
...workspaceManifestFindings(repoRoot, repo.project, workspaces),
|
|
176
|
+
...nestFindings(repoRoot, files, beApps),
|
|
177
|
+
...workspaceDepFindings(repoRoot, files, workspaces),
|
|
178
|
+
];
|
|
179
|
+
}
|
|
@@ -8,19 +8,19 @@ import fs from 'node:fs';
|
|
|
8
8
|
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
9
9
|
import { found, readJson } from './read.mjs';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const PEER_INTEGRATION_MISSING = 'HFS_PEER_INTEGRATION_MISSING';
|
|
12
12
|
const MANIFEST = 'package.json';
|
|
13
13
|
const CATALOG_FILE = new URL('../../../knowledge/hfs/peer-integrations.yaml', import.meta.url);
|
|
14
14
|
|
|
15
15
|
/** The pairs of the catalog: [{ id, when: [{ package, major? }], requires, why }]. */
|
|
16
|
-
|
|
16
|
+
function peerIntegrationPairs() {
|
|
17
17
|
const catalog = parseYaml(fs.readFileSync(CATALOG_FILE, 'utf8'));
|
|
18
18
|
if (catalog?.schema !== 'starci/hfs-peer-integrations@1' || !Array.isArray(catalog.pairs)) throw new Error('knowledge/hfs/peer-integrations.yaml is not a starci/hfs-peer-integrations@1 catalog');
|
|
19
19
|
return catalog.pairs;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** The major a dependency spec allows (`11.2.5`, `^11.0.0`, `~11.1`, `>=11 <12`), or null when the spec names none (a tag, a link, `*`). */
|
|
23
|
-
|
|
23
|
+
function majorOf(spec) {
|
|
24
24
|
const match = /^\s*(?:[\^~]|>=?|=)?\s*v?(\d+)(?:\.|\s|$)/.exec(String(spec));
|
|
25
25
|
return match ? Number(match[1]) : null;
|
|
26
26
|
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
// pipeline.mjs - HFS_CI_MISSING_CANON (R13): CI runs the pinned `
|
|
2
|
-
// .github/workflows/ci.yml a `run:` step that runs
|
|
3
|
-
// `npx @starci/
|
|
4
|
-
// (`npm run lint`, arguments after `--` allowed); a version named in the step is the @starci/
|
|
1
|
+
// pipeline.mjs - HFS_CI_MISSING_CANON (R13): CI runs the pinned `starci app lint`.
|
|
2
|
+
// .github/workflows/ci.yml a `run:` step that runs the one lint entry: `starci app lint`,
|
|
3
|
+
// `npx @starci/cli[@<version>] app lint`, or `npm run <script>` whose command runs `starci app lint`
|
|
4
|
+
// (`npm run lint`, arguments after `--` allowed); a version named in the step is the @starci/cli pin of
|
|
5
5
|
// canon-pins.yaml (an installed one is pinned by the pin check, R15)
|
|
6
|
-
//
|
|
6
|
+
// (the push hook is no lint or typecheck step: it checks the release gate, scripts/hfs/runtime-rules/hook-shape.mjs keeps its shape)
|
|
7
7
|
// A missing file is the slot manifest's finding (HFS_SLOT_REQUIRED_MISSING); a file that is present and lacks the step is this rule's.
|
|
8
8
|
// The redirect of `core.hooksPath` away from husky is the architecture machine's (HFS_HOOKS_PATH_REDIRECTED).
|
|
9
9
|
import { found, readJson, readText } from './read.mjs';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export const hfsPackage = '@starci/hfs';
|
|
11
|
+
const CI_MISSING_CANON = 'HFS_CI_MISSING_CANON';
|
|
12
|
+
const CI_FILE = '.github/workflows/ci.yml';
|
|
13
|
+
const hfsPackage = '@starci/cli';
|
|
15
14
|
const RUN_LINE = /^\s*(?:-\s+)?run:\s*(.+?)\s*$/;
|
|
16
|
-
const HFS_LINT = /^(?:npx\s+(?:--no-install\s+|-y\s+)?)?(?:@starci\/
|
|
15
|
+
const HFS_LINT = /^(?:npx\s+(?:--no-install\s+|-y\s+)?)?(?:@starci\/cli(?:@(\S+))?|starci)\s+app\s+lint(\s.*)?$/;
|
|
17
16
|
const NPM_RUN = /^npm run ([\w:.-]+)(?:\s+--\s+(.*))?$/;
|
|
18
17
|
|
|
19
18
|
const commandsOf = (text) => text.split(/\r?\n/).map((line) => RUN_LINE.exec(line)?.[1]).filter(Boolean).map((command) => command.replace(/^["']|["']$/g, ''));
|
|
@@ -24,8 +23,6 @@ function expanded(command, scripts) {
|
|
|
24
23
|
return run && typeof scripts[run[1]] === 'string' ? `${scripts[run[1]]}${run[2] ? ` ${run[2]}` : ''}` : command;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
const runsStep = (lines, step) => lines.some((line) => new RegExp(`^npm run ${step}(\\s|$)`).test(line));
|
|
28
|
-
|
|
29
26
|
/** The findings of R13 over the repository at `repoRoot`; `pins` is the parsed canon-pins.yaml pin map. */
|
|
30
27
|
export function pipelineFindings({ repoRoot, files, pins }) {
|
|
31
28
|
const findings = [];
|
|
@@ -34,16 +31,9 @@ export function pipelineFindings({ repoRoot, files, pins }) {
|
|
|
34
31
|
if (ci !== null) {
|
|
35
32
|
const scripts = readJson(repoRoot, 'package.json')?.scripts ?? {};
|
|
36
33
|
const lints = commandsOf(ci).flatMap((command) => expanded(command, scripts).split(/\s*&&\s*/)).map((command) => HFS_LINT.exec(command)).filter(Boolean);
|
|
37
|
-
if (!lints.length) findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} has no step that runs \`
|
|
34
|
+
if (!lints.length) findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} has no step that runs \`starci app lint\` (directly or through \`npm run lint\`); CI runs the one lint entry, which includes the repository check`, { step: 'starci app lint' }));
|
|
38
35
|
else if (pinned && !lints.some((match) => match[1] === undefined || match[1] === pinned)) {
|
|
39
|
-
findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} runs
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const prePush = files.includes(PRE_PUSH_FILE) ? readText(repoRoot, PRE_PUSH_FILE) : null;
|
|
43
|
-
if (prePush !== null) {
|
|
44
|
-
const lines = prePush.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
|
|
45
|
-
for (const step of ['typecheck', 'lint']) {
|
|
46
|
-
if (!runsStep(lines, step)) findings.push(found(CI_MISSING_CANON, PRE_PUSH_FILE, `${PRE_PUSH_FILE} does not run \`npm run ${step}\`; pre-push runs typecheck and lint`, { step }));
|
|
36
|
+
findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} runs starci app lint at ${lints[0][1]}, but ${hfsPackage} is pinned at ${pinned}; run the pinned version`, { step: 'starci app lint', pinned }));
|
|
47
37
|
}
|
|
48
38
|
}
|
|
49
39
|
return findings;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// proof-commands.mjs - HFS_PROOF_COMMAND_FILE_MISSING (R105): a proof command a `.starciwork` record names runs files that exist.
|
|
2
2
|
// A record's `requiresProof.<kind>.command` is "the exact command that satisfies this kind, runnable as written" (work-implementation
|
|
3
3
|
// schema); a command that names a spec, a script or a config the repository does not hold cannot be run as written, and a record that
|
|
4
|
-
// keeps one goes quietly false (
|
|
4
|
+
// keeps one goes quietly false (a product repo once kept `node --test <repo>/scripts/provision-keycloak.spec.mjs` after the script was deleted).
|
|
5
5
|
// Every argument of the command that is a repository-relative file path (an extension, no glob, no variable, not absolute, not a `..`
|
|
6
6
|
// path into another repository) must be a tracked file or an existing one; a path an ignored slot owns (dist/, coverage/) is a build
|
|
7
7
|
// product and is not judged. One finding per record, kind and missing path. The `.starciwork` of an app sits at the app root, and a
|
|
@@ -13,7 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
14
14
|
import { found, readText } from './read.mjs';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const PROOF_COMMAND_FILE_MISSING = 'HFS_PROOF_COMMAND_FILE_MISSING';
|
|
17
17
|
const RECORD = /^\.starciwork\/.+\.ya?ml$/;
|
|
18
18
|
const FILE_ARGUMENT = /^(?:\.\/)?[\w@][\w@.\-[\]()]*(?:\/[\w@.\-[\]()]+)*\.[A-Za-z][A-Za-z0-9]{0,7}$/;
|
|
19
19
|
const PATH_SEPARATOR = /\//;
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
5
|
/** Files above this size are not read by a tree check (a tracked binary or a generated dump is no source of these laws). */
|
|
6
|
-
|
|
6
|
+
const MAX_READ_BYTES = 1024 * 1024;
|
|
7
7
|
|
|
8
8
|
/** The text of `rel` under `repoRoot`, or null when it is absent, unreadable, over MAX_READ_BYTES or binary. */
|
|
9
9
|
export function readText(repoRoot, rel) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// included (HFS_RULE_OFF_WITHOUT_REPLACEMENT, R17). This rule adds the forbidden roles by name: what a check or a rule file is called.
|
|
10
10
|
import { found, readJson } from './read.mjs';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const REPO_LOCAL_CHECK = 'HFS_REPO_LOCAL_CHECK';
|
|
13
13
|
const LOCAL_RULE_FILE = /(?:^|\/)(?:eslint-local-rules|eslint-plugin|eslint-local-plugin)[^/]*(?:\/|$)|(?:^|\/)(?:eslint-rules|lint-rules)\//;
|
|
14
14
|
const CHECK_SCRIPT = /(?:^|\/)(?:scripts|tools)\/check-[^/]+$/;
|
|
15
15
|
const RUNS_CHECK = /(?:^|[\s&|;(])(?:\.\/)?(?:scripts|tools)\/check-[^\s&|;)]+/;
|
|
@@ -20,7 +20,7 @@ export function repoLocalCheckFindings({ repoRoot, files }) {
|
|
|
20
20
|
for (const file of files) {
|
|
21
21
|
if (file.includes('node_modules/')) continue;
|
|
22
22
|
if (LOCAL_RULE_FILE.test(file)) findings.push(found(REPO_LOCAL_CHECK, file, `${file} is a local lint rule or plugin; a repository keeps no rule of its own: the rule is proposed to the canon (@starci/eslint-canon-*) in the .claude runtime`));
|
|
23
|
-
else if (CHECK_SCRIPT.test(file)) findings.push(found(REPO_LOCAL_CHECK, file, `${file} is a check kept in the repository; every check lives in the .claude runtime (
|
|
23
|
+
else if (CHECK_SCRIPT.test(file)) findings.push(found(REPO_LOCAL_CHECK, file, `${file} is a check kept in the repository; every check lives in the .claude runtime (starci app check, the canons), so a repository has none to keep`));
|
|
24
24
|
else if (file === 'package.json' || file.endsWith('/package.json')) {
|
|
25
25
|
const pkg = readJson(repoRoot, file);
|
|
26
26
|
for (const [name, command] of Object.entries(pkg?.scripts ?? {})) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// saga.mjs - the saga pattern of a product that declares `patterns: ["saga"]` in hfs.json (R169 to R173). A saga is a feature of the saga kind
|
|
2
|
+
// be/src/features/saga/<saga>/
|
|
3
|
+
// <saga>.saga.service.ts the orchestrator: the list of its steps and compensations (a service, unit-tested beside it)
|
|
4
|
+
// <saga>.saga-state.ts the persisted state of a run, typed with its `status` and its `version` fence
|
|
5
|
+
// steps/<step>.saga-step.ts one step, dispatching one command, naming the event it puts on the wire
|
|
6
|
+
// compensations/<step>.compensation.ts the compensation of that step, naming the failure event that triggers it
|
|
7
|
+
// be/src/features/saga/<saga>/transport/message/<event>.consumer.ts the consumers that advance it, through the inbox
|
|
8
|
+
// be/contracts/<service>/events.json every event, a compensating one declaring `compensates: "<step event>"`
|
|
9
|
+
// Five tree checks (the slot manifest owns the paths: an undeclared saga folder is HFS_SLOT_NOT_ENABLED):
|
|
10
|
+
// R169 BE_SAGA_STEP_COMPENSATION every step has a compensation of the same name and the other way round, and the orchestrator imports them all;
|
|
11
|
+
// R170 BE_SAGA_STATE_VERSIONED every orchestrator has its `.saga-state.ts` whose exported state type carries `status` and a numeric `version`,
|
|
12
|
+
// and imports the fenced store of `platform/saga`;
|
|
13
|
+
// R171 BE_SAGA_EVENT_CONTRACT a step and a compensation each name their event (`readonly event = "<name>"`), both are in a contract, and the
|
|
14
|
+
// contract of the compensation's event says it compensates the step's event;
|
|
15
|
+
// R172 BE_SAGA_CONSUMER_DEDUPE a consumer of a saga feature passes the id of the delivery (`message.eventId`) on to the command, so the inbox can dedupe it;
|
|
16
|
+
// R173 BE_SAGA_E2E_MISSING every compensation path has an e2e spec through the world that names its event and injects a failure
|
|
17
|
+
// (an outage of an infra service, an app or a fake: `during`, `cut`, `latency`, `failNext`, `interruptDatabase`).
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
21
|
+
import { found, readJson, readText } from './read.mjs';
|
|
22
|
+
|
|
23
|
+
const SAGA_STEP_COMPENSATION = 'BE_SAGA_STEP_COMPENSATION';
|
|
24
|
+
const SAGA_STATE_VERSIONED = 'BE_SAGA_STATE_VERSIONED';
|
|
25
|
+
const SAGA_EVENT_CONTRACT = 'BE_SAGA_EVENT_CONTRACT';
|
|
26
|
+
const SAGA_CONSUMER_DEDUPE = 'BE_SAGA_CONSUMER_DEDUPE';
|
|
27
|
+
const SAGA_E2E_MISSING = 'BE_SAGA_E2E_MISSING';
|
|
28
|
+
|
|
29
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const SAGA_FILE = /^be\/src\/features\/saga\/([^/]+)\/(?:(?:steps|compensations)\/)?[^/]+\.(?:saga\.service|saga-state|saga-step|compensation)\.ts$/;
|
|
31
|
+
const ORCHESTRATOR = /^be\/src\/features\/saga\/([^/]+)\/([^/]+)\.saga\.service\.ts$/;
|
|
32
|
+
const STATE = /^be\/src\/features\/saga\/([^/]+)\/([^/]+)\.saga-state\.ts$/;
|
|
33
|
+
const STEP = /^be\/src\/features\/saga\/([^/]+)\/steps\/([^/]+)\.saga-step\.ts$/;
|
|
34
|
+
const COMPENSATION = /^be\/src\/features\/saga\/([^/]+)\/compensations\/([^/]+)\.compensation\.ts$/;
|
|
35
|
+
const CONSUMER = /^be\/src\/features\/saga\/([^/]+)\/transport\/message\/[^/]+\.consumer\.ts$/;
|
|
36
|
+
const E2E_SPEC = /^be\/src\/tests\/e2e\/[^/]+\/[^/]+\.e2e-spec\.ts$/;
|
|
37
|
+
const CONTRACT = /^be\/contracts\/[^/]+\/events\.json$/;
|
|
38
|
+
const USE_TEST_WORLD = /\buseTestWorld\s*\(/;
|
|
39
|
+
const INJECTION = /\.(?:during|cut|latency|failNext|interruptDatabase)\s*\(/;
|
|
40
|
+
const STORE_MODULE = /(?:^|\/)platform\/saga$/;
|
|
41
|
+
|
|
42
|
+
/** The TypeScript compiler of the app (else of the runtime), or null. */
|
|
43
|
+
const typescriptFor = (repoRoot) => loadTypescript(repoRoot, HERE);
|
|
44
|
+
|
|
45
|
+
const parse = (ts, text) => ts.createSourceFile('saga.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
46
|
+
|
|
47
|
+
/** The module specifiers a source file imports or re-exports. */
|
|
48
|
+
function specifiersOf(ts, sourceFile) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const statement of sourceFile.statements) {
|
|
51
|
+
if ((ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) out.push(statement.moduleSpecifier.text);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The string literal of the class property `event` (`readonly event = "order.placed"`), or null. */
|
|
57
|
+
function eventOf(ts, sourceFile) {
|
|
58
|
+
let name = null;
|
|
59
|
+
const visit = (node) => {
|
|
60
|
+
if (ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'event' && node.initializer && ts.isStringLiteralLike(node.initializer)) name = node.initializer.text;
|
|
61
|
+
ts.forEachChild(node, visit);
|
|
62
|
+
};
|
|
63
|
+
visit(sourceFile);
|
|
64
|
+
return name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Whether an exported interface or type alias of the file declares `status` and a `version: number` member. */
|
|
68
|
+
function statesVersion(ts, sourceFile) {
|
|
69
|
+
let versioned = false;
|
|
70
|
+
const memberNames = (members) => new Map(members.filter((member) => ts.isPropertySignature(member) && ts.isIdentifier(member.name)).map((member) => [member.name.text, member.type]));
|
|
71
|
+
for (const statement of sourceFile.statements) {
|
|
72
|
+
if (!statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
|
|
73
|
+
const members = ts.isInterfaceDeclaration(statement) ? statement.members : ts.isTypeAliasDeclaration(statement) && ts.isTypeLiteralNode(statement.type) ? statement.type.members : null;
|
|
74
|
+
if (members === null) continue;
|
|
75
|
+
const named = memberNames(members);
|
|
76
|
+
if (named.has('status') && named.get('version')?.kind === ts.SyntaxKind.NumberKeyword) versioned = true;
|
|
77
|
+
}
|
|
78
|
+
return versioned;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether a source text reads `.eventId` of something (the id of the delivery travels on). */
|
|
82
|
+
function readsEventId(ts, sourceFile) {
|
|
83
|
+
let read = false;
|
|
84
|
+
const visit = (node) => {
|
|
85
|
+
if (ts.isPropertyAccessExpression(node) && node.name.text === 'eventId') read = true;
|
|
86
|
+
ts.forEachChild(node, visit);
|
|
87
|
+
};
|
|
88
|
+
visit(sourceFile);
|
|
89
|
+
return read;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The sagas of the app: every folder of the saga kind (`be/src/features/saga/<saga>/`). */
|
|
93
|
+
const featuresOf = (files) => [...new Set(files.map((file) => SAGA_FILE.exec(file)?.[1]).filter((feature) => feature !== undefined))].sort();
|
|
94
|
+
|
|
95
|
+
/** Findings of R169 to R173 over the tracked paths `files` of the app at `repoRoot`. */
|
|
96
|
+
export function sagaFindings({ repoRoot, files }) {
|
|
97
|
+
const features = featuresOf(files);
|
|
98
|
+
if (features.length === 0) return [];
|
|
99
|
+
const ts = typescriptFor(repoRoot);
|
|
100
|
+
if (ts === null) return [];
|
|
101
|
+
const findings = [];
|
|
102
|
+
const events = new Map();
|
|
103
|
+
for (const contract of files.filter((file) => CONTRACT.test(file))) {
|
|
104
|
+
for (const [name, event] of Object.entries(readJson(repoRoot, contract)?.events ?? {})) events.set(name, { contract, ...event });
|
|
105
|
+
}
|
|
106
|
+
const specs = files.filter((file) => E2E_SPEC.test(file)).map((file) => ({ file, text: readText(repoRoot, file) ?? '' })).filter(({ text }) => USE_TEST_WORLD.test(text));
|
|
107
|
+
const compensationPaths = [];
|
|
108
|
+
for (const feature of features) {
|
|
109
|
+
const under = (pattern) => files.filter((file) => pattern.test(file) && pattern.exec(file)[1] === feature);
|
|
110
|
+
const steps = new Map(under(STEP).map((file) => [STEP.exec(file)[2], file]));
|
|
111
|
+
const compensations = new Map(under(COMPENSATION).map((file) => [COMPENSATION.exec(file)[2], file]));
|
|
112
|
+
const orchestrators = under(ORCHESTRATOR);
|
|
113
|
+
const states = new Map(under(STATE).map((file) => [STATE.exec(file)[2], file]));
|
|
114
|
+
const folder = `be/src/features/saga/${feature}/`;
|
|
115
|
+
// R169
|
|
116
|
+
for (const [stem, file] of steps) if (!compensations.has(stem)) findings.push(found(SAGA_STEP_COMPENSATION, file, `${file} is a saga step with no compensation: add ${folder}compensations/${stem}.compensation.ts, the command that undoes it; every forward step of a saga has one.`, { step: stem }));
|
|
117
|
+
for (const [stem, file] of compensations) if (!steps.has(stem)) findings.push(found(SAGA_STEP_COMPENSATION, file, `${file} is a compensation of no step: ${folder}steps/${stem}.saga-step.ts does not exist; a compensation undoes the step of the same name.`, { step: stem }));
|
|
118
|
+
if (orchestrators.length === 0 && (steps.size > 0 || compensations.size > 0)) findings.push(found(SAGA_STEP_COMPENSATION, folder, `${folder} holds steps or compensations but no orchestrator: add <saga>.saga.service.ts, the list of the steps and their compensations.`));
|
|
119
|
+
for (const orchestrator of orchestrators) {
|
|
120
|
+
const text = readText(repoRoot, orchestrator);
|
|
121
|
+
if (text === null) continue;
|
|
122
|
+
const sourceFile = parse(ts, text);
|
|
123
|
+
const imported = specifiersOf(ts, sourceFile).map((specifier) => path.posix.normalize(path.posix.join(path.posix.dirname(orchestrator), specifier)));
|
|
124
|
+
for (const file of [...steps.values(), ...compensations.values()]) {
|
|
125
|
+
if (!imported.includes(file.replace(/\.ts$/, ''))) findings.push(found(SAGA_STEP_COMPENSATION, orchestrator, `${orchestrator} does not list ${file}: the orchestrator imports every step and every compensation of its saga, so a step nobody runs cannot hide.`, { step: file }));
|
|
126
|
+
}
|
|
127
|
+
// R170
|
|
128
|
+
const stem = ORCHESTRATOR.exec(orchestrator)[2];
|
|
129
|
+
const stateFile = states.get(stem);
|
|
130
|
+
if (stateFile === undefined) findings.push(found(SAGA_STATE_VERSIONED, orchestrator, `${orchestrator} has no ${folder}${stem}.saga-state.ts: the persisted state of a run is typed there, with its status and its version fence.`, { saga: stem }));
|
|
131
|
+
if (!specifiersOf(ts, sourceFile).some((specifier) => STORE_MODULE.test(specifier))) findings.push(found(SAGA_STATE_VERSIONED, orchestrator, `${orchestrator} does not use the fenced store of platform/saga: a run is persisted and moved only through it (every transition names the version it read), never by a write of its own.`, { saga: stem }));
|
|
132
|
+
}
|
|
133
|
+
for (const [stem, file] of states) {
|
|
134
|
+
const text = readText(repoRoot, file);
|
|
135
|
+
if (text !== null && !statesVersion(ts, parse(ts, text))) findings.push(found(SAGA_STATE_VERSIONED, file, `${file} exports no state type with a \`status\` and a \`version: number\`; a saga run is persisted with a version, the fence that stops a zombie delivery from moving it twice.`, { saga: stem }));
|
|
136
|
+
}
|
|
137
|
+
// R171
|
|
138
|
+
const eventOfFile = new Map();
|
|
139
|
+
for (const [kind, map] of [['step', steps], ['compensation', compensations]]) {
|
|
140
|
+
for (const [stem, file] of map) {
|
|
141
|
+
const text = readText(repoRoot, file);
|
|
142
|
+
const name = text === null ? null : eventOf(ts, parse(ts, text));
|
|
143
|
+
eventOfFile.set(`${kind}:${stem}`, name);
|
|
144
|
+
if (name === null) findings.push(found(SAGA_EVENT_CONTRACT, file, `${file} names no event: a ${kind} of a saga declares \`readonly event = "<name>"\`, the event of the contract it ${kind === 'step' ? 'puts on the wire' : 'answers'}.`, { step: stem }));
|
|
145
|
+
else if (!events.has(name)) findings.push(found(SAGA_EVENT_CONTRACT, file, `${file} names the event "${name}", which no be/contracts/<service>/events.json declares; every event of a saga is in a vendored contract.`, { step: stem, event: name }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const [stem, file] of compensations) {
|
|
149
|
+
const compensationEvent = eventOfFile.get(`compensation:${stem}`);
|
|
150
|
+
const stepEvent = eventOfFile.get(`step:${stem}`);
|
|
151
|
+
if (compensationEvent && stepEvent && events.has(compensationEvent) && events.get(compensationEvent).compensates !== stepEvent) {
|
|
152
|
+
findings.push(found(SAGA_EVENT_CONTRACT, file, `${file}: the contract of "${compensationEvent}" (${events.get(compensationEvent).contract}) does not declare \`compensates: "${stepEvent}"\`; the failure event of a compensation says which step's event it undoes.`, { step: stem, event: compensationEvent }));
|
|
153
|
+
}
|
|
154
|
+
if (compensationEvent) compensationPaths.push({ file, stem, event: compensationEvent });
|
|
155
|
+
}
|
|
156
|
+
// R172
|
|
157
|
+
for (const file of under(CONSUMER)) {
|
|
158
|
+
const text = readText(repoRoot, file);
|
|
159
|
+
if (text !== null && !readsEventId(ts, parse(ts, text))) findings.push(found(SAGA_CONSUMER_DEDUPE, file, `${file} is a consumer of the saga ${feature} but never passes the id of the delivery on (\`message.eventId\`); the saga takes every event through the inbox, so the consumer hands the id to its command.`, { feature }));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// R173
|
|
163
|
+
for (const { file, stem, event } of compensationPaths) {
|
|
164
|
+
const named = specs.filter(({ text }) => text.includes(event));
|
|
165
|
+
const expected = `be/src/tests/e2e/<area>/<name>.e2e-spec.ts`;
|
|
166
|
+
if (named.length === 0) findings.push(found(SAGA_E2E_MISSING, file, `${file}: the compensation path of "${event}" has no e2e spec; add ${expected}, which boots the apps with useTestWorld, drives the failure and reads the compensated state back.`, { step: stem, event }));
|
|
167
|
+
else if (!named.some(({ text }) => INJECTION.test(text))) findings.push(found(SAGA_E2E_MISSING, named[0].file, `${named[0].file} names "${event}" but injects no failure; the compensation path is proven with a fault through the world (an outage of an infra service or an app with \`during\`, \`cut\`, \`latency\`, a fake's \`failNext\`, \`interruptDatabase\`), not only the happy flow.`, { step: stem, event }));
|
|
168
|
+
}
|
|
169
|
+
return findings;
|
|
170
|
+
}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// - a line of any other tracked text file that matches a secret pattern.
|
|
7
7
|
// A finding names the file, the line and the pattern, never the value.
|
|
8
8
|
import { FORBIDDEN_FILES, secretHits } from '../../lib/secret-patterns.mjs';
|
|
9
|
-
import { isSopsEnvelope } from '../../lib/
|
|
9
|
+
import { isSopsEnvelope } from '../../lib/sops-envelope.mjs';
|
|
10
10
|
import { found, readText } from './read.mjs';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const PLAINTEXT_SECRET = 'HFS_PLAINTEXT_SECRET';
|
|
13
13
|
/** Files whose lines are not secrets: a lockfile carries integrity hashes, never a credential. */
|
|
14
14
|
const NOT_SCANNED = /(^|\/)package-lock\.json$/;
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ export const slotOwnsSecrets = (slot) => slot?.rules?.includes(PLAINTEXT_SECRET)
|
|
|
19
19
|
/**
|
|
20
20
|
* The findings of R06 for one tracked file that no forbidden slot claims: a secret by being (an env file, a key file), an `.enc` that is
|
|
21
21
|
* no sops envelope, or a line that matches a secret pattern. `text` is the file's text, or null when it cannot be read (binary, too
|
|
22
|
-
* large, absent). The whole-tree check reads the work tree and the commit hook (`
|
|
22
|
+
* large, absent). The whole-tree check reads the work tree and the commit hook (`starci app hygiene`) reads the staged blob: one judgement.
|
|
23
23
|
*/
|
|
24
24
|
export function secretFileFindings({ file, text }) {
|
|
25
25
|
const being = FORBIDDEN_FILES.find((rule) => rule.test(file));
|