@starci/hfs 4.0.8 → 4.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +81 -39
- package/emit/contracts.mjs +56 -9
- package/emit/db-types.mjs +169 -0
- package/emit/events.mjs +41 -0
- package/emit/operations-worker.mjs +1 -1
- package/emit/operations.mjs +2 -2
- package/emit/schema-worker.mjs +1 -1
- package/lint/bound-sys.cjs +50 -0
- package/lint/run.mjs +131 -24
- package/package.json +16 -10
- package/report/sonar.mjs +8 -8
- package/runtime/engine/admission.mjs +308 -0
- package/runtime/engine/canonical-json.mjs +10 -0
- package/runtime/engine/config.mjs +486 -0
- package/runtime/engine/db/blob.mjs +315 -0
- package/runtime/engine/db/ledger-paths.mjs +83 -0
- package/runtime/engine/db/ledger.mjs +1118 -0
- package/runtime/engine/db/machine-connection.mjs +135 -0
- package/runtime/engine/db/machine-schema.mjs +84 -0
- package/runtime/engine/db/machine.mjs +1367 -0
- package/runtime/engine/db/migrations/machine/0001-init.sql +924 -0
- package/runtime/engine/db/migrations/runtime/0001-init.sql +1108 -0
- package/runtime/engine/db/provider-reservations.mjs +101 -0
- package/runtime/engine/digest.mjs +16 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/engine/secrets.mjs +130 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +162 -31
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +912 -120
- package/runtime/knowledge/patterns/be/api.yaml +369 -0
- package/runtime/knowledge/patterns/be/cli.yaml +297 -0
- package/runtime/knowledge/patterns/be/domain.yaml +113 -0
- package/runtime/knowledge/patterns/be/event-bus.yaml +285 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +261 -0
- package/runtime/knowledge/patterns/be/projections.yaml +178 -0
- package/runtime/knowledge/patterns/be/queues.yaml +236 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +140 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +222 -0
- package/runtime/knowledge/patterns/be/saga.yaml +248 -0
- package/runtime/knowledge/patterns/be/webhooks.yaml +221 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
- package/runtime/knowledge/sonar-gate.yaml +52 -18
- package/runtime/modules/kernel/failure-codes.yaml +611 -51
- package/runtime/modules/models/registry.yaml +553 -0
- package/runtime/modules/models/runtimes.yaml +695 -0
- package/runtime/modules/ops/_labels.yaml +53 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +51 -0
- package/runtime/scripts/api/fs/is-link-like.mjs +25 -0
- package/runtime/scripts/api/fs/lib.mjs +16 -2
- package/runtime/scripts/api/fs/safe-remove.mjs +12 -132
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/api/git/lib.mjs +18 -26
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/ls-tree.mjs +5 -0
- package/runtime/scripts/api/git/merge-base.mjs +9 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/api/git/show.mjs +6 -0
- package/runtime/scripts/api/node/lib.mjs +14 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/process/lib.mjs +111 -0
- package/runtime/scripts/api/process/owned-process.mjs +78 -0
- package/runtime/scripts/api/process/resolve-real-tool.mjs +46 -0
- package/runtime/scripts/api/process/run-program.mjs +7 -0
- package/runtime/scripts/api/process/stop-owned-process.mjs +9 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -12
- package/runtime/scripts/api/sops/lib.mjs +255 -0
- package/runtime/scripts/api/sops/seal.mjs +22 -0
- package/runtime/scripts/connectors/lib.mjs +488 -0
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
- package/runtime/scripts/hfs/architecture/config.mjs +9 -6
- package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
- package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
- package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +17 -4
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
- package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
- package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
- package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
- package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
- package/runtime/scripts/hfs/architecture.mjs +4 -4
- package/runtime/scripts/hfs/check.mjs +75 -72
- package/runtime/scripts/hfs/coverage-scope.mjs +162 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/path-findings.mjs +1 -1
- package/runtime/scripts/hfs/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +51 -0
- package/runtime/scripts/hfs/rules/cli.mjs +37 -0
- package/runtime/scripts/hfs/rules/contract-compat.mjs +71 -0
- package/runtime/scripts/hfs/rules/contract.mjs +10 -9
- package/runtime/scripts/hfs/rules/database-config.mjs +102 -0
- package/runtime/scripts/hfs/rules/database-constants.mjs +13 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +111 -0
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +233 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +336 -0
- package/runtime/scripts/hfs/rules/database.mjs +68 -0
- package/runtime/scripts/hfs/rules/deps.mjs +1 -1
- package/runtime/scripts/hfs/rules/docker.mjs +259 -0
- package/runtime/scripts/hfs/rules/edition.mjs +111 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +153 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -1
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +9 -9
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -6
- package/runtime/scripts/hfs/rules/kinds.mjs +84 -0
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +1 -1
- package/runtime/scripts/hfs/rules/monorepo.mjs +179 -0
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +3 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +11 -21
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -2
- package/runtime/scripts/hfs/rules/read.mjs +1 -1
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +2 -2
- package/runtime/scripts/hfs/rules/saga.mjs +170 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/services.mjs +220 -0
- package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks.mjs +3 -3
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +89 -0
- package/runtime/scripts/hfs/rules/test-topology.mjs +2 -2
- package/runtime/scripts/hfs/secret.mjs +209 -0
- package/runtime/scripts/hfs/slots.mjs +78 -108
- package/runtime/scripts/hfs/sql/pg-parse.mjs +46 -0
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/hfs/tree.mjs +5 -4
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
- package/runtime/scripts/hfs/view.mjs +3 -2
- package/runtime/scripts/lib/clip.mjs +19 -0
- package/runtime/scripts/lib/display-names.mjs +259 -0
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +103 -0
- package/runtime/scripts/lib/example-refs.mjs +158 -0
- package/runtime/scripts/lib/fs-kind.mjs +14 -4
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/graphql-contract.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json-schema.mjs +52 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +24 -5
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/package-at.mjs +37 -0
- package/runtime/scripts/lib/path-key.mjs +57 -2
- package/runtime/scripts/lib/process-identity.mjs +6 -0
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/read-yaml.mjs +18 -0
- package/runtime/scripts/lib/redact.mjs +161 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/same-text.mjs +5 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sleep-sync.mjs +2 -1
- package/runtime/scripts/lib/sleep.mjs +7 -0
- package/runtime/scripts/lib/sops-envelope.mjs +112 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/lib/walk.mjs +4 -3
- package/scaffold/add-app.mjs +174 -0
- package/scaffold/add-cli-lite.mjs +257 -0
- package/scaffold/add-inbox-lite.mjs +314 -0
- package/scaffold/add-table.mjs +344 -0
- package/scaffold/add.mjs +299 -0
- package/scaffold/app.mjs +358 -69
- package/scaffold/edition-gate.mjs +25 -0
- package/scaffold/image.mjs +19 -0
- package/scaffold/lite-exports.mjs +186 -0
- package/scaffold/service.mjs +32 -26
- package/scaffold/supabase-ports.mjs +26 -0
- package/src/main.mjs +327 -0
- package/sync/cli.mjs +6 -9
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +10 -10
- package/sync/index.mjs +167 -104
- package/sync/managed.mjs +19 -11
- package/sync/sonar-key.mjs +5 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +14 -14
- package/templates/app/ci-workflows/github/workflows/images.yml +37 -0
- package/templates/app/ci-workflows/sonar-steps.yml +9 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +66 -0
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +31 -0
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +37 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +32 -0
- package/templates/app/docker-ignore/dockerignore +50 -0
- package/templates/app/gitignore-lite +35 -0
- package/templates/app/hooks/husky/pre-commit +3 -8
- package/templates/app/hooks/husky/pre-push +45 -6
- package/templates/app/package-scripts/core.json +8 -0
- package/templates/app/package-scripts/package.json +3 -10
- package/templates/app/package-scripts-lite/package.json +15 -0
- package/templates/app/quality-config/codecov.yml +15 -3
- package/templates/app/quality-config/sonar-base.properties +4 -0
- package/templates/app/quality-config/sonar-project.properties +1 -4
- package/templates/app/quality-config-lite/sonar-project.properties +5 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/primary-notes.sql +5 -0
- package/templates/app/skeleton/.starciwork/features/system-health/index.yaml +4 -0
- package/templates/app/skeleton/.starciwork/{features/index.yaml → index.yaml} +2 -2
- package/templates/app/skeleton/README.md +7 -5
- package/templates/app/skeleton-lite/.sops.yaml +3 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/secrets/.gitkeep +1 -0
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +55 -0
- package/templates/app/skeleton-lite/README.md +40 -0
- package/templates/app/skeleton-lite/supabase/config.toml +41 -0
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +56 -0
- package/templates/app/skeleton-lite/supabase/seed.sql +0 -0
- package/templates/app/task-graph/turbo.json +23 -0
- package/templates/app/tool-config-lite/prettierignore +2 -0
- package/templates/app/tool-config-lite/prettierrc +1 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +38 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +10 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +13 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +3 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +12 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +7 -0
- package/templates/be/common/main-listen.ts.partial +14 -0
- package/templates/be/common/sql-ident.ts.partial +7 -0
- package/templates/be/common/sql-tag.ts.partial +5 -0
- package/templates/be/image/api/Dockerfile +38 -0
- package/templates/be/image/cli/Dockerfile +38 -0
- package/templates/be/image/worker/Dockerfile +36 -0
- package/templates/be/inbox/database-client.ts.tpl +50 -0
- package/templates/be/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/migration.sql.tpl +29 -0
- package/templates/be/inbox/module-definition.ts.tpl +6 -0
- package/templates/be/inbox/module.ts.tpl +14 -0
- package/templates/be/inbox/options.ts.tpl +2 -0
- package/templates/be/inbox/service.ts.tpl +33 -0
- package/templates/be/inbox/sql.ts.tpl +16 -0
- package/templates/be/inbox/webhook-door.ts.tpl +36 -0
- package/templates/be/patterns/api/application.command.http.ts.tpl +10 -0
- package/templates/be/patterns/api/application.command.ts.tpl +10 -0
- package/templates/be/patterns/api/application.contracts.ts.tpl +11 -0
- package/templates/be/patterns/api/application.handler.http.ts.tpl +22 -0
- package/templates/be/patterns/api/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/api/feature.index.http.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.index.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.input.ts.tpl +13 -0
- package/templates/be/patterns/api/graphql.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +7 -0
- package/templates/be/patterns/api/graphql.resolver.ts.tpl +27 -0
- package/templates/be/patterns/api/graphql.type.ts.tpl +9 -0
- package/templates/be/patterns/api/http.controller.ts.tpl +27 -0
- package/templates/be/patterns/api/http.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/http.module.ts.tpl +7 -0
- package/templates/be/patterns/api/http.request.ts.tpl +10 -0
- package/templates/be/patterns/api/http.response.ts.tpl +5 -0
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +15 -0
- package/templates/be/patterns/cli/group.cli.ts.tpl +11 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +7 -0
- package/templates/be/patterns/cli/sub.run.cli.spec.ts.tpl +17 -0
- package/templates/be/patterns/cli/sub.run.cli.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +22 -0
- package/templates/be/patterns/event-bus/platform/event-bus.config.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-bus.decorators.ts.tpl +46 -0
- package/templates/be/patterns/event-bus/platform/event-bus.log-events.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module.ts.tpl +52 -0
- package/templates/be/patterns/event-bus/platform/event-bus.options.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +28 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +165 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +93 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +90 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +136 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +56 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +272 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +128 -0
- package/templates/be/patterns/event-bus/platform/event-transport.port.ts.tpl +180 -0
- package/templates/be/patterns/event-bus/platform/event.policy.spec.ts.tpl +67 -0
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +38 -0
- package/templates/be/patterns/event-bus/platform/index.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.spec.ts.tpl +348 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +241 -0
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +8 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +37 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +26 -0
- package/templates/be/patterns/jobs/job.index.ts.tpl +1 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.processor.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.step.ts.tpl +19 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +150 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +100 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +138 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +69 -0
- package/templates/be/patterns/jobs/platform/jobs.config.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +62 -0
- package/templates/be/patterns/jobs/platform/jobs.decorators.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/jobs.error.ts.tpl +22 -0
- package/templates/be/patterns/jobs/platform/jobs.index.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +9 -0
- package/templates/be/patterns/jobs/platform/jobs.messages.ts.tpl +15 -0
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform/jobs.module.ts.tpl +32 -0
- package/templates/be/patterns/jobs/platform/jobs.options.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.port.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/jobs/platform/persistence.job.entity.ts.tpl +53 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.rows.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/jobs/platform/persistence.migration.ts.tpl +29 -0
- package/templates/be/patterns/outbox/platform/index.ts.tpl +2 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +96 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +78 -0
- package/templates/be/patterns/outbox/platform/outbox.log-events.ts.tpl +5 -0
- package/templates/be/patterns/projections/migration.ts.tpl +18 -0
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/projections/projection.entity.ts.tpl +9 -0
- package/templates/be/patterns/projections/projection.index.ts.tpl +3 -0
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/projections/projection.module.ts.tpl +18 -0
- package/templates/be/patterns/projections/projection.options.ts.tpl +2 -0
- package/templates/be/patterns/projections/projection.projection.spec.ts.tpl +55 -0
- package/templates/be/patterns/projections/projection.projection.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +142 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +81 -0
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +9 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +128 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +115 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +91 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +15 -0
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +31 -0
- package/templates/be/patterns/queues/platform/queue.decorators.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +7 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +22 -0
- package/templates/be/patterns/queues/platform/queue.policy.spec.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/queue.policy.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +14 -0
- package/templates/be/patterns/queues/queue.index.ts.tpl +3 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +23 -0
- package/templates/be/patterns/reactors/application.command.ts.tpl +10 -0
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +12 -0
- package/templates/be/patterns/reactors/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +19 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +20 -0
- package/templates/be/patterns/reactors/reactor.index.ts.tpl +1 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.index.ts.tpl +1 -0
- package/templates/be/patterns/realtime/channel.input.ts.tpl +12 -0
- package/templates/be/patterns/realtime/channel.subscription.spec.ts.tpl +51 -0
- package/templates/be/patterns/realtime/channel.subscription.ts.tpl +32 -0
- package/templates/be/patterns/realtime/channel.type.ts.tpl +9 -0
- package/templates/be/patterns/saga/saga.compensate.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.compensate.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.compensate.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.compensation.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.complete.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.complete.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.complete.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.index.ts.tpl +1 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +23 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +20 -0
- package/templates/be/patterns/saga/saga.service.spec.ts.tpl +70 -0
- package/templates/be/patterns/saga/saga.service.ts.tpl +44 -0
- package/templates/be/patterns/saga/saga.state.ts.tpl +14 -0
- package/templates/be/patterns/saga/saga.step.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.undo.command.ts.tpl +10 -0
- package/templates/be/patterns/saga/saga.undo.contracts.ts.tpl +5 -0
- package/templates/be/patterns/saga/saga.undo.handler.ts.tpl +21 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +72 -0
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +36 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.index.ts.tpl +1 -0
- package/templates/be/patterns/webhooks/webhook.request.ts.tpl +9 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -2
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -15
- package/templates/be/skeleton/apps/cli/src/app.module.ts +30 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +22 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +24 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/index.ts +1 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.ts +18 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +33 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +34 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +13 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/note/errors/note.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/note/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/note/note.contracts.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.spec.ts +76 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.ts +37 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/entities/note.entity.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/migrations/1790000000000-init-note.ts +22 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.rows.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +41 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +77 -0
- package/templates/be/skeleton/src/modules/platform/database/database.options.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/database.port.ts +65 -0
- package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/database/index.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.spec.ts +84 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.config.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.connection.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +75 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +88 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +29 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +10 -2
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +63 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +172 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +52 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +37 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +81 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +50 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +7 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +8 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +17 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +54 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +49 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +28 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +5 -0
- package/templates/be/table/error.ts.tpl +16 -0
- package/templates/be/table/index.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +51 -0
- package/templates/be/table/module-definition.ts.tpl +7 -0
- package/templates/be/table/module.ts.tpl +14 -0
- package/templates/be/table/options.ts.tpl +2 -0
- package/templates/be/table/rows.ts.tpl +4 -0
- package/templates/be/table/service.ts.tpl +23 -0
- package/templates/be/table/sql.ts.tpl +4 -0
- package/templates/be/tool-config/jest.config.js +3 -1
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +14 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +26 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +6 -0
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +23 -0
- package/templates/common/gitignore.base +1 -2
- package/templates/fe/app/package.json.tpl +1 -0
- package/templates/fe/app/tsconfig.json +12 -0
- package/templates/fe/common/app-layout.tsx.partial +8 -0
- package/templates/fe/common/site-shell.tsx.partial +9 -0
- package/templates/fe/image/next/Dockerfile +42 -0
- package/templates/fe/skeleton/apps/app/next.config.ts +23 -0
- package/templates/fe/skeleton/apps/app/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/loading.tsx +2 -2
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/not-found.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/global-error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +18 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/component.tsx +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +28 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/proxy.ts +1 -0
- package/templates/fe/skeleton/apps/{__app__ → landing}/next.config.ts +4 -2
- package/templates/fe/skeleton/apps/landing/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/app/global-error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/component.tsx +18 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/component.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +7 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/vi.json +31 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/proxy.ts +1 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +49 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +24 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +20 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +23 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/ErrorNotice/index.tsx +14 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/GlobalErrorNotice/index.tsx +25 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LoadingNotice/index.tsx +9 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +21 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +6 -0
- package/templates/fe/skeleton-lite/apps/web/next.config.ts +22 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +37 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/global-error.tsx +10 -0
- package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/app/globals.css +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/health/live/route.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/component.tsx +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +41 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/component.tsx +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +38 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/ErrorNotice/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/GlobalErrorNotice/index.tsx +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/LoadingNotice/index.tsx +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +18 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/component.tsx +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/index.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppGlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +30 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +3 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +59 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +70 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +50 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +48 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +16 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +43 -0
- package/templates/fe/table/read.ts.tpl +16 -0
- package/templates/fe/table/write.ts.tpl +16 -0
- package/upgrade/check.mjs +137 -0
- package/upgrade/index.mjs +524 -0
- package/bin/hfs.mjs +0 -233
- package/runtime/scripts/api/fs/rmdir-link.mjs +0 -12
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +0 -20
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +0 -9
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +0 -35
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -19
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +0 -11
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -24
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -14
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -21
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +0 -15
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -23
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +0 -2
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +0 -25
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +0 -19
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +0 -9
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +0 -4
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +0 -10
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.contracts.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.handler.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.query.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/index.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/system-health.module.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/live.controller.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/system-health-http.module.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/app/health/live/route.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/modules/config/index.ts +0 -0
- /package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/postcss.config.mjs +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// display-names.mjs — the names a person reads for a workflow, an operation and an op job.
|
|
2
|
+
//
|
|
3
|
+
// Owner request 2026-09-27: kernels, workflows and op workers need easy-to-understand names.
|
|
4
|
+
// The ids stay the keys everywhere (workflow_id, op_id, job_id); these are labels only:
|
|
5
|
+
// workflow workflows.display_name (starci kernel rename; derived at define-goal), else the goal slug in
|
|
6
|
+
// workflows.title, else the workflow id. Vietnamese, `<Product> · <what it does>`.
|
|
7
|
+
// operation modules/ops/_labels.yaml (vi/en), else the op id.
|
|
8
|
+
// op job `<op label> · <what> · <workflow name>`: `what` is the job's target — an explicit
|
|
9
|
+
// enqueue --what, the work-graph node it covers, the Work record it owns, the slice
|
|
10
|
+
// its title names, its feature path, its cut — at most JOB_WHAT_MAX characters.
|
|
11
|
+
// Every human-facing surface reads these helpers: the [Kernel]/[Op] Orca titles, starci kernel status,
|
|
12
|
+
// the supervisor digest and progress report, Telegram notices and the harness UI.
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { clipLine, squash } from './clip.mjs';
|
|
16
|
+
import { ownerLanguage, translator } from './i18n.mjs';
|
|
17
|
+
import { list } from './list.mjs';
|
|
18
|
+
import { parseJson } from './json.mjs';
|
|
19
|
+
import { readYamlFile } from './read-yaml.mjs';
|
|
20
|
+
import { normRel, pathsOverlap } from './path-key.mjs';
|
|
21
|
+
import { PRODUCT_NAME_SEGMENT } from './example-refs.mjs';
|
|
22
|
+
|
|
23
|
+
export const WORKFLOW_NAME_MAX = 48;
|
|
24
|
+
const DISPLAY_NAME_LIMIT = 80;
|
|
25
|
+
export const JOB_WHAT_MAX = 40;
|
|
26
|
+
const NAME_SEPARATOR = ' · ';
|
|
27
|
+
|
|
28
|
+
const LABELS_FILE = new URL('../../modules/ops/_labels.yaml', import.meta.url);
|
|
29
|
+
let labelsCache = null;
|
|
30
|
+
/** {op: {vi, en}} from modules/ops/_labels.yaml; {} when the file is unreadable. */
|
|
31
|
+
export function opLabelMap() {
|
|
32
|
+
if (labelsCache) return labelsCache;
|
|
33
|
+
labelsCache = Object.freeze({ ...(readYamlFile(LABELS_FILE)?.labels ?? {}) });
|
|
34
|
+
return labelsCache;
|
|
35
|
+
}
|
|
36
|
+
/** The human label of an op (a `op#instance` leg label reads as its op), else the op id itself. */
|
|
37
|
+
export function opLabel(op, language = ownerLanguage()) {
|
|
38
|
+
const id = String(op ?? '').trim();
|
|
39
|
+
if (!id) return '';
|
|
40
|
+
const entry = opLabelMap()[id] ?? opLabelMap()[id.split('#')[0]];
|
|
41
|
+
const label = entry?.[language] ?? entry?.vi;
|
|
42
|
+
return typeof label === 'string' && label.trim() ? label.trim() : id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One line, whitespace collapsed; '' for nothing. */
|
|
46
|
+
/** `text` cut at a word boundary to at most `max` characters, ending in an ellipsis when cut. */
|
|
47
|
+
function clipWords(text, max) {
|
|
48
|
+
const s = squash(text);
|
|
49
|
+
if (s.length <= max) return s;
|
|
50
|
+
const room = s.slice(0, max - 1);
|
|
51
|
+
const at = room.lastIndexOf(' ');
|
|
52
|
+
return `${(at >= max / 2 ? room.slice(0, at) : room).replace(/[\s,;:.·–—-]+$/, '')}…`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A display name as the api stores it: one line, trimmed, 1..DISPLAY_NAME_LIMIT characters. Throws
|
|
57
|
+
* {code: 'rename-bad-title'} otherwise.
|
|
58
|
+
*/
|
|
59
|
+
export function normalizeDisplayName(name) {
|
|
60
|
+
const s = squash(name);
|
|
61
|
+
if (!s) throw Object.assign(new Error('the display name is empty'), { code: 'rename-bad-title' });
|
|
62
|
+
if (s.length > DISPLAY_NAME_LIMIT) throw Object.assign(new Error(`the display name is ${s.length} characters; at most ${DISPLAY_NAME_LIMIT}`), { code: 'rename-bad-title' });
|
|
63
|
+
return s;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A workflows row's name: display_name, else the goal slug (title), else the workflow id. */
|
|
67
|
+
export function workflowDisplayName(row) {
|
|
68
|
+
if (!row) return null;
|
|
69
|
+
return squash(row.display_name) || squash(row.title) || row.workflow_id || null;
|
|
70
|
+
}
|
|
71
|
+
/** The name of one workflow of `db` (its id when the row is missing). */
|
|
72
|
+
export function workflowNameOf(db, workflowId) {
|
|
73
|
+
if (!workflowId) return null;
|
|
74
|
+
try { return workflowDisplayName(db.prepare('SELECT * FROM workflows WHERE workflow_id=?').get(workflowId)) ?? workflowId; }
|
|
75
|
+
catch { return workflowId; }
|
|
76
|
+
}
|
|
77
|
+
/** workflow_id -> name for every workflow of `db` (`displayOnly` keeps only rows with a display_name). */
|
|
78
|
+
export function workflowNames(db, { displayOnly = false } = {}) {
|
|
79
|
+
try {
|
|
80
|
+
const rows = db.prepare('SELECT * FROM workflows').all().filter((row) => !displayOnly || row.display_name);
|
|
81
|
+
return new Map(rows.map((row) => [row.workflow_id, displayOnly ? row.display_name : workflowDisplayName(row)]));
|
|
82
|
+
} catch { return new Map(); }
|
|
83
|
+
}
|
|
84
|
+
/** `Name (id)` for a line that must still carry the key, or the id alone when there is no other name. */
|
|
85
|
+
export const nameWithId = (name, id) => (name && name !== id ? `${name} (${id})` : String(id ?? name ?? ''));
|
|
86
|
+
|
|
87
|
+
/** The name of an attached file record ({name?, abs}): its declared name, else its basename, forward-slashed. */
|
|
88
|
+
export const attachedNameOf = (file) => String(file.name ?? path.basename(String(file.abs ?? ''))).replace(/\\/g, '/');
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------------- workflow names
|
|
91
|
+
const BRAND_CASE = { starci: 'StarCi' };
|
|
92
|
+
const REPO_ROLE_SUFFIX = /[-_](backend|be|frontend|fe|api|server|web|app)$/i;
|
|
93
|
+
/** The product's display word(s) from a project or repository name: `shop-be` -> `Shop`. */
|
|
94
|
+
export function productName(raw) {
|
|
95
|
+
const base = squash(raw).replace(REPO_ROLE_SUFFIX, '');
|
|
96
|
+
if (!base) return '';
|
|
97
|
+
return base.split(/[-_\s]+/).filter(Boolean)
|
|
98
|
+
.map((w) => BRAND_CASE[w.toLowerCase()] ?? (w.length <= 3 && /^[a-z]+$/i.test(w) && w === w.toUpperCase() ? w : `${w[0].toUpperCase()}${w.slice(1)}`)).join(' ');
|
|
99
|
+
}
|
|
100
|
+
/** The first clause of the owner's goal text: up to the first sentence end, line break, colon or dash aside. */
|
|
101
|
+
function firstClause(text) {
|
|
102
|
+
const s = String(text ?? '').split(/\r?\n/).map(squash).find(Boolean) ?? '';
|
|
103
|
+
const clause = s.split(/(?<=[.!?;:])\s|\s[—–-]\s|\s\(/)[0] ?? '';
|
|
104
|
+
return clause.replace(/[\s.!?;:,]+$/, '').trim();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A readable name for a new goal (define-goal): `<Product> · <first clause of the goal text>`, at
|
|
108
|
+
* most WORKFLOW_NAME_MAX characters; `fallback` (the goal slug) when the text yields nothing usable.
|
|
109
|
+
*/
|
|
110
|
+
export function deriveWorkflowDisplayName({ text, product = null, fallback = null, max = WORKFLOW_NAME_MAX } = {}) {
|
|
111
|
+
const prod = productName(product);
|
|
112
|
+
// The product leads the name already: a goal text that opens with it ("<product>: …") starts after it.
|
|
113
|
+
let body = squash(text);
|
|
114
|
+
if (prod && body.toLowerCase().startsWith(prod.toLowerCase())) body = body.slice(prod.length).replace(/^[\s:·,–—-]+/, '');
|
|
115
|
+
const clause = firstClause(body);
|
|
116
|
+
const head = prod ? `${prod}${NAME_SEPARATOR}` : '';
|
|
117
|
+
const room = max - head.length;
|
|
118
|
+
if (!clause || !/[\p{L}\p{N}]/u.test(clause) || room < 8) return fallback ? clipWords(fallback, max) : (prod || null);
|
|
119
|
+
return `${head}${clipWords(`${clause[0].toUpperCase()}${clause.slice(1)}`, room)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ------------------------------------------------------------------------------------ op job names
|
|
123
|
+
const keyOf = (p) => normRel(p, { fold: true });
|
|
124
|
+
// A path this broad names no target in particular (the whole Work tree, every feature).
|
|
125
|
+
const BROAD = /^(\.starciwork|\.starciwork\/index\.yaml|\.starciwork\/features|\.starciwork\/features\/index\.yaml|src|apps|packages)?$/;
|
|
126
|
+
const meets = pathsOverlap;
|
|
127
|
+
|
|
128
|
+
/** The work-graph node the job covers most specifically (its longest owned path that meets the job's). */
|
|
129
|
+
function coveredNode(nodes, paths) {
|
|
130
|
+
const keys = paths.map(keyOf).filter((k) => k && !BROAD.test(k));
|
|
131
|
+
let best = null, bestLen = -1;
|
|
132
|
+
for (const node of list(nodes)) {
|
|
133
|
+
for (const owned of list(node?.ownedPaths).map(keyOf).filter((k) => k && !BROAD.test(k))) {
|
|
134
|
+
if (keys.some((k) => meets(k, owned)) && owned.length > bestLen) { best = node; bestLen = owned.length; }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return best;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const titleCache = new Map();
|
|
141
|
+
/** The `title:` of the Work record at `<repo>/<p>` (a record folder or its index.yaml), else null. */
|
|
142
|
+
function recordTitle(repo, p) {
|
|
143
|
+
if (!repo || !p) return null;
|
|
144
|
+
const rel = String(p).replaceAll('\\', '/').replace(/\/+$/, '');
|
|
145
|
+
if (!rel.startsWith('.starciwork/') || BROAD.test(rel.toLowerCase())) return null;
|
|
146
|
+
const file = path.join(repo, rel.endsWith('.yaml') ? rel : `${rel}/index.yaml`);
|
|
147
|
+
if (!file.endsWith('index.yaml')) return null;
|
|
148
|
+
let mtime = null;
|
|
149
|
+
try { mtime = fs.statSync(file).mtimeMs; } catch { return null; }
|
|
150
|
+
const hit = titleCache.get(file);
|
|
151
|
+
if (hit && hit.mtime === mtime) return hit.title;
|
|
152
|
+
let title = null;
|
|
153
|
+
try {
|
|
154
|
+
const head = fs.readFileSync(file, 'utf8').slice(0, 4096);
|
|
155
|
+
const m = /^title:[ \t]*(.*)$/m.exec(head);
|
|
156
|
+
let value = m ? m[1] : '';
|
|
157
|
+
// A folded or literal block scalar (title: >-) continues on the indented lines right below it.
|
|
158
|
+
if (m && /^[>|][-+]?\s*$/.test(value)) {
|
|
159
|
+
const below = head.slice(m.index + m[0].length).split(/\r?\n/).slice(1);
|
|
160
|
+
const end = below.findIndex((l) => !/^\s+\S/.test(l));
|
|
161
|
+
value = (end < 0 ? below : below.slice(0, end)).join(' ');
|
|
162
|
+
}
|
|
163
|
+
title = squash(value.replace(/^(['"])(.*)\1$/, '$2')) || null;
|
|
164
|
+
} catch { title = null; }
|
|
165
|
+
if (titleCache.size > 2000) titleCache.clear();
|
|
166
|
+
titleCache.set(file, { mtime, title });
|
|
167
|
+
return title;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Folder names that say what kind of record sits there, not which one.
|
|
171
|
+
const FAMILY = new Set(['ui', 'fr', 'br', 'nfr', 'uat', 'sds', 'contract', 'data', 'journey', 'decision', 'scope', 'business', 'integration',
|
|
172
|
+
'impl', 'operations', 'evidence', 'e', 'assets', 'tools', 'src', 'apps', 'features', 'provision', 'shell', 'brand', 'lib', 'components', 'pages']);
|
|
173
|
+
const words = (s) => s.replace(/[-_]+/g, ' ').trim();
|
|
174
|
+
/** A short label from an owned path: `features/collab/ui/office` -> `collab / office`. */
|
|
175
|
+
export function pathLabel(p) {
|
|
176
|
+
const segs = String(p ?? '').replaceAll('\\', '/').split('/').filter(Boolean).filter((s) => s !== '.starciwork');
|
|
177
|
+
const at = segs.indexOf('features');
|
|
178
|
+
if (at >= 0 && segs[at + 1]) {
|
|
179
|
+
const feature = segs[at + 1];
|
|
180
|
+
const rest = segs.slice(at + 2).filter((s) => !s.includes('.') && !FAMILY.has(s.toLowerCase()) && !PRODUCT_NAME_SEGMENT.test(s));
|
|
181
|
+
const leaf = rest.at(-1);
|
|
182
|
+
return leaf && leaf !== feature ? `${words(feature)} / ${words(leaf)}` : words(feature);
|
|
183
|
+
}
|
|
184
|
+
const plain = segs.filter((s) => !FAMILY.has(s.toLowerCase()) && !/^wf-/i.test(s));
|
|
185
|
+
const last = plain.at(-1);
|
|
186
|
+
return last ? words(last.replace(/\.[a-z0-9]+$/i, '')) : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A work-graph node's short label from its id: `login.sign-in` -> `login / sign in`. */
|
|
190
|
+
export function nodeLabel(node) {
|
|
191
|
+
const id = String(node?.id ?? '');
|
|
192
|
+
const domain = String(node?.domain ?? '');
|
|
193
|
+
const leaf = domain && id.startsWith(`${domain}.`) ? id.slice(domain.length + 1) : id;
|
|
194
|
+
return domain && leaf && leaf !== domain ? `${words(domain)} / ${words(leaf.replaceAll('.', ' '))}` : words(id.replaceAll('.', ' '));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* What an op job works on, for a human: at most JOB_WHAT_MAX characters, or null when nothing names it.
|
|
199
|
+
* `payload` is the job payload, `op` its op id, `nodes` the workflow's latest work-graph nodes, `repo`
|
|
200
|
+
* the ledger owner (to read Work record titles).
|
|
201
|
+
*/
|
|
202
|
+
export function jobWhat({ payload, op = null, nodes = null, repo = null, max = JOB_WHAT_MAX, language = ownerLanguage() } = {}) {
|
|
203
|
+
const p = payload ?? {};
|
|
204
|
+
const owned = list(p.owned_paths ?? p.ownedPaths).map((x) => (typeof x === 'string' ? x : x?.path)).filter(Boolean);
|
|
205
|
+
const records = list(p.records).filter((x) => typeof x === 'string');
|
|
206
|
+
const cut = p.cut && typeof p.cut === 'object' ? p.cut : null;
|
|
207
|
+
const title = squash(p.title);
|
|
208
|
+
const opId = String(op ?? p.opId ?? '');
|
|
209
|
+
const rest = opId && title.toLowerCase().startsWith(opId.toLowerCase()) ? title.slice(opId.length).replace(/^[\s:#·–—-]+/, '') : null;
|
|
210
|
+
const specific = [...owned, ...records].filter((x) => !BROAD.test(keyOf(x)));
|
|
211
|
+
// A title that already fits is the target's own name; a sentence-long one gives way to the short id.
|
|
212
|
+
const fits = (t) => (t && squash(t).length <= max ? t : null);
|
|
213
|
+
const node = coveredNode(nodes, owned);
|
|
214
|
+
const candidates = [
|
|
215
|
+
() => squash(p.displayWhat),
|
|
216
|
+
() => { for (const x of specific) { const t = fits(recordTitle(repo, x)); if (t) return t; } return null; },
|
|
217
|
+
() => fits(node?.title),
|
|
218
|
+
() => (node?.id ? nodeLabel(node) : null),
|
|
219
|
+
() => rest,
|
|
220
|
+
() => (specific[0] ? pathLabel(specific[0]) : null),
|
|
221
|
+
() => (rest === null && title && title !== opId ? firstClause(title) : null),
|
|
222
|
+
() => (cut?.id ? words(String(cut.id)) : null),
|
|
223
|
+
];
|
|
224
|
+
let what = null;
|
|
225
|
+
for (const pick of candidates) { const v = squash(pick()); if (v) { what = v; break; } }
|
|
226
|
+
const tail = cut && Number(cut.total) > 1 && Number.isInteger(Number(cut.ordinal)) ? ` ${cut.ordinal}/${cut.total}` : '';
|
|
227
|
+
if (!what) return tail ? translator(language)('part{tail}', { tail }) : null;
|
|
228
|
+
return `${clipWords(what, Math.max(8, max - tail.length))}${tail}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** `<op label> · <what> · <workflow name>`, skipping a missing part. */
|
|
232
|
+
export function jobDisplayName({ op, what = null, workflowName = null, language = ownerLanguage() } = {}) {
|
|
233
|
+
return [opLabel(op, language), squash(what), squash(workflowName)].filter(Boolean).join(NAME_SEPARATOR);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The display name of one jobs row of `db`: reads its workflow's name and latest work-graph nodes unless
|
|
238
|
+
* given. `cache` (a Map) shares the workflow reads across many jobs.
|
|
239
|
+
*/
|
|
240
|
+
export function jobDisplayNameOf(db, job, { repo = null, workflowName = null, nodes = undefined, cache = null, language = ownerLanguage() } = {}) {
|
|
241
|
+
if (!job) return null;
|
|
242
|
+
const payload = typeof job.payload_json === 'string' ? parseJson(job.payload_json, {}) : (job.payload ?? {});
|
|
243
|
+
const op = job.op_id ?? job.opId ?? payload.opId ?? null;
|
|
244
|
+
const wf = job.workflow_id ?? job.workflowId ?? null;
|
|
245
|
+
const memo = cache ?? new Map();
|
|
246
|
+
const wfInfo = memo.get(wf) ?? (() => {
|
|
247
|
+
const info = { name: workflowNameOf(db, wf), nodes: null, repo: null };
|
|
248
|
+
// The ledger owner's root (to read Work record titles) when the caller names none: the workflow's first source root.
|
|
249
|
+
try { info.repo = parseJson(db.prepare('SELECT source_roots_json FROM workflows WHERE workflow_id=?').get(wf)?.source_roots_json)?.[0] ?? null; } catch { info.repo = null; }
|
|
250
|
+
try {
|
|
251
|
+
const row = db.prepare("SELECT graph_json FROM work_graph_versions WHERE workflow_id=? ORDER BY version DESC LIMIT 1").get(wf);
|
|
252
|
+
info.nodes = row ? parseJson(row.graph_json)?.nodes ?? null : null;
|
|
253
|
+
} catch { info.nodes = null; }
|
|
254
|
+
memo.set(wf, info);
|
|
255
|
+
return info;
|
|
256
|
+
})();
|
|
257
|
+
const what = jobWhat({ payload, op, nodes: nodes === undefined ? wfInfo.nodes : nodes, repo: repo ?? wfInfo.repo, language });
|
|
258
|
+
return jobDisplayName({ op, what, workflowName: workflowName ?? wfInfo.name, language });
|
|
259
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// dockerfile.mjs - a Dockerfile read by its own grammar (no regular-expression matching of instruction names inside the text):
|
|
2
|
+
// one statement per logical line (a trailing backslash continues it, comment lines inside a continuation are dropped), the
|
|
3
|
+
// instruction keyword is the first word, the stages open at each FROM. Parser directives (`# syntax=`, `# escape=`) and
|
|
4
|
+
// comments are kept apart from the instructions. Here-documents are not part of the subset the canon uses and are read as text.
|
|
5
|
+
|
|
6
|
+
/** Split a shell-form or exec-form argument string into its words (quotes group a word and are removed). */
|
|
7
|
+
export function words(text) {
|
|
8
|
+
const out = [];
|
|
9
|
+
let current = '';
|
|
10
|
+
let quote = null;
|
|
11
|
+
let started = false;
|
|
12
|
+
for (const char of text) {
|
|
13
|
+
if (quote) {
|
|
14
|
+
if (char === quote) quote = null;
|
|
15
|
+
else current += char;
|
|
16
|
+
} else if (char === '"' || char === "'") {
|
|
17
|
+
quote = char;
|
|
18
|
+
started = true;
|
|
19
|
+
} else if (char === ' ' || char === '\t' || char === '\n') {
|
|
20
|
+
if (started || current) out.push(current);
|
|
21
|
+
current = '';
|
|
22
|
+
started = false;
|
|
23
|
+
} else {
|
|
24
|
+
current += char;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (started || current) out.push(current);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The exec-form array of an instruction's argument text (`["node", "x.js"]`), or null when it is not a JSON array of strings. */
|
|
32
|
+
export function execForm(text) {
|
|
33
|
+
const trimmed = text.trim();
|
|
34
|
+
if (!trimmed.startsWith('[')) return null;
|
|
35
|
+
try {
|
|
36
|
+
const value = JSON.parse(trimmed);
|
|
37
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The commands of a shell-form RUN body: the text split at `&&`, `||`, `;` and `|` outside quotes, each as its words. */
|
|
44
|
+
export function shellCommands(body) {
|
|
45
|
+
const commands = [];
|
|
46
|
+
let current = '';
|
|
47
|
+
let quote = null;
|
|
48
|
+
const flush = () => {
|
|
49
|
+
const parts = words(current);
|
|
50
|
+
if (parts.length) commands.push(parts);
|
|
51
|
+
current = '';
|
|
52
|
+
};
|
|
53
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
54
|
+
const char = body[i];
|
|
55
|
+
if (quote) {
|
|
56
|
+
current += char;
|
|
57
|
+
if (char === quote) quote = null;
|
|
58
|
+
} else if (char === '"' || char === "'") {
|
|
59
|
+
quote = char;
|
|
60
|
+
current += char;
|
|
61
|
+
} else if (char === ';' || char === '|' || (char === '&' && body[i + 1] === '&')) {
|
|
62
|
+
if (char !== ';' && body[i + 1] === char) i += 1;
|
|
63
|
+
flush();
|
|
64
|
+
} else if (char === '\\' && body[i + 1] === '\n') {
|
|
65
|
+
i += 1;
|
|
66
|
+
} else {
|
|
67
|
+
current += char;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
flush();
|
|
71
|
+
return commands;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a Dockerfile: { instructions: [{ keyword, text, line, stage }], stages: [{ index, name, image, line, instructions }],
|
|
76
|
+
* comments: [{ text, line }], preamble: [instruction before the first FROM (ARG)] }. `keyword` is upper-case; `text` is the
|
|
77
|
+
* argument text of the logical line; `stage` is the index of the stage the instruction belongs to (-1 before the first FROM).
|
|
78
|
+
*/
|
|
79
|
+
export function parseDockerfile(source) {
|
|
80
|
+
const lines = String(source).replace(/\r\n/g, '\n').split('\n');
|
|
81
|
+
const instructions = [];
|
|
82
|
+
const comments = [];
|
|
83
|
+
let escape = '\\';
|
|
84
|
+
let directives = true;
|
|
85
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
86
|
+
const raw = lines[i];
|
|
87
|
+
const trimmed = raw.trim();
|
|
88
|
+
if (trimmed === '') { directives = false; continue; }
|
|
89
|
+
if (trimmed.startsWith('#')) {
|
|
90
|
+
const body = trimmed.slice(1).trim();
|
|
91
|
+
const directive = directives ? /^(syntax|escape|check)\s*=\s*(\S+)$/i.exec(body) : null;
|
|
92
|
+
if (directive) { if (directive[1].toLowerCase() === 'escape') escape = directive[2]; continue; }
|
|
93
|
+
directives = false;
|
|
94
|
+
comments.push({ text: body, line: i + 1 });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
directives = false;
|
|
98
|
+
const start = i + 1;
|
|
99
|
+
let logical = trimmed;
|
|
100
|
+
while (logical.endsWith(escape) && i + 1 < lines.length) {
|
|
101
|
+
logical = logical.slice(0, -1).trimEnd();
|
|
102
|
+
i += 1;
|
|
103
|
+
while (i < lines.length && lines[i].trim().startsWith('#')) i += 1;
|
|
104
|
+
if (i >= lines.length) break;
|
|
105
|
+
logical = `${logical} ${lines[i].trim()}`;
|
|
106
|
+
}
|
|
107
|
+
const split = logical.search(/\s/);
|
|
108
|
+
const keyword = (split < 0 ? logical : logical.slice(0, split)).toUpperCase();
|
|
109
|
+
instructions.push({ keyword, text: split < 0 ? '' : logical.slice(split).trim(), line: start, stage: -1 });
|
|
110
|
+
}
|
|
111
|
+
const stages = [];
|
|
112
|
+
const preamble = [];
|
|
113
|
+
for (const instruction of instructions) {
|
|
114
|
+
if (instruction.keyword === 'FROM') {
|
|
115
|
+
const parts = words(instruction.text).filter((part) => !part.startsWith('--'));
|
|
116
|
+
const asAt = parts.findIndex((part, index) => index > 0 && part.toUpperCase() === 'AS');
|
|
117
|
+
stages.push({ index: stages.length, name: asAt > 0 ? parts[asAt + 1] ?? null : null, image: parts[0] ?? '', line: instruction.line, instructions: [] });
|
|
118
|
+
}
|
|
119
|
+
instruction.stage = stages.length - 1;
|
|
120
|
+
if (stages.length) stages.at(-1).instructions.push(instruction);
|
|
121
|
+
else preamble.push(instruction);
|
|
122
|
+
}
|
|
123
|
+
return { instructions, stages, comments, preamble };
|
|
124
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// env.mjs - the one place the runtime reads a named environment variable (smell S8-01/S8-02/S11-04; scripts/checks/check-env.mjs).
|
|
2
|
+
// modules/schemas/env.yaml catalogues every variable with its purpose and kind. Production code reads a variable here
|
|
3
|
+
// (readEnv) or takes an injected `env` parameter; it never reads `process.env.NAME` itself. The test runner is observed
|
|
4
|
+
// in exactly one place, isSpecRun: code that must not touch the host's real state during a spec asks it.
|
|
5
|
+
|
|
6
|
+
/** The value of the environment variable `name` in `env` (default: the process environment), or undefined. */
|
|
7
|
+
export const readEnv = (name, env = process.env) => env[name];
|
|
8
|
+
|
|
9
|
+
/** True when `env` (default: the process environment) belongs to a spec run (`node --test` marks its children with NODE_TEST_CONTEXT). */
|
|
10
|
+
export const isSpecRun = (env = process.env) => Boolean(env.NODE_TEST_CONTEXT);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// event-contract.mjs - the async contract between the services of one product (R166 HFS_EVENT_CONTRACT).
|
|
2
|
+
// The one source of the contract is the typed event classes of a service, `be/src/modules/events/<service>/<event>.event.ts`:
|
|
3
|
+
// export interface OrderPlacedPayload { readonly orderId: string; readonly totalMinorUnits: number }
|
|
4
|
+
// export class OrderPlacedEvent extends BaseEvent {
|
|
5
|
+
// static readonly eventName = "order.placed"; static readonly version = 1
|
|
6
|
+
// static readonly compensates = "order.placed" // only an event that reports the failure of a step
|
|
7
|
+
// static create(payload: OrderPlacedPayload): OrderPlacedEvent { ... }
|
|
8
|
+
// }
|
|
9
|
+
// `starci app emit` writes `be/contracts/<service>/events.json` from them (name, version, compensates, and the payload fields of the
|
|
10
|
+
// interface `create` takes); `starci app check` compares the committed file with the same reader. Nothing is executed: both walk the TypeScript
|
|
11
|
+
// syntax tree. The functions take the `ts` module the caller loaded (the repository's own, else the runtime's); this file imports nothing.
|
|
12
|
+
|
|
13
|
+
const EVENT_CONTRACT_SCHEMA = 'starci/event-contract@1';
|
|
14
|
+
const FIELD_TYPE = /^(?:string|number|boolean|string\[\]|number\[\])\??$/;
|
|
15
|
+
|
|
16
|
+
const hasStatic = (ts, member) => member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) === true;
|
|
17
|
+
|
|
18
|
+
/** The literal of a `static readonly <name> = <literal>` member: `{ text }` for a string, `{ number }` for an integer, or null. */
|
|
19
|
+
function staticLiteral(ts, member) {
|
|
20
|
+
if (!ts.isPropertyDeclaration(member) || !hasStatic(ts, member) || !ts.isIdentifier(member.name) || member.initializer === undefined) return null;
|
|
21
|
+
let value = member.initializer;
|
|
22
|
+
while (ts.isAsExpression(value) || ts.isParenthesizedExpression(value)) value = value.expression;
|
|
23
|
+
if (ts.isStringLiteralLike(value)) return { name: member.name.text, text: value.text };
|
|
24
|
+
if (ts.isNumericLiteral(value) && Number.isInteger(Number(value.text))) return { name: member.name.text, number: Number(value.text) };
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The text of a type node when it is one of the wire field types (`string`, `number`, `boolean`, `string[]`, `number[]`), else null. */
|
|
29
|
+
function fieldType(ts, node) {
|
|
30
|
+
if (node.kind === ts.SyntaxKind.StringKeyword) return 'string';
|
|
31
|
+
if (node.kind === ts.SyntaxKind.NumberKeyword) return 'number';
|
|
32
|
+
if (node.kind === ts.SyntaxKind.BooleanKeyword) return 'boolean';
|
|
33
|
+
if (ts.isArrayTypeNode(node)) {
|
|
34
|
+
const element = fieldType(ts, node.elementType);
|
|
35
|
+
return element === 'string' || element === 'number' ? `${element}[]` : null;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The payload fields of the interface or type literal named `typeName` in the file: `{ <field>: <type> }`, optional fields with a `?`. */
|
|
41
|
+
function payloadOf(ts, sourceFile, typeName, problems, where) {
|
|
42
|
+
let members = null;
|
|
43
|
+
for (const statement of sourceFile.statements) {
|
|
44
|
+
if (ts.isInterfaceDeclaration(statement) && statement.name.text === typeName) members = statement.members;
|
|
45
|
+
if (ts.isTypeAliasDeclaration(statement) && statement.name.text === typeName && ts.isTypeLiteralNode(statement.type)) members = statement.type.members;
|
|
46
|
+
}
|
|
47
|
+
if (members === null) {
|
|
48
|
+
problems.push(`${where}: the payload type ${typeName} is not an interface or type literal of the file`);
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
const payload = {};
|
|
52
|
+
for (const member of members) {
|
|
53
|
+
if (!ts.isPropertySignature(member) || !ts.isIdentifier(member.name) || member.type === undefined) {
|
|
54
|
+
problems.push(`${where}: the payload ${typeName} holds a member that is not \`name: type\``);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const type = fieldType(ts, member.type);
|
|
58
|
+
const written = type === null ? null : `${type}${member.questionToken ? '?' : ''}`;
|
|
59
|
+
if (written === null || !FIELD_TYPE.test(written)) problems.push(`${where}: payload field ${member.name.text} must be string, number, boolean, string[] or number[]`);
|
|
60
|
+
else payload[member.name.text] = written;
|
|
61
|
+
}
|
|
62
|
+
return payload;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The event classes of one source text: `{ events: [{ className, name, version, compensates, payload }], problems: [text] }`. A class is an event
|
|
67
|
+
* when it declares `static readonly eventName`; `version` is required, `create(payload: <Interface>)` names the payload.
|
|
68
|
+
*/
|
|
69
|
+
export function readEventClasses(ts, text, file = 'event.ts') {
|
|
70
|
+
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
71
|
+
const events = [];
|
|
72
|
+
const problems = [];
|
|
73
|
+
for (const statement of sourceFile.statements) {
|
|
74
|
+
if (!ts.isClassDeclaration(statement) || statement.name === undefined) continue;
|
|
75
|
+
const literals = statement.members.map((member) => staticLiteral(ts, member)).filter((literal) => literal !== null);
|
|
76
|
+
const name = literals.find((literal) => literal.name === 'eventName')?.text;
|
|
77
|
+
if (name === undefined) continue;
|
|
78
|
+
const className = statement.name.text;
|
|
79
|
+
const version = literals.find((literal) => literal.name === 'version')?.number;
|
|
80
|
+
const compensates = literals.find((literal) => literal.name === 'compensates')?.text;
|
|
81
|
+
if (version === undefined || version < 1) problems.push(`${className} needs \`static readonly version\` as a positive integer`);
|
|
82
|
+
const create = statement.members.find((member) => ts.isMethodDeclaration(member) && hasStatic(ts, member) && ts.isIdentifier(member.name) && member.name.text === 'create');
|
|
83
|
+
const parameter = create?.parameters[0]?.type;
|
|
84
|
+
if (parameter === undefined || !ts.isTypeReferenceNode(parameter) || !ts.isIdentifier(parameter.typeName)) {
|
|
85
|
+
problems.push(`${className} needs \`static create(payload: <Interface>)\` naming its payload interface`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
events.push({ className, name, version: version ?? 0, ...(compensates === undefined ? {} : { compensates }), payload: payloadOf(ts, sourceFile, parameter.typeName.text, problems, className) });
|
|
89
|
+
}
|
|
90
|
+
return { events, problems };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The sorted object: keys in code-point order, recursively (what the snapshot prints). */
|
|
94
|
+
const sorted = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, sorted(value[key])])) : value);
|
|
95
|
+
|
|
96
|
+
/** The text of the snapshot of a service from its event classes: 2-space JSON, sorted keys, one final newline. */
|
|
97
|
+
export function snapshotText(service, events) {
|
|
98
|
+
const table = Object.fromEntries(events.map(({ name, version, compensates, payload }) => [name, { version, payload, ...(compensates === undefined ? {} : { compensates }) }]));
|
|
99
|
+
return `${JSON.stringify(sorted({ schema: EVENT_CONTRACT_SCHEMA, service, events: table }), null, 2)}\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The snapshot's text with line endings folded, for comparing a committed file to a fresh one. */
|
|
103
|
+
export const folded = (text) => text.replace(/\r\n/g, '\n');
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// example-refs.mjs — the ONE runtime source file that may spell a path into the runtime's own examples/
|
|
2
|
+
// tree or a product name the runtime legitimately knows (R206, NO_EXAMPLE_COUPLING): every other source
|
|
3
|
+
// file reaches these through the constants below, and scripts/checks/check-example-coupling.mjs exempts
|
|
4
|
+
// this file's name only - a declared read, never a path allowlist. A constant belongs here only when the
|
|
5
|
+
// runtime genuinely reads that tree or names that product at run time; a pointer that can be generic must
|
|
6
|
+
// be generic in the file that holds it.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
11
|
+
import { validateAgainstSchema } from './json-schema.mjs';
|
|
12
|
+
import { insidePath, samePath, slash } from './path-key.mjs';
|
|
13
|
+
|
|
14
|
+
/** The runtime's example tree root. */
|
|
15
|
+
export const EXAMPLES_ROOT = 'examples';
|
|
16
|
+
|
|
17
|
+
/** Curated basic sample source; operational artifact writers remain external. */
|
|
18
|
+
export const EXAMPLE_RUNTIMES_ROOT = `${EXAMPLES_ROOT}/.runtimes`;
|
|
19
|
+
|
|
20
|
+
/** Every examples/<name>/hfs.json declaration of kind app, sorted by name. */
|
|
21
|
+
export function discoverExampleApps(root) {
|
|
22
|
+
const directory = path.join(root, EXAMPLES_ROOT);
|
|
23
|
+
let entries;
|
|
24
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { return []; }
|
|
25
|
+
return entries.filter((entry) => entry.isDirectory()).filter((entry) => {
|
|
26
|
+
try { return JSON.parse(fs.readFileSync(path.join(directory, entry.name, 'hfs.json'), 'utf8'))?.kind === 'app'; }
|
|
27
|
+
catch { return false; }
|
|
28
|
+
}).map((entry) => entry.name).sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The generic services-block template a starcistacks follow-up leg is pointed at (S11-02: one template
|
|
32
|
+
* for every repository - the per-repository blocks it replaced are spec fixtures under
|
|
33
|
+
* tests/fixtures/starcistacks-services/, never a run-time read). */
|
|
34
|
+
export const STACK_DECLARATION_TEMPLATE = `${EXAMPLES_ROOT}/starcistacks-services/template.services.yaml`;
|
|
35
|
+
|
|
36
|
+
/** The reference app declaration scripts/hfs/derived-fields.mjs samples for per-field slot ownership. */
|
|
37
|
+
export const REFERENCE_APP_MANIFEST = `${EXAMPLES_ROOT}/ecommerce-app/hfs.json`;
|
|
38
|
+
|
|
39
|
+
/** The example app whose installed tree an fe-kit peer link points at by default. */
|
|
40
|
+
export const REFERENCE_EXAMPLE_APP = 'ecommerce-app';
|
|
41
|
+
|
|
42
|
+
/** The grammar css families a product repository may declare (`[data-grammar-family="<id>"]`); the
|
|
43
|
+
* runtime's own product maps to the core sheet. */
|
|
44
|
+
export const GRAMMAR_FAMILIES = Object.freeze({ starci: 'core', nivo: 'nivo' });
|
|
45
|
+
|
|
46
|
+
/** Display aliases of legacy workflow slugs that predate workflows.display_name (the progress report's
|
|
47
|
+
* English fallback; the i18n catalog carries the owner's wording for each value). */
|
|
48
|
+
export const WORKFLOW_ALIASES = Object.freeze({
|
|
49
|
+
'nivo-app-auth': 'AUTH (sign-in)', 'nivo-workspace-provision': 'WSPV (buy & provision workspace)',
|
|
50
|
+
'nivo-modules-agentos': 'Modules (AgentOS)', 'nivo-collab-group-chat': 'Collab (group chat)',
|
|
51
|
+
'starci-next-work-and-stacks': 'StarCi Next – work & stacks', 'starci-next-base-repos': 'StarCi Next – base repos',
|
|
52
|
+
'miamia-work-and-stacks': 'Mia Mia – work & stacks', 'miamia-base-repos': 'Mia Mia – base repos',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** A feature-folder segment that is only the product's own name - no useful leaf (display-names pathLabel). */
|
|
56
|
+
export const PRODUCT_NAME_SEGMENT = /^(nivo|starci|mia)[-\w]*$/i;
|
|
57
|
+
|
|
58
|
+
/** The closed list of product names this runtime shipped against - the names R206
|
|
59
|
+
* (check-example-coupling.mjs) refuses anywhere in runtime source outside this file (a retired
|
|
60
|
+
* name is judged by RT_RETIRED_NAME_LIVE, R207, from modules/kernel/retired-paths.yaml). */
|
|
61
|
+
export const PRODUCT_NAMES = Object.freeze(['mia-mia', 'miamia', 'nivo', 'starci-academy', 'ecommerce-app', 'shape-slot']);
|
|
62
|
+
|
|
63
|
+
/** The catalog of stable pattern IDs pointing at actual executable example source. */
|
|
64
|
+
export const EXAMPLE_CATALOG_FILE = `${EXAMPLES_ROOT}/index.yaml`;
|
|
65
|
+
|
|
66
|
+
function referenceFile(root, relative, { directory = false, exact = false } = {}) {
|
|
67
|
+
if (typeof relative !== 'string' || !relative || relative.includes('\\') || relative.includes(':') || path.isAbsolute(relative)
|
|
68
|
+
|| relative.split('/').some((part) => !part || part === '.' || part === '..')) throw new Error(`invalid example input: ${relative}`);
|
|
69
|
+
const file = path.resolve(root, relative);
|
|
70
|
+
if (!insidePath(root, file)) throw new Error(`example input is outside its declared root: ${relative}`);
|
|
71
|
+
let at = path.parse(file).root;
|
|
72
|
+
for (const segment of file.slice(at.length).split(path.sep).filter(Boolean)) {
|
|
73
|
+
at = path.join(at, segment);
|
|
74
|
+
if (fs.lstatSync(at).isSymbolicLink() || !samePath(fs.realpathSync.native(at), at)
|
|
75
|
+
|| (exact && !fs.readdirSync(path.dirname(at)).includes(path.basename(at))))
|
|
76
|
+
throw new Error(`linked example input: ${relative}`);
|
|
77
|
+
}
|
|
78
|
+
if (!(directory ? fs.lstatSync(file).isDirectory() : fs.lstatSync(file).isFile())
|
|
79
|
+
|| !insidePath(fs.realpathSync.native(root), fs.realpathSync.native(file)))
|
|
80
|
+
throw new Error(`example input is not a contained regular file: ${relative}`);
|
|
81
|
+
return file;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Read and validate complete source references. Duplicate IDs refuse before
|
|
85
|
+
* lookup; source, compiler and test pointers stay within one declared HFS app. */
|
|
86
|
+
export function loadExampleCatalog(root, { file = EXAMPLE_CATALOG_FILE } = {}) {
|
|
87
|
+
const catalog = parseYaml(fs.readFileSync(referenceFile(root, file), 'utf8'));
|
|
88
|
+
const schema = parseYaml(fs.readFileSync(referenceFile(root, 'modules/schemas/code-example-catalog.schema.yaml'), 'utf8'));
|
|
89
|
+
const errors = validateAgainstSchema(catalog, schema);
|
|
90
|
+
if (errors.length) throw new Error(`invalid example catalog: ${errors.join('; ')}`);
|
|
91
|
+
const ids = new Set();
|
|
92
|
+
for (const row of catalog.examples) {
|
|
93
|
+
if (ids.has(row.id)) throw new Error(`duplicate example id: ${row.id}`);
|
|
94
|
+
ids.add(row.id);
|
|
95
|
+
}
|
|
96
|
+
for (const row of catalog.examples) {
|
|
97
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.path)) throw new Error(`invalid example app: ${row.path}`);
|
|
98
|
+
const app = path.join(root, EXAMPLES_ROOT, row.path);
|
|
99
|
+
const declaration = JSON.parse(fs.readFileSync(referenceFile(root, `${EXAMPLES_ROOT}/${row.path}/hfs.json`), 'utf8'));
|
|
100
|
+
if (declaration.kind !== 'app') throw new Error(`example has no HFS app declaration: ${row.id}`);
|
|
101
|
+
if (!row.files.includes(row.entrypoint) || !/\.tsx?$/.test(row.entrypoint)) throw new Error(`example entrypoint is not a listed source: ${row.id}`);
|
|
102
|
+
if (new Set(row.relatedRules).size !== row.relatedRules.length) throw new Error(`duplicate example relatedRules: ${row.id}`);
|
|
103
|
+
for (const key of ['files', 'projects', 'tests']) {
|
|
104
|
+
if (new Set(row[key]).size !== row[key].length) throw new Error(`duplicate example ${key}: ${row.id}`);
|
|
105
|
+
for (const relative of row[key]) {
|
|
106
|
+
if (relative.split('/').some((part) => ['node_modules', 'dist', '.next', '_derived', '.git'].includes(part)))
|
|
107
|
+
throw new Error(`derived or installed example input: ${relative}`);
|
|
108
|
+
referenceFile(app, relative);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (row.files.some((relative) => !/\.(?:tsx?|json)$/.test(relative))) throw new Error(`unsupported example source: ${row.id}`);
|
|
112
|
+
}
|
|
113
|
+
return catalog;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Source, compiler and test inputs of selected stable IDs, relative to the runtime.
|
|
117
|
+
* A null selection requests the declared whole catalog; unknown IDs refuse. */
|
|
118
|
+
export function exampleSourcePaths(root, ids = null, options = {}) {
|
|
119
|
+
const catalog = loadExampleCatalog(root, options), selected = new Set(ids ?? catalog.examples.map((row) => row.id));
|
|
120
|
+
const known = new Set(catalog.examples.map((row) => row.id));
|
|
121
|
+
for (const id of selected) if (!known.has(id)) throw new Error(`unknown example id: ${id}`);
|
|
122
|
+
return [...new Set(catalog.examples.filter((row) => selected.has(row.id))
|
|
123
|
+
.flatMap((row) => [...row.files, ...row.projects, ...row.tests].map((relative) => `${EXAMPLES_ROOT}/${row.path}/${relative}`)))];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Actual example apps' own Work trees in app-name order. Dependency/template
|
|
127
|
+
* trees are not app Work; an absent own tree contributes no checker input. */
|
|
128
|
+
export function exampleWorkRoots(root) {
|
|
129
|
+
return discoverExampleApps(root).flatMap((app) => {
|
|
130
|
+
const relative = `${EXAMPLES_ROOT}/${app}/.starciwork/index.yaml`;
|
|
131
|
+
const directory = path.dirname(path.join(root, relative)), stat = fs.lstatSync(directory, { throwIfNoEntry: false });
|
|
132
|
+
if (!stat) return [];
|
|
133
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`invalid example Work directory: ${app}`);
|
|
134
|
+
return [path.dirname(referenceFile(root, relative))];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Per-call READ options for an actual HFS app's own Work tree. A real project
|
|
139
|
+
* uses the default external store; an example never falls back to that store.
|
|
140
|
+
* The fixture CAS is curated source, not a destination for operational writers. */
|
|
141
|
+
export function exampleArtifactReadOptions(runtimeRoot, workRoot, { resolveRoot = workRoot } = {}) {
|
|
142
|
+
if (typeof runtimeRoot !== 'string' || typeof workRoot !== 'string') throw new TypeError('example artifact context needs runtime and Work paths');
|
|
143
|
+
const base = path.resolve(runtimeRoot), target = path.resolve(workRoot);
|
|
144
|
+
const relative = slash(path.relative(base, target)), parts = relative.split('/');
|
|
145
|
+
if (parts[0]?.toLowerCase() !== EXAMPLES_ROOT) return Object.freeze({});
|
|
146
|
+
const app = parts[1];
|
|
147
|
+
if (parts[0] !== EXAMPLES_ROOT || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(app ?? '')
|
|
148
|
+
|| !discoverExampleApps(base).includes(app) || parts[2] !== '.starciwork')
|
|
149
|
+
throw new Error(`invalid example Work artifact context: ${relative}`);
|
|
150
|
+
referenceFile(base, `${EXAMPLES_ROOT}/${app}/hfs.json`, { exact: true });
|
|
151
|
+
referenceFile(base, `${EXAMPLES_ROOT}/${app}/.starciwork/index.yaml`, { exact: true });
|
|
152
|
+
referenceFile(base, relative, { directory: true, exact: true });
|
|
153
|
+
const resolution = path.resolve(resolveRoot), ownWork = path.join(base, EXAMPLES_ROOT, app, '.starciwork');
|
|
154
|
+
if (!insidePath(resolution, target, { includeSelf: true }) || !insidePath(ownWork, resolution, { includeSelf: true }))
|
|
155
|
+
throw new Error('selected example validation and resolution must share its own Work root');
|
|
156
|
+
referenceFile(base, slash(path.relative(base, resolution)), { directory: true, exact: true });
|
|
157
|
+
return Object.freeze({ root: path.join(base, EXAMPLE_RUNTIMES_ROOT, app, 'artifacts') });
|
|
158
|
+
}
|