@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.
Files changed (473) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
@@ -0,0 +1,223 @@
1
+ // loosening-state.mjs - the state-based loosening check: does the tree, as it stands, hold a gate weaker than the canon says? (owner decision
2
+ // 2026-10-24, owner ruling loosening-state-check.) The history of the tree is never read: a deleted file, a re-pointed row or reworded text
3
+ // cannot be a loosening, and a loosening that stands cannot be hidden by when it was written. The check is red on
4
+ // bypass an inline disable, a type-check escape, a Sonar or coverage ignore, a skipped or focused test, a failure swallowed in a script,
5
+ // a hook or a workflow, a warning budget, a pass-with-no-tests flag, a hook skipped (scripts/lib/bypass-markers.mjs);
6
+ // list-extra an entry of an ignore or exclude list (codecov, sonar, jest, eslint, stylelint, tsconfig, a *ignore file) that the canon does not
7
+ // derive and the design-truths manifest does not allow FOR THAT FILE;
8
+ // threshold a coverage target below 100, a threshold above 0, a gate limit looser than the manifest's;
9
+ // setting a switch that relaxes a gate (`require_ci_to_pass: false`, `informational: true`, `passWithNoTests`, `requireCi: false`) that the
10
+ // manifest does not allow;
11
+ // rule-off an ESLint or stylelint rule turned off or down in a config;
12
+ // strictness a tsconfig strictness option turned off, or skipLibCheck (and the like) turned on outside the file the manifest allows;
13
+ // budget a numeric budget of the slot manifests raised, lowered or gone against the manifest's pins; budget-override: an app or example
14
+ // config (hfs.json, an ESLint config) that states a budget itself; scope-override: a jest config that narrows what runs;
15
+ // exception-file an allowlist, approval, waiver, suppression or baseline file that the manifest does not list, or one with more entries than it allows;
16
+ // pin a file the owner pinned (the manifest, the fixtures of this checker, the vendored YAML) whose digest differs from the pin;
17
+ // fixture-misuse code other than a package spec that imports the fixtures of this checker.
18
+ // Every legitimate exception lives in knowledge/hfs/design-truths.yaml, each with the cite of its owner ruling. The manifest itself is pinned by the
19
+ // digest the latest owner ruling records (pins): a lane that edits it turns the check red until the owner records a ruling. The library is pure: the
20
+ // caller hands the tracked files, a reader, the canon's derivations and the pins.
21
+ import { bypassFileKind, bypassesIn } from './bypass-markers.mjs';
22
+ import { LIST, configKindOf, configRecords, coreOf, exceptionEntries, isAnyDepth, isExceptionFile, maybeTsconfig } from './loosening-config.mjs';
23
+ import { braceVariants, globExpression } from './glob.mjs';
24
+ import { byCodeUnit, list as asArray } from './list.mjs';
25
+ import { budgetRecords } from './loosening-budgets.mjs';
26
+ import { manifestDigest as truthsDigest } from './grammar-vendor-parity.mjs';
27
+
28
+ export { truthsDigest };
29
+
30
+ const TRUTHS_SCHEMA = 'starci/design-truths@1';
31
+ const MANIFEST = 'knowledge/hfs/design-truths.yaml';
32
+
33
+ const LISTS = new Set(Object.values(LIST));
34
+ const RELAXING = Object.freeze({ require_ci_to_pass: 'false', informational: 'true', passWithNoTests: 'true', requireCi: 'false', if_ci_failed: 'success', status: 'off', 'sonar.qualitygate.wait': 'false', 'yaml-alias': 'true' });
35
+
36
+ const hasRuling = (cite) => cite !== null && typeof cite === 'object' && typeof cite.ruling === 'string' && cite.ruling.length > 0;
37
+ /** The file globs an allowance names: one string or a list of them. */
38
+ function globsOf(value) {
39
+ const globs = [];
40
+ for (const glob of [value].flat()) if (typeof glob === 'string') globs.push(glob);
41
+ return globs;
42
+ }
43
+ const matchesFile = (globs, file) => globs.some((glob) => braceVariants(glob).some((variant) => globExpression(variant).test(file)));
44
+
45
+ function readListAllow(truth, allow, out) {
46
+ if (!LISTS.has(allow.list)) out.problems.push(`truth ${truth.id}: unknown list ${allow.list}`);
47
+ if (allow.derivedBy !== undefined) out.derivations.add(allow.derivedBy);
48
+ const entries = asArray(allow.entries);
49
+ if (entries.length > 0 && globsOf(allow.file).length === 0) out.problems.push(`truth ${truth.id}: an allowance of list entries names the file it holds for`);
50
+ if (entries.length > 0) out.literal.push({ list: allow.list, files: globsOf(allow.file), entries });
51
+ }
52
+
53
+ function readAllow(truth, allow, out) {
54
+ if (allow.list !== undefined) readListAllow(truth, allow, out);
55
+ else if (allow.setting !== undefined) out.settings.push({ key: allow.setting, value: String(allow.value) });
56
+ else if (allow.compilerOption !== undefined) out.options.push({ key: allow.compilerOption, value: String(allow.value), files: asArray(allow.files) });
57
+ else if (allow.exceptionFile !== undefined) out.exceptionFiles.set(allow.exceptionFile, Number(allow.maxEntries ?? 0));
58
+ else out.problems.push(`truth ${truth.id}: an allow names no list, setting, compilerOption or exceptionFile`);
59
+ }
60
+
61
+ /**
62
+ * The manifest `doc` as the sets the check reads, plus `problems`: the shapes a malformed manifest breaks (a truth, a group, a budget pin or the gate
63
+ * limits without an id, an owner-ruling cite, a sentence or an allowance is a problem, so an exception can never be anonymous or lane-authored).
64
+ */
65
+ export function readTruths(doc) {
66
+ const out = {
67
+ problems: [], ids: [], fixtureFiles: asArray(doc?.fixtureFiles), vendored: new Set(asArray(doc?.vendored).map((entry) => entry.path)),
68
+ groups: [], literal: [], derivations: new Set(), settings: [], options: [], exceptionFiles: new Map(), limits: doc?.gateLimits ?? null,
69
+ budgets: asArray(doc?.budgets),
70
+ };
71
+ if (doc?.schema !== TRUTHS_SCHEMA) out.problems.push(`schema must be ${TRUTHS_SCHEMA}`);
72
+ if (out.limits && !hasRuling(out.limits.cite)) out.problems.push('gateLimits: an owner-ruling cite is required');
73
+ for (const group of asArray(doc?.outsideTheGates)) {
74
+ if (!group.id || !hasRuling(group.cite)) out.problems.push(`outsideTheGates ${group.id ?? '?'}: id and an owner-ruling cite are required`);
75
+ out.groups.push({ cores: new Set(asArray(group.entries).map(coreOf)), anyDepth: group.anyDepth === true });
76
+ }
77
+ for (const pin of out.budgets) if (!pin.file || !hasRuling(pin.cite)) out.problems.push(`budgets ${pin.file ?? '?'}: file and an owner-ruling cite are required`);
78
+ for (const truth of asArray(doc?.truths)) {
79
+ if (!truth.id || !hasRuling(truth.cite) || !truth.says || asArray(truth.allows).length === 0) out.problems.push(`truth ${truth.id ?? '?'}: id, an owner-ruling cite, says and allows are required`);
80
+ out.ids.push(truth.id);
81
+ for (const allow of asArray(truth.allows)) readAllow(truth, allow, out);
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /** Whether the tree check reads `file`: every tracked file, but the fixtures of this checker (pinned by digest) and the vendored dependency copy (pinned too). */
87
+ export const inScope = (file, truths) => !truths.vendored.has(file) && !truths.fixtureFiles.includes(file);
88
+
89
+ /** Whether any reading of the check applies to `file`: only those files are read at all. */
90
+ const extensionless = (file) => !file.slice(file.lastIndexOf('/') + 1).includes('.');
91
+ const isJudged = (file, truths) => Boolean(bypassFileKind(file) || configKindOf(file) || maybeTsconfig(file) || extensionless(file) || isExceptionFile(file) || truths.limits?.file === file
92
+ || truths.budgets.some((pin) => pin.file === file));
93
+
94
+ const finding = (kind, file, line, detail) => ({ id: `${kind}:${file}:${detail}`, kind, file, line, detail });
95
+
96
+ /** Whether the manifest lists `item` of `list` for `file`: a jest pattern by its exact text, any other entry by its path core. */
97
+ function listedFor(truths, file, list, item) {
98
+ return truths.literal.some((allow) => allow.list === list && matchesFile(allow.files, file)
99
+ && allow.entries.some((entry) => (list === LIST.jest ? entry === item : coreOf(entry) === coreOf(item) && isAnyDepth(entry) === isAnyDepth(item))));
100
+ }
101
+
102
+ /** Whether `item` names only what no gate reads (build output, generated records, sealed custody), rooted unless its group is the same at any depth. */
103
+ const outsideTheGates = (truths, item, list) => list !== LIST.jest && list !== LIST.issues && truths.groups.some((group) => group.cores.has(coreOf(item)) && (group.anyDepth || !isAnyDepth(item)));
104
+
105
+ function listFindings(file, records, truths, canon) {
106
+ const derived = new Map();
107
+ const derivedFor = (list) => {
108
+ if (!derived.has(list)) {
109
+ const from = canon.derive?.(file, list) ?? null;
110
+ derived.set(list, from && truths.derivations.has(from.by) ? new Set(from.items.map((item) => (list === LIST.jest ? item : coreOf(item)))) : new Set());
111
+ }
112
+ return derived.get(list);
113
+ };
114
+ const allowed = (entry) => derivedFor(entry.list).has(entry.list === LIST.jest ? entry.item : coreOf(entry.item)) || outsideTheGates(truths, entry.item, entry.list) || listedFor(truths, file, entry.list, entry.item);
115
+ return records.filter((entry) => !allowed(entry)).map((entry) => finding('list-extra', file, entry.line, `${entry.key}: ${entry.item}`));
116
+ }
117
+
118
+ function numberFindings(file, numbers) {
119
+ return numbers.flatMap((entry) => {
120
+ const loose = entry.key === 'threshold' ? entry.value > 0 : entry.value < 100;
121
+ return loose ? [finding('threshold', file, entry.line, `${entry.key} ${entry.value}`)] : [];
122
+ });
123
+ }
124
+
125
+ const normalised = (value) => ({ yes: 'true', on: 'true', no: 'false', off: 'off' }[String(value).toLowerCase()] ?? String(value).toLowerCase());
126
+
127
+ function settingFindings(file, settings, truths) {
128
+ return settings.map((entry) => ({ ...entry, value: normalised(entry.value) }))
129
+ .filter((entry) => entry.value === RELAXING[entry.key] || (entry.key === 'if_ci_failed' && entry.value !== 'error'))
130
+ .filter((entry) => !truths.settings.some((allow) => allow.key === entry.key && allow.value === entry.value))
131
+ .map((entry) => finding('setting', file, entry.line, `${entry.key}: ${entry.value}`));
132
+ }
133
+
134
+ /** A strictness record is a finding unless the manifest allows that option at that value in that very file (the canon base tsconfig for skipLibCheck). */
135
+ function strictnessFindings(file, strictness, truths) {
136
+ return strictness.filter((entry) => !truths.options.some((allow) => allow.key === entry.key && allow.value === entry.value && allow.files.includes(file)))
137
+ .map((entry) => finding('strictness', file, entry.line, `${entry.key}: ${entry.value}`));
138
+ }
139
+
140
+ function configFindings(file, kind, text, truths, canon, withLists) {
141
+ const records = configRecords(kind, text, file);
142
+ return [
143
+ ...(withLists ? listFindings(file, records.lists, truths, canon) : []),
144
+ ...numberFindings(file, records.numbers),
145
+ ...settingFindings(file, records.settings, truths),
146
+ ...records.rulesOff.map((entry) => finding('rule-off', file, entry.line, entry.rule)),
147
+ ...strictnessFindings(file, records.strictness, truths),
148
+ ...records.budgets.map((entry) => finding('budget-override', file, entry.line, `${entry.key}: a budget is the canon's, never stated by a config`)),
149
+ ...records.scope.map((entry) => finding('scope-override', file, entry.line, `${entry.key}: the canon config states no scope of what runs`)),
150
+ ];
151
+ }
152
+
153
+ /** The gate limits of the file the manifest names (the Sonar gate): a floor lowered or a ceiling raised against the manifest's numbers. */
154
+ function limitFindings(file, text, limits) {
155
+ const found = [];
156
+ text.split(/\r?\n/).forEach((raw, index) => {
157
+ const match = /^\s*(?:-\s+)?([A-Za-z]\w*)\s*:\s*(-?\d+(?:\.\d+)?)\s*(?:#.*)?$/.exec(raw);
158
+ if (!match) return;
159
+ const value = Number(match[2]);
160
+ const floor = limits.floors?.[match[1]];
161
+ const ceiling = limits.ceilings?.[match[1]];
162
+ if ((floor !== undefined && value < floor) || (ceiling !== undefined && value > ceiling)) found.push(finding('threshold', file, index + 1, `${match[1]} ${value}`));
163
+ });
164
+ return found;
165
+ }
166
+
167
+ function exceptionFileFindings(file, text, truths) {
168
+ if (!truths.exceptionFiles.has(file)) return [finding('exception-file', file, 1, 'an exception, approval, allowlist or baseline file the design-truths manifest does not list')];
169
+ const held = exceptionEntries(file, text);
170
+ const allowed = truths.exceptionFiles.get(file);
171
+ return held > allowed ? [finding('exception-file', file, 1, `${held} entries, the manifest allows ${allowed}`)] : [];
172
+ }
173
+
174
+ const FIXTURE_IMPORT = /fixtures\/loosening\//;
175
+ const PACKAGE_SPEC = /^packages\/hfs\/src\/[\w.-]+\.spec\.mjs$/;
176
+
177
+ /** The findings of one file. */
178
+ function fileFindings(file, text, truths, canon, withLists) {
179
+ const found = bypassesIn(file, text).map((hit) => finding('bypass', file, hit.line, hit.label));
180
+ const kind = configKindOf(file, text);
181
+ if (kind) found.push(...configFindings(file, kind, text, truths, canon, withLists));
182
+ if (isExceptionFile(file)) found.push(...exceptionFileFindings(file, text, truths));
183
+ if (truths.limits?.file === file) found.push(...limitFindings(file, text, truths.limits));
184
+ for (const pin of truths.budgets.filter((entry) => entry.file === file)) found.push(...budgetRecords(text, pin).map((detail) => finding('budget', file, 1, detail)));
185
+ if (bypassFileKind(file, text) === 'code' && !PACKAGE_SPEC.test(file) && FIXTURE_IMPORT.test(text)) found.push(finding('fixture-misuse', file, 1, 'only a package spec of packages/hfs/src may import the fixtures of the loosening check'));
186
+ return found;
187
+ }
188
+
189
+ /** The pinned files whose digest differs from the pin, and the declared fixtures and vendored copies the pins do not cover. */
190
+ function pinFindings(truths, pins, read) {
191
+ const declared = [MANIFEST, ...truths.fixtureFiles, ...truths.vendored];
192
+ const found = declared.filter((file) => !Object.hasOwn(pins, file)).map((file) => finding('pin', file, 1, 'no owner ruling pins this file: record its digest in an owner ruling'));
193
+ for (const [file, digest] of Object.entries(pins)) {
194
+ const text = read(file);
195
+ if (typeof text !== 'string') found.push(finding('pin', file, 1, 'a pinned file is missing'));
196
+ else if (truthsDigest(text) !== digest) found.push(finding('pin', file, 1, `its digest ${truthsDigest(text).slice(0, 12)} is not the one the owner pinned (${String(digest).slice(0, 12)}): only an owner ruling may change it`));
197
+ }
198
+ return found;
199
+ }
200
+
201
+ /**
202
+ * Every loosening of the tree: [{id, kind, file, line, detail}] sorted, plus the manifest's own problems as `truths` findings.
203
+ * files the tracked files (repository-relative, forward slashes);
204
+ * read (file) -> text, or null when it cannot be read;
205
+ * truths the manifest document (readTruths shapes it);
206
+ * canon { derive(file, list) -> { by, items } | null }: what the canon derives for a list of a file, allowed only when the manifest names `by`;
207
+ * lists false leaves the ignore and exclude lists out (the app gate: an app's rendered files are held to their render by the managed-file drift);
208
+ * pins { path: sha256 } the latest owner ruling records; undefined skips the pin check (the app gate).
209
+ */
210
+ export function stateFindings({ files, read, truths: doc, canon = {}, lists = true, pins }) {
211
+ const truths = readTruths(doc);
212
+ const found = truths.problems.map((problem) => finding('truths', MANIFEST, 1, problem));
213
+ if (pins !== undefined) found.push(...pinFindings(truths, pins, read));
214
+ for (const file of files) {
215
+ if (!inScope(file, truths) || !isJudged(file, truths)) continue;
216
+ const text = read(file);
217
+ if (typeof text === 'string') found.push(...fileFindings(file, text, truths, canon, lists));
218
+ }
219
+ return found.sort((a, b) => byCodeUnit(a.id, b.id) || a.line - b.line);
220
+ }
221
+
222
+ /** One finding as the self-check and the app gate print it. */
223
+ export const findingLine = (entry) => `${entry.kind} ${entry.file}:${entry.line} ${entry.detail}`;
@@ -0,0 +1,11 @@
1
+ // The parts the Kernel menu and the Supervisor menu share: a template filled from an item's subject, the escape option every
2
+ // item carries, and the plain words of a starci call a seat table is read by.
3
+
4
+ /** `template` with each `{key}` replaced by the subject's value; a key the subject does not know stays as written. */
5
+ export const fillTemplate = (template, subject) => String(template ?? '').replace(/\{(\w+)\}/g, (match, key) => (subject[key] == null ? match : String(subject[key])));
6
+
7
+ /** The escape option of a menu catalog (`escape: {choice, effect}`): no step, a reason as its text. */
8
+ export const escapeOptionOf = (escape, extra = {}) => ({ choice: escape.choice, ...extra, steps: [], effect: escape.effect, text: 'reason', escape: true });
9
+
10
+ /** The first `count` words of an argument list that are not options. */
11
+ export const plainWords = (args, count) => args.filter((value) => !String(value).startsWith('-')).slice(0, count).map(String);
@@ -0,0 +1,25 @@
1
+ // normalize.mjs — text in compare form for matching (phrase hits, repeated questions).
2
+
3
+ const SENTENCE_TRAILING_CHAR = /[\s.?!:;,]/;
4
+
5
+ /** `text` without its trailing run of characters that `chars` (a one-character pattern) matches; a loop, not a trailing-run regex. */
6
+ export const trimTrailingChars = (text, chars) => {
7
+ for (let end = text.length; ; end -= 1) if (end === 0 || !chars.test(text[end - 1])) return text.slice(0, end);
8
+ };
9
+
10
+ /** `text` without the run of `ch` (one character) at each end; loops, not an anchored-run regex. */
11
+ export const trimEdgeChar = (text, ch) => {
12
+ let start = 0, end = text.length;
13
+ while (start < end && text[start] === ch) start += 1;
14
+ while (end > start && text[end - 1] === ch) end -= 1;
15
+ return text.slice(start, end);
16
+ };
17
+
18
+ /**
19
+ * `s` normalized for a literal compare: Unicode `form`-normalized, lowercased, whitespace runs to a
20
+ * single space, ends trimmed; `punct: true` also drops trailing sentence punctuation.
21
+ */
22
+ export const normalizeText = (s, { form = 'NFC', punct = false } = {}) => {
23
+ const out = String(s ?? '').normalize(form).toLowerCase().replace(/\s+/g, ' ').trim();
24
+ return punct ? trimTrailingChars(out, SENTENCE_TRAILING_CHAR) : out;
25
+ };
@@ -0,0 +1,15 @@
1
+ // op-gate.mjs — one section of the runtime's knowledge/op-gate.yaml (testWorld, unitKit, ...):
2
+ // {required, forbidden, outage, ...} as authored. An absent section reads as {}.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { parseYaml } from '../../engine/yaml.mjs';
6
+
7
+ /** The `section` object of knowledge/op-gate.yaml under `runtime`, {} when the doc omits it. Throws when the file is unreadable. */
8
+ const opGateSection = (runtime, section) =>
9
+ (parseYaml(fs.readFileSync(path.join(runtime, 'knowledge', 'op-gate.yaml'), 'utf8'))?.[section]) ?? {};
10
+
11
+ /** The {required, forbidden, outage} rule lists of a gate `section` (absent lists read as []). */
12
+ export const opGateRules = (runtime, section) => {
13
+ const rules = opGateSection(runtime, section);
14
+ return { required: rules.required ?? [], forbidden: rules.forbidden ?? [], outage: rules.outage ?? [] };
15
+ };
@@ -0,0 +1,208 @@
1
+ // redact.mjs — the ONE redaction module (docs/ledger-db.md). Every blob put of text bytes
2
+ // (report attachments, check stdout/stderr/output, terminal transcripts, CLI sessions) and every log write
3
+ // passes through here first; the harness UI server reuses it. A blob whose bytes went through redactText is
4
+ // stamped blobs.redaction = REDACTION_VERSION; bytes that cannot be filtered (images, video, archives) are
5
+ // stamped 'binary'.
6
+ //
7
+ // It blanks: provider tokens and JWTs (secret-patterns.mjs, the push scan's list), auth headers, URL query
8
+ // secrets, URL credentials (scheme://user:pass@host), PEM private-key blocks (the whole block), KEY=value /
9
+ // KEY: value pairs whose key names a secret (FOO_API_KEY=..., password: ...), and the declared secrets of a
10
+ // product repo's .starcistacks (their env-style names as keys, and their values wherever they appear).
11
+ // Keys and surrounding words stay, so a redacted transcript still reads.
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { FORBIDDEN_FILES, SECRET_PATTERNS } from './secret-patterns.mjs';
15
+ import { parseYaml } from '../../engine/yaml.mjs';
16
+ import { altOf } from './source-phrases.mjs';
17
+ import { trimEdgeChar } from './normalize.mjs';
18
+ import { redactResolvedSecrets } from '../../engine/secrets.mjs';
19
+
20
+ const REDACTION_VERSION = 'v1';
21
+ export const MARK = '[redacted]';
22
+
23
+ const withGlobal = (re) => new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);
24
+ const PATTERNS = SECRET_PATTERNS.filter((rule) => rule.name !== 'private-key-block').map((rule) => ({ ...rule, g: withGlobal(rule.re) }));
25
+ // Words that make an env-style key a secret, matched per `_`-separated segment of the name (FOO_API_KEY,
26
+ // KEYCLOAK_ADMIN_PASSWORD, GH_TOKEN, MINIO_SECRET_KEY, DATABASE_URL is not; TESTS_PASSED is not).
27
+ const SECRET_SEGMENTS = new Set(['PASSWORD', 'PASSWD', 'PWD', 'PASS', 'SECRET', 'SECRETS', 'TOKEN', 'APIKEY', 'KEY', 'CREDENTIAL', 'CREDENTIALS', 'COOKIE', 'OTP', 'DSN', 'PAT']);
28
+ const isSecretEnvName = (name) => String(name).replace(/^export\s+/, '').replace(/_FILE$/, '').split('_').some((seg) => SECRET_SEGMENTS.has(seg));
29
+ const PEM_BLOCK = /-----BEGIN ((?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?)-----[\s\S]*?(?:-----END \1-----|$)/g;
30
+ const RULES = [
31
+ { name: 'url-credentials', re: /\b([a-z][a-z0-9+.-]*:\/\/)([^\s:@/"'<>]+):([^\s@/"'<>]+)@/gi, to: (m, scheme, user) => `${scheme}${user}:${MARK}@` },
32
+ { name: 'auth-header', re: /\b(Bearer|Basic|Token)(\s+)[A-Z0-9._~+/=-]{8,}/gi, to: (m, a, b) => `${a}${b}${MARK}` },
33
+ { name: 'url-secret', re: /([?&#](?:access_token|refresh_token|id_token|token|key|api_key|apikey|secret|code|password|otp|sig|signature)=)([^&#\s"']+)/gi, to: (m, a) => `${a}${MARK}` },
34
+ { name: 'otp', re: new RegExp(String.raw`\b(otp|one[-_ ]?time[-_ ]?(?:code|password|pin)|verification[-_ ]?code|2fa[-_ ]?code|${altOf('redact.otpLabel')})(\s*[:=]?\s*["']?)(\d{4,8})\b`, 'giu'), to: (m, a, b) => `${a}${b}${MARK}` },
35
+ // ENV_STYLE_KEY=value / ENV_STYLE_KEY: value (shell exports, dotenv, compose, CLI echo).
36
+ { name: 'env-secret', re: /\b((?:export\s+)?[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*)(\s*[:=]\s*["']?)((?!\[redacted)[^\s"'`]{3,})/g,
37
+ to: (m, a, b, c) => (isSecretEnvName(a) && !c.startsWith('/run/secrets/') && !c.startsWith('$') ? `${a}${b}${MARK}` : m) },
38
+ { name: 'keyed-secret', re: new RegExp([
39
+ String.raw`\b(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|auth[_-]?token|session[_-]?token|private[_-]?key|otp|pin[_-]?code|cookie|set-cookie)`,
40
+ String.raw`(["']?\s*[:=]\s*["']?)((?!\[redacted)[^\s"',;&}]{3,})`,
41
+ ].join(''), 'gi'), to: (m, a, b) => `${a}${b}${MARK}` },
42
+ ];
43
+
44
+ /** Keys whose value is a secret whatever it looks like; the key stays, the value goes. */
45
+ export const SECRET_KEY = new RegExp([
46
+ '^(?:password|passwd|pwd|pass|secret|otp|pin|pincode|pin_code|credential|credentials|authorization|cookie|cookies|set-cookie|private[_-]?key|client[_-]?secret|api[_-]?key|apikey|[a-z_-]*token|[a-z_-]*secret)',
47
+ '$',
48
+ ].join(''), 'i');
49
+
50
+ // ------------------------------------------------------------------------------ declared stack secrets
51
+ // A product repo's .starcistacks/<stack>/stack.yaml `secrets:` block names each secret and the runtime file
52
+ // that holds its value. Values are refreshed from their declared inputs; values never leave the filter.
53
+ const STACK_DIRS = ['.starcistacks'];
54
+ const secretValues = new Set();
55
+ const secretNames = new Set();
56
+ const envName = (name) => trimEdgeChar(String(name).replace(/[^A-Za-z0-9]+/g, '_'), '_').toUpperCase();
57
+ const readSafe = (file) => { try { return fs.readFileSync(file, 'utf8'); } catch { return null; } };
58
+ const listSafe = (dir) => { try { return fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; } };
59
+
60
+ function learnStackDirectory(root, stackDir, entry) {
61
+ const dir = path.join(root, stackDir, entry.name);
62
+ const text = readSafe(path.join(dir, 'stack.yaml'));
63
+ let doc = null;
64
+ try { doc = text ? parseYaml(text) : null; } catch { doc = null; }
65
+ const secrets = doc?.secrets && typeof doc.secrets === 'object' ? doc.secrets : {};
66
+ for (const [name, spec] of Object.entries(secrets)) {
67
+ secretNames.add(envName(name));
68
+ if (typeof spec?.file === 'string') addSecretValue(readSafe(path.resolve(dir, spec.file)));
69
+ }
70
+ // Plaintext runtime copies of the secrets (git-ignored): their values are secrets too.
71
+ for (const file of listSafe(path.join(dir, 'runtime', 'files'))) if (file.isFile()) addSecretValue(readSafe(path.join(dir, 'runtime', 'files', file.name)));
72
+ }
73
+
74
+ /** A value this process must never write out (a secret it resolved itself). Values under 6 chars are ignored. */
75
+ function addSecretValue(value) {
76
+ const v = typeof value === 'string' ? value.trim() : '';
77
+ if (v.length >= 6 && !v.startsWith('[redacted')) secretValues.add(v);
78
+ }
79
+
80
+ /** Learn the declared secrets of every .starcistacks stack under `repoRoot` (idempotent). */
81
+ export function learnStackSecrets(repoRoot) {
82
+ if (typeof repoRoot !== 'string' || !repoRoot) return;
83
+ const root = path.resolve(repoRoot);
84
+ for (const stackDir of STACK_DIRS) {
85
+ for (const entry of listSafe(path.join(root, stackDir))) {
86
+ if (!entry.isDirectory()) continue;
87
+ learnStackDirectory(root, stackDir, entry);
88
+ }
89
+ }
90
+ }
91
+
92
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
93
+ let namesRe = null, namesSize = -1;
94
+ const declaredNameRule = () => {
95
+ if (namesSize !== secretNames.size) {
96
+ namesSize = secretNames.size;
97
+ namesRe = secretNames.size ? new RegExp(`\\b((?:${[...secretNames].map(escapeRe).join('|')})(?:_FILE)?)(\\s*[:=]\\s*["']?)((?!\\[redacted)[^\\s"'\`]{3,})`, 'g') : null;
98
+ }
99
+ return namesRe;
100
+ };
101
+
102
+ // --------------------------------------------------------------------------------------------- text
103
+ const PEM_RULE = 'private-key-block';
104
+
105
+ /** `text` with `re` replaced by `to`, `hits` counting each replacement that changed the match under the rule's name. */
106
+ function replaceCounting(text, re, name, to, hits) {
107
+ re.lastIndex = 0;
108
+ return text.replace(re, (...args) => {
109
+ const out = to(...args);
110
+ if (out !== args[0]) hits.set(name, (hits.get(name) ?? 0) + 1);
111
+ return out;
112
+ });
113
+ }
114
+
115
+ const patternTo = (rule) => (match, value) => {
116
+ if (rule.name === 'assigned-secret') return rule.placeholder?.test(value ?? '') ? match : match.replace(value, MARK);
117
+ return `[redacted:${rule.name}]`;
118
+ };
119
+
120
+ function redactPatterns(text, hits, assignments) {
121
+ for (const rule of PATTERNS) {
122
+ if ((rule.name === 'assigned-secret') !== assignments) continue;
123
+ text = replaceCounting(text, rule.g, rule.name, patternTo(rule), hits);
124
+ }
125
+ return text;
126
+ }
127
+
128
+ /**
129
+ * `text` with every secret value blanked, and what blanked it: {text, hits: Map(rule name -> replacements that changed the text)}. A rule
130
+ * that meets only a placeholder ([redacted...]) counts nothing, so a text redacted before reports no hit. `repoRoots` (optional) are product
131
+ * repos whose .starcistacks declarations are learned first.
132
+ */
133
+ export function redactTracked(text, { repoRoots = [] } = {}) {
134
+ const hits = new Map();
135
+ if (typeof text !== 'string' || !text) return { text, hits };
136
+ for (const root of repoRoots) learnStackSecrets(root);
137
+ const resolved = redactResolvedSecrets(text);
138
+ if (resolved !== text) hits.set('resolved-secret', 1);
139
+ let out = replaceCounting(resolved, PEM_BLOCK, PEM_RULE, (m, kind) => `[redacted:pem ${kind}]`, hits);
140
+ for (const value of secretValues) if (out.includes(value)) { hits.set('stack-secret', (hits.get('stack-secret') ?? 0) + 1); out = out.split(value).join('[redacted:stack-secret]'); }
141
+ out = redactPatterns(out, hits, false);
142
+ const declared = declaredNameRule();
143
+ if (declared) out = replaceCounting(out, declared, 'declared-secret-name', (m, a, b) => `${a}${b}${MARK}`, hits);
144
+ for (const rule of RULES) out = replaceCounting(out, rule.re, rule.name, rule.to, hits);
145
+ // Specific contexts keep their classifications; the widened assignment heuristic handles the remaining values.
146
+ out = redactPatterns(out, hits, true);
147
+ return { text: out, hits };
148
+ }
149
+
150
+ /** `text` with every secret value blanked. */
151
+ export function redactText(text, options = {}) { return redactTracked(text, options).text; }
152
+
153
+ /** The rules that still find a secret in `text` after redaction ran: [{rule, count}], never the matched text. */
154
+ export function survivingSecrets(text, options = {}) { return [...redactTracked(text, options).hits].map(([rule, count]) => ({ rule, count })); }
155
+
156
+ /** A path that is a secret by being one (an env file, a key file, .secrets/): blanked, its rule named. */
157
+ export function redactPath(p) {
158
+ if (typeof p !== 'string') return p;
159
+ const slashed = p.replaceAll('\\', '/');
160
+ const rule = FORBIDDEN_FILES.find((r) => r.test(slashed));
161
+ return rule ? `[redacted:${rule.name}]` : redactText(p);
162
+ }
163
+
164
+ /** A deep copy of `value` with secret-named keys blanked and every string redacted. */
165
+ export function redactData(value, key = null, depth = 0) {
166
+ if (depth > 8) return '[depth]';
167
+ if (key && SECRET_KEY.test(key) && value != null && typeof value !== 'object' && typeof value !== 'boolean') return MARK;
168
+ if (typeof value === 'string') return /(?:path|ref|file)$/i.test(key ?? '') ? redactPath(value) : redactText(value);
169
+ if (Array.isArray(value)) return value.map((v) => redactData(v, null, depth + 1));
170
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactData(v, k, depth + 1)]));
171
+ return value;
172
+ }
173
+
174
+ // -------------------------------------------------------------------------------------------- bytes
175
+ const TEXT_MEDIA = new RegExp([
176
+ String.raw`^text\/`,
177
+ String.raw`|[/+](?:json|xml|yaml|x-yaml|javascript|x-ndjson|x-diff|x-patch|csv|sql|x-sh)\b`,
178
+ String.raw`|^application\/(?:json|xml|yaml|javascript|x-ndjson|x-diff|x-patch|sql)$`,
179
+ ].join(''), 'i');
180
+
181
+ /** True for media types whose bytes are text the filter can read. */
182
+ export const isTextMedia = (mediaType) => TEXT_MEDIA.test(String(mediaType ?? ''));
183
+
184
+ /** The supported text encoding selected by a byte-order mark. */
185
+ export function textEncodingOf(bytes) {
186
+ if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) return 'utf-16le';
187
+ if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) return 'utf-16be';
188
+ if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) return 'utf-8-bom';
189
+ return 'utf-8';
190
+ }
191
+
192
+ /** Decode supported text without accepting malformed bytes as sanitized evidence. */
193
+ export function decodeText(bytes) {
194
+ const encoding = textEncodingOf(bytes);
195
+ return new TextDecoder(encoding === 'utf-8-bom' ? 'utf-8' : encoding, { fatal: true }).decode(bytes);
196
+ }
197
+
198
+ /**
199
+ * Bytes ready for a blob put: text bytes redacted ({redaction:'v1'}), anything else untouched
200
+ * ({redaction:'binary'}). The blob sha is then taken over the returned bytes.
201
+ */
202
+ export function redactBytes(bytes, mediaType, { repoRoots = [] } = {}) {
203
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
204
+ if (!isTextMedia(mediaType)) return { bytes: buf, redaction: 'binary' };
205
+ const text = decodeText(buf);
206
+ const clean = redactText(text, { repoRoots });
207
+ return { bytes: Buffer.from(clean, 'utf8'), redaction: REDACTION_VERSION };
208
+ }
@@ -0,0 +1,21 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ /** The usable runtime linked for `home`, or null with a diagnostic in `skipped` for a stale or unreadable record. */
6
+ export function readRuntimeRecord({ home = os.homedir(), exists = existsSync, read = readFileSync, skipped = [] } = {}) {
7
+ const record = path.join(home, '.starci', 'runtime.json');
8
+ if (!exists(record)) return null;
9
+ let candidate;
10
+ try { candidate = JSON.parse(read(record, 'utf8'))?.root; }
11
+ catch {
12
+ skipped.push(`${record} is not valid JSON`);
13
+ return null;
14
+ }
15
+ if (typeof candidate === 'string' && candidate) {
16
+ const root = path.resolve(candidate);
17
+ if (exists(path.join(root, 'scripts', 'cli', 'main.mjs'))) return { root, source: record };
18
+ }
19
+ skipped.push(`${record} points at ${JSON.stringify(candidate ?? null)}, which has no scripts/cli/main.mjs (stale record)`);
20
+ return null;
21
+ }
@@ -0,0 +1,21 @@
1
+ // The root archive's names equal npm's selection from tracked inputs and verified generated copies.
2
+ import { byCodeUnit } from './list.mjs';
3
+
4
+ const relativeFile = (file) => typeof file === 'string' && file.length > 0 && !/^[a-z]:/i.test(file) && !file.includes('\0') && !file.includes('\\') && !file.split('/').some((part) => !part || part === '.' || part === '..');
5
+
6
+ /** Exact selected-set proof; source-byte equality remains owned by verifyPackedDependencies. */
7
+ export function runtimePackageInventory({ files, tracked, generated = [], selected }) {
8
+ if (!Array.isArray(tracked) || !tracked.length || !tracked.every(relativeFile)) return { unrun: 'the tracked file inventory of the root is unavailable or malformed' };
9
+ if (!Array.isArray(selected) || !selected.length || !selected.every(relativeFile) || new Set(selected).size !== selected.length) return { unrun: 'npm did not return a complete, unique tracked-projection inventory' };
10
+ if (!Array.isArray(generated) || !generated.every(relativeFile)) return { unrun: 'the verified generated file inventory is malformed' };
11
+ const trackedSet = new Set(tracked), generatedSet = new Set(generated), selectedSet = new Set(selected);
12
+ const unknown = selected.filter((file) => !trackedSet.has(file) && !generatedSet.has(file));
13
+ if (unknown.length) return { unrun: `npm selected files outside the tracked projection: ${unknown.slice(0, 5).join(', ')}` };
14
+ const packed = [...files.keys()];
15
+ const foreign = packed.filter((file) => !file.startsWith('package/') || !selectedSet.has(file.slice('package/'.length))).sort(byCodeUnit);
16
+ if (foreign.length) return { red: `root archive carries ${foreign.length} file(s) outside npm's tracked-only selection (ignored build output or scratch): ${foreign.slice(0, 5).join(', ')}` };
17
+ const missing = selected.filter((file) => !files.has(`package/${file}`)).sort(byCodeUnit);
18
+ if (missing.length) return { red: `root archive omits ${missing.length} file(s) from npm's tracked-only selection: ${missing.slice(0, 5).join(', ')}` };
19
+ const generatedCount = selected.filter((file) => !trackedSet.has(file) && generatedSet.has(file)).length;
20
+ return { tracked: selected.length - generatedCount, generated: generatedCount };
21
+ }
@@ -0,0 +1,84 @@
1
+ // Manifests the root archive ships name files by path; a path that does not ship breaks the installed runtime.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { parseYaml } from '../../engine/yaml.mjs';
5
+ import { asList, byCodeUnit } from './list.mjs';
6
+ import { EXAMPLES_ROOT } from './example-refs.mjs';
7
+
8
+ const KNOWLEDGE_SOURCE = 'starci/knowledge-source@1';
9
+ const strings = (value) => asList(value).filter((item) => typeof item === 'string' && item.length > 0);
10
+ const entries = (value) => asList(value).filter((item) => item && typeof item === 'object');
11
+ const joined = (directory, file) => path.posix.normalize(path.posix.join(directory, file));
12
+
13
+ /** The example catalog: each example's files/entrypoint/projects/tests are relative to that example's own directory. */
14
+ const exampleReferences = (doc) => entries(doc?.examples).flatMap((example) => {
15
+ const base = path.posix.join(EXAMPLES_ROOT, String(example.path ?? ''));
16
+ return [...strings(example.files), ...strings(example.entrypoint), ...strings(example.projects), ...strings(example.tests)]
17
+ .map((file) => ({ from: String(example.id ?? '?'), target: joined(base, file) }));
18
+ });
19
+
20
+ /** The schema catalog: schemas[].file and every moduleLocalDocumentKinds[].files entry are relative to the package root. */
21
+ const schemaReferences = (doc) => [
22
+ ...entries(doc?.schemas).flatMap((schema) => strings(schema.file).map((file) => ({ from: String(schema.id ?? '?'), target: joined('.', file) }))),
23
+ ...entries(doc?.moduleLocalDocumentKinds).flatMap((kind) => strings(kind.files).map((file) => ({ from: String(kind.id ?? '?'), target: joined('.', file) }))),
24
+ ];
25
+
26
+ /** The path-bearing keys of a grammar family entry. */
27
+ const FAMILY_PATH_KEYS = ['authority', 'packageSnapshot', 'reusableStyle', 'productComposition', 'index'];
28
+
29
+ /** A knowledge-source index: branches[].path, topics[].path and the family files, relative to the index's own directory. */
30
+ const knowledgeReferences = (directory) => (doc) => [
31
+ ...entries(doc?.branches).flatMap((branch) => strings(branch.path).map((file) => ({ from: String(branch.id ?? '?'), target: joined(directory, file) }))),
32
+ ...entries(doc?.topics).flatMap((topic) => strings(topic.path).map((file) => ({ from: String(topic.title ?? '?'), target: joined(directory, file) }))),
33
+ ...entries(doc?.families).flatMap((family) => FAMILY_PATH_KEYS.flatMap((key) => strings(family[key]).map((file) => ({ from: `${family.id ?? '?'}.${key}`, target: joined(directory, file) })))),
34
+ ];
35
+
36
+ /** Manifests the package must ship; their absence from the selection is red, never a skip. */
37
+ export const REQUIRED_MANIFESTS = Object.freeze([`${EXAMPLES_ROOT}/index.yaml`, 'modules/schemas/index.yaml', 'knowledge/index.yaml']);
38
+
39
+ const KNOWLEDGE_INDEX = /^knowledge\/(?:.+\/)?index\.yaml$/;
40
+
41
+ /** The reader of one manifest file: the two catalogs have their own, every knowledge index reads its own directory. */
42
+ const readerOf = (file) => {
43
+ if (file === REQUIRED_MANIFESTS[0]) return exampleReferences;
44
+ if (file === REQUIRED_MANIFESTS[1]) return schemaReferences;
45
+ return knowledgeReferences(path.posix.dirname(file));
46
+ };
47
+
48
+ /** The manifests to read: the required set plus every selected knowledge index. */
49
+ const manifestFiles = (selected) => [...new Set([...REQUIRED_MANIFESTS, ...selected.filter((name) => KNOWLEDGE_INDEX.test(name))])].sort(byCodeUnit);
50
+
51
+ /** {references} of one manifest, {skip} for an index of another schema, {unrun} or {red} when it cannot be trusted. */
52
+ function readManifest(file, readFile) {
53
+ let doc;
54
+ try { doc = parseYaml(readFile(file)); } catch { return { unrun: `${file} is unreadable or not valid YAML` }; }
55
+ if (doc?.schema !== KNOWLEDGE_SOURCE && !REQUIRED_MANIFESTS.includes(file)) return { skip: true };
56
+ const references = readerOf(file)(doc);
57
+ if (!references.length) return { red: `${file} yields no file references; its reference keys changed or it lists nothing, so nothing in it would be checked` };
58
+ return { references };
59
+ }
60
+
61
+ /**
62
+ * Pre-pack admission: every path a shipped manifest names must be in the archive selection. Manifests are the required
63
+ * set plus every selected knowledge index stamped starci/knowledge-source@1, so a new index is covered unlisted.
64
+ * Returns {unrun} when a manifest cannot be read, {red} naming the first unresolved paths, else {manifests, references, counts}.
65
+ */
66
+ export function manifestAdmission({ root, selected, readFile = (file) => fs.readFileSync(path.join(root, file), 'utf8') }) {
67
+ const shipped = new Set(selected);
68
+ const missing = REQUIRED_MANIFESTS.filter((file) => !shipped.has(file));
69
+ if (missing.length) return { red: `the archive does not ship required manifest(s): ${missing.join(', ')}` };
70
+ const gaps = [];
71
+ const counts = {};
72
+ for (const file of manifestFiles(selected)) {
73
+ const read = readManifest(file, readFile);
74
+ if (read.unrun || read.red) return read;
75
+ if (read.skip) continue;
76
+ counts[file] = read.references.length;
77
+ for (const { from, target } of read.references) if (target.startsWith('../') || !shipped.has(target)) gaps.push(`${file} (${from}) -> ${target}`);
78
+ }
79
+ if (gaps.length) {
80
+ const sorted = [...new Set(gaps)].sort(byCodeUnit);
81
+ return { red: `${sorted.length} shipped manifest reference(s) do not resolve in the archive: ${sorted.slice(0, 5).join('; ')}` };
82
+ }
83
+ return { manifests: Object.keys(counts).length, references: Object.values(counts).reduce((sum, count) => sum + count, 0), counts };
84
+ }