@starci/hfs 4.7.31 → 4.7.33
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 +12 -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 +54 -14
- 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,99 @@
|
|
|
1
|
+
// The release owner's dist-tag judgment and idempotent reconciliation, independent of npm processes.
|
|
2
|
+
|
|
3
|
+
/** Parsed SemVer identifiers, used only to order registry releases and prevent tag rollback. */
|
|
4
|
+
function versionParts(version) {
|
|
5
|
+
const identifiers = /^[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*$/;
|
|
6
|
+
const parts = typeof version === 'string' ? version.split('+') : [];
|
|
7
|
+
if (!parts.length || parts.length > 2 || (parts.length === 2 && !identifiers.test(parts[1]))) throw new Error(`unreadable registry release version ${version}`);
|
|
8
|
+
const boundary = parts[0].indexOf('-');
|
|
9
|
+
const core = (boundary < 0 ? parts[0] : parts[0].slice(0, boundary)).split('.');
|
|
10
|
+
const prerelease = boundary < 0 ? null : parts[0].slice(boundary + 1);
|
|
11
|
+
if (core.length !== 3 || core.some((part) => !/^(0|[1-9]\d*)$/.test(part)) || (prerelease !== null && !identifiers.test(prerelease))) throw new Error(`unreadable registry release version ${version}`);
|
|
12
|
+
const pre = prerelease?.split('.') ?? [];
|
|
13
|
+
if (pre.some((part) => /^0\d+$/.test(part))) throw new Error(`invalid numeric prerelease identifier in ${version}`);
|
|
14
|
+
return { core: core.map(BigInt), pre };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const compareIdentifier = (a, b) => {
|
|
18
|
+
const numericA = /^\d+$/.test(a), numericB = /^\d+$/.test(b);
|
|
19
|
+
if (numericA && numericB) return BigInt(a) < BigInt(b) ? -1 : Number(BigInt(a) > BigInt(b));
|
|
20
|
+
if (numericA !== numericB) return numericA ? -1 : 1;
|
|
21
|
+
return a < b ? -1 : Number(a > b);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** SemVer precedence, including numeric prerelease ordering; build metadata has no precedence. */
|
|
25
|
+
function compareVersions(a, b) {
|
|
26
|
+
const left = versionParts(a), right = versionParts(b);
|
|
27
|
+
for (const i of left.core.keys()) {
|
|
28
|
+
if (left.core[i] !== right.core[i]) return left.core[i] < right.core[i] ? -1 : 1;
|
|
29
|
+
}
|
|
30
|
+
if (!left.pre.length || !right.pre.length) return Number(!left.pre.length) - Number(!right.pre.length);
|
|
31
|
+
for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
|
|
32
|
+
if (left.pre[i] === undefined) return -1;
|
|
33
|
+
if (right.pre[i] === undefined) return 1;
|
|
34
|
+
const compared = compareIdentifier(left.pre[i], right.pre[i]);
|
|
35
|
+
if (compared) return compared;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Numbered prereleases share their core and nonnumeric prefix; stable releases share major/minor. */
|
|
41
|
+
const releaseLine = (version) => {
|
|
42
|
+
const { core, pre } = versionParts(version);
|
|
43
|
+
if (!pre.length) return `${core.slice(0, 2).join('.')}-stable`;
|
|
44
|
+
const prefix = /^\d+$/.test(pre.at(-1)) ? pre.slice(0, -1) : pre;
|
|
45
|
+
return `${core.join('.')}-${prefix.join('.')}`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Plan the release's declared tags from a complete registry read. Unknown reads and historical
|
|
50
|
+
* releases fail closed; a newer tag on another release line is preserved rather than rolled back.
|
|
51
|
+
* Returns serializable {tag, version, current, action} entries for the native dry-run plan.
|
|
52
|
+
*/
|
|
53
|
+
export function runtimeTagPlan({ version, tags, state, uploadTag = null }) {
|
|
54
|
+
versionParts(version);
|
|
55
|
+
if (!Array.isArray(tags) || !tags.length || tags.some((tag) => typeof tag !== 'string' || !/^[a-z][a-z0-9-]*$/i.test(tag)))
|
|
56
|
+
throw new Error('runtime publication requires declared npm dist-tags');
|
|
57
|
+
if (!['present', 'absent'].includes(state?.state) || !Array.isArray(state.versions) || !state.distTags || Array.isArray(state.distTags) || typeof state.distTags !== 'object')
|
|
58
|
+
throw new Error('runtime dist-tags and version inventory could not be read');
|
|
59
|
+
if (state.state === 'present' && !state.versions.includes(version)) throw new Error('runtime version inventory omits the published release');
|
|
60
|
+
const newer = state.versions.find((entry) => releaseLine(entry) === releaseLine(version) && compareVersions(entry, version) > 0);
|
|
61
|
+
if (newer) throw new Error(`runtime release ${version} is behind ${newer} on its release line; dist-tags cannot move backwards`);
|
|
62
|
+
const plan = [...new Set(tags)].map((tag) => {
|
|
63
|
+
const current = state.distTags[tag] ?? null;
|
|
64
|
+
if (current !== null && typeof current !== 'string') throw new Error(`unreadable npm dist-tag ${tag}`);
|
|
65
|
+
const compared = current === null ? -1 : compareVersions(current, version);
|
|
66
|
+
let action = current === version ? 'confirmed' : 'set';
|
|
67
|
+
if (compared > 0) action = 'preserve';
|
|
68
|
+
return { tag, version: compared > 0 ? current : version, current,
|
|
69
|
+
action };
|
|
70
|
+
});
|
|
71
|
+
if (uploadTag && plan.some((entry) => entry.tag === uploadTag && entry.action === 'preserve'))
|
|
72
|
+
throw new Error(`runtime upload channel ${uploadTag} points to a newer release; publication cannot move it backwards`);
|
|
73
|
+
return plan;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Repair only stale declared tags of an integrity-confirmed runtime version, then read every tag
|
|
78
|
+
* back. The registry seam owns effects; a failed write/read throws and can be retried without upload.
|
|
79
|
+
*/
|
|
80
|
+
export function reconcileRuntimeTags({ row, tags, registry, out, sleep = null, waitMs = 0, stepMs = 5000 }) {
|
|
81
|
+
const state = registry.state(row.name, row.version);
|
|
82
|
+
if (state.state !== 'present' || state.integrity !== row.registry.integrity) throw new Error('runtime registry integrity changed before dist-tag reconciliation');
|
|
83
|
+
const plan = runtimeTagPlan({ version: row.version, tags, state });
|
|
84
|
+
for (const entry of plan.filter((item) => item.action === 'set')) {
|
|
85
|
+
const result = registry.tag(row.name, row.version, entry.tag);
|
|
86
|
+
if (!result.ok) throw new Error(`npm dist-tag ${entry.tag} failed (exit ${result.status ?? 'unknown'})`);
|
|
87
|
+
}
|
|
88
|
+
// Registry propagation of a tag write can lag; poll the read-back inside the window before calling it stale.
|
|
89
|
+
const deadline = Date.now() + waitMs;
|
|
90
|
+
let confirmed = registry.state(row.name, row.version);
|
|
91
|
+
while (sleep && Date.now() < deadline && plan.some((entry) => confirmed.distTags?.[entry.tag] !== entry.version)) {
|
|
92
|
+
sleep(stepMs);
|
|
93
|
+
confirmed = registry.state(row.name, row.version);
|
|
94
|
+
}
|
|
95
|
+
if (confirmed.state !== 'present' || confirmed.integrity !== state.integrity || plan.some((entry) => confirmed.distTags?.[entry.tag] !== entry.version))
|
|
96
|
+
throw new Error('runtime dist-tag read-back did not confirm the declared targets and immutable integrity');
|
|
97
|
+
for (const entry of plan) out(` dist-tag ${row.name}@${entry.version} ${entry.tag}: ${entry.action === 'preserve' ? 'newer release preserved' : 'confirmed by registry read-back'}`);
|
|
98
|
+
return plan;
|
|
99
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// seat-env.mjs — the seat identity one Orca terminal's environment claims, and the env a runtime child gets.
|
|
2
|
+
//
|
|
3
|
+
// Orca exports the terminal's identity into the shell it owns (ORCA_TERMINAL_HANDLE, ORCA_PANE_KEY, ORCA_TAB_ID,
|
|
4
|
+
// ORCA_WORKTREE_ID, ORCA_AGENT_* — the agent hook endpoint and token included), and a session can carry the caller
|
|
5
|
+
// markers STARCI_ROLE, STARCI_GUARD_FILE and STARCI_CALLER. Caller and guard logic binds a process to a seat by those
|
|
6
|
+
// variables (scripts/guards/op-caller.mjs callerOf, scripts/guards/command-policy.mjs shimDecision, the seat bindings of
|
|
7
|
+
// scripts/guards/hook-install.mjs), so a child the runtime spawns for its own work — the package proof of the land gate,
|
|
8
|
+
// the scaffold e2e, a staging `npm ci` and its lifecycle scripts — that inherits them is admitted as
|
|
9
|
+
// the seat: a supervisor seat's `node install.js` reads as a raw supervisor tool call (RIGHTS_RAW_TOOL) and a spec's
|
|
10
|
+
// kernel call resolves the seat's custody (kernel-caller-unknown). The seat's own agent tool calls keep the identity;
|
|
11
|
+
// every other child of it gets this env.
|
|
12
|
+
//
|
|
13
|
+
// The seat's PATH can likewise carry the per-user guard-shim directory first (<home>/.starci/bin,
|
|
14
|
+
// packages/cli/src/shim.mjs writeRuntimeShim — put there by scripts/agent/starci-shim.mjs addStarciShimToPath): its
|
|
15
|
+
// wrappers judge `node`, `git`, `npm` and friends through shimDecision before reaching the real tool. Without the
|
|
16
|
+
// seat identity a wrapper binds no role and passes through, but a runtime child that never answers to the seat —
|
|
17
|
+
// the codex app-server probe, whose `codex` launcher resolves `node` through PATH — also gets PATH without the shim
|
|
18
|
+
// directory, so nothing it spawns is ever re-judged.
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
/** The variables that bind a process to the calling seat or claim a caller identity. */
|
|
23
|
+
const SEAT_ENV_VARS = Object.freeze(['ORCA_TERMINAL_HANDLE', 'ORCA_PANE_KEY', 'ORCA_TAB_ID', 'ORCA_WORKTREE_ID', 'STARCI_ROLE', 'STARCI_GUARD_FILE', 'STARCI_CALLER']);
|
|
24
|
+
|
|
25
|
+
/** `parent` (the caller's env) without the seat identity: a child that is not the seat's own agent tool call runs unbound. Pure — the caller names the env; the base tier never reads process.env itself (RT_BASE_IMPURE). */
|
|
26
|
+
export function withoutSeatEnv(parent) {
|
|
27
|
+
const env = { ...parent };
|
|
28
|
+
for (const key of Object.keys(env)) if (SEAT_ENV_VARS.includes(key) || key.startsWith('ORCA_AGENT_')) delete env[key];
|
|
29
|
+
return env;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `parent` without the guard-shim directory on PATH (any casing of the PATH key, the one <home>/.starci/bin entry wherever it sits). Pure — the caller names the env; `home`/`platform` are injectable seams. */
|
|
33
|
+
export function withoutSeatShim(parent, { home = os.homedir(), platform = process.platform } = {}) {
|
|
34
|
+
const env = { ...parent };
|
|
35
|
+
const key = Object.keys(env).find((name) => name.toLowerCase() === 'path');
|
|
36
|
+
if (!key) return env;
|
|
37
|
+
const shim = path.resolve(path.join(home, '.starci', 'bin'));
|
|
38
|
+
const resolved = (entry) => path.resolve(String(entry).replace(/^"(.*)"$/, '$1'));
|
|
39
|
+
const same = (entry) => platform === 'win32' ? resolved(entry).toLowerCase() === shim.toLowerCase() : resolved(entry) === shim;
|
|
40
|
+
const delimiter = platform === 'win32' ? ';' : ':';
|
|
41
|
+
env[key] = String(env[key]).split(delimiter).filter((entry) => entry && !same(entry)).join(delimiter);
|
|
42
|
+
return env;
|
|
43
|
+
}
|
|
@@ -56,6 +56,45 @@ const ASSIGNED_UNQUOTED = new RegExp(
|
|
|
56
56
|
SECRET_NAME + String.raw`(?!['"\`]|env\()([A-Za-z0-9_+=-]{12,})(?![A-Za-z0-9_+=.(\[-])`,
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
+
const SQL_ROLE_HEAD = /^\s*(?:CREATE|ALTER)\s+(?:ROLE|USER)\b/i;
|
|
60
|
+
const SQL_STATEMENT = /[^;\r\n]+/g;
|
|
61
|
+
const SQL_PASSWORD_WORD = /\bPASSWORD\s+/gi;
|
|
62
|
+
const PSQL_PASSWORD_REFERENCE = /:(?:'[A-Za-z_]\w*'|"[A-Za-z_]\w*")/y;
|
|
63
|
+
const SQL_PASSWORD_QUOTED = /'([^'\r\n]{12,})'/y;
|
|
64
|
+
const ASSIGNED_PLACEHOLDER = /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i;
|
|
65
|
+
const SQL_PASSWORD_RULE = { name: 'assigned-secret' };
|
|
66
|
+
|
|
67
|
+
/** Read only password tokens in role statements; sticky value readers never retry at later character positions. */
|
|
68
|
+
function sqlPasswordParts(file, text) {
|
|
69
|
+
const references = [], literals = [];
|
|
70
|
+
if (!/\.(?:sql|sh)(?:\.tpl)?$/i.test(file ?? '')) return { references, literals };
|
|
71
|
+
for (const statement of text.matchAll(SQL_STATEMENT)) {
|
|
72
|
+
if (!SQL_ROLE_HEAD.test(statement[0])) continue;
|
|
73
|
+
for (const keyword of statement[0].matchAll(SQL_PASSWORD_WORD)) {
|
|
74
|
+
const start = statement.index + keyword.index + keyword[0].length;
|
|
75
|
+
PSQL_PASSWORD_REFERENCE.lastIndex = start;
|
|
76
|
+
SQL_PASSWORD_QUOTED.lastIndex = start;
|
|
77
|
+
const reference = PSQL_PASSWORD_REFERENCE.exec(text);
|
|
78
|
+
const literal = SQL_PASSWORD_QUOTED.exec(text);
|
|
79
|
+
if (reference) references.push([start, start + reference[0].length]);
|
|
80
|
+
if (literal) literals.push(literal[1]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { references, literals };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Psql variable references retain their positions while the assignment heuristic sees no literal value there. */
|
|
87
|
+
function maskedReferences(text, references) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
let cursor = 0;
|
|
90
|
+
for (const [start, end] of references) {
|
|
91
|
+
parts.push(text.slice(cursor, start), ' '.repeat(end - start));
|
|
92
|
+
cursor = end;
|
|
93
|
+
}
|
|
94
|
+
parts.push(text.slice(cursor));
|
|
95
|
+
return parts.join('');
|
|
96
|
+
}
|
|
97
|
+
|
|
59
98
|
const NAME_WORD = (word, tail) => new RegExp(`^${NAME_REST}${SEGMENT_BEFORE}(?:${word})${tail}`);
|
|
60
99
|
const SECRET_NAME_TEST = NAME_WORD(SECRET_WORD, String.raw`(?:[_\-.]|[A-Z]|[^A-Za-z0-9_$]|$)`);
|
|
61
100
|
const BENIGN_TAIL_TEST = NAME_WORD(BENIGN_WORD, '$');
|
|
@@ -84,27 +123,37 @@ export const SECRET_PATTERNS = [
|
|
|
84
123
|
// A spec file's keyword-assigned values are test inputs (a spec's sign-in
|
|
85
124
|
// form password and a mocked accessToken once refused a push): the heuristic skips spec files too.
|
|
86
125
|
// A SCREAMING_SNAKE quoted value in source is a public code (an enum member whose value is an upper-case code), not a credential.
|
|
87
|
-
{ name: 'assigned-secret', re: ASSIGNED_QUOTED, sourceCode: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/, placeholder:
|
|
126
|
+
{ name: 'assigned-secret', re: ASSIGNED_QUOTED, sourceCode: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/, placeholder: ASSIGNED_PLACEHOLDER,
|
|
88
127
|
skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$/ },
|
|
89
128
|
// An unquoted value in TypeScript or JavaScript source is an identifier or a type (`token: InjectionToken`,
|
|
90
129
|
// `MODULE_OPTIONS_TOKEN: DATABASE_OPTIONS_TOKEN`), never a literal; the unquoted form is the dotenv, compose and toml shape.
|
|
91
|
-
{ name: 'assigned-secret', re: ASSIGNED_UNQUOTED, placeholder:
|
|
130
|
+
{ name: 'assigned-secret', re: ASSIGNED_UNQUOTED, placeholder: ASSIGNED_PLACEHOLDER,
|
|
92
131
|
skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/ },
|
|
93
132
|
];
|
|
94
133
|
|
|
95
134
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
const
|
|
135
|
+
/** Matched pattern metadata stays private; callers receive pattern names, never credential values. */
|
|
136
|
+
function matchingSecretPatterns(file, text) {
|
|
137
|
+
const matches = [];
|
|
138
|
+
const sql = sqlPasswordParts(file, text);
|
|
139
|
+
const assignments = maskedReferences(text, sql.references);
|
|
99
140
|
for (const rule of SECRET_PATTERNS) {
|
|
100
141
|
if (rule.skipFile?.test(file ?? '')) continue;
|
|
101
|
-
const hit = rule.re.exec(text);
|
|
102
|
-
if (hit && rule.
|
|
103
|
-
if (hit && !rule.placeholder?.test(hit[1] ?? '')) names.push(rule.name);
|
|
142
|
+
const hit = rule.re.exec(rule.name === 'assigned-secret' ? assignments : text);
|
|
143
|
+
if (hit && !rule.placeholder?.test(hit[1] ?? '')) matches.push({ rule, value: hit[1] ?? '' });
|
|
104
144
|
}
|
|
105
|
-
|
|
145
|
+
for (const value of sql.literals) if (!ASSIGNED_PLACEHOLDER.test(value)) matches.push({ rule: SQL_PASSWORD_RULE, value });
|
|
146
|
+
return matches;
|
|
106
147
|
}
|
|
107
148
|
|
|
149
|
+
/** The strict secret-shape judgement used by Work custody, including uppercase keyword-assigned credentials. */
|
|
150
|
+
export const secretPatternHits = (file, text) => matchingSecretPatterns(file, text).map(({ rule }) => rule.name);
|
|
151
|
+
|
|
152
|
+
/** The source judgement also admits public SCREAMING_SNAKE codes; every provider shape remains refused. */
|
|
153
|
+
export const secretHits = (file, text) => matchingSecretPatterns(file, text)
|
|
154
|
+
.filter(({ rule, value }) => !(rule.sourceCode?.test(value) && /\.[cm]?[jt]sx?$/.test(file ?? '')))
|
|
155
|
+
.map(({ rule }) => rule.name);
|
|
156
|
+
|
|
108
157
|
const BADGE_BEGIN = '<!-- >>> starci app sync (managed block; do not edit) >>> -->';
|
|
109
158
|
const BADGE_END = '<!-- <<< starci app sync <<< -->';
|
|
110
159
|
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Shell scanners share the same projection: whole-line comments do not execute.
|
|
2
|
+
|
|
3
|
+
/** Keep executable lines and line breaks while excluding whole-line shell comments. */
|
|
4
|
+
export const shellCommandSource = (text) => String(text ?? '').split(/\r?\n/).filter((line) => !/^\s*#/.test(line)).join('\n');
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// source-phrases.mjs - the Vietnamese phrase lists runtime source matches against owner or product text are DATA,
|
|
2
|
+
// not source: they live in modules/goal/source-phrases.yaml (its leaf keys are declared Vietnamese fields,
|
|
3
|
+
// scripts/lib/language.mjs DECLARED_VIETNAMESE_FIELDS - like the archetypes.yaml phrase lists) so the matchers'
|
|
4
|
+
// source stays English. A list is plain words or regex alternatives, per its comment in the lexicon.
|
|
5
|
+
//
|
|
6
|
+
// phrasesOf('owed.ownerWord') -> the list as written (['<owner word>', ...])
|
|
7
|
+
// altOf('owed.ownerWord') -> the regex alternation 'a|b|c'; '(?!)' - never matches - when absent
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
11
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
12
|
+
|
|
13
|
+
const SOURCE_PHRASES_FILE = 'modules/goal/source-phrases.yaml';
|
|
14
|
+
|
|
15
|
+
const cache = new Map();
|
|
16
|
+
|
|
17
|
+
/** The lexicon's `phrases` map ({<group>: {<list>: string[]}}), cached per root. */
|
|
18
|
+
function sourcePhrases(root = skillRoot) {
|
|
19
|
+
if (!cache.has(root)) {
|
|
20
|
+
let doc = null;
|
|
21
|
+
try { doc = parseYaml(fs.readFileSync(path.join(root, ...SOURCE_PHRASES_FILE.split('/')), 'utf8')); } catch { doc = null; }
|
|
22
|
+
cache.set(root, doc?.phrases && typeof doc.phrases === 'object' ? doc.phrases : {});
|
|
23
|
+
}
|
|
24
|
+
return cache.get(root);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The phrase list `dotKey` (`<group>.<list>`) of `root`, [] when absent. */
|
|
28
|
+
export function phrasesOf(dotKey, root = skillRoot) {
|
|
29
|
+
const value = dotKey.split('.').reduce((v, k) => (v && typeof v === 'object' ? v[k] : undefined), sourcePhrases(root));
|
|
30
|
+
return (Array.isArray(value) ? value : []).map(String);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The regex alternation of `dotKey` ('a|b|c'); '(?!)' - a never-match - when the list is empty. */
|
|
34
|
+
export const altOf = (dotKey, root = skillRoot) => phrasesOf(dotKey, root).join('|') || '(?!)';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// sqlite.mjs — the small better-sqlite3 probes the ledger and machine stores share. The handle is the
|
|
2
|
+
// caller's; these helpers prepare statements on it and return its own result shapes.
|
|
3
|
+
|
|
4
|
+
/** Whether the database holds a table `name` (`views: true` also counts views). */
|
|
5
|
+
export const hasTable = (db, name, { views = false } = {}) =>
|
|
6
|
+
Boolean(db.prepare(`SELECT 1 FROM sqlite_master WHERE type${views ? " IN ('table','view')" : "='table'"} AND name=?`).get(name));
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* INSERT the [column, value] `pairs` into `table` (`orIgnore` spells OR IGNORE); returns the statement's
|
|
10
|
+
* run() result. Column naming and value encoding are the caller's — this is only the statement shape.
|
|
11
|
+
*/
|
|
12
|
+
const insertPairs = (db, table, pairs, { orIgnore = false } = {}) =>
|
|
13
|
+
db.prepare(`INSERT ${orIgnore ? 'OR IGNORE ' : ''}INTO ${table}(${pairs.map((p) => p[0]).join(',')}) VALUES(${pairs.map(() => '?').join(',')})`)
|
|
14
|
+
.run(...pairs.map((p) => p[1]));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The per-store `insertRow`: binds the caller's own [column, value] mapper (`cellsOf` — where the store's
|
|
18
|
+
* column validation and value encoding live) to an insertPairs statement.
|
|
19
|
+
*/
|
|
20
|
+
export const insertRowWith = (cellsOf) => (db, table, row, { orIgnore = false } = {}) =>
|
|
21
|
+
insertPairs(db, table, cellsOf(db, table, row), { orIgnore });
|
|
@@ -5,7 +5,8 @@ import { repositoryOf } from './stack-services.mjs';
|
|
|
5
5
|
|
|
6
6
|
const PINNED_IMAGE = /@sha256:[a-f0-9]{64}$/i;
|
|
7
7
|
const VERSIONED_SECRET = /-v[1-9]\d*$/;
|
|
8
|
-
|
|
8
|
+
// R276 (scripts/hfs/rules/stacks-lite-backing.mjs) judges declared Redis and refuses full-only backing services in lite.
|
|
9
|
+
const BACKING_IMAGE_SQL_AND_CACHE = 'postgres(?:ql)?|postgresql-repmgr|mysql(?:-server)?|mongo(?:db)?|mongodb-community-server';
|
|
9
10
|
const BACKING_IMAGE_BROKERS_AND_OTHERS = '(?:cp-)?kafka(?:-native)?|keycloak|minio|rabbitmq';
|
|
10
11
|
const BACKING_IMAGE = new RegExp(`^(?:${BACKING_IMAGE_SQL_AND_CACHE}|${BACKING_IMAGE_BROKERS_AND_OTHERS})$`, 'i');
|
|
11
12
|
const mapping = value => plain(value) ? value : {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// tar-files.mjs - the regular files of a gzipped tar (an npm tarball), read in memory: {path -> Buffer}. Pure: no disk, no
|
|
2
|
+
// child process. Only what `npm pack` writes is understood (ustar headers, a `path` pax record and the ustar prefix
|
|
3
|
+
// honoured, other pax records skipped); a link or a device entry is ignored.
|
|
4
|
+
import zlib from 'node:zlib';
|
|
5
|
+
|
|
6
|
+
const BLOCK = 512;
|
|
7
|
+
const text = (buf, from, len) => {
|
|
8
|
+
const value = buf.toString('utf8', from, from + len);
|
|
9
|
+
const nul = value.indexOf('\0');
|
|
10
|
+
return nul < 0 ? value : value.slice(0, nul);
|
|
11
|
+
};
|
|
12
|
+
const octal = (buf, from, len) => Number.parseInt(text(buf, from, len).trim() || '0', 8);
|
|
13
|
+
|
|
14
|
+
/** Map<string, Buffer> of the regular files in `tgz` (a Buffer). Throws on a truncated or corrupt archive. */
|
|
15
|
+
export function tarFiles(tgz) {
|
|
16
|
+
const tar = zlib.gunzipSync(tgz);
|
|
17
|
+
const files = new Map();
|
|
18
|
+
let at = 0;
|
|
19
|
+
let paxPath = null;
|
|
20
|
+
while (at + BLOCK <= tar.length) {
|
|
21
|
+
const header = tar.subarray(at, at + BLOCK);
|
|
22
|
+
if (header.every((b) => b === 0)) break;
|
|
23
|
+
const size = octal(header, 124, 12);
|
|
24
|
+
const type = String.fromCodePoint(header[156] || 48);
|
|
25
|
+
const prefix = text(header, 345, 155);
|
|
26
|
+
const name = paxPath ?? (prefix ? `${prefix}/${text(header, 0, 100)}` : text(header, 0, 100));
|
|
27
|
+
const body = tar.subarray(at + BLOCK, at + BLOCK + size);
|
|
28
|
+
if (body.length < size) throw new Error(`tar entry ${name} is truncated`);
|
|
29
|
+
at += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
|
|
30
|
+
if (type === 'x') {
|
|
31
|
+
const record = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(body.toString('utf8'));
|
|
32
|
+
paxPath = record ? record[1] : null;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
paxPath = null;
|
|
36
|
+
if (type === '0') files.set(name, Buffer.from(body));
|
|
37
|
+
}
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
@@ -25,3 +25,13 @@ export function importsOf(ts, sourceFile, { moduleKey = 'module', stringLiterals
|
|
|
25
25
|
|
|
26
26
|
/** Catalog values unwrap parentheses, `as`, and `satisfies`, preserving assertions and non-null nodes. */
|
|
27
27
|
export const unwrapCatalogExpression = (ts, node) => unwrapExpression(ts, node, { typeAssertions: false, nonNullExpressions: false });
|
|
28
|
+
|
|
29
|
+
/** Bind this file in isolation: the compiler resolves lexical names, but imports and library files are never loaded. */
|
|
30
|
+
export function lexicalChecker(ts, source) {
|
|
31
|
+
const options = { noLib: true, noResolve: true, allowJs: true };
|
|
32
|
+
const host = ts.createCompilerHost(options);
|
|
33
|
+
host.getSourceFile = (file) => file === source.fileName ? source : undefined;
|
|
34
|
+
host.fileExists = () => false;
|
|
35
|
+
host.readFile = () => undefined;
|
|
36
|
+
return ts.createProgram([source.fileName], options, host).getTypeChecker();
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// worktree-exclude.mjs — where a product repository keeps the runtime's own git worktrees (scratch trees made by
|
|
2
|
+
// scripts/machine/worktree-git.mjs createScratchWorktree), and the one test every scanner uses to stay out of them.
|
|
3
|
+
//
|
|
4
|
+
// Layout inside a product repository:
|
|
5
|
+
// <repo>/.starciwork/worktrees/<name> one runtime scratch tree
|
|
6
|
+
// The directory is git-excluded (.git/info/exclude), so `git status`,
|
|
7
|
+
// `git ls-files --others --exclude-standard` and sonar never see it; every scanner that walks the filesystem
|
|
8
|
+
// (canon-scan's eslint, starci app lint, the architecture walkers, input digests, proof-integrity, Work readers)
|
|
9
|
+
// skips it with isWorktreesPath / WORKTREES_IGNORE_GLOBS, or it would lint and hash every sibling worktree.
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { posixPath } from './path-key.mjs';
|
|
12
|
+
|
|
13
|
+
/** The worktrees directory, relative to the product repository root (posix). */
|
|
14
|
+
export const WORKTREES_REL = '.starciwork/worktrees';
|
|
15
|
+
/** Glob patterns an ESLint/tsc-style ignore list takes. */
|
|
16
|
+
export const WORKTREES_IGNORE_GLOBS = Object.freeze([`${WORKTREES_REL}/**`, `**/${WORKTREES_REL}/**`]);
|
|
17
|
+
|
|
18
|
+
const posix = posixPath;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* True when `rel` (a path relative to a repository root, either slash) is the worktrees directory or inside it.
|
|
22
|
+
* With `root`, `rel` may be absolute: it is made relative to root first.
|
|
23
|
+
*/
|
|
24
|
+
export function isWorktreesPath(rel, { root = null } = {}) {
|
|
25
|
+
let p = posix(rel);
|
|
26
|
+
if (root && path.isAbsolute(String(rel))) p = posix(path.relative(root, String(rel)));
|
|
27
|
+
while (p.endsWith('/')) p = p.slice(0, -1);
|
|
28
|
+
return p === WORKTREES_REL || p.startsWith(`${WORKTREES_REL}/`) || p.includes(`/${WORKTREES_REL}/`) || p.endsWith(`/${WORKTREES_REL}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when `abs` is a worktrees dir (or inside one) BELOW the scan root `root`. Always relative to the root a walker
|
|
33
|
+
* started from: a walker whose root is itself a runtime tree (<repo>/.starciwork/worktrees/<name>) must still walk it.
|
|
34
|
+
*/
|
|
35
|
+
export const underWorktrees = (root, abs) => isWorktreesPath(path.relative(path.resolve(String(root)), path.resolve(String(abs))));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// worktree-record.mjs — the workflow-worktree registry row a checkpoint or a gate needs.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `rec` must be `workflowId`'s live worktree row: present in the registry, its path on disk, a branch
|
|
6
|
+
* recorded. Throws {code: 'workflow-worktree-missing'} otherwise; returns `rec`.
|
|
7
|
+
*/
|
|
8
|
+
export function requireWorktreeRecord(rec, workflowId) {
|
|
9
|
+
const missing = (message) => { throw Object.assign(new Error(message), { code: 'workflow-worktree-missing' }); };
|
|
10
|
+
if (!rec) missing(`workflow ${workflowId} has no workflow worktree in the registry`);
|
|
11
|
+
if (!rec.path || !fs.existsSync(rec.path)) missing(`the worktree of workflow ${workflowId} (${rec.path ?? '-'}) is gone`);
|
|
12
|
+
if (!rec.branch) missing(`the registry records no branch for the worktree of workflow ${workflowId} (${rec.path})`);
|
|
13
|
+
return rec;
|
|
14
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// artifact-hold.mjs — the retention exemption for the typed-log ledger. A registered ledger's runtime.sqlite
|
|
2
|
+
// (it holds the typed logs, scripts/kernel/typed-logs.mjs) and a tree holding it are never removed by housekeeping, whatever the
|
|
3
|
+
// workflow's phase (running, finished, archived). Every runtime delete of a tree passes artifactHoldReason as
|
|
4
|
+
// scripts/api/fs/safe-remove.mjs safeRemove's `hold` (a call without one is refused), and every housekeeping unlink asks
|
|
5
|
+
// artifactHoldOf first.
|
|
6
|
+
// Proofs, artifacts and logs are deleted ONLY by the owner-approved workflow purge (scripts/work/purge-workflow.mjs:
|
|
7
|
+
// archive to a verified ZIP, then delete the finished workflow as a unit), never here.
|
|
8
|
+
//
|
|
9
|
+
// The ledgers are the ones the machine registry enrols (machine.sqlite `ledgers`).
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { localProjectsRoot, machineFileFor, readMachine } from '../../engine/db/machine.mjs';
|
|
13
|
+
import { pathKey, sameOrUnder } from '../lib/path-key.mjs';
|
|
14
|
+
|
|
15
|
+
const REGISTRY_TTL_MS = 30000;
|
|
16
|
+
const norm = pathKey;
|
|
17
|
+
let cache = { file: null, at: 0, repos: [] };
|
|
18
|
+
|
|
19
|
+
/** The repositories whose ledgers the registry enrols: [{repo, ledger}], cached briefly per registry file; null when the registry cannot be read. */
|
|
20
|
+
function registeredRepos({ env = process.env, now = Date.now() } = {}) {
|
|
21
|
+
const file = machineFileFor(env);
|
|
22
|
+
if (cache.file === file && now - cache.at < REGISTRY_TTL_MS) return cache.repos;
|
|
23
|
+
const repos = fs.existsSync(file) ? readMachine((m) => m.listLedgers().map((l) => ({ ledger: l.file, repo: l.repoRoot })), null, { file, env }) : [];
|
|
24
|
+
cache = { file, at: now, repos };
|
|
25
|
+
return repos;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Null when removing `target` touches no held file; else {ledger, repo, paths, count}.
|
|
30
|
+
* Held: a registered ledger file at or under `target`. An unreadable registry holds every path in or
|
|
31
|
+
* holding the projects root of the host-state directory (fail closed).
|
|
32
|
+
*/
|
|
33
|
+
export function artifactHoldOf(target, { env = process.env, repos = registeredRepos({ env }) } = {}) {
|
|
34
|
+
const t = norm(target);
|
|
35
|
+
if (repos === null) {
|
|
36
|
+
const projects = norm(localProjectsRoot(env));
|
|
37
|
+
return sameOrUnder(t, projects) || sameOrUnder(projects, t) ? { ledger: machineFileFor(env), repo: null, paths: [], count: null, error: 'the machine registry cannot be read' } : null;
|
|
38
|
+
}
|
|
39
|
+
for (const { ledger, repo } of repos) {
|
|
40
|
+
// The typed logs (scripts/kernel/typed-logs.mjs: the ledger's logs table) are append-only history: never swept.
|
|
41
|
+
if (ledger && sameOrUnder(norm(ledger), t) && fs.existsSync(ledger)) return { ledger, repo, paths: [path.basename(ledger)], count: 1 };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** artifactHoldOf as one refusal line, or null. */
|
|
47
|
+
export const artifactHoldReason = (target, options) => {
|
|
48
|
+
const hold = artifactHoldOf(target, options);
|
|
49
|
+
if (!hold) return null;
|
|
50
|
+
return hold.error ? `${hold.error} (${hold.ledger})`
|
|
51
|
+
: `${hold.count} ledger file(s) of ${hold.ledger} (${hold.paths.join(', ')})`;
|
|
52
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ask-receipts.mjs — where the answer to an ask is kept (docs/ledger-db.md §§5–6): serve-ask answers →
|
|
2
|
+
// decisions + blobs; never a file under .starciwork/kernel-evidence.
|
|
3
|
+
//
|
|
4
|
+
// An answer (owner form, Telegram reply, config auto-accept, autopilot) is one starci/ask-answer@1 receipt. It is put
|
|
5
|
+
// in the blob store (redacted), indexed as a kernel artifact of the attempt that asked - job_artifacts name
|
|
6
|
+
// asks/<dispatchId>/answer-<ms>.json, origin kernel - and recorded as one `decisions` row (decider owner | the
|
|
7
|
+
// answering actor, subject the attempt, result {receiptSha256, artifactId, ...}). The receipt's file is the blob
|
|
8
|
+
// file (receiptPath, read-only), and a Work record cites it as `blob:<sha256>` (or by its decision id), never by a
|
|
9
|
+
// repository path.
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { recordDecision } from '../../engine/db/ledger.mjs';
|
|
13
|
+
import { sha256 } from '../../engine/digest.mjs';
|
|
14
|
+
import { artifactRoot, blobPath } from '../../engine/db/blob.mjs';
|
|
15
|
+
import { stageBlob, putArtifact } from './evidence-store.mjs';
|
|
16
|
+
import { slash } from '../lib/path-key.mjs';
|
|
17
|
+
|
|
18
|
+
const RECEIPT_PREFIX = 'asks/';
|
|
19
|
+
const receiptNameOf = (dispatchId, at) => `${RECEIPT_PREFIX}${dispatchId}/answer-${at}.json`;
|
|
20
|
+
const BLOB_REF = /^blob:([a-f0-9]{64})$/;
|
|
21
|
+
|
|
22
|
+
/** The receipt as a staged blob (outside any transaction). */
|
|
23
|
+
export const stageReceipt = (receipt) => stageBlob(Buffer.from(JSON.stringify(receipt, null, 2)), { mediaType: 'application/json' });
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Inside the caller's transaction: index a staged receipt as the asking attempt's kernel artifact and record the
|
|
27
|
+
* answer as a decisions row. Returns {receiptPath, receiptSha, receiptRef, artifactId, decisionId}.
|
|
28
|
+
*/
|
|
29
|
+
function fileAskReceipt(db, { workflowId, dispatchId, receipt, blob, at = Date.now() }) {
|
|
30
|
+
const report = db.prepare('SELECT attempt_id,job_id FROM reports WHERE workflow_id=? AND dispatch_id=?').get(workflowId, dispatchId) ?? null;
|
|
31
|
+
const attempt = report ? db.prepare('SELECT attempt_id,job_id,op_id FROM op_attempts WHERE attempt_id=?').get(report.attempt_id) : null;
|
|
32
|
+
let name = receiptNameOf(dispatchId, at);
|
|
33
|
+
let n = at + 1;
|
|
34
|
+
while (db.prepare('SELECT 1 FROM job_artifacts WHERE workflow_id=? AND name=?').get(workflowId, name)) { name = receiptNameOf(dispatchId, n); n += 1; }
|
|
35
|
+
const { artifactId } = putArtifact(db, { workflowId, attemptId: attempt?.attempt_id ?? null, jobId: attempt?.job_id ?? report?.job_id ?? null, opId: attempt?.op_id ?? null,
|
|
36
|
+
role: 'other', kind: 'report', name, blob, origin: 'kernel', now: at });
|
|
37
|
+
const answeredBy = String(receipt.answeredBy ?? 'unknown');
|
|
38
|
+
const option = receipt.option ?? (receipt.optionIndex != null ? `option ${Number(receipt.optionIndex) + 1}` : 'answered');
|
|
39
|
+
const decisionId = recordDecision(db, { workflowId, decider: answeredBy === 'owner' ? 'owner' : answeredBy, subjectType: attempt ? 'attempt' : null,
|
|
40
|
+
subjectId: attempt ? String(attempt.attempt_id) : null, choice: String(option), rationale: typeof receipt.note === 'string' ? receipt.note.slice(0, 2000) : null,
|
|
41
|
+
result: { kind: 'ask-answer', dispatchId, answeredBy, optionIndex: receipt.optionIndex ?? null, receiptSha256: blob.sha, artifactId }, decidedAt: at });
|
|
42
|
+
return { receiptPath: blob.fileUri, receiptSha: blob.sha, receiptRef: `blob:${blob.sha}`, artifactId, decisionId };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/** Atomically accept one answer with its receipt, decision and terminal lifecycle event. */
|
|
47
|
+
export function commitAskAnswer(ledger, { workflowId, dispatchId, receipt, at = Date.now(), payload = {}, events = [], blob: staged = null }) {
|
|
48
|
+
const blob = staged ?? stageReceipt(receipt);
|
|
49
|
+
const answerDigest = sha256(JSON.stringify({ ...receipt, at: null }));
|
|
50
|
+
return ledger.transaction(db => {
|
|
51
|
+
const latest = db.prepare(`SELECT kind,payload_json FROM events WHERE workflow_id=?
|
|
52
|
+
AND kind IN ('ask-answered','ask-superseded')
|
|
53
|
+
AND json_extract(payload_json,'$.dispatchId')=? ORDER BY seq DESC LIMIT 1`).get(workflowId, dispatchId);
|
|
54
|
+
if (latest?.kind === 'ask-answered' || latest?.kind === 'ask-superseded') {
|
|
55
|
+
const prior = JSON.parse(latest.payload_json);
|
|
56
|
+
return prior.answerDigest === answerDigest
|
|
57
|
+
? { accepted: true, replayed: true, ...prior }
|
|
58
|
+
: { accepted: false, why: latest.kind === 'ask-answered' ? 'already-answered' : 'retired' };
|
|
59
|
+
}
|
|
60
|
+
const filed = fileAskReceipt(db, { workflowId, dispatchId, receipt, blob, at });
|
|
61
|
+
const answer = { dispatchId, receiptPath: filed.receiptPath, receiptSha: filed.receiptSha, decisionId: filed.decisionId,
|
|
62
|
+
answeredBy: receipt.answeredBy, optionIndex: receipt.optionIndex ?? null,
|
|
63
|
+
custodyWritten: receipt.custodyWritten ?? [], envWritten: receipt.envWritten ?? [], pointersWritten: receipt.pointersWritten ?? [], errors: receipt.errors ?? [],
|
|
64
|
+
...payload, answerDigest };
|
|
65
|
+
ledger.appendEvent({ workflowId, entityType: 'report', entityId: dispatchId, kind: 'ask-answered', payload: answer });
|
|
66
|
+
for (const event of events) ledger.appendEvent({ workflowId, entityType: 'report', entityId: dispatchId, ...event,
|
|
67
|
+
payload: { ...event.payload, receiptPath: filed.receiptPath } });
|
|
68
|
+
return { accepted: true, replayed: false, ...filed };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** True when `file` is a blob in the store (a receipt's file lives there). */
|
|
73
|
+
const isBlobFile = (file) => {
|
|
74
|
+
const rel = path.relative(artifactRoot(), path.resolve(file));
|
|
75
|
+
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The file a receipt reference names: `blob:<sha256>`, a blob file, or a path (resolved against `base`). */
|
|
79
|
+
export function receiptFileOf(ref, { base = process.cwd() } = {}) {
|
|
80
|
+
if (typeof ref !== 'string' || !ref.trim()) return null;
|
|
81
|
+
const m = BLOB_REF.exec(ref.trim());
|
|
82
|
+
if (m) return blobPath(m[1]);
|
|
83
|
+
const abs = path.resolve(base, ref);
|
|
84
|
+
return fs.existsSync(abs) ? abs : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** How a Work record cites a receipt file: `blob:<sha256>` for a blob, else the path relative to `repoRoot`. */
|
|
88
|
+
export const receiptRefOf = (file, repoRoot) => (isBlobFile(file) ? `blob:${path.basename(file)}` : path.relative(repoRoot, file).replaceAll('\\', '/'));
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The prelude of every `apply` that settles from a serve-ask answer (scripts/work/draw-review.mjs,
|
|
92
|
+
* scripts/work/brand-direction.mjs): resolve the receipt ref (a `blob:<sha256>` the store kept, or a path inside
|
|
93
|
+
* `repoRoot`), refuse a file outside the repository, and parse it as the `schema` receipt. Returns
|
|
94
|
+
* {receipt, receiptAbs, receiptRel} - receiptRel is how the record cites it.
|
|
95
|
+
*/
|
|
96
|
+
export function readAnswerReceipt(receiptFile, { repoRoot, base = process.cwd(), schema = 'starci/ask-answer@1' } = {}) {
|
|
97
|
+
// The receipt is the blob serve-ask stored (receiptPath, or `blob:<sha256>`); a Work record cites it as
|
|
98
|
+
// blob:<sha256>. A path inside the repository is still read (an example tree's own receipt).
|
|
99
|
+
const receiptAbs = receiptFileOf(String(receiptFile), { base }) ?? path.resolve(String(receiptFile));
|
|
100
|
+
const receiptRel = receiptRefOf(receiptAbs, repoRoot);
|
|
101
|
+
if (!isBlobFile(receiptAbs) && (receiptRel.startsWith('../') || path.isAbsolute(receiptRel))) throw new Error(`receipt ${slash(receiptFile)} is neither a stored answer (blob:<sha256>, the receiptPath serve-ask returned) nor a file inside the repository ${slash(repoRoot)}`);
|
|
102
|
+
let receipt;
|
|
103
|
+
try { receipt = JSON.parse(fs.readFileSync(receiptAbs, 'utf8')); } catch (error) { throw new Error(`receipt ${slash(receiptFile)} is unreadable: ${error.message}`); }
|
|
104
|
+
if (receipt?.schema !== schema) throw new Error(`${slash(receiptFile)} is ${receipt?.schema ?? 'not a receipt'}, not ${schema}`);
|
|
105
|
+
return { receipt, receiptAbs, receiptRel };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Every receipt answering `dispatchId` in a ledger, newest first: [{file, receipt, name}]. */
|
|
109
|
+
export function receiptsAnswering(db, dispatchId) {
|
|
110
|
+
return db.prepare(`SELECT a.name,a.sha256 FROM job_artifacts a WHERE a.origin='kernel' AND a.name LIKE ? ORDER BY a.artifact_id DESC`).all(`${RECEIPT_PREFIX}${dispatchId}/answer-%`)
|
|
111
|
+
.map((r) => { const file = blobPath(r.sha256); let receipt = null; try { receipt = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { receipt = null; } return { file, receipt, name: r.name }; })
|
|
112
|
+
.filter((r) => r.file && r.receipt);
|
|
113
|
+
}
|