@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,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// grammar-proposal.mjs — what a drawing needs that the Grammar's DNA does not have (owner ruling 2026-09-27: "anything
|
|
3
|
+
// DNA lacks becomes a grammar proposal, never invented inline").
|
|
4
|
+
//
|
|
5
|
+
// A drawing that needs a component or variant DNA lacks composes the closest DNA components, marks the spot
|
|
6
|
+
// data-grammar-proposal="<name>" (scripts/work/draw/draw-dna.mjs), and writes the proposal beside its render source or
|
|
7
|
+
// in its draw-loop directory:
|
|
8
|
+
// grammar-proposal.yaml schema starci/grammar-proposal@1, proposals: [{name, gap, anatomy, tokens, claims, render,
|
|
9
|
+
// status?}] - render is the isolated render (an .html/.png path relative to the file, or
|
|
10
|
+
// inline html)
|
|
11
|
+
// grammar-proposal.md one heading per proposal naming it (the name itself, `Meter` variant `segments`, or
|
|
12
|
+
// data-grammar-proposal="Meter.segments"), its section stating the gap in DNA, the anatomy,
|
|
13
|
+
// the tokens, the knowledge rules it claims (rule ids) and an isolated render (an html block
|
|
14
|
+
// or a render path)
|
|
15
|
+
// A proposal missing one of those is no entry: the element that cites it is DRAW_OFF_GRAMMAR_COMPONENT.
|
|
16
|
+
//
|
|
17
|
+
// A proposal is the owner's to accept, never the drawing worker's and never automatic: interface.draw's draw-review
|
|
18
|
+
// ask lists every proposal of the record (question.grammarProposals, their isolated renders among question.assets), the
|
|
19
|
+
// job's settle records one `grammar-proposal-filed` ledger event per proposal (status proposed), and `starci kernel status`
|
|
20
|
+
// lists the workflow's open ones as grammarProposals[] until a grammar lane records `grammar-proposal-resolved`.
|
|
21
|
+
//
|
|
22
|
+
// starci work grammar-proposal list <file|dir>... [--json] every proposal found, complete or not
|
|
23
|
+
// starci work grammar-proposal check <file|dir>... [--json] exit 1 when one is incomplete
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { isMain } from '../lib/is-main.mjs';
|
|
27
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
28
|
+
import { sha256File } from '../../engine/digest.mjs';
|
|
29
|
+
import { openEventItems } from '../lib/event-items.mjs';
|
|
30
|
+
import { isDir, isFile } from './work-io.mjs';
|
|
31
|
+
import { altOf } from '../lib/source-phrases.mjs';
|
|
32
|
+
|
|
33
|
+
export const GRAMMAR_PROPOSAL_FILED = 'grammar-proposal-filed';
|
|
34
|
+
const GRAMMAR_PROPOSAL_RESOLVED = 'grammar-proposal-resolved';
|
|
35
|
+
export const PROPOSAL_FILE_NAMES = Object.freeze(['grammar-proposal.yaml', 'grammar-proposal.yml', 'grammar-proposal.md']);
|
|
36
|
+
const PROPOSAL_FIELDS = Object.freeze(['gap', 'anatomy', 'tokens', 'claims', 'render']);
|
|
37
|
+
/** A proposal is never accepted by the runtime: it stays proposed until the owner, through a grammar lane, decides. */
|
|
38
|
+
export const PROPOSED = 'proposed';
|
|
39
|
+
|
|
40
|
+
/** Section headings inside a proposal, never a proposal's name. */
|
|
41
|
+
const SECTION_WORDS = new Set(['gap', 'why', 'anatomy', 'tokens', 'claims', 'render', 'rules', 'values', 'summary', 'notes', 'rationale', 'a11y', 'accessibility', 'usage', 'example', 'examples']);
|
|
42
|
+
const RULE_ID = /\b[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+\b/;
|
|
43
|
+
const HEADING_DASH_DELIMITER = /(?<!\s)\s+[\u2014\u2013-]\s+/;
|
|
44
|
+
const HEADING_PAREN_DELIMITER = /(?<!\s)\s*\(/;
|
|
45
|
+
const HTML_FENCE_OPENING = /```\s*html/i;
|
|
46
|
+
const LINE_BREAK = /[\r\n\u2028\u2029]/;
|
|
47
|
+
const WHITESPACE = /\s/;
|
|
48
|
+
// A section marker matches in either language: the Vietnamese alternatives are lexicon data
|
|
49
|
+
// (modules/goal/source-phrases.yaml proposal).
|
|
50
|
+
const MD_FIELDS = {
|
|
51
|
+
gap: new RegExp(String.raw`\bgap\b|\bwhy\b|does not (have|publish|describe)|no (dna|component|variant)|lacks?\b|${altOf('proposal.gap')}`, 'i'),
|
|
52
|
+
anatomy: new RegExp(String.raw`\banatomy\b|\bslots?\b|\bparts?\b|${altOf('proposal.anatomy')}`, 'i'),
|
|
53
|
+
tokens: /\btokens?\b|var\(--|--[a-z][\w-]*|\b\d+px\b/i,
|
|
54
|
+
claims: RULE_ID,
|
|
55
|
+
render: /```\s*html|<(div|section|span|article|button|svg)\b|\.(png|html)\b|isolated render/i,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function headingLead(heading) {
|
|
59
|
+
const dash = HEADING_DASH_DELIMITER.exec(heading);
|
|
60
|
+
const paren = HEADING_PAREN_DELIMITER.exec(heading);
|
|
61
|
+
let end = heading.length;
|
|
62
|
+
if (dash) end = dash.index;
|
|
63
|
+
if (paren && paren.index < end) end = paren.index;
|
|
64
|
+
return heading.slice(0, end).trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function markdownHeading(line) {
|
|
68
|
+
let hashCount = 0;
|
|
69
|
+
while (hashCount < 4 && line[hashCount] === '#') hashCount += 1;
|
|
70
|
+
if (hashCount === 0 || !WHITESPACE.test(line[hashCount] ?? '')) return null;
|
|
71
|
+
let contentStart = hashCount;
|
|
72
|
+
while (WHITESPACE.test(line[contentStart] ?? '')) contentStart += 1;
|
|
73
|
+
const content = line.slice(contentStart);
|
|
74
|
+
const { firstText, lastText } = textExtent(content);
|
|
75
|
+
if (firstText < 0) return whitespaceOnlyHeading(line, hashCount);
|
|
76
|
+
if (LINE_BREAK.test(content.slice(firstText, lastText + 1))) return null;
|
|
77
|
+
return { level: hashCount, raw: content.slice(firstText, lastText + 1) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The first and last index of non-whitespace in `content` (both -1 when it has none). */
|
|
81
|
+
function textExtent(content) {
|
|
82
|
+
let firstText = -1;
|
|
83
|
+
let lastText = -1;
|
|
84
|
+
for (const [index, unit] of content.split('').entries()) {
|
|
85
|
+
if (!WHITESPACE.test(unit)) {
|
|
86
|
+
if (firstText < 0) firstText = index;
|
|
87
|
+
lastText = index;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { firstText, lastText };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A heading with no text after its hashes: the last character after the first that is not a line break, if any. */
|
|
94
|
+
function whitespaceOnlyHeading(line, hashCount) {
|
|
95
|
+
const whitespaceTail = line.slice(hashCount);
|
|
96
|
+
for (let index = whitespaceTail.length - 1; index > 0; index -= 1) {
|
|
97
|
+
if (!LINE_BREAK.test(whitespaceTail[index])) return { level: hashCount, raw: whitespaceTail[index] };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function fencedHtml(body) {
|
|
103
|
+
const opening = HTML_FENCE_OPENING.exec(body);
|
|
104
|
+
if (!opening) return null;
|
|
105
|
+
const contentStart = opening.index + opening[0].length;
|
|
106
|
+
const closingIndex = body.indexOf('```', contentStart);
|
|
107
|
+
if (closingIndex < 0) return null;
|
|
108
|
+
return body.slice(contentStart, closingIndex).trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const RENDER_EXTENSIONS = ['.png', '.html'];
|
|
112
|
+
/** The first `<open>file.png|html<close>` span of a body, as {at, text}: a span ends at the first `close` after its `open`. */
|
|
113
|
+
function markedRender(body, open, close) {
|
|
114
|
+
for (let at = body.indexOf(open); at >= 0;) {
|
|
115
|
+
const end = body.indexOf(close, at + 1);
|
|
116
|
+
if (end < 0) return null;
|
|
117
|
+
if (RENDER_EXTENSIONS.some((ext) => end - ext.length >= at + 2 && body.startsWith(ext, end - ext.length))) return { at, text: body.slice(at + 1, end) };
|
|
118
|
+
at = body.indexOf(open, open === close ? end : end + 1);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The render file a proposal body points at, in parentheses or backticks: the earlier of the two. */
|
|
124
|
+
function renderReferenceOf(body) {
|
|
125
|
+
const paren = markedRender(body, '(', ')');
|
|
126
|
+
const tick = markedRender(body, '`', '`');
|
|
127
|
+
if (paren && tick) return (paren.at < tick.at ? paren : tick).text;
|
|
128
|
+
return (paren ?? tick)?.text ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The names a markdown heading declares for its proposal. */
|
|
132
|
+
function headingNames(raw) {
|
|
133
|
+
const names = new Set();
|
|
134
|
+
for (const q of String(raw).matchAll(/data-grammar-proposal=["']([^"']+)["']/g)) names.add(q[1].trim());
|
|
135
|
+
const heading = String(raw).replace(/[`*]/g, '').trim().replace(/^\d+[.)]\s*/, '');
|
|
136
|
+
const variant = /^([A-Z]\w*)\s+(?:variant|slot|part)\s+([A-Za-z][\w-]*)/.exec(heading);
|
|
137
|
+
if (variant) names.add(`${variant[1]}.${variant[2]}`);
|
|
138
|
+
if (!names.size) for (const q of String(raw).matchAll(/`([A-Z]\w*(?:\.[\w-]+)?)`/g)) names.add(q[1]);
|
|
139
|
+
const lead = /^([A-Za-z][\w.-]*)$/.exec(headingLead(heading).split(':')[0].trim());
|
|
140
|
+
if (!names.size && lead && !SECTION_WORDS.has(lead[1].toLowerCase())) names.add(lead[1]);
|
|
141
|
+
return [...names];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function markdownProposals(text, file) {
|
|
145
|
+
const lines = String(text).split(/\r?\n/);
|
|
146
|
+
const heads = [];
|
|
147
|
+
lines.forEach((line, i) => { const heading = markdownHeading(line); if (heading) heads.push({ ...heading, line: i }); });
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const [k, h] of heads.entries()) {
|
|
150
|
+
const names = headingNames(h.raw);
|
|
151
|
+
if (!names.length) continue;
|
|
152
|
+
const end = heads.slice(k + 1).find((n) => n.level <= h.level)?.line ?? lines.length;
|
|
153
|
+
const body = lines.slice(h.line + 1, end).join('\n');
|
|
154
|
+
const fields = Object.fromEntries(Object.entries(MD_FIELDS).map(([f, rx]) => [f, rx.test(body) || (f === 'gap' && rx.test(h.raw))]));
|
|
155
|
+
const missing = PROPOSAL_FIELDS.filter((f) => !fields[f]);
|
|
156
|
+
// A heading whose section states none of the fields (a document title over several proposals) is not a proposal.
|
|
157
|
+
if (missing.length === PROPOSAL_FIELDS.length) continue;
|
|
158
|
+
const gap = /(?:^|\n)#{1,4}\s*gap[^\n]*\n+([^\n#]+)/i.exec(body)?.[1] ?? body.split('\n').find((l) => MD_FIELDS.gap.test(l)) ?? '';
|
|
159
|
+
out.push({ name: names[0], names, file, format: 'md', gap: gap.trim().slice(0, 300), claims: [...new Set([...body.matchAll(new RegExp(RULE_ID.source, 'g'))].map((m) => m[0]))],
|
|
160
|
+
render: fencedHtml(body) ?? renderReferenceOf(body),
|
|
161
|
+
status: PROPOSED, complete: missing.length === 0, missing });
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function yamlProposals(doc, file) {
|
|
167
|
+
let list = [];
|
|
168
|
+
if (Array.isArray(doc?.proposals)) list = doc.proposals;
|
|
169
|
+
else if (doc?.name) list = [doc];
|
|
170
|
+
return list.filter((p) => p && typeof p === 'object' && p.name).map((p) => {
|
|
171
|
+
const missing = PROPOSAL_FIELDS.filter((f) => p[f] == null || p[f] === '' || (Array.isArray(p[f]) && !p[f].length && f !== 'tokens'));
|
|
172
|
+
return { name: String(p.name).trim(), names: [String(p.name).trim()], file, format: 'yaml', gap: String(p.gap ?? '').slice(0, 300),
|
|
173
|
+
claims: Array.isArray(p.claims) ? p.claims.map(String) : [...String(p.claims ?? '').matchAll(new RegExp(RULE_ID.source, 'g'))].map((m) => m[0]),
|
|
174
|
+
render: typeof p.render === 'string' ? p.render : p.render?.path ?? null, status: p.status === PROPOSED || p.status == null ? PROPOSED : String(p.status),
|
|
175
|
+
complete: missing.length === 0, missing };
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Every proposal in the given grammar-proposal files: [{name, names, file, format, gap, claims, render, status, complete, missing}]. */
|
|
180
|
+
export function readProposals(files) {
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const file of files ?? []) {
|
|
183
|
+
let text = '';
|
|
184
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
185
|
+
if (/\.ya?ml$/i.test(file)) {
|
|
186
|
+
let doc = null;
|
|
187
|
+
try { doc = parseYaml(text); } catch { doc = null; }
|
|
188
|
+
out.push(...yamlProposals(doc, file));
|
|
189
|
+
} else out.push(...markdownProposals(text, file));
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function collectProposalFiles(dir, left, out) {
|
|
195
|
+
let entries = [];
|
|
196
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
197
|
+
for (const e of entries) {
|
|
198
|
+
const full = path.join(dir, e.name);
|
|
199
|
+
if (!e.isDirectory()) { if (PROPOSAL_FILE_NAMES.includes(e.name)) out.push(full); }
|
|
200
|
+
else if (left > 0 && !['node_modules', '.git'].includes(e.name)) collectProposalFiles(full, left - 1, out);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The grammar-proposal files under `dir` (at most `depth` levels, never node_modules or .git). */
|
|
205
|
+
export function proposalFilesUnder(dir, depth = 6) {
|
|
206
|
+
const out = [];
|
|
207
|
+
if (isDir(dir)) collectProposalFiles(dir, depth, out);
|
|
208
|
+
else if (isFile(dir) && PROPOSAL_FILE_NAMES.includes(path.basename(dir))) out.push(dir);
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The proposal files among `files` (files or directories). */
|
|
213
|
+
function proposalFilesIn(files) {
|
|
214
|
+
const out = new Map();
|
|
215
|
+
for (const f of files ?? []) for (const p of proposalFilesUnder(f)) out.set(path.resolve(p).toLowerCase(), path.resolve(p));
|
|
216
|
+
return [...out.values()];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The isolated render image of a proposal, when it names a .png that exists. */
|
|
220
|
+
export function proposalImageOf(p) {
|
|
221
|
+
if (!p?.render || !/\.png$/i.test(p.render)) return null;
|
|
222
|
+
const abs = path.resolve(path.dirname(p.file), p.render);
|
|
223
|
+
return isFile(abs) ? abs : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Record one `grammar-proposal-filed` event per proposal the job's files carry, once per (name, file sha256):
|
|
228
|
+
* never an acceptance. Returns the proposals newly recorded. `ledger` is an open ledger (appendEvent).
|
|
229
|
+
*/
|
|
230
|
+
export function recordGrammarProposals(ledger, { job, repo, files, now = Date.now() }) {
|
|
231
|
+
const db = ledger.db;
|
|
232
|
+
const found = readProposals(proposalFilesIn(files));
|
|
233
|
+
const recorded = [];
|
|
234
|
+
for (const p of found) {
|
|
235
|
+
let sha = null;
|
|
236
|
+
try { sha = sha256File(p.file); } catch { sha = null; }
|
|
237
|
+
const rel = path.relative(repo, p.file).replaceAll('\\', '/');
|
|
238
|
+
const seen = db.prepare("SELECT 1 FROM events WHERE workflow_id=? AND kind=? AND json_extract(payload_json,'$.name')=? AND json_extract(payload_json,'$.sha256') IS ? LIMIT 1")
|
|
239
|
+
.get(job.workflow_id, GRAMMAR_PROPOSAL_FILED, p.name, sha);
|
|
240
|
+
if (seen) continue;
|
|
241
|
+
const payload = { name: p.name, file: rel, sha256: sha, jobId: job.job_id, opId: job.op_id, attempt: job.attempt, status: PROPOSED, complete: p.complete,
|
|
242
|
+
...(p.missing.length ? { missing: p.missing } : {}), gap: p.gap, claims: p.claims, render: p.render && p.render.length < 300 ? p.render : null };
|
|
243
|
+
ledger.appendEvent({ workflowId: job.workflow_id, entityType: 'job', entityId: job.job_id, kind: GRAMMAR_PROPOSAL_FILED, createdAt: now, payload });
|
|
244
|
+
recorded.push(payload);
|
|
245
|
+
}
|
|
246
|
+
return recorded;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The workflow's open grammar proposals (filed, not resolved): [{name, file, jobId, opId, filedAt, status, complete}]. */
|
|
250
|
+
export function openGrammarProposals(db, workflowId) {
|
|
251
|
+
return openEventItems(db, workflowId, {
|
|
252
|
+
owedKind: GRAMMAR_PROPOSAL_FILED, resolvedKind: GRAMMAR_PROPOSAL_RESOLVED, keyOf: (p) => p.name,
|
|
253
|
+
item: (p, row) => ({ name: p.name, file: p.file ?? null, jobId: p.jobId ?? null, opId: p.opId ?? null, filedAt: row.created_at, status: PROPOSED, complete: p.complete !== false }),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function main(argv) {
|
|
258
|
+
const [cmd, ...rest] = argv;
|
|
259
|
+
const json = rest.includes('--json');
|
|
260
|
+
const targets = rest.filter((a) => !a.startsWith('--'));
|
|
261
|
+
if (!['list', 'check'].includes(cmd) || !targets.length) {
|
|
262
|
+
process.stderr.write('use: starci work grammar-proposal list|check <file|dir>... [--json]\n');
|
|
263
|
+
return 2;
|
|
264
|
+
}
|
|
265
|
+
const proposals = readProposals(proposalFilesIn(targets.map((t) => path.resolve(t))));
|
|
266
|
+
const incomplete = proposals.filter((p) => !p.complete);
|
|
267
|
+
if (json) process.stdout.write(`${JSON.stringify({ ok: cmd === 'list' || !incomplete.length, proposals }, null, 2)}\n`);
|
|
268
|
+
else {
|
|
269
|
+
const lines = proposals.map((p) => {
|
|
270
|
+
const status = p.complete ? 'ok ' : 'INCOMPLETE';
|
|
271
|
+
const missing = p.missing.length ? ` missing ${p.missing.join(', ')}` : '';
|
|
272
|
+
return `${status} ${p.name} (${path.basename(p.file)})${missing}`;
|
|
273
|
+
}).join('\n') || 'no grammar proposals';
|
|
274
|
+
process.stdout.write(`${lines}\n`);
|
|
275
|
+
}
|
|
276
|
+
return cmd === 'check' && incomplete.length ? 1 : 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isMain(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// png.mjs — the PNG reader and writer the layout-tree tooling composes with.
|
|
2
|
+
//
|
|
3
|
+
// No dependency: `node:zlib` inflates and deflates, and `zlib.crc32` (Node >= 22.2) stamps the chunks. It
|
|
4
|
+
// exists so a composite direction is a deterministic function of its inputs on every host - a consumer
|
|
5
|
+
// install has no sharp, and a native image library would make the same inputs produce different bytes on
|
|
6
|
+
// two machines. What it reads: bit depths 1/2/4/8/16, colour types 0 (greyscale), 2 (truecolour), 3
|
|
7
|
+
// (palette, with tRNS), 4 (greyscale + alpha) and 6 (truecolour + alpha), non-interlaced. Everything else
|
|
8
|
+
// throws `unsupported png: <why>` rather than guessing. Every image is handed back as 8-bit RGBA.
|
|
9
|
+
import zlib from 'node:zlib';
|
|
10
|
+
|
|
11
|
+
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
12
|
+
const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
13
|
+
|
|
14
|
+
const paeth = (a, b, c) => {
|
|
15
|
+
const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
|
16
|
+
if (pa <= pb && pa <= pc) return a;
|
|
17
|
+
return pb <= pc ? b : c;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function pngChunks(buf) {
|
|
21
|
+
let header = null, palette = null, trns = null;
|
|
22
|
+
const idat = [];
|
|
23
|
+
for (let at = 8; at + 8 <= buf.length;) {
|
|
24
|
+
const length = buf.readUInt32BE(at), type = buf.toString('latin1', at + 4, at + 8), start = at + 8;
|
|
25
|
+
if (start + length + 4 > buf.length) throw new Error(`unsupported png: chunk ${type} runs past the end of the file`);
|
|
26
|
+
const body = buf.subarray(start, start + length);
|
|
27
|
+
if (type === 'IHDR') header = { width: body.readUInt32BE(0), height: body.readUInt32BE(4), depth: body[8], colour: body[9], interlace: body[12] };
|
|
28
|
+
else if (type === 'PLTE') palette = body;
|
|
29
|
+
else if (type === 'tRNS') trns = body;
|
|
30
|
+
else if (type === 'IDAT') idat.push(body);
|
|
31
|
+
else if (type === 'IEND') break;
|
|
32
|
+
at = start + length + 4;
|
|
33
|
+
}
|
|
34
|
+
return { header, palette, trns, idat };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sampleAt(line, depth, index) {
|
|
38
|
+
if (depth === 8) return line[index];
|
|
39
|
+
if (depth === 16) return line[index * 2];
|
|
40
|
+
const bit = index * depth, byte = line[bit >> 3], shift = 8 - depth - (bit & 7);
|
|
41
|
+
return (byte >> shift) & ((1 << depth) - 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function unfilterRow(raw, at, line, prior, filter, bpp) {
|
|
45
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
46
|
+
const x = raw[at + 1 + i], a = i >= bpp ? line[i - bpp] : 0, b = prior[i], c = i >= bpp ? prior[i - bpp] : 0;
|
|
47
|
+
let value = x;
|
|
48
|
+
if (filter === 1) value += a;
|
|
49
|
+
else if (filter === 2) value += b;
|
|
50
|
+
else if (filter === 3) value += (a + b) >> 1;
|
|
51
|
+
else if (filter === 4) value += paeth(a, b, c);
|
|
52
|
+
line[i] = value & 0xff;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writePixel(out, offset, sample, colour, scale, palette, trns) {
|
|
57
|
+
if (colour === 3) {
|
|
58
|
+
const index = sample(0);
|
|
59
|
+
out[offset] = palette[index * 3] ?? 0; out[offset + 1] = palette[index * 3 + 1] ?? 0; out[offset + 2] = palette[index * 3 + 2] ?? 0;
|
|
60
|
+
out[offset + 3] = trns && index < trns.length ? trns[index] : 255;
|
|
61
|
+
} else if (colour === 0 || colour === 4) {
|
|
62
|
+
const g = Math.round(sample(0) * scale);
|
|
63
|
+
out[offset] = g; out[offset + 1] = g; out[offset + 2] = g; out[offset + 3] = colour === 4 ? sample(1) : 255;
|
|
64
|
+
} else {
|
|
65
|
+
out[offset] = sample(0); out[offset + 1] = sample(1); out[offset + 2] = sample(2); out[offset + 3] = colour === 6 ? sample(3) : 255;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function decodeRows(raw, header, palette, trns, channels, stride, bpp) {
|
|
70
|
+
const { width, height, depth, colour } = header;
|
|
71
|
+
if (raw.length < (stride + 1) * height) throw new Error('unsupported png: image data shorter than the header declares');
|
|
72
|
+
const out = new Uint8Array(width * height * 4);
|
|
73
|
+
let prior = new Uint8Array(stride);
|
|
74
|
+
const line = new Uint8Array(stride);
|
|
75
|
+
const scale = depth < 8 && colour !== 3 ? 255 / ((1 << depth) - 1) : 1;
|
|
76
|
+
for (let y = 0; y < height; y += 1) {
|
|
77
|
+
const at = y * (stride + 1), filter = raw[at];
|
|
78
|
+
if (filter > 4) throw new Error(`unsupported png: filter type ${filter}`);
|
|
79
|
+
unfilterRow(raw, at, line, prior, filter, bpp);
|
|
80
|
+
const sample = (index) => sampleAt(line, depth, index);
|
|
81
|
+
for (let x = 0; x < width; x += 1) writePixel(out, (y * width + x) * 4, (index) => sample(x * channels + index), colour, scale, palette, trns);
|
|
82
|
+
prior = Uint8Array.from(line);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Decode PNG bytes into {width, height, data: Uint8Array RGBA}. Throws `unsupported png: ...`. */
|
|
88
|
+
export function decodePng(bytes) {
|
|
89
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes ?? []);
|
|
90
|
+
if (buf.length < 8 || !buf.subarray(0, 8).equals(SIGNATURE)) throw new Error('unsupported png: not a PNG signature');
|
|
91
|
+
const { header, palette, trns, idat } = pngChunks(buf);
|
|
92
|
+
if (!header?.width || !header?.height) throw new Error('unsupported png: no usable IHDR');
|
|
93
|
+
const { width, height, depth, colour } = header;
|
|
94
|
+
if (!(colour in CHANNELS)) throw new Error(`unsupported png: colour type ${colour}`);
|
|
95
|
+
if (![1, 2, 4, 8, 16].includes(depth) || ((colour === 2 || colour === 4 || colour === 6) && depth < 8) || (colour === 3 && depth > 8)) throw new Error(`unsupported png: bit depth ${depth} for colour type ${colour}`);
|
|
96
|
+
if (header.interlace !== 0) throw new Error('unsupported png: interlaced (Adam7) images are not read');
|
|
97
|
+
if (colour === 3 && !palette) throw new Error('unsupported png: palette image without PLTE');
|
|
98
|
+
let raw;
|
|
99
|
+
try { raw = zlib.inflateSync(Buffer.concat(idat)); } catch (error) { throw new Error(`unsupported png: image data does not inflate (${error.message})`); }
|
|
100
|
+
const channels = CHANNELS[colour];
|
|
101
|
+
const bitsPerPixel = channels * depth;
|
|
102
|
+
const stride = Math.ceil((width * bitsPerPixel) / 8);
|
|
103
|
+
const bpp = Math.max(1, bitsPerPixel >> 3);
|
|
104
|
+
return { width, height, data: decodeRows(raw, header, palette, trns, channels, stride, bpp) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const chunk = (type, body) => {
|
|
108
|
+
const head = Buffer.alloc(8);
|
|
109
|
+
head.writeUInt32BE(body.length, 0);
|
|
110
|
+
head.write(type, 4, 'latin1');
|
|
111
|
+
const crc = Buffer.alloc(4);
|
|
112
|
+
crc.writeUInt32BE(zlib.crc32(Buffer.concat([Buffer.from(type, 'latin1'), body])) >>> 0, 0);
|
|
113
|
+
return Buffer.concat([head, body, crc]);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Encode {width, height, data RGBA} as PNG: truecolour when fully opaque, else truecolour + alpha. Filter 0, fixed deflate level. */
|
|
117
|
+
export function encodePng({ width, height, data }) {
|
|
118
|
+
let opaque = true;
|
|
119
|
+
for (let i = 3; i < data.length; i += 4) if (data[i] !== 255) { opaque = false; break; }
|
|
120
|
+
const channels = opaque ? 3 : 4, stride = width * channels;
|
|
121
|
+
const raw = Buffer.alloc((stride + 1) * height);
|
|
122
|
+
for (let y = 0; y < height; y += 1) {
|
|
123
|
+
const row = y * (stride + 1);
|
|
124
|
+
raw[row] = 0;
|
|
125
|
+
for (let x = 0; x < width; x += 1) {
|
|
126
|
+
const s = (y * width + x) * 4, d = row + 1 + x * channels;
|
|
127
|
+
raw[d] = data[s]; raw[d + 1] = data[s + 1]; raw[d + 2] = data[s + 2];
|
|
128
|
+
if (!opaque) raw[d + 3] = data[s + 3];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const ihdr = Buffer.alloc(13);
|
|
132
|
+
ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
|
|
133
|
+
ihdr[8] = 8; ihdr[9] = opaque ? 2 : 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
|
134
|
+
return Buffer.concat([SIGNATURE, chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0))]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A blank RGBA image filled with one colour [r, g, b, a]. */
|
|
138
|
+
export function blankImage(width, height, rgba = [255, 255, 255, 255]) {
|
|
139
|
+
const data = new Uint8Array(width * height * 4);
|
|
140
|
+
for (let i = 0; i < data.length; i += 4) { data[i] = rgba[0]; data[i + 1] = rgba[1]; data[i + 2] = rgba[2]; data[i + 3] = rgba[3]; }
|
|
141
|
+
return { width, height, data };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resample `image` to width x height. Downscaling by more than half averages the covered source area (a box
|
|
146
|
+
* filter), everything else is bilinear. Pure arithmetic, so the same inputs give the same pixels anywhere.
|
|
147
|
+
*/
|
|
148
|
+
function resizeBox(image, width, height, sx, sy, src, out) {
|
|
149
|
+
for (let y = 0; y < height; y += 1) {
|
|
150
|
+
const y0 = Math.floor(y * sy), y1 = Math.max(y0 + 1, Math.floor((y + 1) * sy));
|
|
151
|
+
for (let x = 0; x < width; x += 1) {
|
|
152
|
+
const x0 = Math.floor(x * sx), x1 = Math.max(x0 + 1, Math.floor((x + 1) * sx));
|
|
153
|
+
const sum = [0, 0, 0, 0];
|
|
154
|
+
for (let yy = y0; yy < y1; yy += 1) for (let xx = x0; xx < x1; xx += 1) { const s = (yy * image.width + xx) * 4; sum[0] += src[s]; sum[1] += src[s + 1]; sum[2] += src[s + 2]; sum[3] += src[s + 3]; }
|
|
155
|
+
const n = (y1 - y0) * (x1 - x0), d = (y * width + x) * 4;
|
|
156
|
+
for (let c = 0; c < 4; c += 1) out[d + c] = Math.round(sum[c] / n);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { width, height, data: out };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resizeBilinear(image, width, height, sx, sy, src, out) {
|
|
163
|
+
for (let y = 0; y < height; y += 1) {
|
|
164
|
+
const fy = Math.min(image.height - 1, Math.max(0, (y + 0.5) * sy - 0.5)), y0 = Math.floor(fy), y1 = Math.min(image.height - 1, y0 + 1), wy = fy - y0;
|
|
165
|
+
for (let x = 0; x < width; x += 1) {
|
|
166
|
+
const fx = Math.min(image.width - 1, Math.max(0, (x + 0.5) * sx - 0.5)), x0 = Math.floor(fx), x1 = Math.min(image.width - 1, x0 + 1), wx = fx - x0;
|
|
167
|
+
const a = (y0 * image.width + x0) * 4, b = (y0 * image.width + x1) * 4, c = (y1 * image.width + x0) * 4, e = (y1 * image.width + x1) * 4, d = (y * width + x) * 4;
|
|
168
|
+
for (let k = 0; k < 4; k += 1) out[d + k] = Math.round((src[a + k] * (1 - wx) + src[b + k] * wx) * (1 - wy) + (src[c + k] * (1 - wx) + src[e + k] * wx) * wy);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { width, height, data: out };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function resizeImage(image, width, height) {
|
|
175
|
+
if (image.width === width && image.height === height) return { width, height, data: Uint8Array.from(image.data) };
|
|
176
|
+
const out = new Uint8Array(width * height * 4), sx = image.width / width, sy = image.height / height, src = image.data;
|
|
177
|
+
if (sx > 2 || sy > 2) return resizeBox(image, width, height, sx, sy, src, out);
|
|
178
|
+
return resizeBilinear(image, width, height, sx, sy, src, out);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Crop a rectangle out of `image`. */
|
|
182
|
+
export function cropImage(image, { x, y, width, height }) {
|
|
183
|
+
const out = new Uint8Array(width * height * 4);
|
|
184
|
+
for (let row = 0; row < height; row += 1) {
|
|
185
|
+
const s = ((y + row) * image.width + x) * 4;
|
|
186
|
+
out.set(image.data.subarray(s, s + width * 4), row * width * 4);
|
|
187
|
+
}
|
|
188
|
+
return { width, height, data: out };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function compositePixel(base, top, source, target, alphaByte) {
|
|
192
|
+
if (alphaByte === 255) { base.data[target] = top.data[source]; base.data[target + 1] = top.data[source + 1]; base.data[target + 2] = top.data[source + 2]; base.data[target + 3] = 255; return; }
|
|
193
|
+
if (alphaByte === 0) return;
|
|
194
|
+
const alpha = alphaByte / 255, beneath = base.data[target + 3] / 255, outA = alpha + beneath * (1 - alpha);
|
|
195
|
+
for (let c = 0; c < 3; c += 1) base.data[target + c] = Math.round((top.data[source + c] * alpha + base.data[target + c] * beneath * (1 - alpha)) / (outA || 1));
|
|
196
|
+
base.data[target + 3] = Math.round(outA * 255);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Alpha-composite `top` over `base` (mutated) with its top-left corner at (x, y); pixels outside `base` are dropped. */
|
|
200
|
+
export function drawOver(base, top, x, y) {
|
|
201
|
+
for (let row = 0; row < top.height; row += 1) {
|
|
202
|
+
const by = y + row;
|
|
203
|
+
if (by < 0 || by >= base.height) continue;
|
|
204
|
+
for (let col = 0; col < top.width; col += 1) {
|
|
205
|
+
const bx = x + col;
|
|
206
|
+
if (bx < 0 || bx >= base.width) continue;
|
|
207
|
+
const s = (row * top.width + col) * 4, d = (by * base.width + bx) * 4;
|
|
208
|
+
compositePixel(base, top, s, d, top.data[s + 3]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return base;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Darken every pixel of `image` (mutated) by a black scrim of opacity `amount` in [0, 1]. */
|
|
215
|
+
export function dimImage(image, amount) {
|
|
216
|
+
const keep = 1 - amount;
|
|
217
|
+
for (let i = 0; i < image.data.length; i += 4) for (let c = 0; c < 3; c += 1) image.data[i + c] = Math.round(image.data[i + c] * keep);
|
|
218
|
+
return image;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The bounding rectangle of the pixels matching `key` ([r, g, b], within `tolerance` per channel), and how much
|
|
223
|
+
* of that rectangle they fill. A capture or a drawn layout marks its page slot with one flat key colour, so the
|
|
224
|
+
* slot is measured from the image rather than typed in.
|
|
225
|
+
*/
|
|
226
|
+
export function keyRect(image, key = [255, 0, 255], tolerance = 8) {
|
|
227
|
+
let minX = Infinity, minY = Infinity, maxX = -1, maxY = -1, count = 0;
|
|
228
|
+
const hits = (i) => Math.abs(image.data[i] - key[0]) <= tolerance && Math.abs(image.data[i + 1] - key[1]) <= tolerance && Math.abs(image.data[i + 2] - key[2]) <= tolerance && image.data[i + 3] > 200;
|
|
229
|
+
for (let y = 0; y < image.height; y += 1) for (let x = 0; x < image.width; x += 1) {
|
|
230
|
+
if (!hits((y * image.width + x) * 4)) continue;
|
|
231
|
+
count += 1;
|
|
232
|
+
minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y);
|
|
233
|
+
}
|
|
234
|
+
if (!count) return null;
|
|
235
|
+
const rect = { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 };
|
|
236
|
+
return { rect, fill: count / (rect.width * rect.height) };
|
|
237
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// render-tools.mjs - the tools a capture needs (Playwright with a browser, esbuild) and where they come from.
|
|
2
|
+
//
|
|
3
|
+
// A product declares neither: a drawing is captured by the runtime, so the runtime pins and installs both (package.json devDependencies,
|
|
4
|
+
// the browser download Playwright keeps in its own cache). The product's own install, when it has one, comes first (its tailwindcss,
|
|
5
|
+
// React and tsconfig paths resolve from the product regardless); the runtime's install is always the last candidate. This is the same
|
|
6
|
+
// order layout-render.mjs uses for its capture. A host with neither a project install nor the runtime's, or with Playwright and no browser,
|
|
7
|
+
// is RENDER_TOOL_UNAVAILABLE: one typed host fact the dispatch of an op that needs a capture checks before it spends an attempt.
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
10
|
+
import { findPackage, requirePackage } from '../lib/package-at.mjs';
|
|
11
|
+
import { toolSearchDirs } from '../lib/roots.mjs';
|
|
12
|
+
|
|
13
|
+
/** The catalogued code of a host that cannot take a capture (modules/kernel/failure-codes.yaml). */
|
|
14
|
+
export const RENDER_TOOL_UNAVAILABLE = 'RENDER_TOOL_UNAVAILABLE';
|
|
15
|
+
/** The hint an op manifest carries in route.riskHints when its steps capture a render. */
|
|
16
|
+
export const RENDER_CAPABILITY_HINT = 'host-capability-required:render';
|
|
17
|
+
const PLAYWRIGHT_PACKAGES = Object.freeze(['playwright', '@playwright/test', 'playwright-core']);
|
|
18
|
+
|
|
19
|
+
/** The first install of `names` resolvable from each of `dirs` in turn, one per distinct package root. */
|
|
20
|
+
function installsOf(dirs, names) {
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const dir of dirs) {
|
|
24
|
+
const found = findPackage([dir], names);
|
|
25
|
+
if (found && !seen.has(found.root)) { seen.add(found.root); out.push(found); }
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether the browser a Playwright install would launch is on the disk. */
|
|
31
|
+
function browserOf(found) {
|
|
32
|
+
try { return fs.existsSync(requirePackage(found).chromium.executablePath()); } catch { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The Playwright install a capture launches: the first of the project's and the runtime's whose browser exists, else the first install
|
|
37
|
+
* (its launch then names the missing browser). Null when no directory resolves one.
|
|
38
|
+
*/
|
|
39
|
+
export function playwrightInstall(dirs, { runtime = skillRoot } = {}) {
|
|
40
|
+
const installs = installsOf(toolSearchDirs(dirs, runtime), PLAYWRIGHT_PACKAGES);
|
|
41
|
+
return installs.find(browserOf) ?? installs[0] ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The esbuild install a capture bundles with: the project's, else the runtime's; null when neither resolves one. */
|
|
45
|
+
export const esbuildInstall = (dirs, { runtime = skillRoot } = {}) => findPackage(toolSearchDirs(dirs, runtime), ['esbuild']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What this host can capture with, from `dirs` (the tree the op runs in) and the runtime: {ok, missing: [{tool, why}], playwright, esbuild}.
|
|
49
|
+
* `missing` names each absent tool with the one-line reason a Kernel and the owner read.
|
|
50
|
+
*/
|
|
51
|
+
export function renderToolStatus(dirs = [], { runtime = skillRoot } = {}) {
|
|
52
|
+
const playwright = playwrightInstall(dirs, { runtime });
|
|
53
|
+
const esbuild = esbuildInstall(dirs, { runtime });
|
|
54
|
+
const missing = [];
|
|
55
|
+
if (!playwright) missing.push({ tool: 'playwright', why: 'no Playwright install resolves from the project or the runtime' });
|
|
56
|
+
else if (!browserOf(playwright)) missing.push({ tool: 'chromium', why: `Playwright ${playwright.version} resolves at ${playwright.root.split(/[\\/]/).slice(-2).join('/')} and its Chromium download is absent from the browser cache` });
|
|
57
|
+
if (!esbuild) missing.push({ tool: 'esbuild', why: 'no esbuild install resolves from the project or the runtime' });
|
|
58
|
+
return { ok: missing.length === 0, missing, playwright, esbuild };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The one-line instruction for a missing tool: what provisions it. */
|
|
62
|
+
export const renderToolFix = (missing) => missing.map((item) => (item.tool === 'chromium'
|
|
63
|
+
? 'run `npx playwright install chromium` once on the host (the browser cache is the owner\'s to fill)'
|
|
64
|
+
: `install the runtime's dependencies (\`npm install\` in the runtime checkout) so ${item.tool} resolves`)).join('; ');
|