@starci/hfs 4.7.32 → 4.7.34
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 +10 -0
- package/README.md +13 -6
- package/emit/contracts.mjs +4 -3
- package/emit/form-bounds-reader.ts.partial +58 -0
- package/emit/form-bounds.mjs +155 -0
- package/emit/graphql-input-bounds.mjs +97 -0
- package/emit/input-bounds.mjs +44 -0
- package/emit/operation-schemas.mjs +137 -0
- package/emit/operations-worker.mjs +2 -2
- package/emit/operations.mjs +52 -16
- package/emit/schema-worker.mjs +17 -4
- package/emit/type-schema.mjs +17 -5
- package/package.json +9 -2
- package/release/gate.mjs +18 -23
- package/release/index.mjs +1 -1
- package/release/push.mjs +4 -5
- package/runtime/engine/admission.mjs +331 -0
- package/runtime/engine/canonical-json.mjs +14 -0
- package/runtime/engine/db/blob.mjs +380 -0
- package/runtime/engine/db/event-compact.mjs +73 -0
- package/runtime/engine/db/ledger-open.mjs +38 -0
- package/runtime/engine/db/ledger-paths.mjs +87 -0
- package/runtime/engine/db/ledger.mjs +1068 -0
- package/runtime/engine/db/machine-connection.mjs +150 -0
- package/runtime/engine/db/machine-open.mjs +60 -0
- package/runtime/engine/db/machine-schema.mjs +105 -0
- package/runtime/engine/db/machine.mjs +1317 -0
- package/runtime/engine/db/provider-reservations.mjs +127 -0
- package/runtime/engine/db/provision-authority.mjs +76 -0
- package/runtime/engine/db/provision-lock.mjs +29 -0
- package/runtime/engine/db/provision-operations.mjs +146 -0
- package/runtime/engine/db/ref-value.mjs +67 -0
- package/runtime/engine/db/schema/machine.sql +920 -0
- package/runtime/engine/db/schema/runtime.sql +1113 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
- package/runtime/knowledge/hfs/design-truths.yaml +149 -0
- package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
- package/runtime/knowledge/hfs/rules.yaml +269 -48
- package/runtime/knowledge/hfs/slots.yaml +113 -53
- package/runtime/knowledge/patterns/be/api.yaml +46 -20
- package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
- package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
- package/runtime/knowledge/patterns/be/projections.yaml +2 -2
- package/runtime/knowledge/patterns/be/queues.yaml +57 -22
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
- package/runtime/knowledge/patterns/be/saga.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +249 -0
- package/runtime/modules/kernel/failure-codes.yaml +211 -52
- package/runtime/modules/kernel/hook-input.yaml +6 -0
- package/runtime/modules/kernel/protected-zone.yaml +81 -0
- package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
- package/runtime/modules/kernel/roles.yaml +405 -0
- package/runtime/modules/models/registry.yaml +3 -1
- package/runtime/modules/models/runtimes.yaml +2 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
- package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
- package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
- package/runtime/scripts/agent/codex-app-server.mjs +79 -0
- package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
- package/runtime/scripts/agent/host-agents.mjs +80 -0
- package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
- package/runtime/scripts/agent/trust-launch.mjs +191 -0
- package/runtime/scripts/agent/trust.mjs +523 -0
- package/runtime/scripts/api/fs/rename-over.mjs +21 -0
- package/runtime/scripts/api/fs/temp-path.mjs +8 -0
- package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
- package/runtime/scripts/api/git/diff-names.mjs +8 -0
- package/runtime/scripts/api/git/head-time.mjs +10 -0
- package/runtime/scripts/api/git/index-lock.mjs +78 -0
- package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
- package/runtime/scripts/api/git/merge-tree.mjs +5 -0
- package/runtime/scripts/api/git/rev-list.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse.mjs +13 -0
- package/runtime/scripts/api/git/status-query.mjs +5 -0
- package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
- package/runtime/scripts/api/npm/lib.mjs +32 -0
- package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
- package/runtime/scripts/api/npm/run-npm.mjs +6 -0
- package/runtime/scripts/api/npm/view.mjs +30 -0
- package/runtime/scripts/api/npm/whoami.mjs +8 -0
- package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
- package/runtime/scripts/api/process/kill-tree.mjs +22 -0
- package/runtime/scripts/api/process/lib.mjs +125 -0
- package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
- package/runtime/scripts/api/process/process-list.mjs +28 -0
- package/runtime/scripts/api/process/system-tool.mjs +37 -0
- package/runtime/scripts/gates/canon-digest.mjs +81 -0
- package/runtime/scripts/gates/canon-pins.mjs +16 -0
- package/runtime/scripts/gates/commit-proof.mjs +53 -0
- package/runtime/scripts/gates/gate-input.mjs +98 -0
- package/runtime/scripts/gates/gate.mjs +669 -0
- package/runtime/scripts/gates/grammar-dist.mjs +206 -0
- package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
- package/runtime/scripts/gates/test-world-run.mjs +254 -0
- package/runtime/scripts/gates/type-impact.mjs +277 -0
- package/runtime/scripts/gates/type-program.mjs +113 -0
- package/runtime/scripts/guards/assigned-command.mjs +12 -0
- package/runtime/scripts/guards/call-rights.mjs +47 -0
- package/runtime/scripts/guards/command-guard.mjs +468 -0
- package/runtime/scripts/guards/command-policy.mjs +342 -0
- package/runtime/scripts/guards/critic-reach.mjs +82 -0
- package/runtime/scripts/guards/deps-guard.mjs +150 -0
- package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
- package/runtime/scripts/guards/git-pathspec.mjs +225 -0
- package/runtime/scripts/guards/git-policy.mjs +355 -0
- package/runtime/scripts/guards/guards-root.mjs +27 -0
- package/runtime/scripts/guards/hook-install.mjs +343 -0
- package/runtime/scripts/guards/hook-io.mjs +76 -0
- package/runtime/scripts/guards/hook-verdict.mjs +12 -0
- package/runtime/scripts/guards/install-verdict.mjs +41 -0
- package/runtime/scripts/guards/kernel-seat.mjs +63 -0
- package/runtime/scripts/guards/launch-verdict.mjs +74 -0
- package/runtime/scripts/guards/op-starci.mjs +16 -0
- package/runtime/scripts/guards/op-write-scope.mjs +27 -0
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
- package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
- package/runtime/scripts/guards/protected-zone.mjs +56 -0
- package/runtime/scripts/guards/refusals.mjs +27 -0
- package/runtime/scripts/guards/release-definition.mjs +185 -0
- package/runtime/scripts/guards/release-push-gate.mjs +72 -0
- package/runtime/scripts/guards/release-record.mjs +58 -0
- package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
- package/runtime/scripts/guards/rights.mjs +268 -0
- package/runtime/scripts/guards/seat-tools.mjs +47 -0
- package/runtime/scripts/guards/shell-commands.mjs +328 -0
- package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
- package/runtime/scripts/guards/verdict-process.mjs +81 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
- package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +6 -4
- package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +56 -16
- package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
- package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
- package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
- package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
- package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
- package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
- package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
- package/runtime/scripts/hfs/rules/edition.mjs +56 -12
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
- package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
- package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
- package/runtime/scripts/hfs/sealed-members.mjs +3 -3
- package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
- package/runtime/scripts/hfs/slots.mjs +2 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
- package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
- package/runtime/scripts/hfs/view.mjs +2 -1
- package/runtime/scripts/lib/app-data.mjs +11 -0
- package/runtime/scripts/lib/bypass-markers.mjs +272 -75
- package/runtime/scripts/lib/check-format.mjs +7 -0
- package/runtime/scripts/lib/cli-arg.mjs +120 -0
- package/runtime/scripts/lib/css-rules.mjs +220 -0
- package/runtime/scripts/lib/dom-tree.mjs +4 -0
- package/runtime/scripts/lib/escape.mjs +16 -0
- package/runtime/scripts/lib/event-items.mjs +22 -0
- package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
- package/runtime/scripts/lib/git-dir.mjs +120 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
- package/runtime/scripts/lib/guard-command.mjs +24 -0
- package/runtime/scripts/lib/hook-limits.mjs +5 -0
- package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
- package/runtime/scripts/lib/js-lex.mjs +200 -0
- package/runtime/scripts/lib/json-formats.mjs +0 -1
- package/runtime/scripts/lib/language.mjs +94 -1
- package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
- package/runtime/scripts/lib/loosening-config.mjs +361 -0
- package/runtime/scripts/lib/loosening-state.mjs +223 -0
- package/runtime/scripts/lib/menu-parts.mjs +11 -0
- package/runtime/scripts/lib/normalize.mjs +25 -0
- package/runtime/scripts/lib/op-gate.mjs +15 -0
- package/runtime/scripts/lib/redact.mjs +208 -0
- package/runtime/scripts/lib/runtime-location.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
- package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
- package/runtime/scripts/lib/seat-env.mjs +43 -0
- package/runtime/scripts/lib/secret-patterns.mjs +58 -9
- package/runtime/scripts/lib/shell-source.mjs +4 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/swarm-stack.mjs +2 -1
- package/runtime/scripts/lib/tar-files.mjs +39 -0
- package/runtime/scripts/lib/ts-analysis.mjs +10 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
- package/runtime/scripts/lib/worktree-record.mjs +14 -0
- package/runtime/scripts/machine/artifact-hold.mjs +52 -0
- package/runtime/scripts/machine/ask-receipts.mjs +113 -0
- package/runtime/scripts/machine/evidence-store.mjs +223 -0
- package/runtime/scripts/machine/home.mjs +216 -0
- package/runtime/scripts/machine/host-lock.mjs +184 -0
- package/runtime/scripts/machine/lock-recovery.mjs +55 -0
- package/runtime/scripts/machine/roles-contract.mjs +90 -0
- package/runtime/scripts/machine/runtime-change.mjs +20 -0
- package/runtime/scripts/machine/workflow-tree.mjs +122 -0
- package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
- package/runtime/scripts/reconciler/guard-row.mjs +25 -0
- package/runtime/scripts/work/asset-slot.mjs +284 -0
- package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
- package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
- package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
- package/runtime/scripts/work/brand/brand.mjs +892 -0
- package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
- package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
- package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
- package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
- package/runtime/scripts/work/draw/draw-source.mjs +412 -0
- package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
- package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
- package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
- package/runtime/scripts/work/draw-grammar.mjs +175 -0
- package/runtime/scripts/work/draw-render-page.mjs +188 -0
- package/runtime/scripts/work/draw-render.mjs +626 -0
- package/runtime/scripts/work/grammar-proposal.mjs +279 -0
- package/runtime/scripts/work/png.mjs +237 -0
- package/runtime/scripts/work/render-tools.mjs +64 -0
- package/runtime/scripts/work/work-io.mjs +119 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-plan.mjs +40 -13
- package/scaffold/add-queue-lite.mjs +372 -0
- package/scaffold/add.mjs +5 -2
- package/scaffold/app.mjs +1 -2
- package/scaffold/edition-gate.mjs +11 -7
- package/scaffold/lite-exports.mjs +10 -0
- package/scaffold/pattern-file-slots.mjs +33 -0
- package/scaffold/test-world.mjs +1 -1
- package/scaffold/yaml-block-edit.mjs +117 -0
- package/src/main.mjs +5 -4
- package/stack/deploy.mjs +7 -3
- package/stack/post-deploy.mjs +48 -0
- package/sync/index.mjs +10 -0
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/codegen-graphql.mjs.partial +59 -12
- package/templates/app/hooks/husky/pre-commit +2 -2
- package/templates/app/hooks/husky/pre-push +10 -3
- package/templates/app/package-scripts/redis-dev.json +3 -0
- package/templates/app/package-scripts-lite/package.json +1 -0
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
- package/templates/app/skeleton/README.md +2 -0
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/README.md +1 -1
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
- package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
- package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
- package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/inbox/platform/migration.sql.tpl +1 -0
- package/templates/be/inbox/service.spec.ts.tpl +4 -4
- package/templates/be/inbox/service.ts.tpl +3 -1
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
- package/templates/be/inbox/webhook-door.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
- package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
- package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
- package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
- package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
- package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
- package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
- package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
- package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
- package/upgrade/check.mjs +12 -135
- package/upgrade/file-state.mjs +1 -16
- package/upgrade/index.mjs +34 -361
- package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
- package/runtime/scripts/lib/gate-loosening.mjs +0 -292
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
- package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
- package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// draw-render.mjs — the capture interface.draw uses for code-native regions: the drawing is a real render, not an image model's guess.
|
|
3
|
+
//
|
|
4
|
+
// starci work draw-render --html <file> --out <dir> --viewports 390x844,1440x900
|
|
5
|
+
// [--full-page] [--name <base>] [--theme light|dark] [--json]
|
|
6
|
+
// starci work draw-render --component <module> --export <XBase> --props <fixture.json> [--css <file>]...
|
|
7
|
+
// --out <dir> --viewports 390x844,1440x900 [--full-page] [--name <base>] [--theme light|dark] [--json]
|
|
8
|
+
// [--trace] also records a Playwright trace per viewport, <out>/<base>.trace.zip (screenshots + DOM snapshots), named in
|
|
9
|
+
// the record's trace field and indexed by job-artifacts.mjs as subkind playwright-trace. Off by default: a trace
|
|
10
|
+
// is 1-3 MB per viewport and a ui record's assets/ count against Work's byte budget; a draw loop's evidence
|
|
11
|
+
// round or a debugging run turns it on.
|
|
12
|
+
// [--rationale <file>] the rationale.json of the drawing (owner ruling 2026-09-27; default: <html stem>.rationale.json,
|
|
13
|
+
// else rationale.json beside the html). Every capture measures what the render uses (record `rationale`:
|
|
14
|
+
// scripts/work/draw/draw-rationale.mjs measureRationale) and, with a rationale, also writes the annotated redline
|
|
15
|
+
// <base>.redline.png (spacing brackets with value and rule id, DNA labels at component roots; record `redline`).
|
|
16
|
+
// [--product <app dir>] [--grammar auto|product|claude-dist] [--grammar-dist <package root>] [--harness-out <dir>]
|
|
17
|
+
// fixture mode with a real grammar drawing (owner ruling 2026-09-27: interface.draw draws WITH the real grammar
|
|
18
|
+
// components - <XBase>.draw.tsx, scripts/work/draw/draw-source.mjs). --product is the product app whose own
|
|
19
|
+
// node_modules every bare import resolves from (react, @heroui/*, tailwindcss, the icons), so the draw file may
|
|
20
|
+
// live outside it (a ui record dir, a temp dir) and the product checkout is only read. The @starci/grammar the
|
|
21
|
+
// bundle AND the CSS resolve is chosen by scripts/work/draw-grammar.mjs: the product's install when the draw
|
|
22
|
+
// file type-checks against it, else the runtime-built claude-dist through an alias in THIS bundle only; the
|
|
23
|
+
// record's source.grammar says which (grammarSource product@x | claude-dist@y) and upgradeOwed names an owed
|
|
24
|
+
// product upgrade. A draw file that type-checks against no candidate is refused (DRAW_TYPECHECK_FAILED, exit 1).
|
|
25
|
+
// A .draw.tsx is given --product = the nearest package.json above it that depends on @starci/grammar when the
|
|
26
|
+
// flag is absent. Every layout element of the draw file is stamped data-draw-layout, so the capture measures
|
|
27
|
+
// rendered-DOM ownership (record `ownership`: a painting element whose nearest marker is a drawn layout element,
|
|
28
|
+
// not a grammar component) and writes the DOM snapshot <base>.dom.html (data-component copied to
|
|
29
|
+
// data-grammar-component, so the html-reading metrics read the real render). --harness-out keeps the bundled
|
|
30
|
+
// harness (index.html, harness.js, global.css) for the loop's browser metrics.
|
|
31
|
+
// [--state <state> [--base <XBase>]] names the capture <XBase>#<state>--<w>x<h>--<theme>: a drawing is the XBase
|
|
32
|
+
// content only, one per XBase#state (owner ruling 2026-09-27), never the page with its layout chrome; the XBase
|
|
33
|
+
// is --export in fixture mode, --base with --html.
|
|
34
|
+
//
|
|
35
|
+
// Each viewport is loaded from a file:// URL at deviceScaleFactor 2 with prefers-color-scheme <theme>, waits for
|
|
36
|
+
// document.fonts.ready plus SETTLE_MS (modules/models/runtimes.yaml allocation.drawRender.settleMs), and writes <out>/<base>--<w>x<h>--<theme>.png and a .json record beside it:
|
|
37
|
+
// the fonts that loaded, the font families that did not render, page width vs scrollWidth, console errors, failed
|
|
38
|
+
// requests, uncaught page errors and the image sha256. A capture with a missing font, horizontal overflow, an
|
|
39
|
+
// uncaught page error, a failed request or (fixture mode) an empty render is red: exit 1. Usage or environment
|
|
40
|
+
// errors (no Playwright, no Chromium, no esbuild) exit 2. Nothing is ever green by default.
|
|
41
|
+
//
|
|
42
|
+
// Fixture mode renders the named pure export of a product module with the fixture JSON as its props, bundled by the
|
|
43
|
+
// product's own esbuild (so its tsconfig paths, React and CSS imports resolve as in the product). A fixture value
|
|
44
|
+
// "[Function]" becomes a no-op function (the Base's `on` actions). Each --css file is the product's global
|
|
45
|
+
// stylesheet: one that imports tailwindcss is compiled by the product's tailwindcss against the bundle's class
|
|
46
|
+
// candidates, any other is bundled by esbuild.
|
|
47
|
+
//
|
|
48
|
+
// Tailwindcss is the project's own install (package-at.mjs); Playwright and esbuild resolve from the HTML's or component's directory, the working directory, then the runtime's own install (render-tools.mjs).
|
|
49
|
+
import '../api/process/hide-child-windows.mjs';
|
|
50
|
+
import fs from 'node:fs';
|
|
51
|
+
import path from 'node:path';
|
|
52
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
53
|
+
import {sha256} from '../../engine/digest.mjs';
|
|
54
|
+
import { allocationMs } from '../../engine/config.mjs';
|
|
55
|
+
import { findPackage, requirePackage } from '../lib/package-at.mjs';
|
|
56
|
+
import { RENDER_TOOL_UNAVAILABLE, esbuildInstall, playwrightInstall } from './render-tools.mjs';
|
|
57
|
+
import { safeRemove } from '../api/fs/safe-remove.mjs';
|
|
58
|
+
import { artifactHoldReason } from '../machine/artifact-hold.mjs';
|
|
59
|
+
import { eachInOrder, mapInOrder } from '../lib/in-order.mjs';
|
|
60
|
+
import { ACCENT_EXEMPT_SELECTOR } from './draw/draw-taste.mjs';
|
|
61
|
+
import { LAYER_PROBE, measureLayer } from './draw/draw-layer.mjs';
|
|
62
|
+
import { measureInPage } from './draw-render-page.mjs';
|
|
63
|
+
import { MEASURE_SCHEMA, REDLINE_ATTR, REDLINE_LEAF_COMPONENTS, WHY_ATTR, drawRedlines, loadRationale, measureRationale, rationaleFileOf, redlineLabelsOf } from './draw/draw-rationale.mjs';
|
|
64
|
+
import { DRAW_SOURCE_SUFFIX, GRAMMAR_PACKAGE, LAYOUT_ATTR, markLayoutElements, rationaleFileFor, typecheckFindings } from './draw/draw-source.mjs';
|
|
65
|
+
import { grammarDistStatus, grammarDistMessage } from '../gates/grammar-dist.mjs';
|
|
66
|
+
import { PREFERENCES, grammarEntry, resolveDrawGrammar } from './draw-grammar.mjs'; import { isMain } from '../lib/is-main.mjs';
|
|
67
|
+
import { makeTempDir } from '../api/fs/make-temp-dir.mjs';
|
|
68
|
+
|
|
69
|
+
const RECORD_SCHEMA = 'starci/draw-render@1';
|
|
70
|
+
const DEVICE_SCALE_FACTOR = 2;
|
|
71
|
+
export const SETTLE_MS = allocationMs('drawRender.settleMs');
|
|
72
|
+
export const THEMES = Object.freeze(['light', 'dark']);
|
|
73
|
+
/** Root hooks of the grammar components that emitted no data-component before @starci/grammar 0.6.0 (their DNA name for
|
|
74
|
+
* the rendered DOM). 0.6.0 stamps data-component on every renderer root; the map stays for a product still on 0.5.x. */
|
|
75
|
+
const GRAMMAR_ROOT_MARKERS = Object.freeze([['.starci-core-page-container', 'PageContainer'], ['[data-grammar-section-header]', 'SectionHeader'],
|
|
76
|
+
['[data-grammar-surface-card]', 'SurfaceCard'], ['.starci-core-media-frame', 'MediaFrame'], ['.starci-core-surface-copy-group', 'SurfaceCopyGroup'],
|
|
77
|
+
['.starci-core-rank-artwork', 'RankArtwork'], ['[data-grammar-label]', 'Label'], ['.starci-core-horizontal-scroll-region', 'HorizontalScrollRegion'],
|
|
78
|
+
['[data-grammar-scroll-region]', 'VerticalScrollRegion'], ['.starci-core-subnav', 'Subnav'], ['.starci-core-rail', 'Rail'], ['.starci-core-tabs', 'Tabs'],
|
|
79
|
+
['.starci-core-markdown-article', 'MarkdownArticle'], ['[data-grammar-included-mark]', 'IncludedMark'], ['[data-grammar-tooltip]', 'Tooltip']]);
|
|
80
|
+
const FUNCTION_FIXTURE = '[Function]';
|
|
81
|
+
export const EXIT = Object.freeze({ ok: 0, red: 1, usage: 2 });
|
|
82
|
+
const GENERIC_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'ui-serif', 'ui-sans-serif',
|
|
83
|
+
'ui-monospace', 'ui-rounded', 'math', 'emoji', 'fangsong', 'inherit', 'initial', 'unset']);
|
|
84
|
+
/** Family names that are a system-font alias on one platform only: skipped where absent, never missing. */
|
|
85
|
+
const PLATFORM_ALIASES = Object.freeze(['-apple-system', 'blinkmacsystemfont']);
|
|
86
|
+
|
|
87
|
+
class UsageError extends Error {}
|
|
88
|
+
/** Refuse a pinned or selected runtime dist before its JS or CSS can enter a draw bundle. */
|
|
89
|
+
function preflightDrawGrammarDist(packageRoot) {
|
|
90
|
+
const status = grammarDistStatus(packageRoot);
|
|
91
|
+
if (!status.ok) throw new RedError(grammarDistMessage(status), 'DRAW_GRAMMAR_DIST_STALE');
|
|
92
|
+
return status;
|
|
93
|
+
}
|
|
94
|
+
/** A red result before any capture (a draw file that does not type-check): exit 1, with its finding code. */
|
|
95
|
+
class RedError extends Error { constructor(message, code) { super(message); this.code = code; } }
|
|
96
|
+
|
|
97
|
+
const VALUE_FLAGS = new Set(['--html', '--out', '--viewports', '--name', '--theme', '--component', '--export', '--props', '--css', '--state', '--base', '--rationale',
|
|
98
|
+
'--product', '--grammar', '--grammar-dist', '--harness-out']);
|
|
99
|
+
const BOOL_FLAGS = new Set(['--full-page', '--json', '--trace']);
|
|
100
|
+
|
|
101
|
+
/** argv -> options; throws UsageError. */
|
|
102
|
+
export function parseArgs(argv) {
|
|
103
|
+
const o = { css: [], fullPage: false, json: false, theme: 'light' };
|
|
104
|
+
assignArguments(argv, o);
|
|
105
|
+
normalizeCommonArgs(o);
|
|
106
|
+
normalizeDrawName(o);
|
|
107
|
+
return o;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const BOOLEAN_OPTIONS = new Map([['--full-page', 'fullPage'], ['--trace', 'trace']]);
|
|
111
|
+
|
|
112
|
+
function assignArguments(argv, o) {
|
|
113
|
+
for (let i = 0; i < argv.length; i++) {
|
|
114
|
+
const a = argv[i];
|
|
115
|
+
if (BOOL_FLAGS.has(a)) { o[BOOLEAN_OPTIONS.get(a) ?? 'json'] = true; continue; }
|
|
116
|
+
if (!VALUE_FLAGS.has(a)) throw new UsageError(`unknown argument ${a}`);
|
|
117
|
+
const v = argv[++i];
|
|
118
|
+
if (v == null || v.startsWith('--')) throw new UsageError(`${a} needs a value`);
|
|
119
|
+
if (a === '--css') { o.css.push(path.resolve(v)); }
|
|
120
|
+
else { o[a.slice(2)] = v; }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function assertModeArgs(o) {
|
|
125
|
+
const fixture = ['component', 'export', 'props'].filter((k) => o[k]);
|
|
126
|
+
if (o.html && fixture.length) throw new UsageError('--html and --component are exclusive');
|
|
127
|
+
if (!o.html && fixture.length !== 3) throw new UsageError('give --html <file>, or --component <module> --export <XBase> --props <fixture.json>');
|
|
128
|
+
if (o.html && o.css.length) throw new UsageError('--css applies to --component only');
|
|
129
|
+
if (o.html && (o.product || o.grammar || o['grammar-dist'] || o['harness-out'])) throw new UsageError('--product, --grammar, --grammar-dist and --harness-out apply to --component only');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeCommonArgs(o) {
|
|
133
|
+
if (!o.out) throw new UsageError('--out <dir> is required');
|
|
134
|
+
if (!o.viewports) throw new UsageError('--viewports <WxH,...> is required');
|
|
135
|
+
o.viewports = parseViewports(o.viewports);
|
|
136
|
+
if (!THEMES.includes(o.theme)) throw new UsageError(`--theme must be one of ${THEMES.join('|')}`);
|
|
137
|
+
assertModeArgs(o);
|
|
138
|
+
if (o.grammar && !PREFERENCES.includes(o.grammar)) throw new UsageError(`--grammar must be one of ${PREFERENCES.join('|')}`);
|
|
139
|
+
if (o['grammar-dist']) { o.grammarDist = path.resolve(o['grammar-dist']); delete o['grammar-dist']; }
|
|
140
|
+
if (o['harness-out']) { o.harnessOut = path.resolve(o['harness-out']); delete o['harness-out']; }
|
|
141
|
+
if (o.export && !/^[A-Za-z_$][\w$]*$/.test(o.export)) throw new UsageError(`--export ${o.export} is not an identifier`);
|
|
142
|
+
o.mode = o.html ? 'html' : 'component';
|
|
143
|
+
for (const k of ['html', 'component', 'props', 'out', 'rationale', 'product']) {
|
|
144
|
+
if (o[k]) { o[k] = path.resolve(o[k]); }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeDrawName(o) {
|
|
149
|
+
// Owner ruling 2026-09-27: a drawing is the XBase content only, one per XBase#state - `--state <state>` names the
|
|
150
|
+
// capture <XBase>#<state>--<w>x<h>--<theme> (the XBase is --export, or --base for an html render of it).
|
|
151
|
+
if (o.state != null && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(o.state)) throw new UsageError(`--state ${o.state} must be a lowercase slug`);
|
|
152
|
+
if (o.state != null && !o.name) {
|
|
153
|
+
const base = o.base ?? o.export;
|
|
154
|
+
if (!base) throw new UsageError('--state needs the XBase: --export <XBase>, or --base <XBase> with --html');
|
|
155
|
+
o.name = `${base}#${o.state}`;
|
|
156
|
+
}
|
|
157
|
+
o.name ??= o.html ? path.basename(o.html).replace(/\.[^.]+$/, '') : o.export;
|
|
158
|
+
if (!/^[\w.-]+(?:#[a-z0-9-]+)?$/.test(o.name)) throw new UsageError(`--name ${o.name} must be [A-Za-z0-9_.-] (or <XBase>#<state>)`);
|
|
159
|
+
return o;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function parseViewports(text) {
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
const viewports = String(text).split(',').map((s) => s.trim()).filter(Boolean).map((s) => {
|
|
165
|
+
const m = /^(\d{2,5})x(\d{2,5})$/.exec(s);
|
|
166
|
+
if (!m) throw new UsageError(`viewport ${s} is not <width>x<height>`);
|
|
167
|
+
if (seen.has(s)) throw new UsageError(`viewport ${s} is listed twice`);
|
|
168
|
+
seen.add(s);
|
|
169
|
+
return { width: Number(m[1]), height: Number(m[2]) };
|
|
170
|
+
});
|
|
171
|
+
if (!viewports.length) throw new UsageError('--viewports is empty');
|
|
172
|
+
return viewports;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const captureBase = (name, { width, height }, theme) => `${name}--${width}x${height}--${theme}`;
|
|
176
|
+
|
|
177
|
+
const splitFontStack = (stack) => String(stack).split(',').map((f) => f.trim().replace(/^(["'])(.*)\1$/, '$2')).filter(Boolean);
|
|
178
|
+
const isGenericFamily = (family) => GENERIC_FAMILIES.includes(family.toLowerCase());
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Which family each rendered font stack used, and which named families before it did not render.
|
|
182
|
+
* `loaded` are FontFace families with status loaded, `local(family)` answers whether the host has it installed.
|
|
183
|
+
*/
|
|
184
|
+
function resolveFontStacks(stacks, { loaded, local }) {
|
|
185
|
+
const webfonts = new Set(loaded.map((f) => f.toLowerCase()));
|
|
186
|
+
const used = [], missing = new Set();
|
|
187
|
+
for (const stack of stacks) {
|
|
188
|
+
let hit = null;
|
|
189
|
+
for (const family of splitFontStack(stack)) {
|
|
190
|
+
if (isGenericFamily(family)) { hit = { family, source: 'generic' }; break; }
|
|
191
|
+
if (webfonts.has(family.toLowerCase())) { hit = { family, source: 'webfont' }; break; }
|
|
192
|
+
if (local(family)) { hit = { family, source: 'local' }; break; }
|
|
193
|
+
if (!PLATFORM_ALIASES.includes(family.toLowerCase())) missing.add(family);
|
|
194
|
+
}
|
|
195
|
+
used.push({ stack, ...(hit ?? { family: null, source: 'browser-default' }) });
|
|
196
|
+
}
|
|
197
|
+
return { used, missing: [...missing] };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The failure codes of one measured viewport; empty means green. */
|
|
201
|
+
function judgeCapture(m) {
|
|
202
|
+
const failures = [];
|
|
203
|
+
if (m.fonts.missing.length) failures.push('font-missing');
|
|
204
|
+
if (m.layout.horizontalOverflow) failures.push('horizontal-overflow');
|
|
205
|
+
if (m.pageErrors.length) failures.push('page-error');
|
|
206
|
+
if (m.failedRequests.length) failures.push('request-failed');
|
|
207
|
+
if (m.rendered === false) failures.push('empty-render');
|
|
208
|
+
if (m.ownership?.unownedCount) failures.push('off-grammar-dom');
|
|
209
|
+
return failures;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Fixture JSON -> props: every "[Function]" becomes a no-op. */
|
|
213
|
+
const fixtureProps = (value) => {
|
|
214
|
+
if (value === FUNCTION_FIXTURE) { return '__DRAW_NOOP__'; }
|
|
215
|
+
if (Array.isArray(value)) { return value.map(fixtureProps); }
|
|
216
|
+
if (value && typeof value === 'object') { return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, fixtureProps(v)])); }
|
|
217
|
+
return value;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** Candidate class tokens of a bundle for tailwind's build(): everything between quotes and whitespace. */
|
|
221
|
+
const classCandidates = (text) => [...new Set(String(text).split(/[\s"'`\\]+/).filter((t) => t.length > 0 && t.length <= 200))];
|
|
222
|
+
|
|
223
|
+
function rationalePathOf(value) {
|
|
224
|
+
if (typeof value === 'string') { return value; }
|
|
225
|
+
return value?.file ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function dataImageDigest(src) {
|
|
229
|
+
const match = /^data:[^,]*;base64,(.*)$/s.exec(src);
|
|
230
|
+
return match ? sha256(Buffer.from(match[1], 'base64')) : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function httpImageDigest(src, page) {
|
|
234
|
+
const response = await page.request.get(src);
|
|
235
|
+
return response.ok() ? sha256(await response.body()) : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* --------------------------------------------------------------- the page */
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
/* -------------------------------------------------------------- capture */
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The sha256 of the bytes each measured <img> shows (file:, data: or http(s): source; null when unreadable), so the
|
|
246
|
+
* palette gate matches the image against the brand's registered artwork masters by content, never by name.
|
|
247
|
+
*/
|
|
248
|
+
async function artworkDigests(images, page = null) {
|
|
249
|
+
const cache = new Map();
|
|
250
|
+
const out = [];
|
|
251
|
+
await eachInOrder(Array.isArray(images) ? images : [], async (a) => {
|
|
252
|
+
const src = String(a?.src ?? '');
|
|
253
|
+
if (!cache.has(src)) {
|
|
254
|
+
let digest = null;
|
|
255
|
+
try {
|
|
256
|
+
if (src.startsWith('file:')) digest = sha256(fs.readFileSync(fileURLToPath(src)));
|
|
257
|
+
else if (src.startsWith('data:')) digest = dataImageDigest(src);
|
|
258
|
+
else if (/^https?:/i.test(src) && page?.request) digest = await httpImageDigest(src, page);
|
|
259
|
+
} catch { digest = null; }
|
|
260
|
+
cache.set(src, digest);
|
|
261
|
+
}
|
|
262
|
+
out.push({ ...a, src: src.startsWith('data:') ? 'data:' : src, sha256: cache.get(src) });
|
|
263
|
+
});
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function loadPlaywright(dirs, { runtime } = {}) {
|
|
268
|
+
const found = playwrightInstall(dirs, runtime === undefined ? {} : { runtime });
|
|
269
|
+
if (!found) throw new UsageError(`${RENDER_TOOL_UNAVAILABLE}: no playwright install resolvable from ${dirs.filter(Boolean).join(', ')} or the runtime`);
|
|
270
|
+
const pw = requirePackage(found);
|
|
271
|
+
if (!pw.chromium) throw new UsageError(`${found.name} at ${found.root} exports no chromium`);
|
|
272
|
+
return { chromium: pw.chromium, name: found.name, version: found.version, root: found.root };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function captureViewport(browser, { url, viewport, theme, fullPage, file, traceFile = null, rationale = null }) {
|
|
276
|
+
const context = await browser.newContext({ viewport, deviceScaleFactor: DEVICE_SCALE_FACTOR, colorScheme: theme });
|
|
277
|
+
// A trace never decides a capture: a tracing failure only leaves the record without one.
|
|
278
|
+
let tracing = false;
|
|
279
|
+
if (traceFile) { try { await context.tracing.start({ screenshots: true, snapshots: true }); tracing = true; } catch { tracing = false; } }
|
|
280
|
+
try {
|
|
281
|
+
const page = await context.newPage();
|
|
282
|
+
const consoleErrors = [], pageErrors = [], failedRequests = [];
|
|
283
|
+
page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()); });
|
|
284
|
+
page.on('pageerror', (err) => pageErrors.push(String(err?.message ?? err)));
|
|
285
|
+
page.on('requestfailed', (req) => failedRequests.push(`${req.url()} ${req.failure()?.errorText ?? ''}`.trim()));
|
|
286
|
+
page.on('response', (res) => { if (res.status() >= 400) failedRequests.push(`${res.url()} HTTP ${res.status()}`); });
|
|
287
|
+
await page.goto(url, { waitUntil: 'load' });
|
|
288
|
+
await page.evaluate(() => document.fonts.ready.then(() => undefined));
|
|
289
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
290
|
+
await page.evaluate(() => document.fonts.ready.then(() => undefined));
|
|
291
|
+
// A real grammar drawing cannot put data-why on a grammar component (its props are closed): each rationale decision
|
|
292
|
+
// binds by its selector, stamped onto the rendered DOM before it is measured, redlined and snapshotted.
|
|
293
|
+
await page.evaluate(({ entries, whyAttr, markers }) => {
|
|
294
|
+
if (document.documentElement.dataset.drawHarness !== 'component') return;
|
|
295
|
+
// The DNA name of every grammar root, for the html-reading gates, the redline and the snapshot: data-component
|
|
296
|
+
// where the grammar emits it (every root since grammar 0.6.0), else - a 0.5.x install - the root hook of the
|
|
297
|
+
// components that emitted none (PageContainer, SectionHeader, SurfaceCard, MediaFrame, ...).
|
|
298
|
+
for (const el of document.querySelectorAll('[data-component]')) if (!('grammarComponent' in el.dataset)) el.dataset.grammarComponent = el.dataset.component;
|
|
299
|
+
for (const [selector, name] of markers) for (const el of document.querySelectorAll(selector)) if (!('grammarComponent' in el.dataset)) el.dataset.grammarComponent = name;
|
|
300
|
+
for (const e of entries) {
|
|
301
|
+
let els = [];
|
|
302
|
+
try { els = [...document.querySelectorAll(e.selector)]; } catch { els = []; }
|
|
303
|
+
for (const el of els) el.setAttribute(whyAttr, [...new Set([...(el.getAttribute(whyAttr) ?? '').split(/\s+/).filter(Boolean), e.id])].join(' '));
|
|
304
|
+
}
|
|
305
|
+
}, { entries: (rationale?.entries ?? []).filter((e) => typeof e?.selector === 'string' && typeof e?.id === 'string').map((e) => ({ id: e.id, selector: e.selector })), whyAttr: WHY_ATTR, markers: GRAMMAR_ROOT_MARKERS });
|
|
306
|
+
const raw = await measureInPage(page, { generic: GENERIC_FAMILIES, exemptSelector: ACCENT_EXEMPT_SELECTOR, layoutAttr: LAYOUT_ATTR });
|
|
307
|
+
const artwork = await artworkDigests(raw.artwork ?? [], page);
|
|
308
|
+
// The form regions' rendered widths (draw-layer.mjs DRAW_MEASURE_UNCAPPED, MEASURE-4 case-3/case-4), before the
|
|
309
|
+
// full-page unsticking below moves anything.
|
|
310
|
+
let layer = null;
|
|
311
|
+
try { layer = await page.evaluate(measureLayer, LAYER_PROBE); } catch (error) { layer = { error: String(error?.message ?? error).split(/\r?\n/)[0] }; }
|
|
312
|
+
let domFile = null;
|
|
313
|
+
if (raw.dom) { domFile = file.replace(/\.png$/i, '.dom.html'); fs.writeFileSync(domFile, raw.dom); }
|
|
314
|
+
// A full-page image grows the viewport past the fold, so a sticky bar pinned to the bottom edge would be painted
|
|
315
|
+
// over the middle of the page: once measured (above), it is shown in its flow position, at the end of its region.
|
|
316
|
+
if (fullPage) await page.evaluate(() => { for (const el of document.querySelectorAll('body *')) if (getComputedStyle(el).position === 'sticky') el.style.setProperty('position', 'static', 'important'); });
|
|
317
|
+
const png = await page.screenshot({ path: file, fullPage, animations: 'disabled', caret: 'hide' });
|
|
318
|
+
// What the render uses (draw-rationale.mjs, owner ruling 2026-09-27): every distinct gap/padding/inset, radius and
|
|
319
|
+
// type value, the colours, grids, regions and art - measured, never self-reported. Then the redline: the same
|
|
320
|
+
// page annotated (spacing brackets with value and rule id, DNA labels), captured beside the part.
|
|
321
|
+
const tokens = [...new Set((rationale?.entries ?? []).filter((e) => e?.kind === 'colour').flatMap((e) => `${e.decision ?? ''} ${e.value ?? ''}`.match(/--[A-Za-z0-9-]+/g) ?? []))];
|
|
322
|
+
let measure = null;
|
|
323
|
+
try { measure = await page.evaluate(measureRationale, { tokens, whyAttr: WHY_ATTR, redlineAttr: REDLINE_ATTR, schema: MEASURE_SCHEMA }); } catch (error) { measure = { error: String(error?.message ?? error).split('\n')[0] }; }
|
|
324
|
+
let redline = null;
|
|
325
|
+
if (rationale?.entries?.length) {
|
|
326
|
+
const redlineFile = file.replace(/\.png$/i, '.redline.png');
|
|
327
|
+
try {
|
|
328
|
+
await page.evaluate(drawRedlines, { labels: redlineLabelsOf(rationale.entries), whyAttr: WHY_ATTR, redlineAttr: REDLINE_ATTR, leaf: REDLINE_LEAF_COMPONENTS });
|
|
329
|
+
const bytes = await page.screenshot({ path: redlineFile, fullPage, animations: 'disabled', caret: 'hide' });
|
|
330
|
+
redline = { path: redlineFile, sha256: sha256(bytes), rationale: rationale.file ?? null };
|
|
331
|
+
} catch (error) { redline = { error: String(error?.message ?? error).split('\n')[0] }; }
|
|
332
|
+
}
|
|
333
|
+
const localSet = new Set(raw.local.map((f) => f.toLowerCase()));
|
|
334
|
+
const loadedFaces = raw.faces.filter((f) => f.status === 'loaded');
|
|
335
|
+
const resolved = resolveFontStacks(raw.stacks, { loaded: loadedFaces.map((f) => f.family), local: (f) => localSet.has(f.toLowerCase()) });
|
|
336
|
+
const errored = raw.faces.filter((f) => f.status === 'error').map((f) => `${f.family} ${f.weight} ${f.style}`);
|
|
337
|
+
return {
|
|
338
|
+
fonts: {
|
|
339
|
+
loaded: loadedFaces.map((f) => `${f.family} ${f.weight} ${f.style}`),
|
|
340
|
+
used: resolved.used,
|
|
341
|
+
local: raw.local.filter((f) => !loadedFaces.some((face) => face.family.toLowerCase() === f.toLowerCase())),
|
|
342
|
+
missing: [...resolved.missing, ...errored],
|
|
343
|
+
},
|
|
344
|
+
layout: { pageWidth: raw.pageWidth, innerWidth: raw.innerWidth, scrollWidth: raw.scrollWidth, documentHeight: raw.documentHeight,
|
|
345
|
+
horizontalOverflow: raw.scrollWidth > raw.pageWidth, overflowing: raw.overflowing, accentExempt: raw.accentExempt ?? [], artwork },
|
|
346
|
+
consoleErrors, pageErrors, failedRequests,
|
|
347
|
+
anatomy: raw.anatomy ?? { alerts: [], meters: [] },
|
|
348
|
+
layer,
|
|
349
|
+
...(raw.ownership ? { ownership: raw.ownership } : {}),
|
|
350
|
+
...(domFile ? { dom: { path: domFile } } : {}),
|
|
351
|
+
rendered: raw.rendered,
|
|
352
|
+
image: { sha256: sha256(png), bytes: png.length },
|
|
353
|
+
rationale: measure,
|
|
354
|
+
redline,
|
|
355
|
+
};
|
|
356
|
+
} finally {
|
|
357
|
+
if (tracing) { try { await context.tracing.stop({ path: traceFile }); } catch { /* the capture stands without its trace */ } }
|
|
358
|
+
await context.close();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Capture every viewport of one HTML file; writes the PNGs and records, returns the records. */
|
|
363
|
+
export async function captureHtml({ html, out, viewports, theme, fullPage, name, source, playwright, trace = false, rationale = undefined }) {
|
|
364
|
+
fs.mkdirSync(out, { recursive: true });
|
|
365
|
+
// The rationale beside the source (draw-rationale.mjs rationaleFileOf) labels the redline; an explicit one wins.
|
|
366
|
+
const rationaleFile = rationale === undefined ? rationaleFileOf(html) : rationalePathOf(rationale);
|
|
367
|
+
const why = rationaleFile ? { file: rationaleFile, entries: loadRationale(rationaleFile).entries } : null;
|
|
368
|
+
const browser = await playwright.chromium.launch().catch((e) => { throw new UsageError(`${RENDER_TOOL_UNAVAILABLE}: chromium launch failed (${playwright.name} ${playwright.version}): ${e.message.split('\n')[0]}`); });
|
|
369
|
+
const records = [];
|
|
370
|
+
try {
|
|
371
|
+
await eachInOrder(viewports, async (viewport) => {
|
|
372
|
+
const base = captureBase(name, viewport, theme);
|
|
373
|
+
const file = path.join(out, `${base}.png`);
|
|
374
|
+
const traceFile = trace ? path.join(out, `${base}.trace.zip`) : null;
|
|
375
|
+
const m = await captureViewport(browser, { url: pathToFileURL(html).href, viewport, theme, fullPage, file, traceFile, rationale: why });
|
|
376
|
+
const failures = judgeCapture(m);
|
|
377
|
+
const { rendered, image, rationale: measure, redline, ...measured } = m;
|
|
378
|
+
const record = {
|
|
379
|
+
schema: RECORD_SCHEMA,
|
|
380
|
+
ok: failures.length === 0,
|
|
381
|
+
failures,
|
|
382
|
+
source,
|
|
383
|
+
viewport: { ...viewport, deviceScaleFactor: DEVICE_SCALE_FACTOR },
|
|
384
|
+
theme,
|
|
385
|
+
fullPage,
|
|
386
|
+
image: { path: file, ...image },
|
|
387
|
+
...measured,
|
|
388
|
+
...(rendered === null ? {} : { rendered }),
|
|
389
|
+
...(measure ? { rationale: measure } : {}),
|
|
390
|
+
...(redline ? { redline } : {}),
|
|
391
|
+
tool: { playwright: `${playwright.name}@${playwright.version}`, settleMs: SETTLE_MS },
|
|
392
|
+
...(traceFile && fs.existsSync(traceFile) ? { trace: { path: traceFile } } : {}),
|
|
393
|
+
};
|
|
394
|
+
fs.writeFileSync(path.join(out, `${base}.json`), `${JSON.stringify(record, null, 2)}\n`);
|
|
395
|
+
records.push(record);
|
|
396
|
+
});
|
|
397
|
+
} finally {
|
|
398
|
+
await browser.close();
|
|
399
|
+
}
|
|
400
|
+
return records;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* -------------------------------------------------------- fixture mode */
|
|
404
|
+
|
|
405
|
+
/** A package's root found by walking node_modules up from `dir` - for a package whose exports hide its package.json (a workspace package such as @acme/ui). */
|
|
406
|
+
const packageDirUp = (dir, name) => {
|
|
407
|
+
for (let d = path.resolve(dir); ; d = path.dirname(d)) {
|
|
408
|
+
const root = path.join(d, 'node_modules', ...name.split('/'));
|
|
409
|
+
if (fs.existsSync(path.join(root, 'package.json'))) return { root: fs.realpathSync(root), packageFile: path.join(fs.realpathSync(root), 'package.json') };
|
|
410
|
+
if (path.dirname(d) === d) return null;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const cssFileOf = (id, base, aliases = {}) => {
|
|
415
|
+
if (id.startsWith('.') || path.isAbsolute(id)) return path.resolve(base, id);
|
|
416
|
+
const [scope, rest] = id.startsWith('@') ? [id.split('/').slice(0, 2).join('/'), id.split('/').slice(2).join('/')] : [id.split('/')[0], id.split('/').slice(1).join('/')];
|
|
417
|
+
// A draw-time alias (the claude-dist grammar) wins over the product's install, for the CSS exactly as for the JS.
|
|
418
|
+
const found = aliases[scope] ? { root: aliases[scope], packageFile: path.join(aliases[scope], 'package.json') } : (findPackage([base], [scope]) ?? packageDirUp(base, scope));
|
|
419
|
+
if (!found) throw new UsageError(`stylesheet ${id} does not resolve from ${base}`);
|
|
420
|
+
const manifest = JSON.parse(fs.readFileSync(found.packageFile, 'utf8'));
|
|
421
|
+
if (!rest) {
|
|
422
|
+
const entry = manifest.exports?.['.']?.style ?? manifest.style;
|
|
423
|
+
if (!entry) throw new UsageError(`${scope} has no style entry`);
|
|
424
|
+
return path.join(found.root, entry);
|
|
425
|
+
}
|
|
426
|
+
const mapped = manifest.exports?.[`./${rest}`];
|
|
427
|
+
const target = typeof mapped === 'string' ? mapped : mapped?.style ?? mapped?.default ?? `./${rest}`;
|
|
428
|
+
return path.join(found.root, target);
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
async function compileStylesheet(file, { dirs, esbuild, candidates, workDir, index, aliases = {} }) {
|
|
432
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
433
|
+
if (/@import\s+["']tailwindcss(?:\/[^"']*)?["']|@tailwind\b/.test(text)) {
|
|
434
|
+
const found = findPackage([path.dirname(file), ...dirs], ['tailwindcss']);
|
|
435
|
+
if (!found) throw new UsageError(`${file} imports tailwindcss, which does not resolve from ${path.dirname(file)}`);
|
|
436
|
+
const tw = requirePackage(found);
|
|
437
|
+
const compiler = await tw.compile(text, {
|
|
438
|
+
base: path.dirname(file),
|
|
439
|
+
from: file,
|
|
440
|
+
loadStylesheet: async (id, base) => { const p = cssFileOf(id, base, aliases); return { path: p, base: path.dirname(p), content: fs.readFileSync(p, 'utf8') }; },
|
|
441
|
+
loadModule: async (id) => { throw new UsageError(`${file}: tailwind module ${id} (@plugin/@config) is not supported by draw-render`); },
|
|
442
|
+
});
|
|
443
|
+
return compiler.build(candidates);
|
|
444
|
+
}
|
|
445
|
+
const outfile = path.join(workDir, `global-${index}.css`);
|
|
446
|
+
await esbuild.build({ entryPoints: [file], bundle: true, outfile, logLevel: 'silent', loader: ASSET_LOADERS,
|
|
447
|
+
...(aliases[GRAMMAR_PACKAGE] ? { alias: { [GRAMMAR_PACKAGE]: aliases[GRAMMAR_PACKAGE] } } : {}) });
|
|
448
|
+
return fs.readFileSync(outfile, 'utf8');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const ASSET_LOADERS = Object.freeze({ '.png': 'file', '.jpg': 'file', '.jpeg': 'file', '.gif': 'file', '.webp': 'file', '.svg': 'file',
|
|
452
|
+
'.woff': 'file', '.woff2': 'file', '.ttf': 'file', '.otf': 'file' });
|
|
453
|
+
|
|
454
|
+
const inside = (file, dir) => { if (!file || !dir) { return false; } const rel = path.relative(dir, file); return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel)); };
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The esbuild plugin of a real grammar drawing: @starci/grammar[/<sub>] resolves into `grammarRoot` (the chosen
|
|
458
|
+
* grammar, product install or claude-dist), and every other bare import made from outside the product (the draw file,
|
|
459
|
+
* the harness, the claude-dist grammar) resolves from the product app dir - one React, one HeroUI, the product's own.
|
|
460
|
+
* A .draw.tsx is loaded with its layout elements stamped data-draw-layout (rendered-DOM ownership).
|
|
461
|
+
*/
|
|
462
|
+
const drawResolvePlugin = ({ productDir, grammarRoot }) => ({
|
|
463
|
+
name: 'starci-draw-resolve',
|
|
464
|
+
setup(build) {
|
|
465
|
+
if (grammarRoot) build.onResolve({ filter: /^@starci\/grammar(?:\/.*)?$/ }, (args) => {
|
|
466
|
+
const target = grammarEntry(grammarRoot, args.path.slice(GRAMMAR_PACKAGE.length).replace(/^\//, ''));
|
|
467
|
+
return fs.existsSync(target) ? { path: target } : { errors: [{ text: `${args.path} does not resolve inside ${grammarRoot}` }] };
|
|
468
|
+
});
|
|
469
|
+
if (productDir) build.onResolve({ filter: /^[^./]/ }, async (args) => {
|
|
470
|
+
if (args.pluginData?.starciDraw || path.isAbsolute(args.path)) return undefined;
|
|
471
|
+
const fromProduct = inside(args.importer, productDir) && !(grammarRoot && inside(args.importer, grammarRoot));
|
|
472
|
+
if (fromProduct) return undefined;
|
|
473
|
+
const r = await build.resolve(args.path, { kind: args.kind, resolveDir: productDir, pluginData: { starciDraw: true } });
|
|
474
|
+
return r.errors.length ? undefined : { path: r.path, external: r.external, sideEffects: r.sideEffects, namespace: r.namespace };
|
|
475
|
+
});
|
|
476
|
+
build.onLoad({ filter: /\.draw\.tsx$/ }, (args) => ({ contents: markLayoutElements(fs.readFileSync(args.path, 'utf8'), { file: args.path }), loader: 'tsx', resolveDir: path.dirname(args.path) }));
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
/** The product app dir of a draw file: the nearest package.json above it that depends on @starci/grammar. */
|
|
481
|
+
function productDirOf(file) {
|
|
482
|
+
for (let d = path.dirname(path.resolve(file)); ; d = path.dirname(d)) {
|
|
483
|
+
try {
|
|
484
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(d, 'package.json'), 'utf8'));
|
|
485
|
+
if (pkg?.dependencies?.[GRAMMAR_PACKAGE] || pkg?.devDependencies?.[GRAMMAR_PACKAGE]) return d;
|
|
486
|
+
} catch { /* no manifest here */ }
|
|
487
|
+
if (path.dirname(d) === d) return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** The render provenance of the grammar a drawing rendered against (owner ruling 2026-09-27). */
|
|
492
|
+
function grammarProvenance(grammar) {
|
|
493
|
+
return { grammarSource: grammar.grammarSource, source: grammar.pick?.source ?? null, version: grammar.pick?.version ?? null, root: grammar.pick?.root ?? null,
|
|
494
|
+
productVersion: grammar.productVersion ?? null, productRange: grammar.productRange ?? null, upgradeOwed: grammar.upgradeOwed ?? null,
|
|
495
|
+
typecheck: (grammar.attempts ?? []).map((a) => ({ source: a.source, version: a.version, ok: a.ok, errors: a.errors.length })) };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Bundle a pure export with fixture props into <workDir>/index.html; returns that path and the source record.
|
|
500
|
+
* `productDir` resolves every bare import (drawResolvePlugin); `grammar` is the draw-grammar.mjs resolution whose
|
|
501
|
+
* pick the bundle and the CSS alias to, recorded as source.grammar.
|
|
502
|
+
*/
|
|
503
|
+
export async function buildFixtureHarness({ component, exportName, props, css, theme, workDir, cwd = process.cwd(), productDir = null, grammar = null }) {
|
|
504
|
+
for (const f of [component, props, ...css]) if (!fs.existsSync(f)) throw new UsageError(`${f} does not exist`);
|
|
505
|
+
let fixture;
|
|
506
|
+
try { fixture = JSON.parse(fs.readFileSync(props, 'utf8')); } catch (e) { throw new UsageError(`${props}: ${e.message}`); }
|
|
507
|
+
if (!fixture || typeof fixture !== 'object' || Array.isArray(fixture)) throw new UsageError(`${props} must hold the props object`);
|
|
508
|
+
const dirs = [...(productDir ? [productDir] : []), path.dirname(component), cwd];
|
|
509
|
+
const grammarRoot = grammar?.pick?.root ?? null;
|
|
510
|
+
const aliases = grammarRoot && grammar?.pick?.source !== 'product' ? { [GRAMMAR_PACKAGE]: grammarRoot } : {};
|
|
511
|
+
const found = esbuildInstall(dirs);
|
|
512
|
+
if (!found) throw new UsageError(`${RENDER_TOOL_UNAVAILABLE}: no esbuild resolvable from ${dirs.join(', ')} or the runtime`);
|
|
513
|
+
const esbuild = requirePackage(found);
|
|
514
|
+
const entry = [
|
|
515
|
+
"import * as React from 'react';",
|
|
516
|
+
"import { flushSync } from 'react-dom';",
|
|
517
|
+
"import { createRoot } from 'react-dom/client';",
|
|
518
|
+
`import { ${exportName} as Base } from ${JSON.stringify(component.split(path.sep).join('/'))};`,
|
|
519
|
+
'const noop = () => {};',
|
|
520
|
+
`const revive = (v) => v === '__DRAW_NOOP__' ? noop : Array.isArray(v) ? v.map(revive) : v && typeof v === 'object' ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, revive(x)])) : v;`,
|
|
521
|
+
`const props = revive(${JSON.stringify(fixtureProps(fixture))});`,
|
|
522
|
+
`if (typeof Base !== 'function' && !(Base && typeof Base === 'object')) throw new Error(${JSON.stringify(String(exportName) + ' is not a component export')});`,
|
|
523
|
+
"const root = createRoot(document.getElementById('root'));",
|
|
524
|
+
'flushSync(() => root.render(React.createElement(Base, props)));',
|
|
525
|
+
].join('\n');
|
|
526
|
+
try {
|
|
527
|
+
await esbuild.build({
|
|
528
|
+
stdin: { contents: entry, resolveDir: path.dirname(component), loader: 'tsx', sourcefile: 'draw-harness.tsx' },
|
|
529
|
+
bundle: true, format: 'iife', platform: 'browser', jsx: 'automatic', outdir: workDir, entryNames: 'harness',
|
|
530
|
+
assetNames: 'assets/[name]-[hash]', loader: ASSET_LOADERS, logLevel: 'silent',
|
|
531
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
532
|
+
...(productDir || grammarRoot || component.endsWith(DRAW_SOURCE_SUFFIX) ? { plugins: [drawResolvePlugin({ productDir, grammarRoot })] } : {}),
|
|
533
|
+
});
|
|
534
|
+
} catch (e) {
|
|
535
|
+
throw new UsageError(`esbuild could not bundle ${exportName} from ${component}: ${(e.errors ?? []).slice(0, 5).map((x) => (x.location?.file ?? '') + ':' + (x.location?.line ?? '') + ' ' + x.text).join('; ') || e.message}`);
|
|
536
|
+
}
|
|
537
|
+
const js = fs.readFileSync(path.join(workDir, 'harness.js'), 'utf8');
|
|
538
|
+
const candidates = classCandidates(js);
|
|
539
|
+
const globals = await mapInOrder(css.entries(), ([index, file]) => compileStylesheet(file, { dirs, esbuild, candidates, workDir, index, aliases }));
|
|
540
|
+
fs.writeFileSync(path.join(workDir, 'global.css'), globals.join('\n'));
|
|
541
|
+
const links = ['global.css', ...(fs.existsSync(path.join(workDir, 'harness.css')) ? ['harness.css'] : [])].map((h) => `<link rel="stylesheet" href="${h}">`).join('');
|
|
542
|
+
const html = path.join(workDir, 'index.html');
|
|
543
|
+
fs.writeFileSync(html, `<!doctype html><html lang="en" class="${theme}" data-theme="${theme}" data-draw-harness="component" style="color-scheme:${theme}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${links}</head><body><div id="root"></div><script src="harness.js"></script></body></html>\n`);
|
|
544
|
+
const digest = (f) => ({ path: f, sha256: sha256(fs.readFileSync(f)) });
|
|
545
|
+
return { html, source: { mode: 'component', component: digest(component), export: exportName, props: digest(props), css: css.map(digest), esbuild: found.version,
|
|
546
|
+
...(productDir ? { product: productDir } : {}), ...(grammar ? { grammar: grammarProvenance(grammar) } : {}) } };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/* ------------------------------------------------------------------ main */
|
|
550
|
+
|
|
551
|
+
export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
552
|
+
const o = parseArgs(argv);
|
|
553
|
+
if (o.html && !fs.existsSync(o.html)) throw new UsageError(`${o.html} does not exist`);
|
|
554
|
+
const anchor = o.html ? path.dirname(o.html) : path.dirname(o.component);
|
|
555
|
+
// The product app of a drawing that lives outside it (a ui record dir, a temp dir): --product, else the nearest
|
|
556
|
+
// grammar-depending package above the draw file, else above one of its --css stylesheets (the product's own).
|
|
557
|
+
const inferred = o.component ? (o.product ?? productDirOf(o.component) ?? (o.css ?? []).flat().map(productDirOf).find(Boolean) ?? null) : null;
|
|
558
|
+
const playwright = loadPlaywright([anchor, ...(inferred ? [inferred] : []), cwd]);
|
|
559
|
+
if (o.mode === 'html') {
|
|
560
|
+
return captureHtmlPage(o, playwright);
|
|
561
|
+
}
|
|
562
|
+
return captureComponent(o, { cwd, inferred, playwright });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function captureHtmlPage(options, playwright) {
|
|
566
|
+
const source = { mode: 'html', html: { path: options.html, sha256: sha256(fs.readFileSync(options.html)) } };
|
|
567
|
+
return captureHtml({ ...options, source, playwright, rationale: options.rationale ?? undefined });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function captureComponent(options, { cwd, inferred, playwright }) {
|
|
571
|
+
// A real grammar drawing: the product app it renders in, and the grammar it type-checks against (draw-grammar.mjs).
|
|
572
|
+
const drawing = options.component.endsWith(DRAW_SOURCE_SUFFIX) || Boolean(options.product || options.grammar || options.grammarDist);
|
|
573
|
+
const productDir = drawing ? inferred : null;
|
|
574
|
+
if (drawing && !productDir) throw new UsageError(`${options.component}: give --product <app dir> (no package.json depending on ${GRAMMAR_PACKAGE} above it or above a --css file)`);
|
|
575
|
+
let grammar = null;
|
|
576
|
+
if (drawing) {
|
|
577
|
+
if (options.grammarDist) preflightDrawGrammarDist(options.grammarDist);
|
|
578
|
+
grammar = resolveDrawGrammar({ file: options.component, productDir, prefer: options.grammar ?? 'auto', grammarDist: options.grammarDist ?? null });
|
|
579
|
+
if (grammar.pick?.source === 'claude-dist') preflightDrawGrammarDist(grammar.pick.root);
|
|
580
|
+
if (!grammar.ok) throw new RedError(typecheckFindings(grammar.attempts.at(-1) ?? { ok: false, errors: [{ message: grammar.error }] }, { label: path.basename(options.component) })[0]?.detail ?? grammar.error, 'DRAW_TYPECHECK_FAILED');
|
|
581
|
+
}
|
|
582
|
+
const workDir = makeTempDir('starci-draw-render-');
|
|
583
|
+
try {
|
|
584
|
+
const { html, source } = await buildFixtureHarness({ component: options.component, exportName: options.export, props: options.props, css: options.css, theme: options.theme, workDir, cwd, productDir, grammar });
|
|
585
|
+
// A drawing's rationale sits beside its draw source (draw-source.mjs rationaleFileFor), never beside the harness.
|
|
586
|
+
const records = await captureHtml({ ...options, html, source, playwright, rationale: options.rationale ?? (drawing ? rationaleFileFor(options.component) : null) });
|
|
587
|
+
if (options.harnessOut) { fs.mkdirSync(options.harnessOut, { recursive: true }); fs.cpSync(workDir, options.harnessOut, { recursive: true }); }
|
|
588
|
+
return records;
|
|
589
|
+
} finally {
|
|
590
|
+
safeRemove(workDir, { hold: artifactHoldReason });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function renderErrorMessage(error, usage) {
|
|
595
|
+
if (!(usage || error instanceof RedError)) { return error.stack; }
|
|
596
|
+
if (error.code) { return '[' + error.code + '] ' + error.message; }
|
|
597
|
+
return error.message;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function reportRecords(records, json) {
|
|
601
|
+
const ok = records.every((record) => record.ok);
|
|
602
|
+
if (json) { process.stdout.write(`${JSON.stringify({ ok, records }, null, 2)}\n`); }
|
|
603
|
+
else {
|
|
604
|
+
for (const record of records) { process.stdout.write(`${record.ok ? 'ok ' : 'RED '} ${record.image.path}${record.failures.length ? ' ' + record.failures.join(', ') : ''}\n`); }
|
|
605
|
+
}
|
|
606
|
+
process.exitCode = ok ? EXIT.ok : EXIT.red;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function reportError(error, json) {
|
|
610
|
+
const usage = error instanceof UsageError;
|
|
611
|
+
if (json) { process.stdout.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code } : {}) }, null, 2)}\n`); }
|
|
612
|
+
process.stderr.write('draw-render: ' + renderErrorMessage(error, usage) + '\n');
|
|
613
|
+
process.exitCode = usage ? EXIT.usage : EXIT.red;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function main() {
|
|
617
|
+
const argv = process.argv.slice(2);
|
|
618
|
+
const json = argv.includes('--json');
|
|
619
|
+
try {
|
|
620
|
+
reportRecords(await run(argv), json);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
reportError(error, json);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (isMain(import.meta.url)) await main();
|