@starci/hfs 4.3.0 → 4.4.0
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 +11 -0
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/db-types.mjs +10 -1
- package/lint/run.mjs +1 -1
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +3401 -152
- package/runtime/knowledge/hfs/slots.yaml +344 -62
- package/runtime/knowledge/patterns/be/api.yaml +32 -5
- package/runtime/knowledge/patterns/be/cli.yaml +14 -4
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3113 -43
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +41 -0
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
- package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +46 -47
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/contract.mjs +66 -5
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +9 -0
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +9 -8
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-init.mjs +127 -19
- package/runtime/scripts/hfs/secret-sops.mjs +38 -6
- package/runtime/scripts/hfs/secret-store.mjs +56 -54
- package/runtime/scripts/hfs/secret.mjs +34 -5
- package/runtime/scripts/hfs/slot-imports.mjs +18 -5
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +5 -18
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +5 -2
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +14 -6
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +122 -0
- package/scaffold/add-fe-page.mjs +269 -0
- package/scaffold/add-inbox-lite.mjs +74 -42
- package/scaffold/add-table.mjs +110 -87
- package/scaffold/add.mjs +159 -44
- package/scaffold/app.mjs +124 -39
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/src/main.mjs +229 -116
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/index.mjs +108 -52
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +3 -0
- package/templates/app/gitignore-lite +3 -0
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/README.md +5 -3
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
- package/templates/be/inbox/database-client.ts.tpl +12 -11
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +42 -9
- package/templates/be/inbox/service.ts.tpl +23 -8
- package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/index.mjs +53 -40
|
@@ -1,18 +1,90 @@
|
|
|
1
1
|
// fe-contract-documents.mjs - FE_GRAPHQL_CONTRACT (R113): every GraphQL document a front end sends is one its back end
|
|
2
2
|
// serves. An app keeps the contract of each GraphQL service it runs as a snapshot, `be/contracts/<service>/schema.graphql`
|
|
3
3
|
// (`starci app emit`, HFS_CONTRACT_SNAPSHOT_DRIFT keeps it current), and its front end keeps every operation it sends
|
|
4
|
-
// in a `.graphql` file under `fe/`. Each operation is judged against the contract of the service that serves its first
|
|
4
|
+
// in a `.graphql` file or a `gql` template under `fe/`. Each operation is judged against the contract of the service that serves its first
|
|
5
5
|
// root field (the one snapshot whose root type declares it): a root field no contract declares, a field, argument or input
|
|
6
6
|
// field the contract does not declare, a required argument left out, a variable of another type, a leaf with a selection or
|
|
7
7
|
// an object without one is a finding on the document. The finding names the service and every problem.
|
|
8
8
|
import { operationProblems, parseDocument, parseSchema, rootFieldsOf } from '../../lib/graphql-contract.mjs';
|
|
9
9
|
import { found, readText } from './read.mjs';
|
|
10
10
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
13
|
+
import { isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
11
14
|
|
|
12
15
|
export const FE_GRAPHQL_CONTRACT = 'FE_GRAPHQL_CONTRACT';
|
|
13
16
|
|
|
14
17
|
const CONTRACT = /^be\/contracts\/([^/]+)\/schema\.graphql$/;
|
|
15
|
-
const
|
|
18
|
+
const isFrontend = (file) => file.startsWith('fe/') && !file.includes('/__generated__/') && !file.includes('/node_modules/');
|
|
19
|
+
const isDocument = (file) => isFrontend(file) && file.endsWith('.graphql');
|
|
20
|
+
const isSource = (file) => isFrontend(file) && /\.[cm]?[jt]sx?$/.test(file) && !file.endsWith('.d.ts');
|
|
21
|
+
const HERE = fileURLToPath(new URL('.', import.meta.url));
|
|
22
|
+
|
|
23
|
+
/** Local names of gql imports (including a graphql-tag default import), alongside the canonical gql spelling. */
|
|
24
|
+
function addGqlImportNames(ts, statement, names) {
|
|
25
|
+
if (!ts.isImportDeclaration(statement)) return;
|
|
26
|
+
const clause = statement.importClause;
|
|
27
|
+
if (statement.moduleSpecifier.text === 'graphql-tag' && clause?.name) names.add(clause.name.text);
|
|
28
|
+
const bindings = clause?.namedBindings;
|
|
29
|
+
if (!bindings || !ts.isNamedImports(bindings)) return;
|
|
30
|
+
for (const entry of bindings.elements) {
|
|
31
|
+
if ((entry.propertyName ?? entry.name).text === 'gql') names.add(entry.name.text);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function gqlNames(ts, source) {
|
|
36
|
+
const names = new Set(['gql']);
|
|
37
|
+
for (const statement of source.statements) addGqlImportNames(ts, statement, names);
|
|
38
|
+
return names;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isGqlTag = (ts, tag, names) => (ts.isIdentifier(tag) && names.has(tag.text)) || (ts.isPropertyAccessExpression(tag) && tag.name.text === 'gql');
|
|
42
|
+
|
|
43
|
+
/** Resolve literal fragment substitutions without evaluating source code. Unknown or cyclic values cannot be judged. */
|
|
44
|
+
function templateText(ts, node, checker, names, visiting = new Set()) {
|
|
45
|
+
if (ts.isStringLiteralLike(node)) return node.text;
|
|
46
|
+
if (ts.isIdentifier(node)) {
|
|
47
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
48
|
+
const declaration = symbol?.valueDeclaration;
|
|
49
|
+
if (symbol?.declarations?.length !== 1 || !declaration || !ts.isVariableDeclaration(declaration)
|
|
50
|
+
|| !ts.isIdentifier(declaration.name) || !isConstVariable(ts, declaration) || !declaration.initializer || visiting.has(declaration)) return null;
|
|
51
|
+
return templateText(ts, declaration.initializer, checker, names, new Set([...visiting, declaration]));
|
|
52
|
+
}
|
|
53
|
+
if (ts.isTaggedTemplateExpression(node) && isGqlTag(ts, node.tag, names)) return templateText(ts, node.template, checker, names, visiting);
|
|
54
|
+
if (!ts.isTemplateExpression(node)) return null;
|
|
55
|
+
let text = node.head.text;
|
|
56
|
+
for (const span of node.templateSpans) {
|
|
57
|
+
const value = templateText(ts, span.expression, checker, names, visiting);
|
|
58
|
+
if (value === null) return null;
|
|
59
|
+
text += value + span.literal.text;
|
|
60
|
+
}
|
|
61
|
+
return text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Bind this file in isolation: the compiler resolves lexical names, but imports and library files are never loaded. */
|
|
65
|
+
function lexicalChecker(ts, source) {
|
|
66
|
+
const options = { noLib: true, noResolve: true, allowJs: true };
|
|
67
|
+
const host = ts.createCompilerHost(options);
|
|
68
|
+
host.getSourceFile = (file) => file === source.fileName ? source : undefined;
|
|
69
|
+
host.fileExists = () => false;
|
|
70
|
+
host.readFile = () => undefined;
|
|
71
|
+
return ts.createProgram([source.fileName], options, host).getTypeChecker();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Actual tagged-template AST nodes: comments, quoted examples and other template tags are not operations. */
|
|
75
|
+
function gqlDocuments(ts, file, text) {
|
|
76
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
77
|
+
const names = gqlNames(ts, source);
|
|
78
|
+
const templates = [];
|
|
79
|
+
const visit = (node) => {
|
|
80
|
+
if (ts.isTaggedTemplateExpression(node) && isGqlTag(ts, node.tag, names)) templates.push(node);
|
|
81
|
+
ts.forEachChild(node, visit);
|
|
82
|
+
};
|
|
83
|
+
visit(source);
|
|
84
|
+
if (templates.length === 0) return [];
|
|
85
|
+
const checker = lexicalChecker(ts, source);
|
|
86
|
+
return templates.map((node) => ({ text: templateText(ts, node.template, checker, names), ...sourceLocation(source, node) }));
|
|
87
|
+
}
|
|
16
88
|
|
|
17
89
|
/** The parsed contracts of the app: [{ service, path, schema }], or a finding for a snapshot that does not parse. */
|
|
18
90
|
function contractsOf(repoRoot, files, findings) {
|
|
@@ -45,23 +117,51 @@ function operationFindings(file, document, operation, contracts) {
|
|
|
45
117
|
return [];
|
|
46
118
|
}
|
|
47
119
|
|
|
120
|
+
function documentFindings(file, text, contracts) {
|
|
121
|
+
let document;
|
|
122
|
+
try {
|
|
123
|
+
document = parseDocument(text);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return [found(FE_GRAPHQL_CONTRACT, file, `${file} is not a GraphQL document: ${error.message}.`)];
|
|
126
|
+
}
|
|
127
|
+
return document.operations.flatMap((operation) => operationFindings(file, document, operation, contracts));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sourceDocuments(ts, file, text, findings) {
|
|
131
|
+
if (!ts) {
|
|
132
|
+
findings.push(found(FE_GRAPHQL_CONTRACT, file, `${file} gql templates cannot be judged: no TypeScript resolves from the app; install the app's dependencies.`));
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const documents = [];
|
|
136
|
+
for (const template of gqlDocuments(ts, file, text)) {
|
|
137
|
+
if (template.text === null) findings.push(found(FE_GRAPHQL_CONTRACT, file, `${file}:${template.line} gql template has a substitution that cannot be resolved to static GraphQL text.`, { line: template.line, column: template.column }));
|
|
138
|
+
else documents.push({ file, ...template });
|
|
139
|
+
}
|
|
140
|
+
return documents;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function documentsOf(repoRoot, files, findings) {
|
|
144
|
+
const candidates = files.filter((file) => isDocument(file) || isSource(file)).sort(byCodeUnit);
|
|
145
|
+
const ts = candidates.some(isSource) ? loadTypescript(repoRoot, HERE) : null;
|
|
146
|
+
const documents = [];
|
|
147
|
+
for (const file of candidates) {
|
|
148
|
+
const text = readText(repoRoot, file);
|
|
149
|
+
if (text === null) continue;
|
|
150
|
+
if (isDocument(file)) documents.push({ file, text });
|
|
151
|
+
else documents.push(...sourceDocuments(ts, file, text, findings));
|
|
152
|
+
}
|
|
153
|
+
return documents;
|
|
154
|
+
}
|
|
155
|
+
|
|
48
156
|
/** The findings of R113 over the tracked paths `files` of the app at `repoRoot`. */
|
|
49
157
|
export function feContractFindings({ repoRoot, files }) {
|
|
50
|
-
const documents = files.filter(isDocument).sort(byCodeUnit);
|
|
51
|
-
if (documents.length === 0) return [];
|
|
52
158
|
const findings = [];
|
|
159
|
+
const documents = documentsOf(repoRoot, files, findings);
|
|
160
|
+
if (documents.length === 0) return findings;
|
|
53
161
|
const contracts = contractsOf(repoRoot, files, findings);
|
|
54
|
-
for (const file of documents) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
let document;
|
|
58
|
-
try {
|
|
59
|
-
document = parseDocument(text);
|
|
60
|
-
} catch (error) {
|
|
61
|
-
findings.push(found(FE_GRAPHQL_CONTRACT, file, `${file} is not a GraphQL document: ${error.message}.`));
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
for (const operation of document.operations) findings.push(...operationFindings(file, document, operation, contracts));
|
|
162
|
+
for (const { file, text, line, column } of documents) {
|
|
163
|
+
const entries = documentFindings(file, text, contracts);
|
|
164
|
+
findings.push(...entries.map((entry) => line ? { ...entry, line, column } : entry));
|
|
65
165
|
}
|
|
66
166
|
return findings;
|
|
67
167
|
}
|
|
@@ -13,6 +13,8 @@ import { DEPENDENCY_SECTIONS } from './fe-no-tests.mjs';
|
|
|
13
13
|
import { editionRuleParams } from '../edition-slots.mjs';
|
|
14
14
|
|
|
15
15
|
export const FE_STACK_FORBIDDEN = 'FE_STACK_FORBIDDEN';
|
|
16
|
+
export const FE_STACK_UNLISTED = 'FE_STACK_UNLISTED';
|
|
17
|
+
export const FE_STACK_CORE_MISSING = 'FE_STACK_CORE_MISSING';
|
|
16
18
|
const CATALOG_FILE = new URL('../../../knowledge/hfs/fe-stack.yaml', import.meta.url);
|
|
17
19
|
const PACKAGE_JSON = /(?:^|\/)package\.json$/;
|
|
18
20
|
const APP_ROOT_MANIFEST = 'package.json';
|
|
@@ -69,3 +71,58 @@ export function feStackFindings({ repoRoot, files, repo, manifest }) {
|
|
|
69
71
|
const manifests = files.filter((file) => !file.includes('node_modules/') && (atRoot ? file === APP_ROOT_MANIFEST : PACKAGE_JSON.test(file)));
|
|
70
72
|
return manifests.flatMap((rel) => manifestFindings(repoRoot, rel, skip));
|
|
71
73
|
}
|
|
74
|
+
|
|
75
|
+
/** R288 uses the same catalog as the scaffold, including tools installed once at the app root. */
|
|
76
|
+
function stackCatalog() {
|
|
77
|
+
const catalog = parseYaml(fs.readFileSync(CATALOG_FILE, 'utf8'));
|
|
78
|
+
const allowed = new Set();
|
|
79
|
+
const core = new Map();
|
|
80
|
+
for (const concern of catalog.concerns) {
|
|
81
|
+
const rootTool = ['codegen', 'lint'].includes(concern.id);
|
|
82
|
+
for (const name of concern.packages ?? []) {
|
|
83
|
+
allowed.add(name);
|
|
84
|
+
if (concern.tier === 'core') core.set(name, { concern: concern.id, rootTool: rootTool || ['typescript', 'tailwindcss'].includes(name) });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { allowed, core };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const dependenciesOf = pkg => new Set(DEPENDENCY_SECTIONS.flatMap(section => Object.keys(pkg?.[section] ?? {})));
|
|
91
|
+
|
|
92
|
+
function extraPackages(declaration) {
|
|
93
|
+
const entries = declaration?.sides?.fe?.extraPackages ?? declaration?.extraPackages ?? declaration?.fe?.extraPackages ?? [];
|
|
94
|
+
return new Set((Array.isArray(entries) ? entries : []).filter(entry => typeof entry?.name === 'string'
|
|
95
|
+
&& typeof entry.reason === 'string' && entry.reason.trim()).map(entry => entry.name));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function unlistedFindings(rel, pkg, allowed, extras, workspaces) {
|
|
99
|
+
const findings = [];
|
|
100
|
+
for (const section of DEPENDENCY_SECTIONS) for (const name of Object.keys(pkg?.[section] ?? {})) {
|
|
101
|
+
if (allowed.has(name) || extras.has(name) || workspaces.has(name) || forbiddenPackages().has(name)) continue;
|
|
102
|
+
findings.push(found(FE_STACK_UNLISTED, rel, `${rel} ${section} declares ${name}, absent from knowledge/hfs/fe-stack.yaml; add a canonical concern or declare sides.fe.extraPackages with its name and a nonempty reason in hfs.json`, { section, dependency: name }));
|
|
103
|
+
}
|
|
104
|
+
return findings;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Closed FE catalog and core presence over the front-end side (the mixed root also holds BE tools). */
|
|
108
|
+
export function feStackAllowlistFindings({ repoRoot, files, all = files, repo, inScope = () => true }) {
|
|
109
|
+
if (repo.profile !== 'fe' || repo.kind === 'runtime') return [];
|
|
110
|
+
const declaration = readJson(repoRoot, '../hfs.json') ?? repo;
|
|
111
|
+
const manifests = all.filter(file => PACKAGE_JSON.test(file) && !file.includes('node_modules/'));
|
|
112
|
+
const packages = new Map(manifests.map(file => [file, readJson(repoRoot, file)]));
|
|
113
|
+
const workspaces = new Set([...packages].filter(([file]) => file.startsWith('packages/')).map(([, pkg]) => pkg?.name).filter(Boolean));
|
|
114
|
+
const { allowed, core } = stackCatalog();
|
|
115
|
+
const extras = extraPackages(declaration);
|
|
116
|
+
const findings = [...packages].filter(([file]) => inScope(file)).flatMap(([file, pkg]) => unlistedFindings(file, pkg, allowed, extras, workspaces));
|
|
117
|
+
const rootTools = dependenciesOf(readJson(repoRoot, '../package.json'));
|
|
118
|
+
for (const app of repo.apps ?? []) {
|
|
119
|
+
if (app.kind !== 'next' || app.name === 'landing') continue;
|
|
120
|
+
const rel = `apps/${app.name}/package.json`;
|
|
121
|
+
const installed = dependenciesOf(packages.get(rel));
|
|
122
|
+
for (const [name, entry] of core) {
|
|
123
|
+
if (installed.has(name) || (entry.rootTool && rootTools.has(name))) continue;
|
|
124
|
+
findings.push(found(FE_STACK_CORE_MISSING, rel, `${rel} lacks core package ${name} for ${entry.concern} in knowledge/hfs/fe-stack.yaml; declare it ${entry.rootTool ? 'in the app root devDependencies or this workspace' : 'in this product workspace'}`, { dependency: name, concern: entry.concern }));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return findings;
|
|
128
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// feature-files.mjs - R292: a feature stays within ruleParams.featureFiles tracked files, in lite and full.
|
|
2
|
+
import { posixPath } from '../../lib/path-key.mjs';
|
|
3
|
+
import { found } from './read.mjs';
|
|
4
|
+
|
|
5
|
+
export const HFS_FEATURE_FILE_BUDGET = 'HFS_FEATURE_FILE_BUDGET';
|
|
6
|
+
|
|
7
|
+
/** Feature owners come from the slot manifest: BE trigger roots (including cli) and FE feature owners. */
|
|
8
|
+
const isFeature = (slot) => slot?.owner === true && slot.tier === 'feature' && (slot.trigger !== undefined || slot.id === 'fe.feature');
|
|
9
|
+
|
|
10
|
+
/** Count the complete tracked tree, even when a per-file check is scoped to only part of a feature. */
|
|
11
|
+
export function featureFileFindings({ files, all = files, repo, resolver, isRoot = repo.profile === 'app' }) {
|
|
12
|
+
// The app check invokes this again for each side, with that side's paths and edition-aware parameters.
|
|
13
|
+
if (isRoot || !['be', 'fe'].includes(repo.profile)) return [];
|
|
14
|
+
const max = resolver.ruleParams().featureFiles ?? 250;
|
|
15
|
+
const counts = new Map();
|
|
16
|
+
for (const file of new Set(all.map(posixPath))) {
|
|
17
|
+
if (!file || file.endsWith('/')) continue;
|
|
18
|
+
const owner = resolver.ownerOf(file);
|
|
19
|
+
if (!owner || !isFeature(resolver.slot(owner.slot))) continue;
|
|
20
|
+
counts.set(owner.root, (counts.get(owner.root) ?? 0) + 1);
|
|
21
|
+
}
|
|
22
|
+
return [...counts].filter(([, count]) => count > max).map(([root, count]) => found(
|
|
23
|
+
HFS_FEATURE_FILE_BUDGET,
|
|
24
|
+
root,
|
|
25
|
+
`${root} has ${count} tracked files, over the feature budget of ${max}; split it by product capability`,
|
|
26
|
+
{ files: count, max },
|
|
27
|
+
));
|
|
28
|
+
}
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
// Over the whole app (the root scope, app-relative paths: the one package.json and the be contract snapshots sit outside the fe side):
|
|
7
7
|
// FE_WIRE_GENERATED (R52) wire types come from the be contract snapshots the fe side reads in place (its declared reads,
|
|
8
8
|
// `be/contracts/`): the root package.json declares the `codegen` script, and each fe app's generated
|
|
9
|
-
// types on disk (`modules/api/__generated__/`, never tracked)
|
|
9
|
+
// types on disk (`modules/api/graphql/__generated__/`, never tracked) record the schema digest
|
|
10
10
|
// FE_I18N_PLACEMENT (R59) `next-intl` is a dependency of an fe workspace (an fe app manifest, or a shared i18n package under fe/packages/)
|
|
11
11
|
// The files each app must hold (routing.ts, navigation.ts, request.ts, messages/, the [locale] layout) are the slot manifest's requires
|
|
12
12
|
// (HFS_SLOT_REQUIRED_MISSING); which source may hold display text and how copy resolves is eslint-fe's.
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import {
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { found, readJson, readText } from './read.mjs';
|
|
16
17
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
17
18
|
|
|
18
19
|
const WIRE_GENERATED = 'FE_WIRE_GENERATED';
|
|
@@ -21,7 +22,7 @@ const I18N_CATALOG = 'FE_I18N_CATALOG';
|
|
|
21
22
|
export const DEFAULT_LOCALE = 'vi';
|
|
22
23
|
const ROUTE_FILE = /\/(?:page|layout|template|loading|error|not-found)\.[jt]sx?$/;
|
|
23
24
|
const LOCALE = /^[a-z]{2,3}(?:-[A-Za-z0-9]+)*$/;
|
|
24
|
-
const
|
|
25
|
+
export const WIRE_SCHEMA_STAMP = '.schema-sha256';
|
|
25
26
|
|
|
26
27
|
/** Every leaf key of a parsed catalog as a dotted path. */
|
|
27
28
|
function catalogKeys(value, prefix = '') {
|
|
@@ -30,40 +31,58 @@ function catalogKeys(value, prefix = '') {
|
|
|
30
31
|
return entries.length ? entries.flatMap(([key, child]) => catalogKeys(child, prefix ? `${prefix}.${key}` : key)) : [prefix];
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
let
|
|
38
|
-
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
/** Digest protocol for codegen: sorted app-relative schema path, NUL, LF-normalized source, NUL. */
|
|
35
|
+
export function wireSchemaDigest(repoRoot, snapshots) {
|
|
36
|
+
const hash = createHash('sha256');
|
|
37
|
+
for (const snapshot of [...snapshots].sort(byCodeUnit)) {
|
|
38
|
+
let source;
|
|
39
|
+
try { source = fs.readFileSync(path.join(repoRoot, snapshot), 'utf8'); } catch { return null; }
|
|
40
|
+
hash.update(`${snapshot}\0${source.replace(/\r\n/g, '\n')}\0`);
|
|
41
|
+
}
|
|
42
|
+
return hash.digest('hex');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Generated output is untracked, so inspect disk; the digest sidecar alone is not generated wire types. */
|
|
46
|
+
function hasGeneratedTypes(directory) {
|
|
47
|
+
let entries;
|
|
48
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { return false; }
|
|
49
|
+
return entries.some((entry) => entry.isDirectory() ? hasGeneratedTypes(path.join(directory, entry.name)) : entry.isFile() && entry.name.endsWith('.ts'));
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
const scriptsOf = (repoRoot, file) => readJson(repoRoot, file)?.scripts ?? {};
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
function wireDigestProblem(digest, stamped) {
|
|
55
|
+
if (digest === null) return 'cannot be verified against unreadable contract snapshots';
|
|
56
|
+
if (!/^[0-9a-f]{64}$/.test(stamped ?? '')) return 'has no valid schema digest recorded by codegen';
|
|
57
|
+
if (stamped !== digest) return 'records a different schema digest from the contract snapshots';
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function appWireFindings({ repoRoot, files, app, snapshots, digest }) {
|
|
62
|
+
const graphql = `fe/apps/${app}/src/modules/api/graphql/`;
|
|
63
|
+
const generated = `${graphql}__generated__`;
|
|
64
|
+
if (!hasGeneratedTypes(path.join(repoRoot, generated))) {
|
|
65
|
+
const operations = files.some((file) => file.startsWith(graphql) && /^(?:(?:queries|mutations)\/[^/]+\.ts|(?!(?:__generated__|node_modules)\/).*\.graphql)$/.test(file.slice(graphql.length)));
|
|
66
|
+
if (operations) return [found(WIRE_GENERATED, generated, `${app} keeps GraphQL operations but ${generated} has no generated wire types; run \`npm run codegen\``, { app })];
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
54
69
|
if (!snapshots.length) return [];
|
|
70
|
+
const stamped = readText(repoRoot, `${generated}/${WIRE_SCHEMA_STAMP}`)?.trim();
|
|
71
|
+
const problem = wireDigestProblem(digest, stamped);
|
|
72
|
+
if (problem === null) return [];
|
|
73
|
+
return [found(WIRE_GENERATED, generated, `${generated} ${problem}; run \`npm run codegen\` so its wire types and ${WIRE_SCHEMA_STAMP} match the schema`, { app, snapshots })];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** R52's tracked-source obligations, plus generated local artifacts in the workspace phase. */
|
|
77
|
+
function wireFindings({ repoRoot, files, apps, reads, phase }) {
|
|
78
|
+
const snapshots = files.filter((file) => file.endsWith('/schema.graphql') && reads.some((read) => file.startsWith(read)));
|
|
55
79
|
const findings = [];
|
|
56
|
-
if (typeof scriptsOf(repoRoot, 'package.json').codegen !== 'string') findings.push(found(WIRE_GENERATED, 'package.json', `the app keeps contract snapshots (${reads.join(', ')}) but its package.json declares no \`codegen\` script; wire types are generated from the snapshots, never typed by hand`));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let changed;
|
|
63
|
-
try { changed = fs.statSync(path.join(repoRoot, snapshot)).mtimeMs; } catch { continue; }
|
|
64
|
-
if (built + STALE_MS < changed) findings.push(found(WIRE_GENERATED, generated, `${generated} is older than ${snapshot}; run \`npm run codegen\` so the wire types match the contract snapshot`, { app, snapshot }));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
80
|
+
if (snapshots.length && typeof scriptsOf(repoRoot, 'package.json').codegen !== 'string') findings.push(found(WIRE_GENERATED, 'package.json', `the app keeps contract snapshots (${reads.join(', ')}) but its package.json declares no \`codegen\` script; wire types are generated from the snapshots, never typed by hand`));
|
|
81
|
+
// A clean checkout has no ignored build artifacts. The runtime's source-render check judges their tracked inputs;
|
|
82
|
+
// the ordinary app check/lint judges the generated output after the app's codegen step (also required by L4).
|
|
83
|
+
if (phase === 'source') return findings;
|
|
84
|
+
const digest = snapshots.length ? wireSchemaDigest(repoRoot, snapshots) : null;
|
|
85
|
+
findings.push(...apps.flatMap((app) => appWireFindings({ repoRoot, files, app, snapshots, digest })));
|
|
67
86
|
return findings;
|
|
68
87
|
}
|
|
69
88
|
|
|
@@ -74,7 +93,7 @@ function intlDependencyFindings({ repoRoot, files }) {
|
|
|
74
93
|
return workspaces.some(declares) ? [] : [found(I18N_PLACEMENT, 'package.json', 'no fe workspace depends on next-intl (an fe app or a shared fe package declares it); every fe app uses next-intl with the [locale] segment')];
|
|
75
94
|
}
|
|
76
95
|
|
|
77
|
-
function placementFindings({
|
|
96
|
+
function placementFindings({ app, tracked }) {
|
|
78
97
|
const base = `apps/${app}`;
|
|
79
98
|
const findings = [];
|
|
80
99
|
if (!tracked.includes(`${base}/src/proxy.ts`)) findings.push(found(I18N_PLACEMENT, `${base}/src/proxy.ts`, `${app} has no src/proxy.ts; locale routing lives in proxy.ts`, { app }));
|
|
@@ -118,9 +137,9 @@ export function frontendFindings({ repoRoot, files, repo }) {
|
|
|
118
137
|
return repo.apps.filter((a) => a.kind === 'next').flatMap((a) => [placementFindings, catalogFindings].flatMap((check) => check({ repoRoot, app: a.name, tracked: files })));
|
|
119
138
|
}
|
|
120
139
|
|
|
121
|
-
/** The app's front-end findings that read the root
|
|
122
|
-
export function appFrontendFindings({ repoRoot, files, repo }) {
|
|
140
|
+
/** The app's front-end findings that read the root; workspace checks include R52's ignored local artifacts. */
|
|
141
|
+
export function appFrontendFindings({ repoRoot, files, repo, phase = 'workspace' }) {
|
|
123
142
|
const apps = repo.sides.fe.apps.filter((a) => a.kind === 'next').map((a) => a.name);
|
|
124
143
|
// The snapshots are the paths the fe side declares it reads of the be side (hfs.json sides.fe.reads: be/contracts/).
|
|
125
|
-
return [...wireFindings({ repoRoot, files, apps, reads: repo.sides.fe.reads }), ...intlDependencyFindings({ repoRoot, files })];
|
|
144
|
+
return [...wireFindings({ repoRoot, files, apps, reads: repo.sides.fe.reads, phase }), ...intlDependencyFindings({ repoRoot, files })];
|
|
126
145
|
}
|
|
@@ -225,8 +225,9 @@ const MISSING_PARTS = Object.freeze({
|
|
|
225
225
|
outage: 'drive an outage through the world: world.infra.<service>, world.fake.<name>.failNext(...), world.apps.<peer>.during(...) or world.interruptDatabase(...)',
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
-
/** The
|
|
229
|
-
export function integrationSpecFindings({ repoRoot, files }) {
|
|
228
|
+
/** The full-edition test-world proofs of R112; lite owns its integration lane through R284. */
|
|
229
|
+
export function integrationSpecFindings({ repoRoot, files, repo }) {
|
|
230
|
+
if (repo?.edition === 'lite') return [];
|
|
230
231
|
const integrations = files.map((file) => CONFIG_FILE.exec(file)).filter(Boolean).map(([, side, provider]) => ({ side, provider, folder: `${side}src/modules/integrations/${provider}` }));
|
|
231
232
|
if (integrations.length === 0) return [];
|
|
232
233
|
const ts = typescriptFor(repoRoot);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// R283: the product's removed vocabulary is declared once in hfs.json lexicon.removed.
|
|
2
|
+
// Source, identifiers, comments and docs obey it in both editions; the declaration itself is not a use.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { escapeRegExp } from '../../lib/regex.mjs';
|
|
5
|
+
import { found, readJson, readText } from './read.mjs';
|
|
6
|
+
|
|
7
|
+
export const HFS_APP_LEXICON = 'HFS_APP_LEXICON';
|
|
8
|
+
const SOURCE_OR_DOC = /\.(?:[cm]?[jt]sx?|css|scss|sass|vue|svelte|gql|graphql|sql|sh|md|mdx|rst|adoc|txt)$/i;
|
|
9
|
+
const SOURCE_JSON = /(?:^|\/)(?:src|messages|locales)\/.*\.json$/i;
|
|
10
|
+
const OUTSIDE = /(?:^|\/)(?:node_modules|\.git|dist|build|secrets)\/|(?:^|\/)(?:secrets?|secret\.env)(?:\.|$)/i;
|
|
11
|
+
const LETTER_OR_NUMBER = /[\p{L}\p{N}]/u;
|
|
12
|
+
const LOWER = /\p{Ll}/u;
|
|
13
|
+
const UPPER = /\p{Lu}/u;
|
|
14
|
+
|
|
15
|
+
// A whole word also forms one segment of camelCase/PascalCase, a kebab name or a SCREAMING_SNAKE identifier.
|
|
16
|
+
function boundary(text, at) {
|
|
17
|
+
const left = text[at - 1];
|
|
18
|
+
const right = text[at];
|
|
19
|
+
return !left || !right || !LETTER_OR_NUMBER.test(left) || !LETTER_OR_NUMBER.test(right)
|
|
20
|
+
|| (LOWER.test(left) && UPPER.test(right))
|
|
21
|
+
|| (UPPER.test(left) && UPPER.test(right) && LOWER.test(text[at + 1] ?? ''));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function removedWords(repoRoot, repo) {
|
|
25
|
+
if (typeof repoRoot !== 'string') return [];
|
|
26
|
+
// Resolved side declarations contain the edition and slots, so read product metadata from the root declaration.
|
|
27
|
+
const root = repo.profile === 'be' || repo.profile === 'fe' ? path.dirname(repoRoot) : repoRoot;
|
|
28
|
+
const lexicon = repo.lexicon ?? readJson(root, 'hfs.json')?.lexicon;
|
|
29
|
+
return Array.isArray(lexicon?.removed) ? [...new Set(lexicon.removed.filter((word) => typeof word === 'string' && word.trim()).map((word) => word.trim()))] : [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function wordFindings(file, text, word) {
|
|
33
|
+
const matches = text.matchAll(new RegExp(escapeRegExp(word), 'giu'));
|
|
34
|
+
const findings = [];
|
|
35
|
+
const reported = new Set();
|
|
36
|
+
for (const match of matches) {
|
|
37
|
+
const start = match.index;
|
|
38
|
+
const end = start + match[0].length;
|
|
39
|
+
if (!boundary(text, start) || !boundary(text, end)) continue;
|
|
40
|
+
const line = text.slice(0, start).split('\n').length;
|
|
41
|
+
if (reported.has(line)) continue;
|
|
42
|
+
reported.add(line);
|
|
43
|
+
const column = start - text.lastIndexOf('\n', start - 1);
|
|
44
|
+
findings.push(found(HFS_APP_LEXICON, file, `${file}:${line} uses removed product word ${JSON.stringify(word)}; replace it with the current product vocabulary`, { line, column, word }));
|
|
45
|
+
}
|
|
46
|
+
return findings;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Removed product words in the tracked source and docs of this root/side scope, once per word and line. */
|
|
50
|
+
export function lexiconFindings({ repoRoot, files, repo, inScope = () => true }) {
|
|
51
|
+
if (repo.kind === 'runtime' || !['lite', 'full'].includes(repo.edition ?? 'full')) return [];
|
|
52
|
+
const words = removedWords(repoRoot, repo);
|
|
53
|
+
if (!words.length) return [];
|
|
54
|
+
const findings = [];
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
if (!inScope(file) || OUTSIDE.test(file) || !(SOURCE_OR_DOC.test(file) || SOURCE_JSON.test(file))) continue;
|
|
57
|
+
const text = readText(repoRoot, file);
|
|
58
|
+
if (text === null) continue;
|
|
59
|
+
for (const word of words) findings.push(...wordFindings(file, text, word));
|
|
60
|
+
}
|
|
61
|
+
return findings;
|
|
62
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// lite-proof-lane.mjs - R284: declared patterns in lite prove their scenarios in integration specs.
|
|
2
|
+
// Full keeps its existing e2e/integration proof checks. R155 owns BE_PATTERN_SPEC_MISSING in both editions.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { loadSlotManifest, ruleParams } from '../slots.mjs';
|
|
6
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
7
|
+
import { literalText } from '../../lib/ts-ast.mjs';
|
|
8
|
+
import { found, readText } from './read.mjs';
|
|
9
|
+
|
|
10
|
+
const LITE_PROOF_LANE_MISSING = 'BE_LITE_PROOF_LANE_MISSING';
|
|
11
|
+
const PATTERN_SPEC_MISSING = 'BE_PATTERN_SPEC_MISSING';
|
|
12
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const INTEGRATION_SPEC = /^be\/src\/tests\/integration\/.+\.integration-spec\.ts$/;
|
|
14
|
+
const PATTERNS = new Set(['event-bus', 'queue', 'webhooks', 'realtime', 'fenced-job', 'projection', 'saga']);
|
|
15
|
+
// Fallback saga scenarios until the shared manifest provides its scenario table.
|
|
16
|
+
const SAGA_SCENARIOS = ['compensation-on-failure', 'step-order', 'state-version-fences', 'consumer-dedupes'];
|
|
17
|
+
|
|
18
|
+
/** A disabled test/suite must not contribute its own title or any nested test titles. */
|
|
19
|
+
function disabledCall(ts, node) {
|
|
20
|
+
let callee = node.expression;
|
|
21
|
+
let disabled = false;
|
|
22
|
+
while (ts.isPropertyAccessExpression(callee) || ts.isCallExpression(callee)) {
|
|
23
|
+
if (ts.isPropertyAccessExpression(callee) && ['skip', 'todo'].includes(callee.name.text)) disabled = true;
|
|
24
|
+
callee = callee.expression;
|
|
25
|
+
}
|
|
26
|
+
return ts.isIdentifier(callee) && (callee.text === 'xdescribe'
|
|
27
|
+
|| (disabled && ['it', 'test', 'describe'].includes(callee.text)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A test callback is an inline function or a reference to one, rather than a missing/placeholder value. */
|
|
31
|
+
function hasCallback(ts, node) {
|
|
32
|
+
const callback = node.arguments[1];
|
|
33
|
+
return callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)
|
|
34
|
+
|| (ts.isIdentifier(callback) && callback.text !== 'undefined') || ts.isPropertyAccessExpression(callback));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Add scenario ids from literal it/test titles with callbacks; disabled suites and source text provide no proof. */
|
|
38
|
+
function addScenarios(ts, source, proven) {
|
|
39
|
+
const visit = (node) => {
|
|
40
|
+
if (ts.isCallExpression(node) && disabledCall(ts, node)) return;
|
|
41
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)
|
|
42
|
+
&& ['it', 'test'].includes(node.expression.text) && hasCallback(ts, node)) {
|
|
43
|
+
const title = literalText(ts, node.arguments[0]);
|
|
44
|
+
const colon = title?.indexOf(':') ?? -1;
|
|
45
|
+
if (colon > 0) proven.add(title.slice(0, colon));
|
|
46
|
+
}
|
|
47
|
+
ts.forEachChild(node, visit);
|
|
48
|
+
};
|
|
49
|
+
visit(source);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Only tracked, readable integration specs belong to lite's proof lane. */
|
|
53
|
+
function integrationProof(ts, repoRoot, files) {
|
|
54
|
+
const proven = new Set();
|
|
55
|
+
const specs = [];
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
if (!INTEGRATION_SPEC.test(file)) continue;
|
|
58
|
+
const text = readText(repoRoot, file);
|
|
59
|
+
if (text === null) continue;
|
|
60
|
+
specs.push(file);
|
|
61
|
+
if (ts) addScenarios(ts, ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true), proven);
|
|
62
|
+
}
|
|
63
|
+
return { specs, proven };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Findings of R284 and R155 at the app root; side passes must not duplicate them. */
|
|
67
|
+
export function liteProofLaneFindings({ repoRoot, files, all = files, repo, manifest }) {
|
|
68
|
+
if (repo.edition !== 'lite' || !repo.sides?.be) return [];
|
|
69
|
+
const patterns = [...new Set(repo.sides.be.patterns ?? [])].filter((pattern) => PATTERNS.has(pattern));
|
|
70
|
+
if (patterns.length === 0) return [];
|
|
71
|
+
const scenarios = ruleParams(manifest ?? loadSlotManifest(), 'be', 'lite').patternScenarios ?? {};
|
|
72
|
+
const ts = loadTypescript(repoRoot, HERE);
|
|
73
|
+
const { specs, proven } = integrationProof(ts, repoRoot, all);
|
|
74
|
+
const findings = [];
|
|
75
|
+
if (specs.length === 0) {
|
|
76
|
+
const declared = patterns.map((pattern) => `\`${pattern}\``).join(', ');
|
|
77
|
+
findings.push(found(LITE_PROOF_LANE_MISSING, 'hfs.json',
|
|
78
|
+
`Lite declares ${declared} but has no tracked integration spec in be/src/tests/integration; add *.integration-spec.ts with the declared patterns' scenario titles.`, { patterns }));
|
|
79
|
+
}
|
|
80
|
+
for (const pattern of patterns) {
|
|
81
|
+
const required = scenarios[pattern] ?? (pattern === 'saga' ? SAGA_SCENARIOS : []);
|
|
82
|
+
for (const scenario of required) {
|
|
83
|
+
const id = `${pattern}/${scenario}`;
|
|
84
|
+
if (!proven.has(id)) findings.push(found(PATTERN_SPEC_MISSING, 'hfs.json',
|
|
85
|
+
`The lite pattern \`${pattern}\` is declared but no integration spec in be/src/tests/integration holds a test titled \`${id}: ...\`; prove this scenario in the lite integration lane.`, { pattern, scenario: id }));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return findings;
|
|
89
|
+
}
|
|
@@ -183,10 +183,23 @@ function workspaceMissingDependencies(repoRoot, files, folder, names, ts) {
|
|
|
183
183
|
const pkg = readJson(repoRoot, manifest);
|
|
184
184
|
if (!pkg) return [];
|
|
185
185
|
const missing = undeclaredImports(repoRoot, files, folder, pkg, declaredIn(pkg), ts);
|
|
186
|
-
|
|
186
|
+
const findings = [...missing].sort(byCodeUnit).map(([name, file]) => {
|
|
187
187
|
const declaration = names.get(name) ? `declare the workspace package ${name} as "*"` : `declare ${name} in the dependencies of ${manifest} (one version across the workspace, R14)`;
|
|
188
188
|
return found(MONO_WORKSPACE_DEP, manifest, `${file} imports ${name}, which ${manifest} does not declare; ${declaration}.`, { dependency: name, importedBy: file });
|
|
189
189
|
});
|
|
190
|
+
return [...findings, ...workspaceVersionFindings(manifest, pkg, names)];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Every declaration of a sibling workspace uses npm's local workspace version, in every dependency section. */
|
|
194
|
+
function workspaceVersionFindings(manifest, pkg, names) {
|
|
195
|
+
const findings = [];
|
|
196
|
+
for (const section of DEP_SECTIONS) {
|
|
197
|
+
for (const [name, version] of Object.entries(pkg[section] ?? {})) {
|
|
198
|
+
if (!names.has(name) || name === pkg.name || version === '*') continue;
|
|
199
|
+
findings.push(found(MONO_WORKSPACE_DEP, manifest, `${manifest} declares sibling workspace ${name} as ${JSON.stringify(version)} in ${section}; declare the workspace package as "*".`, { dependency: name, section, version }));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return findings;
|
|
190
203
|
}
|
|
191
204
|
|
|
192
205
|
/** R146: what each fe workspace imports, it declares; the root declares no workspace package. */
|
|
@@ -7,6 +7,7 @@
|
|
|
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
|
+
import { isRuntimeExampleApp } from '../../lib/runtime-example.mjs';
|
|
10
11
|
|
|
11
12
|
const CI_MISSING_CANON = 'HFS_CI_MISSING_CANON';
|
|
12
13
|
const CI_FILE = '.github/workflows/ci.yml';
|
|
@@ -15,6 +16,7 @@ const RUN_PREFIX = /^\s*(?:-\s+)?run:/;
|
|
|
15
16
|
const NPX_PREFIX = /^npx\s+(?:(?:--no-install|-y)\s+)?/;
|
|
16
17
|
const LINT_COMMAND = /^(?:@starci\/cli(?:@(\S+))?|starci)\s+app\s+lint/;
|
|
17
18
|
const NPM_RUN_PREFIX = 'npm run ';
|
|
19
|
+
const EXAMPLE_RUN_PREFIX = 'node node_modules/@starci/hfs/runtime/scripts/hfs/example-secrets.mjs --run . -- ';
|
|
18
20
|
const SCRIPT_NAME = /^[\w:.-]+/;
|
|
19
21
|
const ARGS_SEPARATOR = /^\s+--\s+/;
|
|
20
22
|
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
|
|
@@ -72,6 +74,9 @@ function expanded(command, scripts) {
|
|
|
72
74
|
return run.args ? `${scripts[run.script]} ${run.args}` : scripts[run.script];
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/** Only the bundled example runner with its fixed current-app arguments delegates to the following argv. */
|
|
78
|
+
const exampleCommandOf = (command) => command.startsWith(EXAMPLE_RUN_PREFIX) ? command.slice(EXAMPLE_RUN_PREFIX.length) : command;
|
|
79
|
+
|
|
75
80
|
/** The findings of R13 over the repository at `repoRoot`; `pins` is the parsed canon-pins.yaml pin map. */
|
|
76
81
|
export function pipelineFindings({ repoRoot, files, pins }) {
|
|
77
82
|
const findings = [];
|
|
@@ -79,7 +84,9 @@ export function pipelineFindings({ repoRoot, files, pins }) {
|
|
|
79
84
|
const ci = files.includes(CI_FILE) ? readText(repoRoot, CI_FILE) : null;
|
|
80
85
|
if (ci !== null) {
|
|
81
86
|
const scripts = readJson(repoRoot, 'package.json')?.scripts ?? {};
|
|
82
|
-
const
|
|
87
|
+
const commands = commandsOf(ci);
|
|
88
|
+
const selected = isRuntimeExampleApp(repoRoot) ? commands.map(exampleCommandOf) : commands;
|
|
89
|
+
const lints = selected.flatMap((command) => chainedCommands(expanded(command, scripts))).map(lintCommandOf).filter(Boolean);
|
|
83
90
|
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' }));
|
|
84
91
|
else if (pinned && !lints.some((match) => match.version === undefined || match.version === pinned)) {
|
|
85
92
|
findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} runs starci app lint at ${lints[0].version}, but ${hfsPackage} is pinned at ${pinned}; run the pinned version`, { step: 'starci app lint', pinned }));
|