@starci/hfs 4.3.0 → 4.4.1
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 +14 -0
- package/README.md +1 -1
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/contracts.mjs +2 -2
- package/emit/db-types.mjs +12 -3
- package/lint/run.mjs +3 -3
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/report/sonar.mjs +1 -1
- package/runtime/config.example.yaml +7 -6
- package/runtime/engine/config.mjs +10 -18
- package/runtime/engine/removed-vocabulary.mjs +0 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +2855 -163
- package/runtime/knowledge/hfs/slots.yaml +333 -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 +3091 -43
- package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
- package/runtime/modules/models/runtimes.yaml +5 -7
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +13769 -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 +4 -1
- 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 +42 -1
- 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/config.mjs +1 -1
- 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/index.mjs +1 -1
- 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 +4 -4
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- package/runtime/scripts/hfs/architecture.mjs +1 -1
- 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/coverage-scope.mjs +1 -1
- 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 +69 -9
- package/runtime/scripts/hfs/param-names.mjs +1 -1
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +158 -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/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract.mjs +67 -6
- 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 +2 -2
- 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 +3 -2
- 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/hfs/typescript-programs.mjs +0 -8
- 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/env.mjs +2 -2
- package/runtime/scripts/lib/git.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +0 -2
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/language.mjs +1 -1
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +2 -4
- 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 +1 -0
- package/scaffold/add-fe-page.mjs +1 -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/scaffold/supabase-ports.mjs +1 -1
- 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/format.mjs +1 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +110 -54
- 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 +54 -41
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// (sides, app kinds) and refuses a manifest whole on any problem (HFS_MANIFEST_INVALID). Pure: every function takes a
|
|
5
5
|
// parsed manifest or slot and returns a list of problems in the words of modules/schemas/hfs-slots.schema.yaml.
|
|
6
6
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
7
|
-
import { stringList, byCodeUnit } from '../lib/list.mjs';
|
|
7
|
+
import { stringList, byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
|
|
8
8
|
|
|
9
9
|
export const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
10
10
|
export const NAME = /^[a-z][a-z0-9-]*$/;
|
|
@@ -56,8 +56,6 @@ export const unitRolesOf = (manifest) => manifest.ruleParams.be.unitRoles;
|
|
|
56
56
|
/** The roles that carry logic (ruleParams.be.logicRoles): measured at 100 per file inside a coverage-required slot. */
|
|
57
57
|
export const logicRolesOf = (manifest) => manifest.ruleParams.be.logicRoles;
|
|
58
58
|
|
|
59
|
-
/** The roles a feature-tier file is judged thin by (ruleParams.be.thinRoles, R203 BE_FEATURE_THIN). */
|
|
60
|
-
export const thinRolesOf = (manifest) => manifest.ruleParams.be.thinRoles;
|
|
61
59
|
|
|
62
60
|
/** Shape problems of ruleParams.be.unitRoles: unique roles of the suffix vocabulary, each spec `<role>.spec`. */
|
|
63
61
|
export function unitRolesProblems(be) {
|
|
@@ -86,7 +84,7 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
|
|
|
86
84
|
export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
|
|
87
85
|
const strList = (v) => stringList(v);
|
|
88
86
|
|
|
89
|
-
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome'];
|
|
87
|
+
const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exampleNa', 'exemplifiedBy'];
|
|
90
88
|
/** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
|
|
91
89
|
const LITE_OVERLAY_KEYS = ['path', 'requires', 'allows', 'forbids', 'minInstances', 'requiredInstances', 'why'];
|
|
92
90
|
/** A runtime slot has no app kind, side composition, layer or managed template; it may name the generator of a generated copy. */
|
|
@@ -184,6 +182,9 @@ function slotListProblems(slot, at) {
|
|
|
184
182
|
|
|
185
183
|
function slotAppOptionProblems(slot, at) {
|
|
186
184
|
const bad = [];
|
|
185
|
+
bad.push(...slotEditionWhyProblems(slot, at), ...slotCoverageEvidenceProblems(slot, at));
|
|
186
|
+
// Legacy coverage status derives only from an explicit `since`; a slot that omits it is refused, so a new slot can never be taken for an old one.
|
|
187
|
+
if (slot.since === undefined) bad.push(`${at}: since is required (the manifest version that added the slot)`);
|
|
187
188
|
if (slot.editions !== undefined && !(Array.isArray(slot.editions) && slot.editions.length && slot.editions.every((e) => EDITIONS.includes(e)) && new Set(slot.editions).size === slot.editions.length)) bad.push(`${at}: editions must be a non-empty subset of ${EDITIONS.join(', ')}`);
|
|
188
189
|
if (slot.litePresence !== undefined && !(PRESENCE.includes(slot.litePresence) || (isPlainObject(slot.litePresence) && Object.keys(slot.litePresence).length && Object.entries(slot.litePresence).every(([side, value]) => ['be', 'fe'].includes(side) && PRESENCE.includes(value))))) bad.push(`${at}: litePresence must be a presence value or map sides to presence values`);
|
|
189
190
|
if (slot.provider !== undefined) {
|
|
@@ -193,6 +194,40 @@ function slotAppOptionProblems(slot, at) {
|
|
|
193
194
|
return bad;
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
function slotEditionWhyProblems(slot, at) {
|
|
198
|
+
if (slot.editionWhy === undefined) return [];
|
|
199
|
+
return typeof slot.editionWhy === 'string' && slot.editionWhy.trim()
|
|
200
|
+
? [] : [`${at}: editionWhy must be non-empty text`];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const slotEvidencePathValid = (value) => relPath(value) && /^[^\s]+$/.test(value);
|
|
204
|
+
|
|
205
|
+
function slotEvidenceRefValid(ref) {
|
|
206
|
+
if (typeof ref === 'string') return slotEvidencePathValid(ref);
|
|
207
|
+
if (!isPlainObject(ref) || !slotEvidencePathValid(ref.path)) return false;
|
|
208
|
+
if (!Object.keys(ref).every((key) => ['path', 'edition', 'editions'].includes(key))) return false;
|
|
209
|
+
if (ref.edition !== undefined && !EDITIONS.includes(ref.edition)) return false;
|
|
210
|
+
if (ref.editions !== undefined && !nonemptyUniqueList(ref.editions, (edition) => EDITIONS.includes(edition))) return false;
|
|
211
|
+
return ref.edition === undefined || ref.editions === undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const slotEvidenceRefKey = (ref) => typeof ref === 'string' ? ref
|
|
215
|
+
: JSON.stringify([ref.path, ref.edition, ref.editions]);
|
|
216
|
+
|
|
217
|
+
function slotCoverageEvidenceProblems(slot, at) {
|
|
218
|
+
const bad = [];
|
|
219
|
+
if (slot.templateNa !== undefined && !(typeof slot.templateNa === 'string' && slot.templateNa.trim()))
|
|
220
|
+
bad.push(`${at}: templateNa must be non-empty text`);
|
|
221
|
+
if (slot.exampleNa !== undefined && !(typeof slot.exampleNa === 'string' && slot.exampleNa.trim()))
|
|
222
|
+
bad.push(`${at}: exampleNa must be non-empty text`);
|
|
223
|
+
if (slot.exemplifiedBy === undefined) return bad;
|
|
224
|
+
const refs = slot.exemplifiedBy;
|
|
225
|
+
const valid = Array.isArray(refs) && refs.length > 0 && refs.every(slotEvidenceRefValid)
|
|
226
|
+
&& new Set(refs.map(slotEvidenceRefKey)).size === refs.length;
|
|
227
|
+
if (!valid) bad.push(`${at}: exemplifiedBy must be unique repository-relative paths or {path, edition? | editions?} references`);
|
|
228
|
+
return bad;
|
|
229
|
+
}
|
|
230
|
+
|
|
196
231
|
function slotLiteOverlayProblems(slot, at) {
|
|
197
232
|
if (slot.lite === undefined) return [];
|
|
198
233
|
const ok = isPlainObject(slot.lite) && Object.keys(slot.lite).length && Object.keys(slot.lite).every((key) => LITE_OVERLAY_KEYS.includes(key))
|
|
@@ -209,7 +244,7 @@ function slotEntryProblems(slot, at) {
|
|
|
209
244
|
if (slot.entries !== undefined) {
|
|
210
245
|
const listed = strList(slot.entries) && slot.entries.length > 0 && new Set(slot.entries).size === slot.entries.length && slot.entries.every(relPath);
|
|
211
246
|
if (!listed) bad.push(`${at}: entries must be a non-empty list of unique relative file paths`);
|
|
212
|
-
else if (typeof slot.path !== 'string' || !slot.path.endsWith('/')
|
|
247
|
+
else if (typeof slot.path !== 'string' || !slot.path.endsWith('/')) bad.push(`${at}: entries belong to a directory slot (they name its explicit public API files)`);
|
|
213
248
|
else if (Array.isArray(slot.allows) && slot.entries.some((entry) => !slot.allows.includes(entry))) bad.push(`${at}: every entry must be one of the names the slot allows`);
|
|
214
249
|
}
|
|
215
250
|
if (slot.outcomeHome !== undefined && !(slot.outcomeHome === true && typeof slot.path === 'string' && slot.path && !slot.path.endsWith('/'))) bad.push(`${at}: outcomeHome is true and belongs to a file slot (the one file that declares the repository's Outcome union)`);
|
|
@@ -328,6 +363,21 @@ const kindPairValid = pair => isPlainObject(pair) && Object.keys(pair).sort(byCo
|
|
|
328
363
|
&& ['from', 'to'].every(side => folderKindsValid(pair[side]));
|
|
329
364
|
const kindRestrictionValid = kinds => isPlainObject(kinds) && Object.keys(kinds).length > 0
|
|
330
365
|
&& Object.entries(kinds).every(([target, pair]) => NAME.test(target) && kindPairValid(pair));
|
|
366
|
+
const slotIdsValid = list => strList(list) && list.length > 0 && new Set(list).size === list.length
|
|
367
|
+
&& list.every(id => SLOT_ID.test(id) || RUNTIME_SLOT_ID.test(id));
|
|
368
|
+
const slotRestrictionPairValid = pair => isPlainObject(pair) && Object.keys(pair).join() === 'from' && slotIdsValid(pair.from);
|
|
369
|
+
const slotRestrictionValid = slots => isPlainObject(slots) && Object.keys(slots).length > 0
|
|
370
|
+
&& Object.entries(slots).every(([target, pair]) => NAME.test(target) && slotRestrictionPairValid(pair));
|
|
371
|
+
|
|
372
|
+
function tierDefinitionProblems(profile, tier, def) {
|
|
373
|
+
const bad = [];
|
|
374
|
+
const at = `tiers.${profile}.${tier}`;
|
|
375
|
+
if (!isPlainObject(def) || !Array.isArray(def.mayImport) || !def.mayImport.every((t) => NAME.test(String(t)))) bad.push(`${at}.mayImport must be a list of tier names`);
|
|
376
|
+
if (def?.mayImportKinds !== undefined && !kindRestrictionValid(def.mayImportKinds)) bad.push(`${at}.mayImportKinds must map tier names to {from, to} non-empty lists of unique folder kinds`);
|
|
377
|
+
if (def?.mayImportSlots !== undefined && !slotRestrictionValid(def.mayImportSlots)) bad.push(`${at}.mayImportSlots must map tier names to {from} non-empty lists of unique slot ids`);
|
|
378
|
+
for (const key of Object.keys(def ?? {})) if (!['mayImport', 'mayImportKinds', 'mayImportSlots', 'acyclic', 'lowerLayerOnly'].includes(key)) bad.push(`${at}.${key} is not a tier field`);
|
|
379
|
+
return bad;
|
|
380
|
+
}
|
|
331
381
|
|
|
332
382
|
/** Shape problems of the tier map of one profile. */
|
|
333
383
|
export function tierMapProblems(profile, tiers) {
|
|
@@ -335,9 +385,7 @@ export function tierMapProblems(profile, tiers) {
|
|
|
335
385
|
if (!isPlainObject(tiers) || !Object.keys(tiers).length) return [`tiers.${profile} must be a non-empty map`];
|
|
336
386
|
for (const [tier, def] of Object.entries(tiers)) {
|
|
337
387
|
if (!NAME.test(tier)) bad.push(`tiers.${profile}.${tier} is not a tier name`);
|
|
338
|
-
|
|
339
|
-
if (def?.mayImportKinds !== undefined && !kindRestrictionValid(def.mayImportKinds)) bad.push(`tiers.${profile}.${tier}.mayImportKinds must map tier names to {from, to} non-empty lists of unique folder kinds`);
|
|
340
|
-
for (const key of Object.keys(def ?? {})) if (!['mayImport', 'mayImportKinds', 'acyclic', 'lowerLayerOnly'].includes(key)) bad.push(`tiers.${profile}.${tier}.${key} is not a tier field`);
|
|
388
|
+
bad.push(...tierDefinitionProblems(profile, tier, def));
|
|
341
389
|
}
|
|
342
390
|
return bad;
|
|
343
391
|
}
|
|
@@ -347,10 +395,22 @@ function tierFolderKindProblems(manifest, profile, tier, kinds, at) {
|
|
|
347
395
|
return kinds.filter(kind => !known.has(kind)).map(kind => `${at} names ${kind}, which is not a folder kind of the ${tier} tier`);
|
|
348
396
|
}
|
|
349
397
|
|
|
350
|
-
|
|
398
|
+
function tierSlotRestrictionProblems(manifest, profile, tier, def) {
|
|
399
|
+
const bad = [];
|
|
400
|
+
const known = new Set(manifest.slots.filter(slot => slot.profiles.includes(profile) && slot.tier === tier).map(slot => slot.id));
|
|
401
|
+
for (const [target, restriction] of Object.entries(def.mayImportSlots ?? {})) {
|
|
402
|
+
const at = `tiers.${profile}.${tier}.mayImportSlots.${target}`;
|
|
403
|
+
if (!def.mayImport.includes(target)) bad.push(`${at} restricts a tier that mayImport does not allow`);
|
|
404
|
+
for (const id of restriction.from) if (!known.has(id)) bad.push(`${at}.from names ${id}, which is not a slot of the ${profile} ${tier} tier`);
|
|
405
|
+
}
|
|
406
|
+
return bad;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Kind and slot restrictions narrow allowed tier edges and name declared source and target roles. */
|
|
351
410
|
export function tierKindProblems(manifest, profile) {
|
|
352
411
|
const bad = [];
|
|
353
412
|
for (const [tier, def] of Object.entries(manifest.tiers[profile])) {
|
|
413
|
+
bad.push(...tierSlotRestrictionProblems(manifest, profile, tier, def));
|
|
354
414
|
for (const [target, kinds] of Object.entries(def.mayImportKinds ?? {})) {
|
|
355
415
|
const at = `tiers.${profile}.${tier}.mayImportKinds.${target}`;
|
|
356
416
|
if (!def.mayImport.includes(target)) bad.push(`${at} restricts a tier that mayImport does not allow`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// param-names.mjs - the one shape of `ruleParams.be.paramNames` of knowledge/hfs/slots.yaml (R85 injected-param-name): a non-empty list of unique
|
|
2
2
|
// entries `{type | typeSuffix, names, nameSuffix?}`. The loader of scripts/hfs/slots.mjs calls it; the schema (modules/schemas/hfs-slots.schema.yaml)
|
|
3
|
-
// states the same shape
|
|
3
|
+
// states the same shape.
|
|
4
4
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
5
5
|
|
|
6
6
|
const UPPER = /^[A-Z][A-Za-z0-9]*$/u;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Infrastructure refusals are shared with the dependency-free runtime copy generator.
|
|
2
|
+
// They describe checks that could not run, rather than catalogued obligations.
|
|
3
|
+
export const REFUSAL_CODES = Object.freeze([
|
|
4
|
+
'HFS_REPO_UNREADABLE',
|
|
5
|
+
'HFS_DECLARATION_INVALID', 'HFS_MANIFEST_MAJOR_MISMATCH', 'HFS_MANIFEST_INVALID', 'HFS_FORMAT_TOOL_MISSING', 'HFS_EDITION_INVALID',
|
|
6
|
+
]);
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
5
5
|
import { parseYamlCached } from '../lib/yaml-cached.mjs';
|
|
6
|
+
import { jsonClone } from '../lib/json-clone.mjs';
|
|
6
7
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
7
8
|
import { SEMVER } from './manifest-shape.mjs';
|
|
8
9
|
import { enforcerJudgedInEdition, judgedInEdition, ruleEditionProblems } from './edition-slots.mjs';
|
|
@@ -15,10 +16,129 @@ const RULE_KINDS = Object.freeze(['codemod', 'lint', 'check', 'design']);
|
|
|
15
16
|
const FINDING_CODE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
|
|
16
17
|
const ENFORCER_ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
17
18
|
const FILE_ENFORCERS = new Set(['machine', 'hfs', 'work-validate', 'sonar', 'runtime']);
|
|
18
|
-
const RULE_KEYS = new Set(['id', 'code', 'law', 'scope', 'kinds', 'gates', 'failureCodes', 'editions', 'enforcers', 'cases']);
|
|
19
|
-
const CASE_KEYS = new Set(['id', 'date', 'source', 'ruling', 'quote']);
|
|
19
|
+
const RULE_KEYS = new Set(['id', 'code', 'law', 'scope', 'kinds', 'gates', 'failureCodes', 'editions', 'codeEditions', 'enforcers', 'cases', 'docs', 'exemplifiedBy', 'proves', 'witness', 'editionWhy', 'amendedBy', 'templateNa', 'specNa', 'exampleNa']);
|
|
20
|
+
const CASE_KEYS = new Set(['id', 'date', 'source', 'ruling', 'quote', 'supersedes', 'status', 'staleWhen', 'amendedBy']);
|
|
20
21
|
const CASE_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
21
|
-
const ENFORCER_KEYS = new Set(['kind', 'id', 'status', 'at', 'editions']);
|
|
22
|
+
const ENFORCER_KEYS = new Set(['kind', 'id', 'status', 'at', 'editions', 'editionWhy']);
|
|
23
|
+
const CASE_REFERENCE = /^(?:case-[1-9][0-9]*|(?:ruling:)?R[0-9]{2,3}(?:\/case-[1-9][0-9]*)?|owner:[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
|
|
24
|
+
const EVIDENCE_KEYS = new Set(['path', 'edition', 'editions']);
|
|
25
|
+
const PROOF_KEYS = new Set([...EVIDENCE_KEYS, 'codes', 'fixtures']);
|
|
26
|
+
const WITNESS_KEYS = new Set(['edition', 'base', 'mutate', 'expect']);
|
|
27
|
+
const MUTATION_KEYS = new Set(['op', 'path', 'content']);
|
|
28
|
+
|
|
29
|
+
function textProblem(value, label, bad) {
|
|
30
|
+
if (typeof value !== 'string' || !value.trim()) bad.push(`${label} must be non-empty text`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function dateProblem(value, label, bad) {
|
|
34
|
+
const date = typeof value === 'string' && CASE_DATE.test(value) ? new Date(`${value}T00:00:00Z`) : null;
|
|
35
|
+
if (!date || !Number.isFinite(date.getTime()) || date.toISOString() !== `${value}T00:00:00.000Z`) bad.push(`${label} must be a valid YYYY-MM-DD date`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function relativePathProblem(value, label, bad, anchor = false) {
|
|
39
|
+
const parts = typeof value === 'string' ? value.split('#') : [];
|
|
40
|
+
const file = parts[0];
|
|
41
|
+
const valid = typeof value === 'string' && value.trim() === value && value.length > 0
|
|
42
|
+
&& !/[\r\n\u0000\\]/.test(value) && !/^(?:\/|[a-z][a-z0-9+.-]*:)/i.test(value) && !file.includes(':')
|
|
43
|
+
&& !file.split('/').some((part) => ['', '.', '..'].includes(part))
|
|
44
|
+
&& (parts.length === 1 || (anchor && parts.length === 2 && /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(parts[1])));
|
|
45
|
+
if (!valid) bad.push(`${label} must be a repository-relative path${anchor ? ' with an optional #anchor' : ''}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function unknownKeys(item, allowed, label, bad) {
|
|
49
|
+
for (const key of Object.keys(item)) if (!allowed.has(key)) bad.push(`${label} has unknown key ${key}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function nonEmptyList(value, label, bad) {
|
|
53
|
+
if (!Array.isArray(value) || !value.length) { bad.push(`${label} must be a non-empty list`); return false; }
|
|
54
|
+
const normalized = value.map((value) => JSON.stringify(value, (_key, item) => isPlainObject(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item));
|
|
55
|
+
if (new Set(normalized).size !== value.length) bad.push(`${label} repeats a value`);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function optionalText(item, key, label, bad) {
|
|
60
|
+
if (item[key] !== undefined) textProblem(item[key], `${label}.${key}`, bad);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function evidenceEditionProblems(item, label, bad) {
|
|
64
|
+
if (item.edition !== undefined && !['full', 'lite'].includes(item.edition)) bad.push(`${label}.edition must be full or lite`);
|
|
65
|
+
bad.push(...ruleEditionProblems(item, label));
|
|
66
|
+
if (item.edition !== undefined && item.editions !== undefined) bad.push(`${label} names edition or editions, not both`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function codeListProblems(codes, label, bad) {
|
|
70
|
+
if (!nonEmptyList(codes, label, bad)) return;
|
|
71
|
+
for (const code of codes) if (typeof code !== 'string' || !FINDING_CODE.test(code)) bad.push(`${label} must contain UPPER_SNAKE finding codes`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function evidenceMapProblems(item, key, label, bad) {
|
|
75
|
+
unknownKeys(item, key === 'docs' ? EVIDENCE_KEYS : PROOF_KEYS, label, bad);
|
|
76
|
+
relativePathProblem(item.path, `${label}.path`, bad, key === 'docs');
|
|
77
|
+
evidenceEditionProblems(item, label, bad);
|
|
78
|
+
if (item.codes !== undefined) codeListProblems(item.codes, `${label}.codes`, bad);
|
|
79
|
+
if (item.fixtures !== undefined && nonEmptyList(item.fixtures, `${label}.fixtures`, bad)) item.fixtures.forEach((value) => textProblem(value, `${label}.fixtures`, bad));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function evidenceProblems(rule, key, label, bad) {
|
|
83
|
+
if (rule[key] === undefined || !nonEmptyList(rule[key], `${label}.${key}`, bad)) return;
|
|
84
|
+
rule[key].forEach((item, index) => {
|
|
85
|
+
const at = `${label}.${key}[${index}]`;
|
|
86
|
+
if (typeof item === 'string') { relativePathProblem(item, at, bad, key === 'docs'); return; }
|
|
87
|
+
if (!isPlainObject(item)) { bad.push(`${at} must be a path or a map`); return; }
|
|
88
|
+
evidenceMapProblems(item, key, at, bad);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mutationProblems(item, label, bad) {
|
|
93
|
+
if (!isPlainObject(item)) { bad.push(`${label} must be a map`); return; }
|
|
94
|
+
unknownKeys(item, MUTATION_KEYS, label, bad);
|
|
95
|
+
if (!['add', 'edit', 'delete'].includes(item.op)) bad.push(`${label}.op must be add, edit or delete`);
|
|
96
|
+
relativePathProblem(item.path, `${label}.path`, bad);
|
|
97
|
+
if (['add', 'edit'].includes(item.op) && typeof item.content !== 'string') bad.push(`${label}.content must be text for add or edit`);
|
|
98
|
+
if (item.op === 'delete' && item.content !== undefined) bad.push(`${label}.content is absent for delete`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function witnessEntryProblems(item, label, bad) {
|
|
102
|
+
if (!isPlainObject(item)) { bad.push(`${label} must be a map`); return; }
|
|
103
|
+
if (Object.hasOwn(item, 'path')) { evidenceMapProblems(item, 'proves', label, bad); return; }
|
|
104
|
+
unknownKeys(item, WITNESS_KEYS, label, bad);
|
|
105
|
+
if (!['full', 'lite'].includes(item.edition)) bad.push(`${label}.edition must be full or lite`);
|
|
106
|
+
relativePathProblem(item.base, `${label}.base`, bad);
|
|
107
|
+
codeListProblems(item.expect, `${label}.expect`, bad);
|
|
108
|
+
if (nonEmptyList(item.mutate, `${label}.mutate`, bad)) item.mutate.forEach((mutation, index) => mutationProblems(mutation, `${label}.mutate[${index}]`, bad));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function witnessProblems(rule, label, bad) {
|
|
112
|
+
const value = rule.witness;
|
|
113
|
+
if (value === undefined) return;
|
|
114
|
+
const at = `${label}.witness`;
|
|
115
|
+
if (isPlainObject(value)) {
|
|
116
|
+
unknownKeys(value, new Set(['status', 'reason', 'date']), at, bad);
|
|
117
|
+
if (value.status !== 'unreachable') bad.push(`${at}.status must be unreachable`);
|
|
118
|
+
textProblem(value.reason, `${at}.reason`, bad);
|
|
119
|
+
dateProblem(value.date, `${at}.date`, bad);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!nonEmptyList(value, at, bad)) return;
|
|
123
|
+
value.forEach((item, index) => witnessEntryProblems(item, `${at}[${index}]`, bad));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function caseReferenceProblems(value, label, bad) {
|
|
127
|
+
const values = Array.isArray(value) ? value : [value];
|
|
128
|
+
if (!nonEmptyList(values, label, bad)) return;
|
|
129
|
+
for (const ref of values) if (typeof ref !== 'string' || !CASE_REFERENCE.test(ref)) bad.push(`${label} must name case-N, R<id>[/case-N], ruling:R<id>[/case-N] or owner:<id>`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function caseTransitionProblems(item, label, bad) {
|
|
133
|
+
if (item.status !== undefined && !['live', 'superseded', 'amended'].includes(item.status)) bad.push(`${label}.status must be live, superseded or amended`);
|
|
134
|
+
for (const key of ['supersedes', 'amendedBy']) if (item[key] !== undefined) caseReferenceProblems(item[key], `${label}.${key}`, bad);
|
|
135
|
+
if (item.staleWhen !== undefined && nonEmptyList(item.staleWhen, `${label}.staleWhen`, bad)) {
|
|
136
|
+
for (const pattern of item.staleWhen) {
|
|
137
|
+
if (typeof pattern !== 'string' || !pattern.trim()) { bad.push(`${label}.staleWhen must contain non-empty regex strings`); continue; }
|
|
138
|
+
try { new RegExp(pattern, 'i'); } catch { bad.push(`${label}.staleWhen has an invalid regex ${JSON.stringify(pattern)}`); }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
22
142
|
|
|
23
143
|
function vocabularyProblems(catalog, key, names, bad) {
|
|
24
144
|
if (!isPlainObject(catalog[key])) { bad.push(`${key} must be a map`); return; }
|
|
@@ -75,13 +195,15 @@ function checkRuleCases(rule, label, bad) {
|
|
|
75
195
|
if (typeof item.date !== 'string' || !CASE_DATE.test(item.date)) bad.push(`${at}.date must be YYYY-MM-DD`);
|
|
76
196
|
if (item.source !== 'owner') bad.push(`${at}.source must be owner`);
|
|
77
197
|
for (const key of ['ruling', ...(item.quote === undefined ? [] : ['quote'])]) if (typeof item[key] !== 'string' || !item[key].trim()) bad.push(`${at}.${key} must be text`);
|
|
198
|
+
caseTransitionProblems(item, at, bad);
|
|
78
199
|
});
|
|
79
200
|
}
|
|
80
201
|
|
|
81
|
-
function checkRuleGates(label, gates, bad) {
|
|
202
|
+
function checkRuleGates(rule, label, gates, bad) {
|
|
82
203
|
if (!gates.length) return;
|
|
83
|
-
|
|
84
|
-
|
|
204
|
+
const releaseGate = gates.includes('land') || gates.includes('ci');
|
|
205
|
+
const runtimeGate = rule.scope === 'runtime' && gates.includes('runtime');
|
|
206
|
+
if (!releaseGate && !runtimeGate) bad.push(`${label} must run at land or ci or, for runtime scope, the runtime gate`);
|
|
85
207
|
}
|
|
86
208
|
|
|
87
209
|
function checkRuleFailureCodes(rule, label, codeOwner, bad) {
|
|
@@ -97,6 +219,19 @@ function checkRuleFailureCodes(rule, label, codeOwner, bad) {
|
|
|
97
219
|
}
|
|
98
220
|
}
|
|
99
221
|
|
|
222
|
+
function checkCodeEditions(rule, label, bad) {
|
|
223
|
+
const value = rule.codeEditions;
|
|
224
|
+
if (value === undefined) return;
|
|
225
|
+
if (!isPlainObject(value) || !Object.keys(value).length) { bad.push(`${label}.codeEditions must be a non-empty map`); return; }
|
|
226
|
+
const editions = Array.isArray(rule.editions) ? rule.editions : ['full', 'lite'];
|
|
227
|
+
for (const [code, selected] of Object.entries(value)) {
|
|
228
|
+
const at = `${label}.codeEditions.${code}`;
|
|
229
|
+
if (!Array.isArray(rule.failureCodes) || !rule.failureCodes.includes(code)) bad.push(`${at} must name a code owned by this rule`);
|
|
230
|
+
enumListProblems({ editions: selected }, at, 'editions', ['full', 'lite'], bad);
|
|
231
|
+
if (Array.isArray(selected) && selected.some((edition) => !editions.includes(edition))) bad.push(`${at} must be a subset of the rule's editions`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
100
235
|
function checkEnforcerIdentity(enforcer, label, seen, bad) {
|
|
101
236
|
if (!ENFORCER_FAMILIES.includes(enforcer.kind)) bad.push(`${label}.kind must be one of ${ENFORCER_FAMILIES.join(', ')}`);
|
|
102
237
|
if (!ENFORCER_ID.test(String(enforcer.id))) bad.push(`${label}.id must be kebab-case`);
|
|
@@ -119,6 +254,7 @@ function checkEnforcer(enforcer, index, ruleLabel, seen, bad) {
|
|
|
119
254
|
if (!ENFORCER_KEYS.has(key)) bad.push(`${label} has unknown key ${key}`);
|
|
120
255
|
}
|
|
121
256
|
bad.push(...ruleEditionProblems(enforcer, label));
|
|
257
|
+
optionalText(enforcer, 'editionWhy', label, bad);
|
|
122
258
|
checkEnforcerIdentity(enforcer, label, seen, bad);
|
|
123
259
|
if (enforcer.status !== undefined && enforcer.status !== 'planned') bad.push(`${label}.status is either absent or planned`);
|
|
124
260
|
checkEnforcerLocation(enforcer, label, bad);
|
|
@@ -140,11 +276,19 @@ function checkRule(rule, index, catalog, codeOwner, bad) {
|
|
|
140
276
|
checkRuleIdentity(rule, index, catalog, at, label, bad);
|
|
141
277
|
checkRuleText(rule, label, bad);
|
|
142
278
|
checkRuleCases(rule, label, bad);
|
|
279
|
+
if (rule.amendedBy !== undefined) caseReferenceProblems(rule.amendedBy, `${label}.amendedBy`, bad);
|
|
280
|
+
optionalText(rule, 'editionWhy', label, bad);
|
|
281
|
+
optionalText(rule, 'templateNa', label, bad);
|
|
282
|
+
optionalText(rule, 'specNa', label, bad);
|
|
283
|
+
optionalText(rule, 'exampleNa', label, bad);
|
|
284
|
+
for (const key of ['docs', 'exemplifiedBy', 'proves']) evidenceProblems(rule, key, label, bad);
|
|
285
|
+
witnessProblems(rule, label, bad);
|
|
143
286
|
enumListProblems(rule, label, 'kinds', RULE_KINDS, bad);
|
|
144
287
|
bad.push(...ruleEditionProblems(rule, label));
|
|
145
288
|
const gates = enumListProblems(rule, label, 'gates', RULE_GATES, bad);
|
|
146
|
-
checkRuleGates(label, gates, bad);
|
|
289
|
+
checkRuleGates(rule, label, gates, bad);
|
|
147
290
|
checkRuleFailureCodes(rule, label, codeOwner, bad);
|
|
291
|
+
checkCodeEditions(rule, label, bad);
|
|
148
292
|
checkRuleEnforcers(rule, label, bad);
|
|
149
293
|
if (Array.isArray(rule.gates)) {
|
|
150
294
|
const hasSonar = rule.enforcers.some((enforcer) => isPlainObject(enforcer) && enforcer.kind === 'sonar');
|
|
@@ -183,15 +327,18 @@ export function loadRuleCatalog({ root = skillRoot, file = path.join(root, HFS_R
|
|
|
183
327
|
const byCode = new Map(list.flatMap((rule) => rule.failureCodes.map((code) => [code, rule])));
|
|
184
328
|
return Object.freeze({
|
|
185
329
|
version: doc.version, major, minor, patch,
|
|
186
|
-
gates: deepFreeze(
|
|
187
|
-
enforcerKinds: deepFreeze(
|
|
330
|
+
gates: deepFreeze(jsonClone(doc.gates)),
|
|
331
|
+
enforcerKinds: deepFreeze(jsonClone(doc.enforcerKinds)),
|
|
188
332
|
rules: list,
|
|
189
333
|
/** The rule with this id (R01..), or null. */
|
|
190
334
|
rule: (id) => byId.get(id) ?? null,
|
|
191
335
|
/** The rule that owns this failure code (its own or a sub-check code), or null. */
|
|
192
336
|
byCode: (code) => byCode.get(code) ?? null,
|
|
193
|
-
/** Whether a finding code is judged under `edition`:
|
|
194
|
-
judgedIn: (code, edition = 'full') =>
|
|
337
|
+
/** Whether a finding code is judged under `edition`: codeEditions narrows its owner's editions; uncatalogued codes remain judged. */
|
|
338
|
+
judgedIn: (code, edition = 'full') => {
|
|
339
|
+
const owner = byCode.get(code);
|
|
340
|
+
return judgedInEdition(owner, edition) && (!owner?.codeEditions?.[code] || owner.codeEditions[code].includes(edition));
|
|
341
|
+
},
|
|
195
342
|
enforcerJudgedIn: (kind, id, edition = 'full') => enforcerJudgedInEdition(list, kind, id, edition),
|
|
196
343
|
/** The rules that run at a gate. */
|
|
197
344
|
forGate: (gate) => list.filter((rule) => rule.gates.includes(gate)),
|
|
@@ -4,22 +4,33 @@
|
|
|
4
4
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
5
5
|
import { paramNamesOk } from './param-names.mjs';
|
|
6
6
|
import { kindParamProblems, scenarioProblem } from './declaration-slots.mjs';
|
|
7
|
-
import { roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
|
|
8
|
-
import { byCodeUnit } from '../lib/list.mjs';
|
|
7
|
+
import { NAME, roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
|
|
8
|
+
import { byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
|
|
9
9
|
|
|
10
10
|
const PROFILES = ['be', 'fe'];
|
|
11
11
|
const BE_OWN = ['infraOwners', 'eventBus', 'specDoubles', 'paramNames', 'suffixes', 'bannedSuffixes', 'contractShape', 'unitRoles', 'logicRoles', 'thinRoles', 'patternScenarios', 'kindPatterns', 'addKinds'];
|
|
12
12
|
const FORM_NAMES = new Set(['call', 'new', 'curried', 'object', 'primitive', 'array']);
|
|
13
13
|
const blockOk = (v) => isPlainObject(v) && Number.isInteger(v.lines) && v.lines >= 2 && Number.isInteger(v.tokens) && v.tokens >= 1 && Object.keys(v).length === 2;
|
|
14
14
|
const fileLinesOk = (v) => isPlainObject(v) && Number.isInteger(v.soft) && v.soft >= 1 && typeof v.hardGrowth === 'boolean' && Object.keys(v).length === 2;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const SHARED_PARAMS = {
|
|
16
|
+
fileLines: fileLinesOk,
|
|
17
|
+
duplicateBlock: blockOk,
|
|
18
|
+
functionLines: v => Number.isInteger(v) && v >= 1,
|
|
19
|
+
featureFiles: v => Number.isInteger(v) && v >= 1,
|
|
20
|
+
};
|
|
21
|
+
const SHARED_KEYS = Object.keys(SHARED_PARAMS);
|
|
22
|
+
const sharedParam = (key, v) => Object.hasOwn(SHARED_PARAMS, key) && SHARED_PARAMS[key](v);
|
|
23
|
+
const sideOk = (side, own, extra = []) => own.every((k) => side[k] !== undefined) && Object.entries(side).every(([k, v]) => own.includes(k) || extra.includes(k) || sharedParam(k, v));
|
|
24
|
+
const liteKeyOk = (lite, key, keys) => key !== 'lite' && keys.includes(key)
|
|
25
|
+
&& (!['functionLines', 'featureFiles'].includes(key) || sharedParam(key, lite[key]));
|
|
26
|
+
const liteOk = (lite, keys) => lite === undefined || (isPlainObject(lite) && Object.keys(lite).length > 0 && Object.keys(lite).every((k) => liteKeyOk(lite, k, keys)));
|
|
18
27
|
const memberMap = (v, allowEmpty) => isPlainObject(v) && Object.keys(v).length > 0 && Object.values(v).every((list) => Array.isArray(list) && (allowEmpty || list.length > 0) && list.every((x) => /^[A-Z][A-Za-z0-9]*$/.test(String(x))) && new Set(list).size === list.length);
|
|
19
|
-
const roleList = (v) =>
|
|
28
|
+
const roleList = (v) => nonemptyUniqueList(v, (role) => NAME.test(String(role)));
|
|
20
29
|
const formsOk = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => FORM_NAMES.has(x)) && new Set(v).size === v.length;
|
|
21
30
|
const doubleOk = (v) => isPlainObject(v) && /^[A-Za-z][A-Za-z0-9]*$/.test(String(v.double)) && formsOk(v.forms);
|
|
22
31
|
const regexOk = (v) => { try { return typeof v === 'string' && v.length > 0 && Boolean(new RegExp(v)); } catch { return false; } };
|
|
32
|
+
const dbExtensionsOk = (value) => Array.isArray(value) && value.every((name) => typeof name === 'string' && name.length > 0) && new Set(value).size === value.length;
|
|
33
|
+
const dbParamsOk = (db) => db === undefined || (isPlainObject(db) && Object.entries(db).every(([key, value]) => key === 'extensions' && dbExtensionsOk(value)));
|
|
23
34
|
|
|
24
35
|
function beSpecificProblems(be, manifest) {
|
|
25
36
|
const bad = [];
|
|
@@ -43,14 +54,15 @@ function beSpecificProblems(be, manifest) {
|
|
|
43
54
|
export function ruleParamsProblems(m) {
|
|
44
55
|
const bad = [];
|
|
45
56
|
const rp = m.ruleParams;
|
|
46
|
-
if (!isPlainObject(rp) || Object.keys(rp).some((k) => ![...PROFILES, 'common'].includes(k)) || !isPlainObject(rp.common) || !isPlainObject(rp.be) || (rp.fe !== undefined && !isPlainObject(rp.fe))) return ['ruleParams must be a map with common, be and optionally fe'];
|
|
57
|
+
if (!isPlainObject(rp) || Object.keys(rp).some((k) => ![...PROFILES, 'common', 'db'].includes(k)) || !isPlainObject(rp.common) || !isPlainObject(rp.be) || (rp.fe !== undefined && !isPlainObject(rp.fe))) return ['ruleParams must be a map with common, be and optionally fe and db'];
|
|
47
58
|
else {
|
|
48
59
|
// common holds the parameters both sides share; a side may restate one of them (a valid value) as an override, nothing else.
|
|
49
60
|
// Edition lite: a side's optional `lite` map overrides keys of the same side (never `lite` itself) when the app declares edition lite.
|
|
50
|
-
if (
|
|
51
|
-
if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN,
|
|
61
|
+
if (!sideOk(rp.common, []) || !fileLinesOk(rp.common.fileLines) || !blockOk(rp.common.duplicateBlock)) bad.push('ruleParams.common needs fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}; optional functionLines and featureFiles must be positive integers');
|
|
62
|
+
if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns and addKinds (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + ') and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
|
|
52
63
|
bad.push(...beSpecificProblems(rp.be, m));
|
|
53
|
-
if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite,
|
|
64
|
+
if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite, SHARED_KEYS))) bad.push('ruleParams.fe may only restate the shared parameters of ruleParams.common, and carry a lite override of them');
|
|
65
|
+
if (!dbParamsOk(rp.db)) bad.push('ruleParams.db may contain only extensions: a list of unique non-empty extension names');
|
|
54
66
|
}
|
|
55
67
|
return bad;
|
|
56
68
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// a15-findings.mjs - the one place where the alpha.15 content rules (R247-R280) are wired into `starci app check`.
|
|
2
|
+
// Each rule module of a batch exports a synchronous finder `({ repoRoot, files, all, repo, resolver, manifest, isRoot, inScope }) => finding[]`
|
|
3
|
+
// and is wired here by appending ONE import line and ONE entry to FINDERS. Nothing else in this file changes; the file is union-merged.
|
|
4
|
+
// A finder judges its own edition (`repo.edition`): lite and full are both in scope unless the rule says otherwise.
|
|
5
|
+
// finders-import-begin
|
|
6
|
+
import { featureFileFindings } from './feature-files.mjs';
|
|
7
|
+
import { lexiconFindings } from './lexicon.mjs';
|
|
8
|
+
import { assetsFindings } from './assets.mjs';
|
|
9
|
+
import { unusedSymbolFindings } from './unused-symbol.mjs';
|
|
10
|
+
import { databaseBaselineFindings } from './database-baseline.mjs';
|
|
11
|
+
import { databaseQueuesFindings } from './database-queues.mjs';
|
|
12
|
+
import { databaseConfigFindings } from './database-config.mjs';
|
|
13
|
+
import { docFreshFindings } from './doc-fresh.mjs';
|
|
14
|
+
import { releaseProvenanceFindings } from './release-provenance.mjs';
|
|
15
|
+
import { feStackAllowlistFindings } from './fe-stack.mjs';
|
|
16
|
+
import { appEnvRequiredFindings } from './app-env-required.mjs';
|
|
17
|
+
import { ageKeyCustodyFindings } from './secrets.mjs';
|
|
18
|
+
import { canonCopyDriftFindings } from './canon-copy-drift.mjs';
|
|
19
|
+
import { secretCustodyOnlyFindings } from './secret-custody-only.mjs';
|
|
20
|
+
import { bootProofFindings } from './boot-proof.mjs';
|
|
21
|
+
import { coverageBelowFindings } from './coverage-below.mjs';
|
|
22
|
+
import { deployScanFindings } from './deploy-scan.mjs';
|
|
23
|
+
import { deployWorkflowFindings } from './deploy-workflow.mjs';
|
|
24
|
+
import { scriptHygieneFindings } from './script-hygiene.mjs';
|
|
25
|
+
import { supplyChainFindings } from './supply-chain.mjs';
|
|
26
|
+
import { envParityFindings } from './env-parity.mjs';
|
|
27
|
+
import { envValueShapeFindings } from './env-value-shape.mjs';
|
|
28
|
+
import { liteProofLaneFindings } from './lite-proof-lane.mjs';
|
|
29
|
+
import { stacksSwarmFindings } from './stacks-swarm.mjs';
|
|
30
|
+
import { stacksReleaseFindings } from './stacks-release.mjs';
|
|
31
|
+
import { stacksIngressFindings } from './stacks-ingress.mjs';
|
|
32
|
+
import { stacksLiteBackingFindings } from './stacks-lite-backing.mjs';
|
|
33
|
+
import { stackToolingFindings } from './stack-tooling.mjs';
|
|
34
|
+
import { stacksDeployFindings } from './stacks-deploy.mjs';
|
|
35
|
+
// finders-import-end
|
|
36
|
+
|
|
37
|
+
/** The failure codes the finders above emit, so `starci app check` ships their labels (check.mjs adds them to CHECK_CODES). Append-only; the coordinator regenerates it at merge time. */
|
|
38
|
+
export const A15_CODES = [
|
|
39
|
+
// codes-begin
|
|
40
|
+
'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_API_EXPOSED', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_CRON_OWNED', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_SERVICE_ROLE_SCOPE', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
|
|
41
|
+
// codes-end
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const FINDERS = [
|
|
45
|
+
// finders-begin
|
|
46
|
+
featureFileFindings,
|
|
47
|
+
lexiconFindings,
|
|
48
|
+
assetsFindings,
|
|
49
|
+
unusedSymbolFindings,
|
|
50
|
+
databaseBaselineFindings,
|
|
51
|
+
databaseQueuesFindings,
|
|
52
|
+
databaseConfigFindings,
|
|
53
|
+
docFreshFindings,
|
|
54
|
+
releaseProvenanceFindings,
|
|
55
|
+
feStackAllowlistFindings,
|
|
56
|
+
appEnvRequiredFindings,
|
|
57
|
+
ageKeyCustodyFindings,
|
|
58
|
+
canonCopyDriftFindings,
|
|
59
|
+
secretCustodyOnlyFindings,
|
|
60
|
+
bootProofFindings,
|
|
61
|
+
coverageBelowFindings,
|
|
62
|
+
deployScanFindings,
|
|
63
|
+
deployWorkflowFindings,
|
|
64
|
+
scriptHygieneFindings,
|
|
65
|
+
supplyChainFindings,
|
|
66
|
+
envParityFindings,
|
|
67
|
+
envValueShapeFindings,
|
|
68
|
+
liteProofLaneFindings,
|
|
69
|
+
stacksSwarmFindings,
|
|
70
|
+
stacksReleaseFindings,
|
|
71
|
+
stacksIngressFindings,
|
|
72
|
+
stacksLiteBackingFindings,
|
|
73
|
+
stackToolingFindings,
|
|
74
|
+
stacksDeployFindings,
|
|
75
|
+
// finders-end
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** Findings of every alpha.15 content rule over one repository. */
|
|
79
|
+
export function a15Findings(context) {
|
|
80
|
+
// `starci app check` always passes the full context; a partial one (a finder spec driving the registry with only the fields its rule reads) must not crash the finders of other rules.
|
|
81
|
+
const full = Boolean(context.repoRoot && context.repo && context.resolver && context.manifest);
|
|
82
|
+
return FINDERS.flatMap((finder) => {
|
|
83
|
+
if (full) return finder(context);
|
|
84
|
+
try { return finder(context); } catch { return []; }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A02: include every deployment and supply-chain finding in the bundled app-check catalog.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// R289: every declared back-end app owns a tracked typed environment boundary.
|
|
2
|
+
// ESLint's app-env-required rule checks its interface, reader and bootstrap delegation.
|
|
3
|
+
import { found, readText } from './read.mjs';
|
|
4
|
+
|
|
5
|
+
const REQUIRED_CODE = 'BE_APP_ENV_REQUIRED';
|
|
6
|
+
|
|
7
|
+
/** Missing files cannot be visited by ESLint, so the content check owns their presence. */
|
|
8
|
+
export function appEnvRequiredFindings({ repoRoot, files, all = files, repo, inScope = () => true }) {
|
|
9
|
+
if (repo.profile !== 'be' || !['lite', 'full'].includes(repo.edition ?? 'full')) return [];
|
|
10
|
+
const tracked = new Set(all);
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const app of repo.apps ?? []) {
|
|
13
|
+
const file = `apps/${app.name}/src/${app.name}.env.ts`;
|
|
14
|
+
if (!inScope(file)) continue;
|
|
15
|
+
if (tracked.has(file) && readText(repoRoot, file) !== null) continue;
|
|
16
|
+
findings.push(found(REQUIRED_CODE, file,
|
|
17
|
+
`${app.name} requires a tracked ${file} exporting its environment interface and read<App>Env(env: EnvSource); main.ts builds EnvSource once and delegates environment reads to that boundary`));
|
|
18
|
+
}
|
|
19
|
+
return findings;
|
|
20
|
+
}
|
|
@@ -27,7 +27,7 @@ import { testTopologyFindings } from './test-topology.mjs';
|
|
|
27
27
|
* The findings of the app-root rules: `files` are the root's own tracked paths, `all` every tracked path of the app, `repo` the
|
|
28
28
|
* resolved app declaration, `resolver` its slot resolver and `pins` the canon pins.
|
|
29
29
|
*/
|
|
30
|
-
export function appRootFindings({ repoRoot, files, all, repo, resolver, pins }) {
|
|
30
|
+
export function appRootFindings({ repoRoot, files, all, repo, resolver, pins, phase }) {
|
|
31
31
|
return [
|
|
32
32
|
...depFindings({ repoRoot, files: all }),
|
|
33
33
|
...peerIntegrationFindings({ repoRoot, files: all }),
|
|
@@ -45,7 +45,7 @@ export function appRootFindings({ repoRoot, files, all, repo, resolver, pins })
|
|
|
45
45
|
...pipelineFindings({ repoRoot, files, pins }),
|
|
46
46
|
...testTopologyFindings({ repoRoot, files }),
|
|
47
47
|
...proofCommandFindings({ repoRoot, files: all, resolver, sides: Object.keys(repo.sides ?? {}) }),
|
|
48
|
-
...appFrontendFindings({ repoRoot, files: all, repo }),
|
|
48
|
+
...appFrontendFindings({ repoRoot, files: all, repo, phase }),
|
|
49
49
|
...stacksFindings({ repoRoot, files, resolver }),
|
|
50
50
|
];
|
|
51
51
|
}
|