@starci/hfs 4.7.32 → 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 +9 -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
@@ -19,7 +19,7 @@ const ARGUMENT = /^[A-Za-z0-9_@%+=:,./-]+$/u;
19
19
  const UPPER_ENV = KEY;
20
20
 
21
21
  const FIELDS = {
22
- deploy: ['stack', 'remote', 'images', 'volumes', 'secrets', 'environment', 'configs', 'passEnv', 'verify', 'jobs', 'tasks', 'backup', 'ingress', 'registry'],
22
+ deploy: ['stack', 'remote', 'images', 'volumes', 'secrets', 'environment', 'configs', 'passEnv', 'postDeploy', 'verify', 'jobs', 'tasks', 'backup', 'ingress', 'registry'],
23
23
  remote: ['host', 'releasesBase', 'sealed'],
24
24
  sealed: ['ops', 'sshKey', 'knownHosts'],
25
25
  image: ['app', 'buildArgs'],
@@ -35,7 +35,7 @@ const FIELDS = {
35
35
  http: ['name', 'url', 'method', 'body', 'status', 'json', 'contains', 'retry'],
36
36
  exec: ['name', 'service', 'command', 'stdout'],
37
37
  jobs: ['service', 'command'],
38
- task: ['service', 'command'],
38
+ task: ['service', 'command', 'timeoutSeconds'],
39
39
  backup: ['image', 'keepDays', 'dir', 'volumes'],
40
40
  backupVolume: ['name', 'volume'],
41
41
  registry: ['host', 'visibility'],
@@ -228,9 +228,11 @@ function jobs(c, block, services) {
228
228
  }
229
229
 
230
230
  const TASK_NAME = /^[a-z][a-z0-9-]{0,40}$/u;
231
+ /** The seconds a post-deploy task may run before the deploy counts it failed and rolls back. */
232
+ const DEFAULT_TASK_TIMEOUT_SECONDS = 600;
231
233
 
232
234
  /** The one-off commands (`starci app stack task <name>`): name -> { service, command }, each run inside a running task of the service. */
233
- function tasks(c, block, services) {
235
+ function tasksOf(c, block, services) {
234
236
  if (block === undefined) return {};
235
237
  if (!plain(block)) { c.problem('deploy.tasks', 'must be a mapping'); return {}; }
236
238
  const out = {};
@@ -240,11 +242,24 @@ function tasks(c, block, services) {
240
242
  if (!closedFields(c, at, entry, FIELDS.task)) continue;
241
243
  nameField(c, `${at}.service`, entry.service);
242
244
  if (services.size > 0 && !services.has(entry.service)) c.problem(`${at}.service`, `${entry.service} is not a service of the stack source`);
243
- out[name] = { service: entry.service, command: command(c, `${at}.command`, entry.command) };
245
+ if (entry.timeoutSeconds !== undefined && !(Number.isInteger(entry.timeoutSeconds) && entry.timeoutSeconds >= 1 && entry.timeoutSeconds <= 3600)) c.problem(`${at}.timeoutSeconds`, 'must be a whole number of seconds from 1 to 3600');
246
+ out[name] = { service: entry.service, command: command(c, `${at}.command`, entry.command), timeoutSeconds: entry.timeoutSeconds ?? DEFAULT_TASK_TIMEOUT_SECONDS };
244
247
  }
245
248
  return out;
246
249
  }
247
250
 
251
+ /** The release tasks that run after convergence and before verification: a unique list of declared task names, in run order. */
252
+ function postDeploy(c, block, declared) {
253
+ if (block === undefined) return [];
254
+ if (!Array.isArray(block)) { c.problem('deploy.postDeploy', 'must be a list of declared task names'); return []; }
255
+ block.forEach((name, index) => {
256
+ const at = `deploy.postDeploy[${index}]`;
257
+ if (typeof name !== 'string' || !Object.hasOwn(declared, name)) c.problem(at, 'must name a task declared under deploy.tasks');
258
+ else if (block.indexOf(name) !== index) c.problem(at, `${name} is listed twice`);
259
+ });
260
+ return block;
261
+ }
262
+
248
263
  const REGISTRY_HOST = /^[a-z0-9][a-z0-9.-]*(?::\d{1,5})?$/u;
249
264
  const VISIBILITY = ['public', 'private'];
250
265
 
@@ -295,6 +310,7 @@ export function normalizeDeploy(block, { project, probe }) {
295
310
  const services = probe.services ?? new Set();
296
311
  const members = probe.members ?? new Set();
297
312
  if (block.stack !== undefined) nameField(c, 'deploy.stack', block.stack);
313
+ const tasks = tasksOf(c, block.tasks, services);
298
314
  const config = {
299
315
  stack: block.stack ?? project,
300
316
  remote: remote(c, block.remote),
@@ -306,7 +322,8 @@ export function normalizeDeploy(block, { project, probe }) {
306
322
  passEnv: block.passEnv === undefined ? [] : stringList(c, 'deploy.passEnv', block.passEnv, KEY),
307
323
  verify: verify(c, block.verify, services),
308
324
  jobs: jobs(c, block.jobs, services),
309
- tasks: tasks(c, block.tasks, services),
325
+ tasks,
326
+ postDeploy: postDeploy(c, block.postDeploy, tasks),
310
327
  backup: backup(c, block.backup),
311
328
  ingress: ingress(c, block.ingress, probe),
312
329
  registry: registry(c, block.registry),
@@ -13,7 +13,8 @@ import { ingressIssues } from './rules/stacks-ingress.mjs';
13
13
  import { liteBackingIssues } from './rules/stacks-lite-backing.mjs';
14
14
  import { releaseBackupIssues, scheduleIssues } from './rules/stacks-release.mjs';
15
15
  import { stacksDeploymentContexts } from './rules/stacks-runtime.mjs';
16
- import { appEdition, applicationStackFindings, liteCustodyDirectory } from './starcistacks-runtime.mjs';
16
+ import { readJson } from './rules/read.mjs';
17
+ import { applicationStackFindings, liteCustodyDirectory } from './starcistacks-runtime.mjs';
17
18
 
18
19
  const RESULT='starci/application-stacks-check@1';
19
20
  const STACKS_DIR='.starcistacks';
@@ -32,7 +33,7 @@ const HARDENING_CODES=new Set([
32
33
  'STACKS_PLACEMENT_MISSING','STACKS_HEALTHCHECK_MISSING','STACKS_UPDATE_POLICY_MISSING','STACKS_REALTIME_SCALED',
33
34
  'STACKS_RELEASE_UNDECLARED','BACKUP_UNDECLARED','BACKUP_PROOF_MISSING','STACKS_MODEL_UNAVAILABLE',
34
35
  'HFS_SCHEDULE_UNDECLARED','STACKS_INGRESS_UNDECLARED','STACKS_INGRESS_UNKNOWN',
35
- 'STACKS_LITE_SELFHOSTED_DB','STACKS_SIDECAR_VOLUME_UNDECLARED','STACKS_SIDECAR_DB_UNSUPPORTED',
36
+ 'STACKS_LITE_SELFHOSTED_DB','STACKS_SIDECAR_VOLUME_UNDECLARED','STACKS_SIDECAR_DB_UNSUPPORTED','STACKS_LITE_REDIS_PERSISTENCE',
36
37
  ]);
37
38
 
38
39
  function regular(file,{root=null}={}){
@@ -296,11 +297,29 @@ const checkServiceEnvironment=(services,add)=>{
296
297
  for(const [service,definition] of Object.entries(services))for(const [key] of composeEntries(definition?.environment, { isMapping: object }))
297
298
  if(sensitive.test(key)&&!/_FILE$/i.test(key))add('plaintext-sensitive-environment',`services.${service}.environment.${key}`,'sensitive configuration is present as a direct environment value; value redacted');
298
299
  };
299
- const checkModel=({model,ids,spec,manifest,lite,add})=>{
300
+ const dependencyComponent=(id,component,apps)=>component?.stateful===true||component?.database!==undefined
301
+ ||!apps.has(component?.app)&&!apps.has(id)&&!['service','job'].includes(component?.role);
302
+ const checkDependencyContainers=({services,spec,manifest,repo,add})=>{
303
+ const apps=new Set([...(Array.isArray(repo?.apps)?repo.apps:[]),
304
+ ...Object.values(object(repo?.sides)?repo.sides:{}).flatMap(side=>Array.isArray(side?.apps)?side.apps:[])].map(app=>app.name));
305
+ for(const id of Object.keys(object(spec?.components)?spec.components:{})){
306
+ const component=manifest.components?.[id];
307
+ if(nonempty(component?.app)&&!apps.has(component.app))
308
+ add('HFS_STACKS_SHAPE',`components.${id}.app`,'an application component must name an app in the HFS declaration');
309
+ if(!dependencyComponent(id,component,apps))continue;
310
+ const service=services[id];
311
+ if(!object(service)||!nonempty(service.image)&&!nonempty(service.build)&&!nonempty(service.build?.context))
312
+ add('HFS_STACKS_SHAPE',`services.${id}`,'a selected dependency must run in a Docker Linux container with a rendered image or build context');
313
+ else if(service.platform!==undefined&&(typeof service.platform!=='string'||!/^linux(?:\/[^/]+){0,2}$/.test(service.platform)))
314
+ add('HFS_STACKS_SHAPE',`services.${id}.platform`,'a selected dependency must use a Docker Linux container platform');
315
+ }
316
+ };
317
+ const checkModel=({model,ids,spec,manifest,lite,repo,add})=>{
300
318
  const services=object(model.services)?model.services:{};
301
319
  checkServiceIds(services,ids,add);
302
320
  if(unresolvedCompose(model))add('compose-placeholder-unresolved','composeModel','rendered Compose still contains an unresolved interpolation placeholder in a host-resolved field');
303
321
  checkServiceEnvironment(services,add);
322
+ checkDependencyContainers({services,spec,manifest,repo,add});
304
323
  if(object(spec)&&spec.runtime==='docker-swarm')for(const issue of swarmModelIssues({model,components:manifest.components,bindings:spec.components,lite}))
305
324
  add('HFS_STACKS_SHAPE',issue.at,issue.message);
306
325
  };
@@ -327,12 +346,12 @@ const checkKubernetesDeclaration=(manifest,add)=>{
327
346
  if(!['deferred','supported'].includes(manifest.k8s?.status)||!String(manifest.k8s?.reason??'').trim())
328
347
  add('k8s-status-invalid','k8s','Kubernetes must be explicitly deferred or supported with a reason');
329
348
  };
330
- const checkSelectedEnvironment=({environment,environmentNames,environments,stacksRoot,ids,root,model,manifest,lite,add})=>{
349
+ const checkSelectedEnvironment=({environment,environmentNames,environments,stacksRoot,ids,root,model,manifest,lite,repo,add})=>{
331
350
  if(nonempty(environment)&&!environmentNames.has(String(environment)))
332
351
  add('environment-unknown','environment',`environment must be one the declaration admits: ${[...environmentNames].join(', ')}`);
333
352
  const spec=environments[String(environment)],envDir=path.join(stacksRoot,String(environment??''));
334
353
  if(object(spec))checkEnvironmentSpec({spec,environment,envDir,ids,root,add});
335
- checkModel({model,ids,spec,manifest,lite,add});
354
+ checkModel({model,ids,spec,manifest,lite,repo,add});
336
355
  };
337
356
  const checkStacksManifest=(manifest,model,{stacksRoot,environment,root,repo,add})=>{
338
357
  for(const issue of validateSchema(manifest))add('schema-shape-invalid',issue.path,issue.message);
@@ -342,7 +361,7 @@ const checkStacksManifest=(manifest,model,{stacksRoot,environment,root,repo,add}
342
361
  checkStacksTree({stacksRoot,manifest,environmentNames,add,lite});
343
362
  checkKubernetesDeclaration(manifest,add);
344
363
  for(const issue of swarmInventoryIssues({components:manifest.components,lite}))add('HFS_STACKS_SHAPE',issue.at,issue.message);
345
- checkSelectedEnvironment({environment,environmentNames,environments,stacksRoot,ids,root,model,manifest,lite,add});
364
+ checkSelectedEnvironment({environment,environmentNames,environments,stacksRoot,ids,root,model,manifest,lite,repo,add});
346
365
  };
347
366
 
348
367
  function deploymentAvailable(model,errors){
@@ -370,8 +389,10 @@ function checkSwarmHardening({manifest,model,environment,root,files,repo,errors,
370
389
 
371
390
  export function checkApplicationStacks({repoRoot,environment,deploymentModelFile,files,repo}={}){
372
391
  const root=path.resolve(String(repoRoot??'')),stacksRoot=path.join(root,STACKS_DIR),manifestFile=path.join(stacksRoot,'application-stacks.yaml'),errors=[];
373
- const declaration=repo??{edition:appEdition(root)};
392
+ const diskDeclaration=readJson(root,'hfs.json'),declaration={edition:'full',...(object(diskDeclaration)?diskDeclaration:{}),...(object(repo)?repo:{})};
374
393
  const add=(code,at,message)=>errors.push({code,path:at,message});
394
+ if(fs.existsSync(path.join(root,'hfs.json'))&&!object(diskDeclaration))
395
+ add('HFS_STACKS_SHAPE','hfs.json','the HFS declaration must be a readable JSON object before application identities can be checked');
375
396
  checkInputPaths({environment,stacksRoot,manifestFile,deploymentModelFile,add});
376
397
  const {manifest,model}=loadInputs({manifestFile,deploymentModelFile,errors,add});
377
398
  checkStacksManifest(manifest,model,{stacksRoot,environment,root,repo:declaration,add});
@@ -14,7 +14,7 @@ const isAbsoluteAny = (text) => path.isAbsolute(text) || /^[A-Za-z]:\//.test(tex
14
14
  *
15
15
  * @param {object} opened - The result of `openHfs` (manifest, repo and resolver methods).
16
16
  * @param {string} repoRoot - The absolute repository root.
17
- * @returns {object} The frozen view: profile, apps, connections, ruleParams, relative, classify, slotOf, tierOf, ownerOf, slot.
17
+ * @returns {object} The frozen view: profile, apps, connections, patterns, ruleParams, relative, classify, slotOf, tierOf, ownerOf, slot.
18
18
  */
19
19
  function hfsView(opened, repoRoot) {
20
20
  const rel = (file) => {
@@ -35,6 +35,7 @@ function hfsView(opened, repoRoot) {
35
35
  edition: opened.repo.edition ?? 'full',
36
36
  apps: opened.repo.apps,
37
37
  connections: opened.repo.connections,
38
+ patterns: Object.freeze([...(opened.repo.patterns ?? [])]),
38
39
  ruleParams: opened.ruleParams(),
39
40
  /** The repository-relative, forward-slash form of a linted filename. */
40
41
  relative: rel,
@@ -0,0 +1,11 @@
1
+ // app-data.mjs — the per-OS application-data base directory (the parent of 'orca', 'codex-runtime-home'
2
+ // and friends): %APPDATA% on Windows, ~/Library/Application Support on macOS, $XDG_CONFIG_HOME|~/.config elsewhere.
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ /** The application-data base directory of this platform: {env, platform, home}: the caller passes the environment. */
7
+ export const appDataBase = ({ env, platform = process.platform, home = os.homedir() } = {}) => {
8
+ if (platform === 'win32') return env.APPDATA || path.join(home, 'AppData', 'Roaming');
9
+ if (platform === 'darwin') return path.join(home, 'Library', 'Application Support');
10
+ return env.XDG_CONFIG_HOME || path.join(home, '.config');
11
+ };
@@ -1,116 +1,313 @@
1
1
  // bypass-markers.mjs - what a BYPASS of a check looks like in text (owner ruling loosening-always-red: any bypass is red, always).
2
- // One table serves both sides of the no-bypass law:
3
- // product scripts/hfs/rules/bypass-markers.mjs (HFS_BYPASS_MARKER, `starci app check`) and `starci app hygiene` (the pre-commit
4
- // hook) read a whole file and refuse every marker in it;
5
- // runtime scripts/lib/gate-loosening.mjs reads the ADDED lines of a diff of a watched check, canon template or workflow.
6
- // A marker is a way to make a check pass without satisfying it: an inline disable of any tool, a type-check escape, a Sonar or
7
- // coverage exclusion comment, a skipped or focused test, a failure swallowed in a hook or a workflow step.
8
- // The patterns are assembled from fragments (joined) so that this file never spells a marker itself: the runtime detector watches
9
- // its own library like every other check script.
2
+ // One table serves both sides of the no-bypass law, and both read a whole file as it stands (no history):
3
+ // product scripts/hfs/rules/bypass-markers.mjs (HFS_BYPASS_MARKER, `starci app check`) and `starci app hygiene` (the pre-commit hook);
4
+ // runtime scripts/lib/loosening-state.mjs (RT_LOOSENING_STATE, `starci runtime check`).
5
+ // A marker is a way to make a check pass without satisfying it: an inline disable of any tool, a type-check escape, a Sonar or coverage
6
+ // exclusion comment, a skipped or focused test, a failure swallowed in a hook, a script or a workflow step, a hook skipped.
7
+ // A directive is read where it acts: a suppression word at the start of a comment, a test marker in code, a swallowed failure in a shell line or
8
+ // in the string a script hands a shell. The same words quoted as data (a message, a ban list, a fixture) are no directive.
9
+ // No file is skipped for its place or its name: a generated file or a folder named build is read like any other (a generator that emits a marker
10
+ // is a loosening too); build output is untracked and never reaches a tree check.
11
+ // The patterns are assembled from fragments (joined) so that this file never spells a marker itself.
12
+ import { langOf, lexSource, splitLines } from './js-lex.mjs';
13
+
10
14
  const join = (...parts) => parts.join('');
11
- const COMMENT_OPEN = /\/\/|\/\*/g;
12
15
 
13
16
  const SUPPRESSION_WORDS = [
14
17
  join('eslint', '-disable'), join('stylelint', '-disable'), join('tslint', ':disable'), join('prettier', '-ignore'), join('biome', '-ignore'),
15
- join('@ts', '-ignore'), join('@ts', '-expect-error'), join('@ts', '-nocheck'), join('NO', 'SONAR'),
16
- join('istanbul', ' ignore'), join('c8', ' ignore'), join('v8', ' ignore'), join('jscpd', ':ignore'),
18
+ join('@ts', '-ignore'), join('@ts', '-expect-error'), join('@ts', '-nocheck'),
19
+ join('istanbul', ' ignore'), join('c8', ' ignore'), join('v8', ' ignore'), join('jscpd', ':ignore'), join('eslint', '-env'),
17
20
  ];
18
21
  const escaped = (word) => word.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
19
- const COMMENT_MARKER = new RegExp(`(${SUPPRESSION_WORDS.map(escaped).join('|')})`);
22
+ /** A suppression directive opens its comment: after the opener only blanks and asterisks may precede it. */
23
+ const DIRECTIVE = new RegExp(String.raw`^[\s*/]*(${SUPPRESSION_WORDS.map(escaped).join('|')})`);
24
+ /** Inline configuration ESLint honours at the start of a comment: a rule level, declared globals or exports. */
25
+ const INLINE_CONFIG = new RegExp(String.raw`^[\s*/]*(${join('eslint')}\s+[@\w/.-]+\s*:|globals?\s|exported\b)`);
26
+ /** The type-check escapes: TypeScript reads them on the last line of a block comment too. */
27
+ const TYPE_ESCAPES = SUPPRESSION_WORDS.filter((word) => word.startsWith('@ts'));
28
+ /** Sonar reads its exclusion word anywhere in a comment. */
29
+ const SONAR_WORD = join('NO', 'SONAR');
20
30
 
21
- const TEST_MARKERS = [
22
- [new RegExp(String.raw`\b(?:it|test|describe|suite|context)\.(?:skip|only|todo|failing)\b`), 'a skipped or focused test'],
31
+ const RUNNER = '(?:it|test|describe|suite)';
32
+ const ANYWHERE_TESTS = [
33
+ [new RegExp(String.raw`\b${RUNNER}(?:\.\w+)*\.(?:skip|only|todo|failing)\b`), 'a skipped or focused test'],
34
+ [new RegExp(String.raw`\b${RUNNER}(?:\.\w+)*\.(?:skipIf|runIf)\(`), 'a conditionally skipped test'],
35
+ [new RegExp(String.raw`\{[^}]*\b(?:skip|only|todo)\b[^}]*\}\s*=\s*${RUNNER}\b`), 'a skip taken from the test runner'],
36
+ ];
37
+ /** Names that are ordinary identifiers outside a test file: read only in a spec or test file. */
38
+ const SPEC_TESTS = [
23
39
  [new RegExp(String.raw`\b(?:xit|xtest|xdescribe|fit|fdescribe)\(`), 'a skipped or focused test'],
24
- [new RegExp(String.raw`\.skipIf\(|\.runIf\(`), 'a conditionally skipped test'],
25
- [new RegExp(String.raw`\bskip\s*:\s*(?!false\b)\S`), 'a skip option'],
40
+ [new RegExp(String.raw`\b(?:skip|todo)\s*:\s*(?!false\b|\d|\[)\S|\bonly\s*:\s*true\b`), 'a skip option'],
41
+ [new RegExp(String.raw`\.\s*(?:skip|todo)\s*\(`), 'a skipped test through its context'],
26
42
  [new RegExp(String.raw`\bpending\(\)`), 'a pending test'],
27
43
  ];
44
+ const TEST_KEYS = new Set(['skip', 'only', 'todo', 'failing']);
45
+
46
+ const CHILD = '(?:execSync|spawnSync|execFileSync)';
47
+ /** Exit codes that a script resets or a helper swallows (scripts, hooks and tools; never specs). */
48
+ const EXIT_PATTERNS = [
49
+ [new RegExp(String.raw`\btry\s*\{[^{}]*\b${CHILD}\b[^{}]*\}\s*catch\s*(?:\(\s*\w*\s*\))?\s*\{\s*\}`), 'a child failure swallowed by an empty catch'],
50
+ [new RegExp(String.raw`\b${CHILD}\([^;]*\);\s*process\.exit\(\s*0\s*\)`), 'an exit 0 right after a child whose status is never read'],
51
+ ];
52
+ const EXIT_EVENTS = new Set(['exit', 'uncaughtException', 'unhandledRejection']);
53
+ const EXIT_ZERO = /exitCode\s*=\s*0|exit\(\s*0\s*\)/;
54
+
55
+ /** The text after the `=>` of a handler argument that follows the event name (`, () => ...`), or null when it is not one. */
56
+ function arrowBody(after) {
57
+ const text = after.trimStart();
58
+ const arrow = text.indexOf('=>');
59
+ return text.startsWith(',') && arrow > 0 && arrow < 60 ? text.slice(arrow + 2).trimStart() : null;
60
+ }
61
+
62
+ /** Whether the handler that starts at `after` (what follows the event name) is empty, resets the exit code or exits 0. */
63
+ function resetsExit(after) {
64
+ const body = arrowBody(after);
65
+ if (body === null) return false;
66
+ if (!body.startsWith('{')) return EXIT_ZERO.test(body.slice(0, 40));
67
+ const inner = body.slice(1, Math.max(1, body.indexOf('}')));
68
+ return inner.trim() === '' || EXIT_ZERO.test(inner);
69
+ }
70
+
71
+ /** Whether a line commits with a short flag group that holds n, which skips the hooks (`-n`, `-nm`, `-an`). */
72
+ function commitsWithShortSkip(line) {
73
+ const words = line.split(/\s+/);
74
+ const at = words.indexOf('commit');
75
+ return words.includes('git') && at >= 0 && words.slice(at + 1).some((word) => /^-[a-zA-Z]+$/.test(word) && word.includes('n'));
76
+ }
28
77
 
29
78
  const SWALLOW = [
30
- [/\|\|\s*(?:true|:)(?![\w-])/, 'a failure swallowed with `|| true`'],
79
+ [/\|\|\s*(?:true\b|:|"true"|'true'|\/(?:usr\/)?bin\/true|exit\s+0\b|return\s+0\b|echo\b|printf\b)/, 'a failure swallowed with or-true'],
80
+ [/;\s*(?:true\b(?!\s*[,:(.=])|:\s*$)/, 'a failure swallowed with semicolon-true'],
31
81
  [/\bcontinue-on-error\s*:\s*(?!false\b)\S/, join('continue', '-on-error')],
32
- [/\bset\s+\+e\b/, 'set +e'],
33
- [/\bHUSKY\s*=\s*0\b/, 'HUSKY=0'],
82
+ [/\bset\s+\+[a-z]*e/, join('set ', '+e')],
83
+ [/\bset\s+\+o\s+errexit\b/, join('set ', '+o errexit')],
84
+ [/\btrap\s+['"]?exit\s+0/, join('trap ', 'exit 0')],
85
+ [/\bHUSKY\s*=\s*0\b/, join('HUSKY', '=0')],
34
86
  [/--no-verify\b/, '--no-verify'],
87
+ [{ test: commitsWithShortSkip }, join('git commit ', '-n')],
88
+ [/-c\s+core\.hooksPath=[/\w]/, join('core.hooks', 'Path redirected')],
35
89
  [/\bfail_ci_if_error\s*:\s*false\b/, 'fail_ci_if_error: false'],
36
- ];
37
-
38
- // A weakening of the run itself that today's rendered canon still spells in places (the CI template runs jest with it); the runtime
39
- // detector refuses it as an ADDED line, the product rule waits until the canon is rid of it.
40
- const PENDING = [
41
- [/--passWithNoTests\b/, '--passWithNoTests'],
90
+ [/--(?:passWithNoTests|pass-with-no-tests)\b/, '--passWithNoTests'],
42
91
  [/--coverageThreshold\b/, '--coverageThreshold'],
92
+ [/--no-error-on-unmatched-pattern\b/, '--no-error-on-unmatched-pattern'],
93
+ [/--max-warnings(?:\s+|=)(?!0\b)\S/, 'a warning budget'],
94
+ [/--continue-on-error\b|--keep-going\b/, join('--continue', '-on-error')],
95
+ [/\beslint\b[^\n|;&]*--quiet\b/, 'eslint --quiet'],
96
+ [/--(?:rule\s+['"]?[@\w/-]+:\s*(?:off|0)|ignore-pattern|testPathIgnorePatterns)\b/, 'a check narrowed on the command line'],
97
+ [/-Dsonar\.(?:[\w.]*(?:exclusions|inclusions|sources)\b|qualitygate\.wait=(?!true\b))/, 'a Sonar scope set on the command line'],
98
+ [/sonar\.qualitygate\.wait\s*=\s*false/, 'a Sonar gate not waited for'],
43
99
  ];
100
+ /** The failures a script hands a shell in a string: the commands only, never a flag a message names. */
101
+ const COMMAND_SWALLOW = SWALLOW.filter(([, label]) => /or-true|semicolon-true|set \+|trap exit|HUSKY|git commit -n|Path redirected/.test(label));
102
+ const GIT_VERBS = new Set(['commit', 'push', 'merge', 'rebase', 'cherry-pick', 'am', 'revert']);
103
+ /** A git command line carrying the flag: after the flag only more flags, operators or quoted and variable words follow, never prose. */
104
+ const GIT_COMMAND = /^\s*git\s(?:\S+\s)*?--no-verify(?:\s+(?:-\S*|[&|;>"'$]\S*))*\s*$/;
105
+ const GIT_LINE_WITH_MESSAGE = /^\s*git\s.*--no-verify\b.*\s-m\s/;
44
106
 
107
+ /** Workflow lines that make a job or a step not run, or a shell not stop on a failure. */
108
+ const WORKFLOW_PATTERNS = [
109
+ [/^\s*shell:\s*bash\s*\{0\}/, 'a shell that does not stop on a failure'],
110
+ ];
45
111
  const CODE_FILE = /\.(?:[cm]?[jt]sx?|vue|svelte|css|scss|less)$/;
46
- const SHELL_FILE = /(?:^|\/)(?:\.husky\/(?!_\/)[^/]+|[^/]+\.sh|hooks\/husky\/[^/]+)$/;
47
- const WORKFLOW_FILE = /(?:^|\/)(?:\.github|github)\/(?:workflows|actions)\/.+\.ya?ml$/;
112
+ const SHELL_FILE = /(?:^|\/)(?:\.(?:husky|githooks)\/(?!_\/)[^/]+|[^/]+\.sh|hooks\/husky\/[^/]+)$/;
113
+ const WORKFLOW_FILE = /(?:^|\/)(?:(?:\.github|github)\/(?:workflows|actions)\/.+|ci-workflows(?:-lite)?\/[^/]+|lefthook)\.ya?ml$/;
114
+ const DOCKERFILE = /(?:^|\/)Dockerfile(?:\.[\w-]+)?$/;
48
115
  const MANIFEST_FILE = /(?:^|\/)package\.json$/;
49
- const SKIPPED_TREE = /(?:^|\/)(?:node_modules|dist|coverage|\.next|build)\//;
50
- const GENERATED_NAME = /(?:^|\/)(?:package-lock\.json|[^/]+\.generated\.[^/]+|[^/]+\.min\.[^/]+)$/;
116
+ const TEST_FILE = /(?:\.(?:spec|test|e2e-spec|integration-spec|contract-spec)\.[cm]?[jt]sx?$|(?:^|\/)(?:__tests__|tests?|e2e)\/)/;
117
+ const SHEBANG = /^#!\s*(?:\/usr\/bin\/env\s+)?\S*(?:sh|bash|zsh|dash)\b/;
118
+ const NAMED_SCRIPT = /\.\w+$/;
51
119
 
52
- /** The kind of text `file` holds for the markers: 'code', 'shell' (hooks, scripts), 'workflow', 'manifest', or null for a file no marker is read in. */
53
- export function bypassFileKind(file) {
54
- if (SKIPPED_TREE.test(file) || GENERATED_NAME.test(file)) return null;
120
+ /** The kind of text `file` holds for the markers: 'code', 'shell' (hooks, scripts), 'workflow', 'dockerfile', 'manifest', or null. `text` lets an extensionless file with a shell shebang count as a script. */
121
+ export function bypassFileKind(file, text = '') {
55
122
  if (SHELL_FILE.test(file)) return 'shell';
56
123
  if (WORKFLOW_FILE.test(file)) return 'workflow';
124
+ if (DOCKERFILE.test(file)) return 'dockerfile';
57
125
  if (MANIFEST_FILE.test(file)) return 'manifest';
58
126
  if (CODE_FILE.test(file)) return 'code';
59
- return null;
60
- }
61
-
62
- /** Whether `index` of `line` is outside every quoted string (a marker spelled inside a string is data, not a directive). */
63
- function outsideQuotes(line, index) {
64
- let quote = null;
65
- for (let at = 0; at < index; at += 1) {
66
- const char = line[at];
67
- if (quote) {
68
- if (char === '\\') at += 1;
69
- else if (char === quote) quote = null;
70
- } else if (char === '"' || char === "'" || char === '`') quote = char;
71
- }
72
- return quote === null;
127
+ return !NAMED_SCRIPT.test(file.slice(file.lastIndexOf('/') + 1)) && SHEBANG.test(text) ? 'shell' : null;
128
+ }
129
+
130
+ /** The text of a shell line without the read-only git queries in its `$(...)` and backticks: `|| true` inside one is a query, not a bypass. */
131
+ const QUERY = String.raw`git\s+(?:tag|rev-parse|config|ls-files|describe|log|show|status|branch|remote|diff|symbolic-ref|rev-list)\b[^()\`]*`;
132
+ const withoutSubstitutions = (line) => line.replace(new RegExp(String.raw`\$\(\s*${QUERY}\)`, 'g'), '').replace(new RegExp(String.raw`\`\s*${QUERY}\``, 'g'), '');
133
+
134
+ const labelsOf = (pairs, text) => pairs.filter(([pattern]) => pattern.test(text)).map(([, label]) => label);
135
+
136
+ /** The labels of the bypass markers on one `line` of a shell, Dockerfile or workflow file ([] when none). */
137
+ function textLabelsOf(kind, line) {
138
+ const text = withoutSubstitutions(line);
139
+ if (/^\s*#/.test(text)) return [];
140
+ const own = kind === 'workflow' ? WORKFLOW_PATTERNS : [];
141
+ return [...labelsOf(SWALLOW, text), ...labelsOf(own, text)];
73
142
  }
74
143
 
75
- /** The text of a shell line without its `$(...)` and backtick substitutions: `|| true` inside one is a query, not a bypass. */
76
- const withoutSubstitutions = (line) => line.replace(/\$\([^()]*\)/g, '').replace(/`[^`]*`/g, '');
144
+ const CONSTANT_FALSE = /^\s*(?:-\s+)?if:\s*(?:\$\{\{\s*)?false\b/;
145
+ const CHECK_STEP = /(?:run|uses|name):[^\n]*(?:test|lint|check|audit|tsc|coverage|sonar|codecov|jest|eslint|stylelint|prettier|verify|scan)/i;
77
146
 
78
- const hitsOf = (pairs, line) => pairs.filter(([pattern]) => pattern.test(line)).map(([, label]) => label);
147
+ /** A job or a check step of a workflow that a constant false condition keeps from running (a setup step disabled for an app without build values is none). */
148
+ function disabledHits(text) {
149
+ const lines = text.split('\n');
150
+ const hits = [];
151
+ lines.forEach((content, index) => {
152
+ if (!CONSTANT_FALSE.test(content)) return;
153
+ const indent = content.search(/\S/);
154
+ const label = 'a constant false condition';
155
+ if (indent === 4) { hits.push({ line: index + 1, label, text: content.trim() }); return; }
156
+ let start = index;
157
+ while (start > 0 && !/^\s*-\s/.test(lines[start]) && lines[start].search(/\S/) >= indent) start -= 1;
158
+ let end = index + 1;
159
+ while (end < lines.length && (lines[end].trim() === '' || lines[end].search(/\S/) >= indent) && !/^\s*-\s/.test(lines[end])) end += 1;
160
+ if (CHECK_STEP.test(lines.slice(start, end).join('\n'))) hits.push({ line: index + 1, label, text: content.trim() });
161
+ });
162
+ return hits;
163
+ }
164
+
165
+ /** A job-level `if: always()` of a workflow that never reads a result of the jobs it needs. */
166
+ function alwaysHits(text) {
167
+ const reads = /needs\.[\w-]+\.result|needs\.\*|toJSON\(needs\)/.test(text);
168
+ return text.split('\n').flatMap((content, index) => (/^ {4}if:\s*(?:\$\{\{\s*)?always\(\)/.test(content) && !reads
169
+ ? [{ line: index + 1, label: 'always() without reading the results of the needed jobs', text: content.trim() }] : []));
170
+ }
79
171
 
80
- function commentHit(line) {
81
- for (const open of line.matchAll(COMMENT_OPEN)) {
82
- if (!outsideQuotes(line, open.index)) continue;
83
- const marker = COMMENT_MARKER.exec(line.slice(open.index));
84
- if (marker) return marker[1];
172
+ /** The directive a comment line is, or null: a suppression word opens its comment; the Sonar word may stand anywhere in one. */
173
+ function commentDirective(comment, opening) {
174
+ const inline = comment.block ? INLINE_CONFIG.exec(comment.text)?.[1]?.trim() : undefined;
175
+ const open = opening ? (DIRECTIVE.exec(comment.text)?.[1] ?? inline) : null;
176
+ const closing = comment.last ? TYPE_ESCAPES.find((word) => comment.text.replace(/^[\s*]+/, '').startsWith(word)) : null;
177
+ return open ?? closing ?? (comment.text.includes(SONAR_WORD) ? SONAR_WORD : null);
178
+ }
179
+
180
+ /** The comments with the line a directive may open: the first non-blank line of each comment. */
181
+ function commentHits(comments) {
182
+ let waiting = false;
183
+ return comments.flatMap((comment) => {
184
+ if (comment.first) waiting = true;
185
+ const opening = waiting && comment.text.trim() !== '';
186
+ if (opening) waiting = false;
187
+ const directive = commentDirective(comment, opening);
188
+ return directive ? [{ line: comment.line, label: directive, text: comment.text.trim() }] : [];
189
+ });
190
+ }
191
+
192
+ const OPENERS = '([{';
193
+ const CLOSERS = ')]}';
194
+
195
+ /** The index of the bracket that encloses `at` in the blanked `code` (walking back, skipping balanced pairs), or -1. */
196
+ function enclosingOpen(code, at) {
197
+ let depth = 0;
198
+ for (let index = at; index >= 0; index -= 1) {
199
+ if (CLOSERS.includes(code[index])) depth += 1;
200
+ else if (OPENERS.includes(code[index]) && depth === 0) return index;
201
+ else if (OPENERS.includes(code[index])) depth -= 1;
85
202
  }
86
- return null;
87
- }
88
-
89
- /** The labels of the bypass markers on one `line` of a file of `kind` ([] when none). `pending` also reads the markers the canon still spells. */
90
- export function bypassLabelsOf(kind, line, { pending = false } = {}) {
91
- if (kind === 'code') {
92
- const comment = commentHit(line);
93
- const tests = TEST_MARKERS.filter(([pattern]) => {
94
- const hit = pattern.exec(line);
95
- return hit && outsideQuotes(line, hit.index);
96
- }).map(([, label]) => label);
97
- return [...(comment ? [comment] : []), ...tests];
203
+ return -1;
204
+ }
205
+
206
+ /** The index of the `]` that closes the `[` at `open`, or -1. */
207
+ function closingBracket(code, open) {
208
+ let depth = 0;
209
+ for (let index = open + 1; index < code.length; index += 1) {
210
+ if (code[index] === '[') depth += 1;
211
+ else if (code[index] === ']' && depth === 0) return index;
212
+ else if (code[index] === ']') depth -= 1;
98
213
  }
99
- if (kind === 'shell' || kind === 'workflow' || kind === 'manifest') {
100
- const text = withoutSubstitutions(line);
101
- if (kind === 'manifest' && !/"[^"]+"\s*:\s*"/.test(line)) return [];
102
- if (/^\s*#/.test(text)) return [];
103
- return [...hitsOf(SWALLOW, text), ...(pending ? hitsOf(PENDING, text) : [])];
214
+ return -1;
215
+ }
216
+
217
+ /** The offsets of the brackets of the array that holds `at` in the blanked `code`, or null when no array does. */
218
+ function arrayAround(code, at) {
219
+ const open = enclosingOpen(code, at);
220
+ if (open < 0 || code[open] !== '[') return null;
221
+ const close = closingBracket(code, open);
222
+ return close < 0 ? null : [open, close];
223
+ }
224
+
225
+ /** Whether the string `piece` hands git the hook-skipping flag: a git command line, or an element of an array that also names a git verb. */
226
+ function skipsHooks(piece, code, strings) {
227
+ if (GIT_COMMAND.test(piece.text) || GIT_LINE_WITH_MESSAGE.test(piece.text)) return true;
228
+ if (piece.text.trim() !== '--no-verify') return false;
229
+ const around = arrayAround(code, piece.at);
230
+ return around !== null && strings.some((other) => other.at > around[0] && other.at < around[1] && GIT_VERBS.has(other.text));
231
+ }
232
+
233
+ /** A test marker spelled as an index (`describe['skip']`): a string equal to a marker right after a `[`. */
234
+ const indexedMarker = (piece, code) => TEST_KEYS.has(piece.text) && code.slice(0, piece.at).trimEnd().endsWith('[');
235
+
236
+ /** An `exit` handler of the process that resets the exit code or swallows the failure. */
237
+ function exitHandler(piece, code) {
238
+ if (!EXIT_EVENTS.has(piece.text)) return false;
239
+ const before = code.slice(Math.max(0, piece.at - 24), piece.at);
240
+ return /process\s*\.\s*(?:on|once)\(\s*['"]?$/.test(before) && resetsExit(code.slice(piece.at + piece.text.length + 1, piece.at + piece.text.length + 200));
241
+ }
242
+
243
+ function stringHits(strings, code) {
244
+ const hits = [];
245
+ for (const piece of strings) {
246
+ piece.text.split('\n').forEach((content, offset) => {
247
+ for (const label of labelsOf(COMMAND_SWALLOW, withoutSubstitutions(content))) hits.push({ line: piece.line + offset, label, text: content.trim() });
248
+ });
249
+ if (skipsHooks(piece, code, strings)) hits.push({ line: piece.line, label: '--no-verify', text: piece.text.trim() });
250
+ if (/\bHUSKY\b/.test(code.slice(Math.max(0, piece.at - 12), piece.at)) && /^["']?0$/.test(piece.text.trim())) hits.push({ line: piece.line, label: join('HUSKY', '=0'), text: piece.text });
251
+ }
252
+ return hits;
253
+ }
254
+
255
+ /** The exit patterns of a script, matched over the whole code (a try block and the call that follows it span lines). */
256
+ function exitHits(code, sourceLines) {
257
+ return EXIT_PATTERNS.flatMap(([pattern, label]) => [...code.matchAll(new RegExp(pattern.source, 'g'))].map((match) => {
258
+ const line = splitLines(code.slice(0, match.index)).length;
259
+ return { line, label, text: (sourceLines[line - 1] ?? '').trim() };
260
+ }));
261
+ }
262
+
263
+ function codeHits(file, text) {
264
+ const { code, comments, strings } = lexSource(text, langOf(file));
265
+ const sourceLines = splitLines(text);
266
+ const spec = TEST_FILE.test(file);
267
+ const hits = commentHits(comments);
268
+ const tests = spec ? [...ANYWHERE_TESTS, ...SPEC_TESTS] : ANYWHERE_TESTS.slice(0, 2);
269
+ splitLines(code).forEach((content, index) => {
270
+ for (const label of labelsOf(tests, content)) hits.push({ line: index + 1, label, text: (sourceLines[index] ?? '').trim() });
271
+ });
272
+ if (!spec) hits.push(...exitHits(code, sourceLines));
273
+ for (const piece of strings) {
274
+ if (spec && indexedMarker(piece, code)) hits.push({ line: piece.line, label: 'a skipped or focused test', text: piece.text });
275
+ if (!spec && exitHandler(piece, code)) hits.push({ line: piece.line, label: 'a process exit handler that resets the exit code', text: piece.text });
104
276
  }
105
- return [];
277
+ const seen = new Set();
278
+ return [...hits, ...stringHits(strings, code)].sort((x, y) => x.line - y.line).filter((hit) => !seen.has(hit.line) && seen.add(hit.line));
279
+ }
280
+
281
+ /** package.json: every command a shell runs (scripts, lint-staged, husky, simple-git-hooks) and a tool config kept outside its own file. */
282
+ const COMMAND_KEYS = ['scripts', 'lint-staged', 'husky', 'simple-git-hooks'];
283
+ const TOOL_BLOCKS = ['jest', 'eslintConfig', 'stylelint'];
284
+
285
+ function commandStrings(value, name) {
286
+ if (typeof value === 'string') return [[name, value]];
287
+ if (value === null || typeof value !== 'object') return [];
288
+ return Object.entries(value).flatMap(([key, inner]) => commandStrings(inner, Array.isArray(value) ? name : key));
289
+ }
290
+
291
+ function manifestHits(text) {
292
+ let doc;
293
+ try { doc = JSON.parse(text); } catch { return []; }
294
+ const lines = text.split('\n');
295
+ const lineOf = (name) => lines.findIndex((line) => line.includes(JSON.stringify(name))) + 1;
296
+ const commands = COMMAND_KEYS.flatMap((key) => commandStrings(doc?.[key], key));
297
+ const hits = commands.flatMap(([name, command]) => labelsOf(SWALLOW, withoutSubstitutions(command)).map((label) => ({ line: lineOf(name), label, text: command.trim() })));
298
+ const blocks = TOOL_BLOCKS.filter((key) => doc?.[key] !== undefined).map((key) => ({ line: lineOf(key), label: `a ${key} config kept in package.json`, text: key }));
299
+ return [...hits, ...blocks];
106
300
  }
107
301
 
108
302
  /**
109
303
  * Every bypass marker of the text of one file: [{line, label, text}] (line numbers from 1). The file's kind decides the patterns;
110
304
  * a file of no kind has none.
111
305
  */
112
- export function bypassesIn(file, text, options = {}) {
113
- const kind = bypassFileKind(file);
306
+ export function bypassesIn(file, text) {
307
+ const kind = bypassFileKind(file, text);
114
308
  if (!kind || typeof text !== 'string') return [];
115
- return text.split(/\r?\n/).flatMap((content, index) => bypassLabelsOf(kind, content, options).map((label) => ({ line: index + 1, label, text: content.trim() })));
309
+ if (kind === 'code') return codeHits(file, text);
310
+ if (kind === 'manifest') return manifestHits(text);
311
+ const hits = text.split(/\r?\n/).flatMap((content, index) => textLabelsOf(kind, content).map((label) => ({ line: index + 1, label, text: content.trim() })));
312
+ return kind === 'workflow' ? [...hits, ...alwaysHits(text), ...disabledHits(text)] : hits;
116
313
  }
@@ -0,0 +1,7 @@
1
+ // check-format.mjs — the one-line-per-check report every canon check CLI shares.
2
+
3
+ const MARK = { pass: 'pass', fail: 'FAIL', skip: 'skip' };
4
+
5
+ /** `header` plus one ' [outcome] id: detail' line per check of `checks`, joined with '\n'. */
6
+ export const formatCheckLines = (header, checks) =>
7
+ [header, ...(checks ?? []).map((entry) => ` [${MARK[entry.outcome]}] ${entry.id}: ${entry.detail}`)].join('\n');