@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,55 @@
|
|
|
1
|
+
// lock-recovery.mjs — the compositions around the ONE stale `.git/index.lock` recovery (scripts/api/git/index-lock.mjs
|
|
2
|
+
// indexLock): the git-process probe over the host's process table (scripts/api/process/process-list.mjs), the Supervisor
|
|
3
|
+
// audit event a removal records, and the command guard's pre-flight (scripts/guards/command-guard.mjs). The housekeeping
|
|
4
|
+
// area `gitlocks` (scripts/housekeeping/hk-git-locks.mjs) uses the probe and the recorder too.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { checkoutOf, gitDirOf, isGitImage, lockStat } from '../lib/git-dir.mjs';
|
|
7
|
+
import { indexLock } from '../api/git/index-lock.mjs';
|
|
8
|
+
import { processList } from '../api/process/process-list.mjs';
|
|
9
|
+
import { isSpecRun } from '../lib/env.mjs';
|
|
10
|
+
|
|
11
|
+
const LOCK_EVENT = 'git-index-lock-removed';
|
|
12
|
+
|
|
13
|
+
/** Git-family processes on this host: [{pid, name, commandLine}], or null when the probe failed. A spec run (NODE_TEST_CONTEXT) never reads the host process table. */
|
|
14
|
+
export function listGitProcesses({ platform = process.platform, run, env = process.env } = {}) {
|
|
15
|
+
if (isSpecRun(env)) return null;
|
|
16
|
+
const rows = processList({ where: "Name LIKE 'git%'", run, platform, timeoutMs: 30000 });
|
|
17
|
+
return rows ? rows.map((p) => ({ pid: p.pid, name: String(p.name ?? ''), commandLine: p.cmd })).filter((p) => isGitImage(p.name)) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `record` for api/git/index-lock.mjs indexLock: one Supervisor audit event (machine.sqlite sup_events) per removal, naming who removed it. */
|
|
21
|
+
export async function supervisorLockRecorder({ env = process.env, by, jobId = null, workflowId = null } = {}) {
|
|
22
|
+
const { withSupervisor, supervisorEvent } = await import('./home.mjs');
|
|
23
|
+
return (result) => withSupervisor((m) => supervisorEvent(m, { entityType: 'repo', entityId: result.repo, kind: LOCK_EVENT,
|
|
24
|
+
payload: { lock: result.lock, ageMs: Math.round(result.ageMs), bytes: result.bytes, mtime: result.mtime, by, jobId, workflowId } }), { env });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The command guard's git pre-flight (scripts/guards/command-guard.mjs): when the checkout `cwd` is in holds an index
|
|
29
|
+
* lock older than the declared window, try the recovery once and say what happened on stderr. Costs one stat when no
|
|
30
|
+
* lock stands. Never throws: the command runs either way.
|
|
31
|
+
*/
|
|
32
|
+
export async function preflightIndexLock({ cwd = process.cwd(), guard = null, env = process.env, say = () => {}, now = Date.now(), list = listGitProcesses } = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const repo = checkoutOf(cwd);
|
|
35
|
+
const gitDir = repo ? gitDirOf(repo) : null;
|
|
36
|
+
const st = gitDir ? lockStat(path.join(gitDir, 'index.lock')) : null;
|
|
37
|
+
if (!st) return null;
|
|
38
|
+
const { allocationMs } = await import('../../engine/config.mjs');
|
|
39
|
+
const staleMs = allocationMs('housekeeping.gitIndexLockStaleMs');
|
|
40
|
+
if (now - st.mtimeMs < staleMs) return null;
|
|
41
|
+
const record = await supervisorLockRecorder({ env, by: 'command-guard', jobId: guard?.jobId ?? null, workflowId: guard?.workflowId ?? null });
|
|
42
|
+
const result = indexLock({ repo, staleMs, now, list, record });
|
|
43
|
+
const age = `${Math.round((result.ageMs ?? 0) / 60000)} min`;
|
|
44
|
+
if (result.state === 'removed') say(`starci guard: removed a stale ${result.lock} (${age} old, no git process on this repository); recorded as ${LOCK_EVENT}.`);
|
|
45
|
+
else if (result.state === 'held') say(`starci guard: ${result.lock} is ${age} old but a git process may still hold it (${result.holders.map((h) => h.pid).join(', ')}); left in place - retry shortly, never delete it by hand.`);
|
|
46
|
+
else if (result.state !== 'fresh' && result.state !== 'absent') {
|
|
47
|
+
const error = result.error ? `: ${result.error}` : '';
|
|
48
|
+
say(`starci guard: ${result.lock} is ${age} old and was left in place (${result.state}${error}).`);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
say(`starci guard: index-lock check error (${error?.message ?? error}); running git anyway`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// roles-contract.mjs — reads the one roles contract (modules/kernel/roles.yaml) and renders a role's block: the Markdown a surface
|
|
2
|
+
// carries between its markers, and the compact lines the op prompt prints at launch. The roles-contract self-check compares
|
|
3
|
+
// every surface with this rendering.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
10
|
+
export const ROLES_FILE = 'modules/kernel/roles.yaml';
|
|
11
|
+
|
|
12
|
+
/** The roles contract document of the tree at `root`. */
|
|
13
|
+
export const rolesContract = (root = ROOT) => parseYaml(fs.readFileSync(path.join(root, ROLES_FILE), 'utf8'));
|
|
14
|
+
|
|
15
|
+
export const markerOf = (id) => ({ begin: `<!-- roles:begin ${id} -->`, end: `<!-- roles:end ${id} -->` });
|
|
16
|
+
|
|
17
|
+
const bullets = (label, items) => (items?.length ? [`- ${label}:`, ...items.map((item) => ` - ${item}`)] : []);
|
|
18
|
+
const nameOf = (doc, id) => (id === 'runtime' ? 'the runtime' : doc.roles.find((role) => role.id === id)?.label ?? id);
|
|
19
|
+
|
|
20
|
+
/** The lines that state who a role reports to, who oversees it and what it owns. */
|
|
21
|
+
function chainLines(doc, role) {
|
|
22
|
+
const up = role.reportsTo ? `${nameOf(doc, role.reportsTo)} (${role.reportsUpWhen})` : 'no one';
|
|
23
|
+
return [`- Owns: ${role.owns}. Decides alone: ${role.decidesAlone}.`,
|
|
24
|
+
`- Reports to: ${up}. Overseen by: ${role.overseenBy.map((id) => nameOf(doc, id)).join(', ') || 'no one'}.`,
|
|
25
|
+
`- Measure: ${role.measure}.`];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The token budget line of a role that declares one. */
|
|
29
|
+
function budgetLines(role) {
|
|
30
|
+
const b = role.tokenBudget;
|
|
31
|
+
const wake = role.wakeBudget;
|
|
32
|
+
if (b) return [`- Token budget (${b.status}): ${b.perAttempt.default} per ${b.unit ?? 'attempt'}; over it, ${b.onExceed}.`];
|
|
33
|
+
if (wake) {
|
|
34
|
+
const boot = wake.perBoot ? `, ${wake.perBoot.turns} turns and ${wake.perBoot.tokens} tokens for its boot (the contract files and the rev-ack manifest)` : '';
|
|
35
|
+
return [`- Wake budget (${wake.status}): ${wake.perWake.turns} turns and ${wake.perWake.tokens} tokens per wake${boot}; over it, ${wake.onExceed}.`];
|
|
36
|
+
}
|
|
37
|
+
return role.noBudget ? [`- No budget: ${role.noBudget}.`] : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The line that states how a role learns that the runtime tree changed (its revisionAck declaration). */
|
|
41
|
+
function revisionLines(role) {
|
|
42
|
+
const decl = role.revisionAck;
|
|
43
|
+
if (!decl) return [];
|
|
44
|
+
if (decl.noAck) return [`- Runtime changes: ${decl.noAck}.`];
|
|
45
|
+
if (decl.admission) return [`- Runtime changes: ${decl.reason}.`];
|
|
46
|
+
return [`- Runtime changes (modules/kernel/revision-scope.yaml): woken once with exactly the changed files that concern it, it reads them and attests with ${decl.verb}; a change that concerns it nothing costs it nothing; it is replaced by a fresh seat, at its next yield, only when a rule of its contract was removed or reversed or its boot prompt changed.`];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The guard binding, the happy errors and the bug surface of a role that declares them (the owner's two classes of error). */
|
|
50
|
+
function standardLines(role) {
|
|
51
|
+
const unbound = role.noSeat ? [`- Guard: none by design; ${role.noSeatReason}.`] : [];
|
|
52
|
+
const guard = role.guard ? [`- Guard: its terminals are bound as the "${role.guard.role}" role of modules/kernel/command-policy.yaml.`] : unbound;
|
|
53
|
+
const happy = role.happyErrors?.map((error) => `${error.id} (policy row ${error.row}): ${error.what}`);
|
|
54
|
+
const bugs = role.bugSurface?.map((entry) => `${entry.bug}: ${entry.signal}`);
|
|
55
|
+
return [...guard, ...bullets('Happy errors it handles (the system working as designed, handled inside the chain through the policy)', happy),
|
|
56
|
+
...bullets('A bug in this role (the chain neither fixes nor works around it; Debug removes it with a change to .claude) is detected by', bugs)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The Debug lines that state whom it audits and when it retires. */
|
|
60
|
+
function debugLines(doc, role) {
|
|
61
|
+
if (!role.oversees) return [];
|
|
62
|
+
const criteria = role.endCondition.criteria.map((c) => `${c.id}: ${c.rule}`);
|
|
63
|
+
return [`- Audits: ${role.oversees.map((id) => nameOf(doc, id)).join(', ')}.`, ...bullets('Retires when', criteria)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The Markdown block of one role (without its markers). */
|
|
67
|
+
export function renderRoleBlock(doc, id) {
|
|
68
|
+
const role = doc.roles.find((entry) => entry.id === id);
|
|
69
|
+
const lines = [`**${role.label}** (${ROLES_FILE}#${id}): ${role.scope}`, ...bullets('Does', role.does), ...bullets('Must clean up', role.cleanup),
|
|
70
|
+
...bullets('Never', role.never), ...chainLines(doc, role), ...budgetLines(role), ...revisionLines(role), ...standardLines(role), ...debugLines(doc, role), `- Principles: ${role.binds.join(' ')} (${ROLES_FILE}, principles).`];
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The compact lines of a role for a launch prompt. */
|
|
75
|
+
export function renderRoleLines(id, root = ROOT) {
|
|
76
|
+
const doc = rolesContract(root);
|
|
77
|
+
const role = doc.roles.find((entry) => entry.id === id);
|
|
78
|
+
return [`role: ${role.label} (${ROLES_FILE}#${id}) - ${role.scope}`, ...role.never.map((item) => ` never: ${item}`),
|
|
79
|
+
...role.cleanup.map((item) => ` clean up: ${item}`), ` reports to: ${nameOf(doc, role.reportsTo)} only`];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The text with the role's block replaced between its markers, or appended with markers when it has none. */
|
|
83
|
+
export function withRoleBlock(text, doc, id) {
|
|
84
|
+
const { begin, end } = markerOf(id);
|
|
85
|
+
const block = `${begin}\n${renderRoleBlock(doc, id)}\n${end}`;
|
|
86
|
+
const from = text.indexOf(begin);
|
|
87
|
+
const to = text.indexOf(end);
|
|
88
|
+
if (from < 0 || to < from) return `${text.trimEnd()}\n\n${block}\n`;
|
|
89
|
+
return text.slice(0, from) + block + text.slice(to + end.length);
|
|
90
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// runtime-change.mjs — the typed refusal of a runtime change by the Supervisor. Changing .claude belongs to Debug and the owner's
|
|
2
|
+
// release flow (modules/kernel/roles.yaml chain.runtimeChange); the verbs and the code are read from there, so the command guard,
|
|
3
|
+
// the file-write guard and the CLI dispatcher refuse with one voice.
|
|
4
|
+
import { rolesContract } from './roles-contract.mjs';
|
|
5
|
+
|
|
6
|
+
export const RUNTIME_CHANGE_CODE = 'RUNTIME_CHANGE_OWNED_BY_DEBUG';
|
|
7
|
+
|
|
8
|
+
/** The refusal fields {code, reason, remedy} for `words` (the group, verb and subcommand a caller runs), or null when they change nothing. */
|
|
9
|
+
export function runtimeChangeRefusal(words) {
|
|
10
|
+
const { runtimeChange } = rolesContract().chain;
|
|
11
|
+
const hit = runtimeChange.verbs.find((verb) => verb.every((word, index) => words[index] === word));
|
|
12
|
+
if (!hit) return null;
|
|
13
|
+
return { code: RUNTIME_CHANGE_CODE, reason: `the Supervisor changes no runtime code: starci ${hit.join(' ')} belongs to Debug`, remedy: runtimeChange.use };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The refusal fields for a Supervisor write inside the runtime checkout. */
|
|
17
|
+
export function runtimeWriteRefusal() {
|
|
18
|
+
const { runtimeChange } = rolesContract().chain;
|
|
19
|
+
return { code: RUNTIME_CHANGE_CODE, reason: 'the Supervisor changes no file of the runtime checkout: .claude belongs to Debug and the owner\'s release flow', remedy: runtimeChange.use };
|
|
20
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// workflow-tree.mjs - the host registry's view of a workflow's worktree (machine.sqlite worktrees, kind workflow) and the
|
|
2
|
+
// base an op's gate measures against there. Read by the Kernel (scripts/kernel/workflow-worktree.mjs,
|
|
3
|
+
// workflow-checkpoint.mjs) and by scripts/gates/gate.mjs, which finds its workflow from the directory it runs in.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { mergeBase } from '../api/git/merge-base.mjs';
|
|
7
|
+
import { diffNames } from '../api/git/diff-names.mjs';
|
|
8
|
+
import { log as gitLog } from '../api/git/log.mjs';
|
|
9
|
+
import { revParse as gitRevParse } from '../api/git/rev-parse.mjs';
|
|
10
|
+
import { withMachine } from '../../engine/db/machine.mjs';
|
|
11
|
+
import { isInside } from '../lib/walk.mjs';
|
|
12
|
+
import { requireWorktreeRecord } from '../lib/worktree-record.mjs';
|
|
13
|
+
|
|
14
|
+
const SHA = /^[0-9a-f]{40,64}$/;
|
|
15
|
+
const insidePath = (child, parent) => isInside(path.resolve(parent), path.resolve(child));
|
|
16
|
+
const fail = ({ code }, message) => Object.assign(new Error(message), { code });
|
|
17
|
+
const revParse = (cwd, ref) => { const sha = gitRevParse(cwd, ref); return sha && SHA.test(sha) ? sha : null; };
|
|
18
|
+
const mainOf = (ctx) => ctx?.main ?? 'main';
|
|
19
|
+
const envOf = (ctx) => ctx?.env ?? process.env;
|
|
20
|
+
|
|
21
|
+
/** A registry row of a workflow worktree as a record: {workflowId, orcaWorktreeId, path, branch, checkpoint, repoRoot, ...}, or null. */
|
|
22
|
+
export const workflowRecordOf = (row) => (row ? { workflowId: row.workflow_id, orcaWorktreeId: row.orca_id, path: path.resolve(row.path), branch: row.branch ?? null,
|
|
23
|
+
checkpoint: row.checkpoint_sha ?? null, repoRoot: row.repo_root, ledgerId: row.ledger_id ?? null, createdAt: row.created_at,
|
|
24
|
+
releasePending: row.release_pending_at != null } : null);
|
|
25
|
+
|
|
26
|
+
/** The live workflow worktree of `workflowId`: {workflowId, orcaWorktreeId, path, branch, checkpoint, repoRoot}, or null. */
|
|
27
|
+
export function workflowWorktreeOf(ctx, workflowId) {
|
|
28
|
+
const env = envOf(ctx);
|
|
29
|
+
try {
|
|
30
|
+
return workflowRecordOf(withMachine((m) => m.db.prepare("SELECT * FROM worktrees WHERE kind='workflow' AND workflow_id=? AND orca_id IS NOT NULL AND removed_at IS NULL ORDER BY created_at DESC LIMIT 1").get(workflowId), { env }));
|
|
31
|
+
} catch (cause) { throw Object.assign(new Error(`the workflow worktree registry is unavailable: ${cause.message}`, { cause }), { code: 'worktree-registry-unavailable' }); }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The live workflow worktree whose directory is `dir` (or holds it), or null: how a tool run inside a worktree
|
|
36
|
+
* (scripts/gates/gate.mjs) finds its workflow through the registry, never by parsing a branch or folder name.
|
|
37
|
+
*/
|
|
38
|
+
export function workflowWorktreeAt(ctx, dir) {
|
|
39
|
+
const env = envOf(ctx);
|
|
40
|
+
const at = path.resolve(dir);
|
|
41
|
+
const key = (p) => { let r = path.resolve(p); try { r = fs.realpathSync.native(r); } catch { /* missing */ } return process.platform === 'win32' ? r.toLowerCase() : r; };
|
|
42
|
+
try {
|
|
43
|
+
const rows = withMachine((m) => m.db.prepare("SELECT * FROM worktrees WHERE kind='workflow' AND orca_id IS NOT NULL AND removed_at IS NULL ORDER BY created_at DESC").all(), { env });
|
|
44
|
+
const row = rows.find((r) => key(r.path) === key(at)) ?? rows.find((r) => insidePath(key(at), key(r.path)));
|
|
45
|
+
return workflowRecordOf(row ?? null);
|
|
46
|
+
} catch (cause) { throw Object.assign(new Error(`the workflow worktree registry is unavailable: ${cause.message}`, { cause }), { code: 'worktree-registry-unavailable' }); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The registry functions a caller may replace (ctx.worktree in a spec, the functions above in the runtime). */
|
|
50
|
+
const registryOf = (ctx) => ({ workflowWorktreeOf, workflowWorktreeAt, ...ctx?.worktree });
|
|
51
|
+
|
|
52
|
+
/** The registry record of the workflow's worktree, its directory present: {workflowId, orcaWorktreeId, path, branch, checkpoint}. */
|
|
53
|
+
function presentRecordOf(ctx, workflowId) {
|
|
54
|
+
return requireWorktreeRecord(registryOf(ctx).workflowWorktreeOf(ctx, workflowId), workflowId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The gate base of the workflow worktree that holds `dir` (part A's workflowWorktreeAt over the registry), or null when
|
|
59
|
+
* `dir` is no workflow worktree: scripts/gates/gate.mjs measures an op there against its previous checkpoint.
|
|
60
|
+
*/
|
|
61
|
+
export function gateBaseAt(ctx, dir) {
|
|
62
|
+
const rec = registryOf(ctx).workflowWorktreeAt(ctx, dir);
|
|
63
|
+
return rec ? gateBaseOf(ctx, rec.workflowId) : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The base an op's gate measures against: the previous checkpoint, else the merge-base of the workflow branch with main. */
|
|
67
|
+
/**
|
|
68
|
+
* The newest checkpoint commit of the workflow's own chain on the branch (subject `checkpoint <workflowId>: ...`, first-parent line from HEAD), or null. A registry
|
|
69
|
+
* record that lost its checkpoint pointer (a tree put back by the custody repair) must not fall back to the merge-base with main: that would put the base of
|
|
70
|
+
* every settled op's gate, and the target of a reset, behind the work those ops committed.
|
|
71
|
+
*/
|
|
72
|
+
function chainCheckpointOf(rec) {
|
|
73
|
+
const r = gitLog(['--first-parent', '--format=%H%x09%s', '-n', '256', 'HEAD'], { cwd: rec.path, timeout: 30_000 });
|
|
74
|
+
if (r.status !== 0) return null;
|
|
75
|
+
const prefix = `checkpoint ${rec.workflowId}:`;
|
|
76
|
+
for (const line of String(r.stdout ?? '').split(/\r?\n/)) {
|
|
77
|
+
const [sha, subject] = line.split('\t');
|
|
78
|
+
if (SHA.test(sha ?? '') && subject?.startsWith(prefix)) return sha;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function gateBaseOf(ctx, workflowId) {
|
|
84
|
+
const rec = presentRecordOf(ctx, workflowId);
|
|
85
|
+
const chained = rec.checkpoint ? null : chainCheckpointOf(rec);
|
|
86
|
+
if (chained) return chained;
|
|
87
|
+
if (rec.checkpoint) {
|
|
88
|
+
const sha = revParse(rec.path, rec.checkpoint);
|
|
89
|
+
if (!sha) throw fail({ code: 'workflow-gate-base-unknown' }, `the checkpoint ${rec.checkpoint} of workflow ${workflowId} is not a commit of ${rec.path}`);
|
|
90
|
+
return sha;
|
|
91
|
+
}
|
|
92
|
+
const base = mergeBase(rec.path, `refs/heads/${mainOf(ctx)}`, 'HEAD') ?? '';
|
|
93
|
+
if (!SHA.test(base)) throw fail({ code: 'workflow-gate-base-unknown' }, `workflow ${workflowId} has no checkpoint and ${rec.branch} has no merge-base with ${mainOf(ctx)}`);
|
|
94
|
+
return base;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const CHAIN_WALK_MAX = 64;
|
|
98
|
+
/**
|
|
99
|
+
* The bases an op's gate may have measured against, newest first (per side, WFWT2 2.2): the current checkpoint, then each
|
|
100
|
+
* older checkpoint of the chain while the checkpoint stepped over changed none of `owned` (the op's owned paths, the
|
|
101
|
+
* leases checkpointOp commits), stopping at the chain's start (the merge-base with main). A be checkpoint committed while
|
|
102
|
+
* an fe op ran therefore never forces the fe op to re-run its gate: what differs between the bases is the other side's
|
|
103
|
+
* committed, gated work, and the finish gates the whole branch again. An op that owns no path or owns '.' gets the current
|
|
104
|
+
* checkpoint only.
|
|
105
|
+
*/
|
|
106
|
+
export function gateBasesOf(ctx, workflowId, { owned = [] } = {}) {
|
|
107
|
+
const current = gateBaseOf(ctx, workflowId);
|
|
108
|
+
const paths = owned.map(String).filter(Boolean);
|
|
109
|
+
if (!paths.length || paths.some((p) => p === '.' || p === './')) return [current];
|
|
110
|
+
const rec = presentRecordOf(ctx, workflowId);
|
|
111
|
+
const root = mergeBase(rec.path, `refs/heads/${mainOf(ctx)}`, current);
|
|
112
|
+
const bases = [current];
|
|
113
|
+
for (let at = current; at !== root && bases.length < CHAIN_WALK_MAX;) {
|
|
114
|
+
const parent = revParse(rec.path, `${at}^`);
|
|
115
|
+
if (!parent) break;
|
|
116
|
+
const touched = diffNames(rec.path, parent, at, { paths: paths.map((p) => `:(literal)${p}`) });
|
|
117
|
+
if (touched === null || touched.length) break;
|
|
118
|
+
bases.push(parent);
|
|
119
|
+
at = parent;
|
|
120
|
+
}
|
|
121
|
+
return bases;
|
|
122
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The checklist row builders of `start` (scripts/reconciler/start.mjs) and of the row modules split out of it.
|
|
2
|
+
|
|
3
|
+
/** One checklist row. status green|red|warn; required rows decide the exit code, warn never does; a green row with `idle: true` is an agent seat that is not running because nothing needs it yet. */
|
|
4
|
+
const item = (group, id, name, status, detail = '', { fix = null, required = true, idle = false } = {}) => ({ group, id, name, status, required: required && status !== 'warn', detail, ...(fix && status !== 'green' ? { fix } : {}), ...(idle && status === 'green' ? { idle: true } : {}) });
|
|
5
|
+
export const green = (group, id, name, detail, opts) => item(group, id, name, 'green', detail, opts);
|
|
6
|
+
export const red = (group, id, name, detail, fix, opts) => item(group, id, name, 'red', detail, { fix, ...opts });
|
|
7
|
+
export const warn = (group, id, name, detail, fix) => item(group, id, name, 'warn', detail, { fix, required: false });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// guard-row.mjs — the host checklist row of the command guard hook: the exact hook command launch trust registers is run
|
|
2
|
+
// the way a seat's shell runs it (scripts/api/process/probe-guard-command.mjs), because a hook that does not resolve is a
|
|
3
|
+
// non-blocking error and leaves every guarded command unguarded. Required: red names each failing shell.
|
|
4
|
+
import { probeGuardCommand } from '../api/process/probe-guard-command.mjs';
|
|
5
|
+
import { toolGuardCommand } from '../lib/guard-command.mjs';
|
|
6
|
+
import { green, red } from './checklist-items.mjs';
|
|
7
|
+
|
|
8
|
+
const NAME = 'command guard resolvable';
|
|
9
|
+
const FIX = 'starci runtime link, then launch again to refresh the registered Node and linked runtime CLI paths';
|
|
10
|
+
|
|
11
|
+
/** The row of one probe of `toolGuardCommand()`; never throws. Seam: `probe`. */
|
|
12
|
+
export function guardCommandRow({ home, probe = probeGuardCommand } = {}) {
|
|
13
|
+
let command;
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
command = toolGuardCommand({ home });
|
|
17
|
+
result = probe({ command, home });
|
|
18
|
+
}
|
|
19
|
+
catch (error) { return red('preflight', 'guard-command', NAME, `${command ?? 'linked runtime guard'}: ${String(error?.message ?? error).slice(0, 200)}`, FIX); }
|
|
20
|
+
if (!result.ok) return red('preflight', 'guard-command', NAME, `${command} does not run: ${result.reason}`.slice(0, 400), FIX);
|
|
21
|
+
const ran = result.shells.filter((entry) => !entry.skipped).map((entry) => entry.shell).join(', ');
|
|
22
|
+
const skipped = result.shells.filter((entry) => entry.skipped).map((entry) => entry.skipped);
|
|
23
|
+
const note = skipped.length ? ` (skipped: ${skipped.join('; ')})` : '';
|
|
24
|
+
return green('preflight', 'guard-command', NAME, `${command} runs through ${ran}${note}`);
|
|
25
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// asset-slot.mjs — artwork a drawing owes to interface.asset (owner ruling 2026-09-27: "mascot/illustration comes from
|
|
3
|
+
// interface.asset, never reused ad hoc").
|
|
4
|
+
//
|
|
5
|
+
// A drawing that needs artwork (a mascot, an illustration, a hero image) never reuses a file it found: it marks the
|
|
6
|
+
// place data-asset-slot="<id>" on the MediaFrame / Image / img (a placeholder src, or none, is fine) and requests it in
|
|
7
|
+
// asset-request.md beside its render source (or in the ui record directory) - one heading per slot naming its id, the
|
|
8
|
+
// section its purpose, placement, pixel size and format, and the brand master it references. The DNA gate refuses
|
|
9
|
+
// artwork without a slot and a slot without a request (DRAW_ASSET_SLOT_UNDECLARED, scripts/work/draw/draw-dna.mjs), and
|
|
10
|
+
// the draw records each slot in ui.artworkSlots[] (status owed).
|
|
11
|
+
//
|
|
12
|
+
// interface.asset owes every open slot: it generates the artwork under the brand record's imagery.promptRules (and
|
|
13
|
+
// masters), then replaces the placeholder in the render source - src the produced file, data-asset-sha256 its bytes'
|
|
14
|
+
// sha256, data-asset-prompt the prompt.txt it was generated from - and records ui.artworkSlots[].file/sha256/status.
|
|
15
|
+
// The brand masters (brand/assets/**, the unicorn masters) are the LANDING's art and the style reference only (owner
|
|
16
|
+
// ruling 2026-09-27): every product slot is owed a NEW generation - a new pose/composition per surface in the same
|
|
17
|
+
// brand style - so a master may stand in only as the placeholder. A slot is FILLED only when data-asset-sha256 names
|
|
18
|
+
// the sha256 of the file its src resolves to, those bytes are not byte-identical to any brand master, and its prompt
|
|
19
|
+
// (data-asset-prompt, else <file>.prompt.txt beside it) exists; anything else is OWED. A done ui record with an owed
|
|
20
|
+
// slot is refused (ASSET_SLOT_UNFILLED, scripts/work/validate/check-example-work.mjs).
|
|
21
|
+
//
|
|
22
|
+
// The ledger: settling an interface.draw or interface.asset job records one `asset-slot-owed` event per owed slot of
|
|
23
|
+
// the files it binds and one `asset-slot-filled` per filled one; `starci kernel status` lists the open ones as assetSlotsOwed[]
|
|
24
|
+
// and its nextActions propose an interface.asset leg for them (scripts/kernel/cli.mjs).
|
|
25
|
+
//
|
|
26
|
+
// starci work asset-slot list <file|dir>... [--json] every slot, owed or filled, with its request
|
|
27
|
+
// starci work asset-slot check <file|dir>... [--json] exit 1 when a slot has no request
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { isMain } from '../lib/is-main.mjs';
|
|
31
|
+
import { sha256File } from '../../engine/digest.mjs';
|
|
32
|
+
import { openEventItems } from '../lib/event-items.mjs';
|
|
33
|
+
import { ASSET_SLOT_ATTR, COMPONENT_ATTR, parseHtml, walkElements } from './draw/draw-dna.mjs';
|
|
34
|
+
import { isDir, isFile, slash } from './work-io.mjs';
|
|
35
|
+
|
|
36
|
+
export { ASSET_SLOT_ATTR };
|
|
37
|
+
const ASSET_SHA_ATTR = 'data-asset-sha256';
|
|
38
|
+
const ASSET_REQUEST_FILE = 'asset-request.md';
|
|
39
|
+
const ASSET_SLOT_OWED = 'asset-slot-owed';
|
|
40
|
+
const ASSET_SLOT_FILLED = 'asset-slot-filled';
|
|
41
|
+
const ASSET_PROMPT_ATTR = 'data-asset-prompt';
|
|
42
|
+
export const ASSET_OP = 'interface.asset';
|
|
43
|
+
export const ASSET_SLOT_UNFILLED = 'ASSET_SLOT_UNFILLED';
|
|
44
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'draw-loop']);
|
|
45
|
+
|
|
46
|
+
/** The ui record directory above `file` (the nearest dir holding index.yaml, at most 6 levels up), or null. */
|
|
47
|
+
export function uiDirOf(file) {
|
|
48
|
+
let dir = isDir(file) ? file : path.dirname(file);
|
|
49
|
+
for (let i = 0; i < 7; i++) {
|
|
50
|
+
if (isFile(path.join(dir, 'index.yaml'))) return dir;
|
|
51
|
+
const up = path.dirname(dir);
|
|
52
|
+
if (up === dir) break;
|
|
53
|
+
dir = up;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The asset-request.md files that belong to a render source: beside it, up to its ui record dir, and in `dirs`. */
|
|
59
|
+
function requestFilesFor(htmlFile, dirs = []) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const add = (dir) => { const f = path.join(dir, ASSET_REQUEST_FILE); if (isFile(f) && !out.includes(f)) out.push(f); };
|
|
62
|
+
const stop = uiDirOf(htmlFile);
|
|
63
|
+
let dir = path.dirname(htmlFile);
|
|
64
|
+
for (let i = 0; i < 7; i++) {
|
|
65
|
+
add(dir);
|
|
66
|
+
if (!stop || dir === stop) break;
|
|
67
|
+
const up = path.dirname(dir);
|
|
68
|
+
if (up === dir) break;
|
|
69
|
+
dir = up;
|
|
70
|
+
}
|
|
71
|
+
for (const d of dirs) add(d);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const SLUG = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
76
|
+
const HEADING_PATTERN = new RegExp(['^#{1,6}', String.raw`\s+`, '(.*)$'].join(''));
|
|
77
|
+
const SLOT_LINE_PATTERN = new RegExp([String.raw`^\s*`, '[-*]?', String.raw`\s*(?:slot|id)\s*:\s*`, '`?', String.raw`([\w.-]+)`, '`?', String.raw`\s*$`].join(''), 'i');
|
|
78
|
+
/** The slot requests of asset-request.md files: [{id, file, brief}] - a heading per slot, or `slot: <id>` lines. */
|
|
79
|
+
function readAssetRequests(files) {
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const file of files ?? []) readFileRequests(file, out);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readFileRequests(file, out) {
|
|
86
|
+
let text = '';
|
|
87
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return; }
|
|
88
|
+
const lines = text.split(/\r?\n/);
|
|
89
|
+
let current = null;
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
current = requestLine(line, current, file, out);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requestLine(line, current, file, out) {
|
|
96
|
+
const heading = HEADING_PATTERN.exec(line);
|
|
97
|
+
const slotLine = SLOT_LINE_PATTERN.exec(line);
|
|
98
|
+
if (heading) {
|
|
99
|
+
const id = requestedHeadingId(heading[1].trim());
|
|
100
|
+
return id ? pushRequest(id.trim(), file, out) : null;
|
|
101
|
+
}
|
|
102
|
+
if (slotLine) return current?.id !== slotLine[1] ? pushRequest(slotLine[1], file, out) : current;
|
|
103
|
+
if (current && line.trim()) current.brief = `${current.brief} ${line.trim()}`.trim().slice(0, 400);
|
|
104
|
+
return current;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function requestedHeadingId(heading) {
|
|
108
|
+
return new RegExp(String.raw`${ASSET_SLOT_ATTR}\s*=\s*["']([^"']+)["']`).exec(heading)?.[1] ?? /`([^`]+)`/.exec(heading)?.[1] ?? (SLUG.test(heading) ? heading : null);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pushRequest(id, file, out) {
|
|
112
|
+
const current = { id, file, brief: '' };
|
|
113
|
+
out.push(current);
|
|
114
|
+
return current;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The Set of slot ids the render source's asset-request.md files request. */
|
|
118
|
+
export const assetRequestIdsFor = (htmlFile, dirs = []) => new Set(readAssetRequests(requestFilesFor(htmlFile, dirs)).map((r) => r.id));
|
|
119
|
+
|
|
120
|
+
/** The .starciwork directory above `file`, or null. */
|
|
121
|
+
function workRootAbove(file) {
|
|
122
|
+
let dir = path.dirname(path.resolve(file));
|
|
123
|
+
for (let i = 0; i < 12; i++) {
|
|
124
|
+
if (path.basename(dir) === '.starciwork') return dir;
|
|
125
|
+
if (isDir(path.join(dir, '.starciwork')) && isFile(path.join(dir, '.starciwork', 'brand', 'index.yaml'))) return path.join(dir, '.starciwork');
|
|
126
|
+
const up = path.dirname(dir);
|
|
127
|
+
if (up === dir) break;
|
|
128
|
+
dir = up;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Calls `onFile(fullPath, name)` for every file under `dir`, `depth` directory levels down; `enter(name)` admits a directory. */
|
|
134
|
+
function eachFileUnder(dir, depth, onFile, enter = () => true) {
|
|
135
|
+
let entries = [];
|
|
136
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
137
|
+
for (const e of entries) {
|
|
138
|
+
const full = path.join(dir, e.name);
|
|
139
|
+
if (!e.isDirectory()) onFile(full, e.name);
|
|
140
|
+
else if (depth > 0 && enter(e.name)) eachFileUnder(full, depth - 1, onFile, enter);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The sha256 of every brand master file (<work>/brand/assets/**): the landing's art, never a product slot's bytes. */
|
|
145
|
+
function brandMasterShas(workRoot) {
|
|
146
|
+
if (!workRoot) return new Set();
|
|
147
|
+
const out = new Set();
|
|
148
|
+
eachFileUnder(path.join(workRoot, 'brand', 'assets'), 6, (full) => { try { out.add(sha256File(full)); } catch { /* unreadable */ } });
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The slots of one render source: [{id, tag, component, src, sha256, file, master, prompt, filled}]; `htmlFile`
|
|
154
|
+
* resolves the src and the prompt, `masters` is the Set of brand master sha256s a filled slot must not equal.
|
|
155
|
+
*/
|
|
156
|
+
function slotsOfHtml(html, { htmlFile = null, masters = new Set() } = {}) {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const el of walkElements(parseHtml(html))) {
|
|
159
|
+
const slot = slotOfElement(el, htmlFile, masters);
|
|
160
|
+
if (slot) out.push(slot);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The prompt file a slot names (relative to its render source), else the `.prompt.txt` beside its filled asset. */
|
|
166
|
+
function promptFileOf(promptAttr, htmlFile, actual, file) {
|
|
167
|
+
if (promptAttr && htmlFile) return path.resolve(path.dirname(htmlFile), promptAttr);
|
|
168
|
+
return actual ? file.replace(/\.[^.\\/]+$/, '.prompt.txt') : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function slotOfElement(el, htmlFile, masters) {
|
|
172
|
+
const id = (el.attrs[ASSET_SLOT_ATTR] ?? '').trim();
|
|
173
|
+
if (!id) return null;
|
|
174
|
+
const img = el.tag === 'img' ? el : walkElements(el).find((d) => d.tag === 'img') ?? el;
|
|
175
|
+
const src = (img.attrs.src ?? el.attrs.src ?? '').trim() || null;
|
|
176
|
+
const sha = (el.attrs[ASSET_SHA_ATTR] ?? img.attrs[ASSET_SHA_ATTR] ?? '').trim().toLowerCase() || null;
|
|
177
|
+
const { file, actual } = actualAssetFile(src, htmlFile);
|
|
178
|
+
const master = Boolean(actual && masters.has(actual));
|
|
179
|
+
const promptFile = promptFileOf((el.attrs[ASSET_PROMPT_ATTR] ?? img.attrs[ASSET_PROMPT_ATTR] ?? '').trim(), htmlFile, actual, file);
|
|
180
|
+
const prompt = promptFile && isFile(promptFile) ? promptFile : null;
|
|
181
|
+
return { id, tag: el.tag, component: el.attrs[COMPONENT_ATTR] ?? null, src, sha256: sha, file: actual ? file : null, master, prompt,
|
|
182
|
+
filled: slotIsFilled(sha, actual, master, prompt) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Filled: the declared sha256 is the asset's actual bytes, which are not a brand master, and a prompt file exists.
|
|
186
|
+
const slotIsFilled = (sha, actual, master, prompt) => Boolean(sha && actual && sha === actual && !master && prompt);
|
|
187
|
+
|
|
188
|
+
function actualAssetFile(src, htmlFile) {
|
|
189
|
+
let file = null, actual = null;
|
|
190
|
+
if (src && htmlFile && !/^(data:|https?:|\/\/)/i.test(src)) {
|
|
191
|
+
file = path.resolve(path.dirname(htmlFile), decodeURI(src.split(/[?#]/)[0]));
|
|
192
|
+
try { actual = isFile(file) ? sha256File(file) : null; } catch { actual = null; }
|
|
193
|
+
}
|
|
194
|
+
return { file, actual };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The render sources among `targets` (files, or directories walked without draw-loop rounds). */
|
|
198
|
+
function renderSourcesIn(targets, depth = 6) {
|
|
199
|
+
const out = new Map();
|
|
200
|
+
const add = (file) => { if (/\.html?$/i.test(file)) out.set(path.resolve(file).toLowerCase(), path.resolve(file)); };
|
|
201
|
+
for (const t of targets ?? []) {
|
|
202
|
+
if (isDir(t)) eachFileUnder(t, depth, add, (name) => !SKIP_DIRS.has(name));
|
|
203
|
+
else if (isFile(t)) add(t);
|
|
204
|
+
}
|
|
205
|
+
return [...out.values()];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Every slot of the render sources among `targets`: [{key, id, ui, html, requested, request, filled, src, sha256}].
|
|
210
|
+
* `key` is `<ui record dir, repo-relative>#<slot id>` - one slot per ui record, however many parts show it.
|
|
211
|
+
*/
|
|
212
|
+
export function assetSlotsOf(targets, { repo = process.cwd() } = {}) {
|
|
213
|
+
const bySlot = new Map();
|
|
214
|
+
const mastersByRoot = new Map();
|
|
215
|
+
for (const html of renderSourcesIn(targets)) addHtmlSlots(bySlot, mastersByRoot, html, repo);
|
|
216
|
+
return [...bySlot.values()];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function addHtmlSlots(bySlot, mastersByRoot, html, repo) {
|
|
220
|
+
let text = '';
|
|
221
|
+
try { text = fs.readFileSync(html, 'utf8'); } catch { return; }
|
|
222
|
+
if (!text.includes(ASSET_SLOT_ATTR)) return;
|
|
223
|
+
const requests = readAssetRequests(requestFilesFor(html));
|
|
224
|
+
const ui = slash(path.relative(repo, uiDirOf(html) ?? path.dirname(html)));
|
|
225
|
+
const workRoot = workRootAbove(html);
|
|
226
|
+
if (!mastersByRoot.has(workRoot)) mastersByRoot.set(workRoot, brandMasterShas(workRoot));
|
|
227
|
+
const masters = mastersByRoot.get(workRoot);
|
|
228
|
+
for (const s of slotsOfHtml(text, { htmlFile: html, masters })) {
|
|
229
|
+
const key = `${ui}#${s.id}`;
|
|
230
|
+
const request = requests.find((r) => r.id === s.id) ?? null;
|
|
231
|
+
const slot = { key, id: s.id, ui, html: slash(path.relative(repo, html)), requested: Boolean(request), request: request ? { file: slash(path.relative(repo, request.file)), brief: request.brief } : null,
|
|
232
|
+
filled: s.filled, src: s.src, sha256: s.sha256, master: s.master, prompt: s.prompt ? slash(path.relative(repo, s.prompt)) : null };
|
|
233
|
+
const prior = bySlot.get(key);
|
|
234
|
+
// A slot is filled only when every part that shows it carries the filled bytes.
|
|
235
|
+
bySlot.set(key, prior ? { ...prior, filled: prior.filled && slot.filled } : slot);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Record one `asset-slot-owed` event per owed slot of the job's files and one `asset-slot-filled` per filled one
|
|
241
|
+
* (once per slot key and sha). Returns {owed, filled} newly recorded. `ledger` is an open ledger (appendEvent).
|
|
242
|
+
*/
|
|
243
|
+
export function recordAssetSlots(ledger, { job, repo, files, now = Date.now() }) {
|
|
244
|
+
const db = ledger.db;
|
|
245
|
+
const recorded = { owed: [], filled: [] };
|
|
246
|
+
for (const s of assetSlotsOf(files, { repo })) {
|
|
247
|
+
const kind = s.filled ? ASSET_SLOT_FILLED : ASSET_SLOT_OWED;
|
|
248
|
+
const seen = db.prepare("SELECT 1 FROM events WHERE workflow_id=? AND kind=? AND json_extract(payload_json,'$.key')=? AND json_extract(payload_json,'$.sha256') IS ? AND json_extract(payload_json,'$.jobId')=? LIMIT 1")
|
|
249
|
+
.get(job.workflow_id, kind, s.key, s.sha256, job.job_id);
|
|
250
|
+
if (seen) continue;
|
|
251
|
+
const payload = { key: s.key, id: s.id, ui: s.ui, html: s.html, src: s.src, sha256: s.sha256, requested: s.requested, request: s.request?.file ?? null, jobId: job.job_id, opId: job.op_id, attempt: job.attempt };
|
|
252
|
+
ledger.appendEvent({ workflowId: job.workflow_id, entityType: 'job', entityId: job.job_id, kind, createdAt: now, payload });
|
|
253
|
+
recorded[s.filled ? 'filled' : 'owed'].push(payload);
|
|
254
|
+
}
|
|
255
|
+
return recorded;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The workflow's open asset slots (owed, not filled since): [{key, id, ui, html, request, jobId, opId, owedAt}]. */
|
|
259
|
+
export function openAssetSlots(db, workflowId) {
|
|
260
|
+
return openEventItems(db, workflowId, {
|
|
261
|
+
owedKind: ASSET_SLOT_OWED, resolvedKind: ASSET_SLOT_FILLED, keyOf: (p) => p.key,
|
|
262
|
+
item: (p, row) => ({ key: p.key, id: p.id ?? null, ui: p.ui ?? null, html: p.html ?? null, request: p.request ?? null, requested: p.requested !== false, jobId: p.jobId ?? null, opId: p.opId ?? null, owedAt: row.created_at }),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function main(argv) {
|
|
267
|
+
const [cmd, ...rest] = argv;
|
|
268
|
+
const json = rest.includes('--json');
|
|
269
|
+
const targets = rest.filter((a) => !a.startsWith('--')).map((t) => path.resolve(t));
|
|
270
|
+
if (!['list', 'check'].includes(cmd) || !targets.length) {
|
|
271
|
+
process.stderr.write('use: starci work asset-slot list|check <file|dir>... [--json]\n');
|
|
272
|
+
return 2;
|
|
273
|
+
}
|
|
274
|
+
const slots = assetSlotsOf(targets);
|
|
275
|
+
const unrequested = slots.filter((s) => !s.requested);
|
|
276
|
+
if (json) process.stdout.write(`${JSON.stringify({ ok: cmd === 'list' || !unrequested.length, slots }, null, 2)}\n`);
|
|
277
|
+
else {
|
|
278
|
+
const lines = slots.map((s) => `${s.filled ? 'filled' : 'OWED '} ${s.key}${s.requested ? '' : ' (NO REQUEST in asset-request.md)'} ${s.html}`).join('\n');
|
|
279
|
+
process.stdout.write(`${lines || 'no asset slots'}\n`);
|
|
280
|
+
}
|
|
281
|
+
return cmd === 'check' && unrequested.length ? 1 : 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (isMain(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|