@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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// assets.mjs - R282: small build-time binaries belong in be.assets; user media, model weights and datasets do not belong in git.
|
|
2
|
+
// The owner assets policy has no numeric cap yet. 512000 bytes follows the existing repo.docs-media budget in slots.yaml;
|
|
3
|
+
// ruleParams.be.assets {bytes (or maxBytes), extensions} supplies a manifest's explicit cap and build-time extension allowlist.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { editionRuleParams } from '../edition-slots.mjs';
|
|
7
|
+
import { found } from './read.mjs';
|
|
8
|
+
|
|
9
|
+
export const HFS_ASSET_OVERSIZE = 'HFS_ASSET_OVERSIZE';
|
|
10
|
+
export const HFS_ASSET_KIND = 'HFS_ASSET_KIND';
|
|
11
|
+
export const DEFAULT_ASSET_BYTES = 512000;
|
|
12
|
+
const BUILD_EXTENSIONS = ['.ttf', '.otf', '.woff', '.woff2', '.eot', '.png', '.svg', '.webp', '.jpg', '.jpeg', '.gif', '.ico', '.avif', '.bmp', '.tif', '.tiff'];
|
|
13
|
+
const MEDIA_EXTENSIONS = new Set(['.mp3', '.mp4', '.wav', '.ogg', '.oga', '.opus', '.flac', '.aac', '.m4a', '.wma', '.aiff', '.aif', '.mid', '.midi', '.mov', '.m4v', '.webm', '.avi', '.mkv', '.mpeg', '.mpg', '.wmv', '.flv', '.3gp']);
|
|
14
|
+
const IMAGE_EXTENSIONS = new Set(BUILD_EXTENSIONS.filter((ext) => !['.ttf', '.otf', '.woff', '.woff2', '.eot'].includes(ext)));
|
|
15
|
+
const MODEL_EXTENSIONS = new Set(['.onnx', '.pt', '.pth', '.ckpt', '.safetensors', '.gguf', '.ggml', '.tflite', '.keras', '.caffemodel', '.h5', '.hdf5', '.npy', '.npz', '.pkl', '.pickle', '.joblib', '.weights']);
|
|
16
|
+
const DATASET_EXTENSIONS = new Set(['.parquet', '.arrow', '.feather', '.avro', '.tfrecord', '.tfrecords', '.jsonl', '.ndjson']);
|
|
17
|
+
const COMPRESSED = /\.(?:gz|bz2|xz|zst|zip|tar|tgz)$/i;
|
|
18
|
+
const SOURCE_OR_DOC = /\.(?:[cm]?[jt]sx?|md|mdx|rst)$/i;
|
|
19
|
+
const DATA_DIRECTORY = /(?:^|\/)(?:datasets?|training-data|training-set|weights|checkpoints)(?:\/|$)/i;
|
|
20
|
+
const MODEL_DIRECTORY = /(?:^|\/)models?(?:\/|$)/i;
|
|
21
|
+
const DOCS_MEDIA = /^docs\/.+\.(?:png|svg|webp)$/i;
|
|
22
|
+
const USER_ASSET_DIRECTORY = /^src\/assets\/(?:media|uploads?|music|audio|videos?)(?:\/|$)/i;
|
|
23
|
+
const MODEL_BINARY = /(?:^|\/)(?:[^/]*[-_.])?(?:model|weights|checkpoint)(?:[-_.][^/]*)?\.bin$/i;
|
|
24
|
+
|
|
25
|
+
function assetPolicy(manifest, edition) {
|
|
26
|
+
const config = editionRuleParams(manifest?.ruleParams?.be ?? {}, edition).assets ?? {};
|
|
27
|
+
const cap = config.bytes ?? config.maxBytes;
|
|
28
|
+
const bytes = Number.isSafeInteger(cap) && cap > 0 ? cap : DEFAULT_ASSET_BYTES;
|
|
29
|
+
const extensions = Array.isArray(config.extensions) ? config.extensions : BUILD_EXTENSIONS;
|
|
30
|
+
return { bytes, extensions: new Set(extensions.filter((ext) => typeof ext === 'string').map((ext) => `.${ext.replace(/^\./, '').toLowerCase()}`)) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function uncompressedName(file) {
|
|
34
|
+
let name = file;
|
|
35
|
+
while (COMPRESSED.test(name)) name = name.replace(COMPRESSED, '');
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function refusedKind(file, extensions) {
|
|
40
|
+
const name = uncompressedName(file);
|
|
41
|
+
const ext = path.posix.extname(name).toLowerCase();
|
|
42
|
+
const asset = file.startsWith('src/assets/');
|
|
43
|
+
if (MODEL_EXTENSIONS.has(ext) || MODEL_BINARY.test(name) || (ext === '.bin' && MODEL_DIRECTORY.test(file))) return 'model weights';
|
|
44
|
+
if (DATASET_EXTENSIONS.has(ext)) return 'a dataset or model artifact';
|
|
45
|
+
// repo.docs-media owns these images even when their subject is a dataset, weights or checkpoints.
|
|
46
|
+
if (DOCS_MEDIA.test(file)) return null;
|
|
47
|
+
if (DATA_DIRECTORY.test(file) && !SOURCE_OR_DOC.test(file)) return 'a dataset or model artifact';
|
|
48
|
+
if (MEDIA_EXTENSIONS.has(ext) || USER_ASSET_DIRECTORY.test(file)) return 'user media';
|
|
49
|
+
if (asset) {
|
|
50
|
+
if (!/^src\/assets\/[^/]+\/.+/.test(file)) return 'an asset outside a per-kind folder';
|
|
51
|
+
if (!extensions.has(path.posix.extname(file).toLowerCase())) return 'an asset extension outside ruleParams.be.assets.extensions';
|
|
52
|
+
} else if (IMAGE_EXTENSIONS.has(ext)) return 'media outside be.assets';
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fileBytes(repoRoot, file) {
|
|
57
|
+
try {
|
|
58
|
+
const stat = fs.lstatSync(path.join(repoRoot, file));
|
|
59
|
+
return stat.isFile() ? stat.size : null;
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Side-relative tracked paths only: the app root and FE are left to their own rules, avoiding duplicate root/side findings. */
|
|
64
|
+
export function assetsFindings({ repoRoot, files = [], repo, manifest, isRoot = false, inScope = () => true }) {
|
|
65
|
+
const edition = repo?.edition ?? 'full';
|
|
66
|
+
if (isRoot || repo?.profile !== 'be' || !['lite', 'full'].includes(edition)) return [];
|
|
67
|
+
const policy = assetPolicy(manifest, edition);
|
|
68
|
+
const findings = [];
|
|
69
|
+
for (const raw of new Set(files)) {
|
|
70
|
+
if (!inScope(raw)) continue;
|
|
71
|
+
const file = raw.replaceAll('\\', '/');
|
|
72
|
+
const kind = refusedKind(file, policy.extensions);
|
|
73
|
+
if (kind) findings.push(found(HFS_ASSET_KIND, file, `${file} is ${kind}; keep only small build-time binaries in src/assets/<kind>/, put user media in object storage, and keep model weights and datasets outside git`));
|
|
74
|
+
if (!file.startsWith('src/assets/')) continue;
|
|
75
|
+
const bytes = fileBytes(repoRoot, file);
|
|
76
|
+
if (bytes !== null && bytes > policy.bytes) findings.push(found(HFS_ASSET_OVERSIZE, file, `${file} is ${bytes} bytes, above the ${policy.bytes}-byte per-file build-time asset budget; store it externally and fetch it at runtime`, { bytes, maxBytes: policy.bytes }));
|
|
77
|
+
}
|
|
78
|
+
return findings;
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// R272: lite proves each app's real Nest composition with doubles; full keeps its e2e proof.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { appWiring } from '../app-wiring.mjs';
|
|
5
|
+
|
|
6
|
+
export const BE_APP_BOOT_PROOF_MISSING = 'BE_APP_BOOT_PROOF_MISSING';
|
|
7
|
+
|
|
8
|
+
/** Root scopes delegate to the be side, so each app is compiled once. */
|
|
9
|
+
export function bootProofFindings({ repoRoot, files, all = files, repo, isRoot = Boolean(repo?.sides), inScope = () => true }) {
|
|
10
|
+
if (repo?.edition !== 'lite' || isRoot || repo?.profile === 'fe' || repo?.kind === 'fe') return [];
|
|
11
|
+
// Dependencies not installed (a fresh scaffold, a lint-only checkout): the Nest graph cannot be compiled, so the proof is unavailable here; CI and every installed app judge it.
|
|
12
|
+
const installed = [repoRoot, path.dirname(repoRoot ?? '.')].some((dir) => dir && fs.existsSync(path.join(dir, 'node_modules')));
|
|
13
|
+
return appWiring({ repoRoot, files: all, repo }).findings
|
|
14
|
+
.filter(finding => installed || finding.reason !== 'module or proof could not be compiled')
|
|
15
|
+
.filter(finding => inScope(finding.path));
|
|
16
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// canon-copy-drift.mjs - HFS_CANON_COPY_DRIFT (R253): a handful of scaffolded files are hash-sensitive canon,
|
|
2
|
+
// not files to edit. The drift judgement is one sha256 against the bytes of the be/skeleton template the file was
|
|
3
|
+
// scaffolded from - the file equals canon, or it has drifted. A finding carries the path and both hashes, never
|
|
4
|
+
// file content.
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { found } from './read.mjs';
|
|
9
|
+
|
|
10
|
+
const CANON_COPY = 'HFS_CANON_COPY_DRIFT';
|
|
11
|
+
const TEMPLATE_ROOTS = [
|
|
12
|
+
path.resolve(import.meta.dirname, '../../../packages/hfs/templates'),
|
|
13
|
+
path.resolve(import.meta.dirname, '../../../../templates'),
|
|
14
|
+
];
|
|
15
|
+
const TEMPLATE_DIR = 'be/skeleton/';
|
|
16
|
+
|
|
17
|
+
// The three managed files the standard's secret-custody law pins byte-for-byte.
|
|
18
|
+
const CANON_PATHS = [
|
|
19
|
+
'src/modules/platform/config/env-source.config.ts',
|
|
20
|
+
'src/modules/platform/http-security/origin.guard.ts',
|
|
21
|
+
'src/modules/platform/http-security/webhook-signature.service.ts',
|
|
22
|
+
];
|
|
23
|
+
const REGEX_ESCAPE = String.raw`\$&`;
|
|
24
|
+
const CANON_SUFFIX = new RegExp(`(?:^|/)(${CANON_PATHS.map((rel) => rel.replace(/[.*+?^${}()|[\]\\]/g, REGEX_ESCAPE)).join('|')})$`);
|
|
25
|
+
|
|
26
|
+
const templateBytes = (rel) => {
|
|
27
|
+
for (const root of TEMPLATE_ROOTS) {
|
|
28
|
+
const file = path.join(root, TEMPLATE_DIR, rel);
|
|
29
|
+
try {
|
|
30
|
+
const bytes = fs.readFileSync(file);
|
|
31
|
+
if (bytes.length) return bytes;
|
|
32
|
+
} catch { /* next root */ }
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
37
|
+
|
|
38
|
+
export function canonCopyDriftFindings({ repoRoot, files, inScope }) {
|
|
39
|
+
const findings = [];
|
|
40
|
+
for (const file of files.filter((f) => !inScope || inScope(f))) {
|
|
41
|
+
const hit = CANON_SUFFIX.exec(file.replaceAll('\\', '/'));
|
|
42
|
+
if (!hit) continue;
|
|
43
|
+
const expected = templateBytes(hit[1]);
|
|
44
|
+
if (!expected) continue;
|
|
45
|
+
let actual;
|
|
46
|
+
try {
|
|
47
|
+
actual = fs.readFileSync(path.join(repoRoot, file));
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const expectedHash = sha256(expected);
|
|
52
|
+
const actualHash = sha256(actual);
|
|
53
|
+
if (actualHash === expectedHash) continue;
|
|
54
|
+
findings.push(found(CANON_COPY, file,
|
|
55
|
+
`${file} differs from the canon copy it was scaffolded from (sha256 ${actualHash.slice(0, 12)} vs ${expectedHash.slice(0, 12)}); restore the managed file's bytes - it is hash-sensitive canon, not a file to edit`,
|
|
56
|
+
{ expectedHash, actualHash }));
|
|
57
|
+
}
|
|
58
|
+
return findings;
|
|
59
|
+
}
|
|
@@ -9,13 +9,68 @@
|
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
12
13
|
import { found, readText } from './read.mjs';
|
|
13
14
|
import { safeRemove } from '../../api/fs/safe-remove.mjs';
|
|
14
15
|
import { makeTempDir } from '../../api/fs/make-temp-dir.mjs';
|
|
16
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
15
17
|
|
|
16
18
|
export const CONTRACT_SNAPSHOT_DRIFT = 'HFS_CONTRACT_SNAPSHOT_DRIFT';
|
|
17
19
|
const GRAPHQL_TRANSPORT_SLOT = 'be.transport.graphql';
|
|
18
20
|
const SNAPSHOT_OF = { graphql: 'schema.graphql', json: 'openapi.json' };
|
|
21
|
+
const GRAPHQL_PLATFORM = /^src\/modules\/platform\/graphql(?:\/|\.ts$|$)/;
|
|
22
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
|
|
24
|
+
/** A side-relative source import through a relative path or the canonical backend aliases. */
|
|
25
|
+
function sourcePath(from, specifier) {
|
|
26
|
+
const alias = [['@modules/', 'src/modules/'], ['@features/', 'src/features/']].find(([prefix]) => specifier.startsWith(prefix));
|
|
27
|
+
let relative = null;
|
|
28
|
+
if (alias) relative = `${alias[1]}${specifier.slice(alias[0].length)}`;
|
|
29
|
+
else if (specifier.startsWith('.')) relative = path.posix.join(path.posix.dirname(from), specifier);
|
|
30
|
+
return relative === null ? null : path.posix.normalize(relative).replace(/\.js$/, '.ts');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A tracked source file of an import, including directory entries and JavaScript-extension TypeScript imports. */
|
|
34
|
+
function sourceOf(base, tracked) {
|
|
35
|
+
return [base, `${base}.ts`, `${base}/index.ts`].find(file => tracked.has(file)) ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Runtime imports and re-exports, parsed by the same TypeScript loader as the neighbouring content rules. */
|
|
39
|
+
function runtimeSpecifiers(ts, file, text) {
|
|
40
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
41
|
+
const specifiers = [];
|
|
42
|
+
const visit = node => {
|
|
43
|
+
if (ts.isImportTypeNode(node)) return;
|
|
44
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
45
|
+
const clause = node.importClause ?? node;
|
|
46
|
+
const bindings = ts.isImportDeclaration(node) ? clause.namedBindings : node.exportClause;
|
|
47
|
+
const onlyTypes = clause.isTypeOnly || (!clause.name && bindings?.elements?.length && bindings.elements.every(item => item.isTypeOnly));
|
|
48
|
+
if (!onlyTypes && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) specifiers.push(node.moduleSpecifier.text);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0] && ts.isStringLiteral(node.arguments[0])) specifiers.push(node.arguments[0].text);
|
|
52
|
+
ts.forEachChild(node, visit);
|
|
53
|
+
};
|
|
54
|
+
visit(source);
|
|
55
|
+
return specifiers;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The import closure of one app's composition, including its groups and the feature transports it owns. */
|
|
59
|
+
function servesGraphql(app, tracked, classified, sources) {
|
|
60
|
+
if ([...classified.values()].some(file => file.slot === GRAPHQL_TRANSPORT_SLOT && file.bindings?.app === app.name)) return true;
|
|
61
|
+
const pending = [`apps/${app.name}/src/app.module.ts`];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
while (pending.length) {
|
|
64
|
+
const file = pending.pop();
|
|
65
|
+
if (seen.has(file) || !tracked.has(file)) continue;
|
|
66
|
+
seen.add(file);
|
|
67
|
+
if (classified.get(file)?.slot === GRAPHQL_TRANSPORT_SLOT) return true;
|
|
68
|
+
const bases = sources(file).map(specifier => sourcePath(file, specifier)).filter(base => base !== null);
|
|
69
|
+
if (bases.some(base => GRAPHQL_PLATFORM.test(base))) return true;
|
|
70
|
+
pending.push(...bases.map(base => sourceOf(base, tracked)).filter(Boolean));
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
19
74
|
|
|
20
75
|
/** sha256 of a file's text with line endings folded, or null when it cannot be read. */
|
|
21
76
|
function contractHash(file) {
|
|
@@ -26,13 +81,19 @@ function contractHash(file) {
|
|
|
26
81
|
export function contractFindings({ repoRoot, files, repo, resolver }) {
|
|
27
82
|
const findings = [];
|
|
28
83
|
if (repo.profile !== 'be') return findings;
|
|
29
|
-
const serves = files.some((file) => resolver.classifyPath(file).slot === GRAPHQL_TRANSPORT_SLOT);
|
|
30
84
|
const tracked = new Set(files);
|
|
85
|
+
const classified = new Map(files.map(file => [file, resolver.classifyPath(file)]));
|
|
86
|
+
const ts = loadTypescript(repoRoot, HERE);
|
|
87
|
+
const imports = new Map();
|
|
88
|
+
const sources = file => {
|
|
89
|
+
if (!imports.has(file)) imports.set(file, ts ? runtimeSpecifiers(ts, file, readText(repoRoot, file) ?? '') : []);
|
|
90
|
+
return imports.get(file);
|
|
91
|
+
};
|
|
31
92
|
for (const app of repo.apps.filter((a) => a.kind === 'api')) {
|
|
32
93
|
const operations = `contracts/${app.name}/${SNAPSHOT_OF.json}`;
|
|
33
94
|
if (tracked.has(`apps/${app.name}/src/operations.ts`) && !tracked.has(operations)) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, operations, `${app.name} declares an operation table (apps/${app.name}/src/operations.ts) but ${operations} is not committed; declare be.contract.openapi in hfs.json sides.be.optionalSlots, run \`npm run contract:emit\` and commit the snapshot`, { app: app.name }));
|
|
34
|
-
//
|
|
35
|
-
if (!
|
|
95
|
+
// Composition groups and transport ownership are app-specific: another service's GraphQL files do not apply here.
|
|
96
|
+
if (!servesGraphql(app, tracked, classified, sources)) continue;
|
|
36
97
|
const snapshot = `contracts/${app.name}/${SNAPSHOT_OF.graphql}`;
|
|
37
98
|
if (!tracked.has(snapshot)) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `${app.name} serves GraphQL but ${snapshot} is not committed; declare be.contract.graphql in hfs.json sides.be.optionalSlots, run \`npm run contract:emit\` and commit the snapshot`, { app: app.name }));
|
|
38
99
|
}
|
|
@@ -62,8 +123,8 @@ function artifactFindings({ repoRoot, tracked, scratch, app, artifact, what, emi
|
|
|
62
123
|
}
|
|
63
124
|
const fresh = contractHash(path.join(scratch, snapshot));
|
|
64
125
|
if (committed === null) {
|
|
65
|
-
// the uncommitted snapshot itself is reported by contractFindings above; here it only counts as not judged
|
|
66
126
|
apps.push({ app: app.name, artifact, status: 'not-committed' });
|
|
127
|
+
findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `${app.name} emits ${snapshot} but it is not committed or readable; run \`npm run contract:emit\` and commit the snapshot`, { app: app.name, drift: 'not-committed' }));
|
|
67
128
|
} else if (committed === fresh) {
|
|
68
129
|
apps.push({ app: app.name, artifact, status: 'fresh' });
|
|
69
130
|
} else {
|
|
@@ -76,7 +137,7 @@ function artifactFindings({ repoRoot, tracked, scratch, app, artifact, what, emi
|
|
|
76
137
|
* R23, full pass: each committed `contracts/<app>/schema.graphql` and `contracts/<app>/openapi.json` equals what `emit` writes for
|
|
77
138
|
* the app now. `emit({ repoRoot, declaration, outDir })` is `emitContracts` of packages/hfs/emit and answers `{ written, skipped }`;
|
|
78
139
|
* it is injected because the runtime scripts do not import the hfs package. Every api app is emitted on its own into a temp
|
|
79
|
-
* directory, so one app that cannot emit does not hide the others; an uncommitted snapshot is
|
|
140
|
+
* directory, so one app that cannot emit does not hide the others; an uncommitted emitted snapshot is a finding here too. Answers
|
|
80
141
|
* `{ findings, apps }`, `apps` being what was judged: `[{ app, artifact, status }]`, `artifact` being `schema.graphql` or
|
|
81
142
|
* `openapi.json` and `status` one of `fresh`, `stale`, `not-committed`, `none` (nothing emitted, nothing committed; GraphQL only),
|
|
82
143
|
* `left-behind` (committed, nothing emitted) or `emit-failed`.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// R273: an explicitly supplied lcov report proves 100% per measured logic file in either edition.
|
|
2
|
+
// The denominator is the existing slot-derived coverage scope, never the source records present in the report.
|
|
3
|
+
// lcov carries lines, functions and branches; the unit runner also enforces its statement threshold.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { coverageScope } from '../coverage-scope.mjs';
|
|
7
|
+
import { editionRuleParams, effectiveSlot, slotInEdition } from '../edition-slots.mjs';
|
|
8
|
+
import { globExpression } from '../../lib/glob.mjs';
|
|
9
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
10
|
+
import { found } from './read.mjs';
|
|
11
|
+
|
|
12
|
+
export const HFS_COVERAGE_BELOW = 'HFS_COVERAGE_BELOW';
|
|
13
|
+
const METRICS = ['lines', 'functions', 'branches'];
|
|
14
|
+
const SUMMARY = { LF: ['lines', 'found'], LH: ['lines', 'hit'], FNF: ['functions', 'found'], FNH: ['functions', 'hit'], BRF: ['branches', 'found'], BRH: ['branches', 'hit'] };
|
|
15
|
+
const SOURCE = /\.ts$/;
|
|
16
|
+
const SPEC = /(?:\.(?:spec|test)|-spec)\.ts$/;
|
|
17
|
+
const countOf = (value) => /^\d+$/.test(value) && Number.isSafeInteger(Number(value)) ? Number(value) : null;
|
|
18
|
+
const slash = (value) => path.posix.normalize(String(value).replaceAll('\\', '/'));
|
|
19
|
+
|
|
20
|
+
const emptyRecord = (source) => ({ source, lines: new Map(), functions: new Map(), branches: new Map(), summary: {}, problems: [] });
|
|
21
|
+
const addHits = (map, key, hits) => map.set(key, Math.min(Number.MAX_SAFE_INTEGER, (map.get(key) ?? 0) + hits));
|
|
22
|
+
const parseProblem = (record, line, tag) => record.problems.push(`invalid ${tag} at lcov line ${line}`);
|
|
23
|
+
|
|
24
|
+
function lineData(record, value, line) {
|
|
25
|
+
const [number, count] = value.split(',');
|
|
26
|
+
const hits = countOf(count ?? '');
|
|
27
|
+
if (countOf(number) === null || Number(number) < 1 || hits === null) parseProblem(record, line, 'DA');
|
|
28
|
+
else addHits(record.lines, String(Number(number)), hits);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function functionData(record, tag, value, line) {
|
|
32
|
+
const at = value.indexOf(',');
|
|
33
|
+
const count = countOf(value.slice(0, at));
|
|
34
|
+
let name = value.slice(at + 1);
|
|
35
|
+
// FN permits an optional end line before the function name; FNDA's names may themselves contain commas.
|
|
36
|
+
if (tag === 'FN' && /^\d+,/.test(name)) name = name.slice(name.indexOf(',') + 1);
|
|
37
|
+
if (at < 0 || count === null || !name || (tag === 'FN' && count < 1)) parseProblem(record, line, tag);
|
|
38
|
+
else if (tag === 'FN') { if (!record.functions.has(name)) record.functions.set(name, 0); }
|
|
39
|
+
else addHits(record.functions, name, count);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function branchData(record, value, line) {
|
|
43
|
+
const parts = value.split(',');
|
|
44
|
+
const hits = parts[3] === '-' ? 0 : countOf(parts[3] ?? '');
|
|
45
|
+
if (parts.length !== 4 || parts.slice(0, 3).some((part) => countOf(part) === null) || Number(parts[0]) < 1 || hits === null) parseProblem(record, line, 'BRDA');
|
|
46
|
+
else addHits(record.branches, parts.slice(0, 3).map(Number).join(','), hits);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function summaryData(record, tag, value, line) {
|
|
50
|
+
const count = countOf(value);
|
|
51
|
+
if (count === null || Object.hasOwn(record.summary, tag)) parseProblem(record, line, tag);
|
|
52
|
+
else record.summary[tag] = count;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function recordData(record, tag, value, line, problems) {
|
|
56
|
+
if (!['DA', 'FN', 'FNDA', 'BRDA', ...Object.keys(SUMMARY)].includes(tag)) return;
|
|
57
|
+
if (!record) { problems.push(`${tag} outside a source record at lcov line ${line}`); return; }
|
|
58
|
+
if (tag === 'DA') lineData(record, value, line);
|
|
59
|
+
else if (tag === 'FN' || tag === 'FNDA') functionData(record, tag, value, line);
|
|
60
|
+
else if (tag === 'BRDA') branchData(record, value, line);
|
|
61
|
+
else summaryData(record, tag, value, line);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Parse conventional lcov records without executing or reading any referenced source path. */
|
|
65
|
+
function parseLcov(text) {
|
|
66
|
+
const records = [];
|
|
67
|
+
const problems = [];
|
|
68
|
+
let current = null;
|
|
69
|
+
const flush = () => { if (current) records.push(current); current = null; };
|
|
70
|
+
for (const [index, raw] of String(text).replace(/^\uFEFF/, '').split(/\r?\n/).entries()) {
|
|
71
|
+
const line = raw.trim();
|
|
72
|
+
if (!line) continue;
|
|
73
|
+
if (line === 'end_of_record') { flush(); continue; }
|
|
74
|
+
const colon = line.indexOf(':');
|
|
75
|
+
const tag = line.slice(0, colon);
|
|
76
|
+
const value = line.slice(colon + 1);
|
|
77
|
+
if (tag === 'SF') {
|
|
78
|
+
flush();
|
|
79
|
+
if (!value || value.includes('\0')) problems.push(`invalid SF at lcov line ${index + 1}`);
|
|
80
|
+
else current = emptyRecord(value);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
recordData(current, tag, value, index + 1, problems);
|
|
84
|
+
}
|
|
85
|
+
flush();
|
|
86
|
+
if (!records.length) problems.push('lcov has no source records');
|
|
87
|
+
return { records, problems };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Apply the same edition data view as the slot resolver before deriving the coverage scope. */
|
|
91
|
+
function manifestOfEdition(manifest, edition) {
|
|
92
|
+
return {
|
|
93
|
+
...manifest,
|
|
94
|
+
slots: manifest.slots.filter((slot) => slotInEdition(manifest, slot, edition)).map((slot) => effectiveSlot(slot, 'be', edition)),
|
|
95
|
+
ruleParams: { ...manifest.ruleParams, be: editionRuleParams(manifest.ruleParams.be, edition) },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function inventory({ files, all, repo, manifest }) {
|
|
100
|
+
const prefix = repo.profile === 'app' || repo.sides ? 'be/' : '';
|
|
101
|
+
const effective = manifestOfEdition(manifest, repo.edition ?? 'full');
|
|
102
|
+
const scope = coverageScope(effective, repo.providers ?? repo.sides?.be?.providers ?? []);
|
|
103
|
+
const roots = scope.roots.map((dir) => globExpression(`${dir}**`));
|
|
104
|
+
const excludes = scope.excludes.map((dir) => globExpression(`${dir}**`));
|
|
105
|
+
const candidates = [...new Set((all ?? files ?? []).map(slash))].filter((file) => file.startsWith(prefix) && SOURCE.test(file) && !SPEC.test(file)).sort(byCodeUnit);
|
|
106
|
+
const measured = [];
|
|
107
|
+
const unmeasured = [];
|
|
108
|
+
for (const file of candidates) {
|
|
109
|
+
const relative = file.slice(prefix.length);
|
|
110
|
+
const role = /\.([a-z][a-z0-9-]*)\.ts$/.exec(relative)?.[1];
|
|
111
|
+
const measuredFile = scope.roles.includes(role) && roots.some((root) => root.test(relative)) && !excludes.some((exclude) => exclude.test(relative));
|
|
112
|
+
(measuredFile ? measured : unmeasured).push(file);
|
|
113
|
+
}
|
|
114
|
+
return { measured, unmeasured, unmeasuredScopes: scope.none.map((entry) => `${prefix}${entry}`), scope, prefix };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sourcePath(source) {
|
|
118
|
+
if (source.startsWith('file:')) {
|
|
119
|
+
try {
|
|
120
|
+
const uri = new URL(source);
|
|
121
|
+
source = decodeURIComponent(uri.pathname).replace(/^\/([a-z]:\/)/i, '$1');
|
|
122
|
+
} catch { return null; }
|
|
123
|
+
}
|
|
124
|
+
return slash(source);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function matchesSource(source, file, prefix) {
|
|
128
|
+
const relative = prefix && file.startsWith(prefix) ? file.slice(prefix.length) : file;
|
|
129
|
+
const choices = [file, relative, ...(prefix ? [] : [`be/${file}`])];
|
|
130
|
+
const windows = /^[a-z]:\//i.test(source) || source.startsWith('//');
|
|
131
|
+
const absolute = source.startsWith('/') || windows;
|
|
132
|
+
const key = windows ? source.toLowerCase() : source;
|
|
133
|
+
return choices.some((choice) => {
|
|
134
|
+
const candidate = windows ? choice.toLowerCase() : choice;
|
|
135
|
+
return key === candidate || (absolute && key.endsWith(`/${candidate}`));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function recordsByFile(records, measured, prefix) {
|
|
140
|
+
const mapped = new Map(measured.map((file) => [file, []]));
|
|
141
|
+
const problems = [];
|
|
142
|
+
for (const record of records) {
|
|
143
|
+
const source = sourcePath(record.source);
|
|
144
|
+
const matches = source === null ? [] : measured.filter((file) => matchesSource(source, file, prefix));
|
|
145
|
+
if (matches.length > 1) problems.push(`lcov source ${record.source} matches more than one measured file`);
|
|
146
|
+
else if (matches.length === 1) mapped.get(matches[0]).push(record);
|
|
147
|
+
}
|
|
148
|
+
return { mapped, problems };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function metricOf(record, metric) {
|
|
152
|
+
const tags = Object.keys(SUMMARY).filter((tag) => SUMMARY[tag][0] === metric);
|
|
153
|
+
const [foundTag, hitTag] = tags;
|
|
154
|
+
const details = record[metric];
|
|
155
|
+
const found = (details.size > 0 || record.summary[foundTag] === 0) ? details.size : record.summary[foundTag];
|
|
156
|
+
const hit = details.size ? [...details.values()].filter((count) => count > 0).length : record.summary[hitTag];
|
|
157
|
+
const problems = [];
|
|
158
|
+
if (found === undefined || hit === undefined) problems.push(`${metric} coverage is unavailable`);
|
|
159
|
+
else if (hit > found) problems.push(`${metric} hits exceed the denominator`);
|
|
160
|
+
if (details.size) {
|
|
161
|
+
if (record.summary[foundTag] !== undefined && record.summary[foundTag] !== found) problems.push(`${foundTag} disagrees with its records`);
|
|
162
|
+
if (record.summary[hitTag] !== undefined && record.summary[hitTag] !== hit) problems.push(`${hitTag} disagrees with its records`);
|
|
163
|
+
}
|
|
164
|
+
return { found, hit, problems, detailed: details.size > 0 };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function measuredMetric(records, metric) {
|
|
168
|
+
const values = records.map((record) => metricOf(record, metric));
|
|
169
|
+
const problems = values.flatMap((value) => value.problems);
|
|
170
|
+
const details = new Map();
|
|
171
|
+
for (const record of records) for (const [key, hits] of record[metric]) addHits(details, key, hits);
|
|
172
|
+
// Detailed test-name records merge by line/function/branch identity. Without identities, every summary must prove 100%.
|
|
173
|
+
const hasOnlyDetails = values.every((value) => value.detailed || value.found === 0);
|
|
174
|
+
const valid = values.every((value) => !value.problems.length);
|
|
175
|
+
const worst = values.reduce((lowest, value) => {
|
|
176
|
+
const rate = (entry) => entry.found === 0 ? 1 : (entry.hit ?? 0) / (entry.found ?? 1);
|
|
177
|
+
return rate(value) < rate(lowest) || (rate(value) === rate(lowest) && value.found > lowest.found) ? value : lowest;
|
|
178
|
+
});
|
|
179
|
+
const found = hasOnlyDetails ? details.size : (worst.found ?? 0);
|
|
180
|
+
const hit = hasOnlyDetails ? [...details.values()].filter((count) => count > 0).length : (worst.hit ?? 0);
|
|
181
|
+
let percent = null;
|
|
182
|
+
if (valid) percent = found === 0 ? 100 : 100 * hit / found;
|
|
183
|
+
if (valid && hit !== found) problems.push(`${metric} ${hit}/${found} (${(100 * hit / found).toFixed(2)}%) is below 100%`);
|
|
184
|
+
return { metric: { found, hit, percent }, problems };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function measuredResult(file, records) {
|
|
188
|
+
const problems = records.flatMap((record) => record.problems);
|
|
189
|
+
const metrics = {};
|
|
190
|
+
if (!records.length) return { path: file, metrics, problems: ['no lcov source record for this measured logic file'], ok: false };
|
|
191
|
+
for (const metric of METRICS) {
|
|
192
|
+
const result = measuredMetric(records, metric);
|
|
193
|
+
metrics[metric] = result.metric;
|
|
194
|
+
problems.push(...result.problems);
|
|
195
|
+
}
|
|
196
|
+
return { path: file, metrics, problems: [...new Set(problems)], ok: problems.length === 0 };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Read-only R273 report for an app or be side. `lcov` injects report text; `coverageFrom` selects a path relative to repoRoot.
|
|
201
|
+
* Only an explicit report enables the check; ordinary app check does not infer a stale report from coverage/lcov.info.
|
|
202
|
+
*/
|
|
203
|
+
export function coverageBelowReport({ repoRoot, files, all, repo, manifest, coverageFrom, lcov } = {}) {
|
|
204
|
+
const enabled = coverageFrom !== undefined || lcov !== undefined;
|
|
205
|
+
const reportPath = typeof coverageFrom === 'string' && coverageFrom ? slash(coverageFrom) : '<lcov>';
|
|
206
|
+
const empty = { ok: true, enabled, reportPath, metrics: METRICS, measured: [], unmeasured: [], unmeasuredScopes: [], results: [], findings: [] };
|
|
207
|
+
if (!enabled || !repo || !['app', 'be'].includes(repo.profile)) return empty;
|
|
208
|
+
const boundary = inventory({ files, all, repo, manifest });
|
|
209
|
+
const report = { ...empty, ...boundary };
|
|
210
|
+
const reportFailure = (message) => { report.findings.push(found(HFS_COVERAGE_BELOW, reportPath, message)); };
|
|
211
|
+
if (lcov === undefined) {
|
|
212
|
+
if (typeof coverageFrom !== 'string' || !coverageFrom) reportFailure('--coverage-from needs an lcov file path');
|
|
213
|
+
else {
|
|
214
|
+
try { lcov = fs.readFileSync(path.resolve(repoRoot, coverageFrom), 'utf8'); }
|
|
215
|
+
catch (error) { reportFailure(`lcov report cannot be read (${error.code ?? 'read failure'}); provide a fresh unit coverage report`); }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (lcov !== undefined) {
|
|
219
|
+
const parsed = parseLcov(lcov);
|
|
220
|
+
const { mapped, problems } = recordsByFile(parsed.records, boundary.measured, boundary.prefix);
|
|
221
|
+
for (const problem of [...parsed.problems, ...problems]) reportFailure(problem);
|
|
222
|
+
report.results = boundary.measured.map((file) => measuredResult(file, mapped.get(file)));
|
|
223
|
+
for (const result of report.results.filter((entry) => !entry.ok)) report.findings.push(found(HFS_COVERAGE_BELOW, result.path, `${result.path}: ${result.problems.join('; ')}; measured logic files require 100% lines, functions and branches`, { coverage: result.metrics }));
|
|
224
|
+
}
|
|
225
|
+
report.ok = report.findings.length === 0;
|
|
226
|
+
return report;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Synchronous finder for the alpha.15 registry; explicit coverage reports may also be passed through extraFindings. */
|
|
230
|
+
export const coverageBelowFindings = (context) => coverageBelowReport(context).findings;
|