@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
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ // draw-taste.mjs — the taste metrics a machine can measure on a drawing (owner r5 ruling 2026-09-27: "correct is not
3
+ // beautiful" - too many hairline bands per card, red everywhere, badges on every fact). Thresholds are
4
+ // modules/models/runtimes.yaml allocation.drawLoop (accentBudget, bandsPerCardMax, badgesPerEntityMax):
5
+ //
6
+ // DRAW_ACCENT_BUDGET the accent colour fills more than `accentBudget` of the render's area. Measured on the PNG:
7
+ // a pixel within deltaE ACCENT_TOLERANCE (OKLab x100) of the accent token counts only where
8
+ // it belongs to a solid ACCENT_BLOCK x ACCENT_BLOCK block of accent (a fill - a button, a meter,
9
+ // a band - never a text stroke or a hairline), and the brand art band is excluded: the rects
10
+ // draw-render.mjs records under layout.accentExempt ([data-brand-art], data-grammar-part
11
+ // artwork-band, a data-grammar-proposal naming Artwork). The accent is the render's own
12
+ // `--accent` custom property (resolved through var()), else the brand primary;
13
+ // DRAW_TOO_MANY_BANDS a card (SurfaceCard, SurfaceListCard, SurfaceAccordionCard) splits into more than
14
+ // `bandsPerCardMax` bands: hairline-separated sections (Divider, <hr>, separator/divider/
15
+ // hairline classes - bands = separators + 1) or children marked band;
16
+ // DRAW_TOO_MANY_BADGES an entity (a card, a static state row, a list/table row) carries more than
17
+ // `badgesPerEntityMax` badges (Badge, StateMark, badge/chip/pill classes) of its own.
18
+ //
19
+ // starci work draw-taste --html <render.html> [--png <part.png>]... [--json]
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { isMain } from '../../lib/is-main.mjs';
23
+ import { ancestorsOf } from '../../lib/dom-tree.mjs';
24
+ import { allocationSettings } from '../../../engine/config.mjs';
25
+ import { decodePng } from '../png.mjs';
26
+ import { parseColor, parseCssCustomProperties } from '../brand/brand.mjs';
27
+ import { COMPONENT_ATTR, PART_ATTR, classesOf, elementsOf, parseHtml, visibleElement, walkElements } from './draw-dna.mjs';
28
+
29
+ export const DRAW_ACCENT_BUDGET = 'DRAW_ACCENT_BUDGET';
30
+ export const DRAW_TOO_MANY_BANDS = 'DRAW_TOO_MANY_BANDS';
31
+ export const DRAW_TOO_MANY_BADGES = 'DRAW_TOO_MANY_BADGES';
32
+ /** How close (OKLab deltaE x100) a pixel is to the accent to count as accent. */
33
+ const ACCENT_TOLERANCE = 8;
34
+ /** The side of the solid block (device pixels) an accent pixel must belong to: fills count, strokes do not. */
35
+ const ACCENT_BLOCK = 5;
36
+ /** The selector draw-render.mjs measures as the brand art band (exempt from the accent budget). */
37
+ export const ACCENT_EXEMPT_SELECTOR = '[data-brand-art], [data-accent-exempt], [data-grammar-part="artwork-band"], [data-grammar-proposal*="Artwork"], [data-grammar-proposal*="artwork"]';
38
+
39
+ /** modules/models/runtimes.yaml allocation.drawLoop - the draw loop's and the taste metrics' numbers. Throws when absent. */
40
+ export function drawLoopSettings() {
41
+ const s = allocationSettings().drawLoop;
42
+ if (!s || typeof s !== 'object') throw new Error('modules/models/runtimes.yaml allocation.drawLoop must declare the draw loop settings');
43
+ for (const k of ['maxRounds', 'stallRounds', 'beautyMin', 'accentBudget', 'bandsPerCardMax', 'badgesPerEntityMax']) {
44
+ if (!Number.isFinite(Number(s[k]))) throw new Error(`modules/models/runtimes.yaml allocation.drawLoop.${k} must be a number`);
45
+ }
46
+ return s;
47
+ }
48
+
49
+ const CARDS = new Set(['SurfaceCard', 'SurfaceListCard', 'SurfaceAccordionCard']);
50
+ const ENTITY_PARTS = /(^|-)(surface-fact|list-box-item|list-box-row|data-table-row|accordion-row|timeline-item|static-row)$/;
51
+ const SEPARATOR_CLASS = /(^|[-_])(separator|divider|hairline|rule)($|[-_])/i;
52
+ const BAND_CLASS = /(^|[-_])band($|[-_]|s$)/i;
53
+ const BADGE_CLASS = /(^|[-_])(badge|chip|pill|status-pill)($|[-_]{2}|$)/i;
54
+ const CSS_VAR_WITH_FALLBACK = /^var\(\s*(--[\w-]+)\s*,\s*((?:[^\s)][^)]*)?)\)$/;
55
+ const CSS_VAR_WITHOUT_FALLBACK = /^var\(\s*(--[\w-]+)\s*\)$/;
56
+ const nameOf = (el) => String(el?.attrs?.[COMPONENT_ATTR] ?? '').trim();
57
+ const partOf = (el) => String(el?.attrs?.[PART_ATTR] ?? '').trim();
58
+ /** A component root (not one of its own parts that repeats the component attribute). */
59
+ const rootOf = (el, names) => names.has(nameOf(el)) && !(partOf(el) && ancestorsOf(el).some((a) => nameOf(a)) && nameOf(ancestorsOf(el).find((a) => nameOf(a))) === nameOf(el));
60
+ const isSeparator = (el) => nameOf(el) === 'Divider' || el.tag === 'hr' || /divider/.test(partOf(el)) || classesOf(el).some((c) => SEPARATOR_CLASS.test(c));
61
+ const isBandMarked = (el) => /(^|-)band$/.test(partOf(el)) || classesOf(el).some((c) => BAND_CLASS.test(c) && !/(badge|brand)/i.test(c));
62
+ const isBadge = (el) => ['Badge', 'StateMark'].includes(nameOf(el)) && !(partOf(el) && nameOf(el.parent ?? {}) === nameOf(el))
63
+ || (!nameOf(el) && classesOf(el).some((c) => BADGE_CLASS.test(c) && !/(dot|icon|label|text)$/i.test(c)));
64
+
65
+ /** The bands of one card: separators + 1 when it has hairlines, else its children marked band, else 1. */
66
+ function bandsOfCard(card) {
67
+ let level = elementsOf(card).filter(visibleElement);
68
+ // Unwrap single content wrappers (surface-content, a lone div) to reach the band level.
69
+ while (level.length === 1 && !isBandMarked(level[0]) && !isSeparator(level[0]) && elementsOf(level[0]).length) level = elementsOf(level[0]).filter(visibleElement);
70
+ const separators = level.filter(isSeparator).length;
71
+ const marked = level.filter(isBandMarked).length;
72
+ return Math.max(separators ? separators + 1 : 0, marked, 1);
73
+ }
74
+
75
+ const isEntity = (el) => rootOf(el, CARDS) || nameOf(el) === 'StaticStateRow' || ENTITY_PARTS.test(partOf(el))
76
+ || ((el.tag === 'li' || el.tag === 'tr') && ancestorsOf(el).some((a) => CARDS.has(nameOf(a)) || nameOf(a) === 'DataTable'));
77
+
78
+ /** The badges an entity carries itself (a nested entity's badges are its own). */
79
+ function badgesOfEntity(entity) {
80
+ let n = 0;
81
+ const visit = (el) => {
82
+ for (const c of elementsOf(el)) {
83
+ if (!visibleElement(c)) continue;
84
+ if (isEntity(c)) continue;
85
+ if (isBadge(c)) { n += 1; continue; }
86
+ visit(c);
87
+ }
88
+ };
89
+ visit(entity);
90
+ return n;
91
+ }
92
+
93
+ const describe = (el) => {
94
+ const classes = classesOf(el);
95
+ const className = classes.length ? `.${classes.slice(0, 2).join('.')}` : '';
96
+ const name = nameOf(el);
97
+ const namePart = name ? ` ${name}` : '';
98
+ return `<${el.tag}${className}>${namePart}`;
99
+ };
100
+
101
+ /** The html taste findings (bands, badges) of one render source: [{code, detail, count, examples}]. */
102
+ export function htmlTasteFindings(html, { settings = drawLoopSettings(), label = 'the render' } = {}) {
103
+ const tree = parseHtml(html);
104
+ const all = walkElements(tree).filter(visibleElement);
105
+ const out = [];
106
+ const bandMax = Number(settings.bandsPerCardMax), badgeMax = Number(settings.badgesPerEntityMax);
107
+ const heavy = all.filter((el) => rootOf(el, CARDS)).map((el) => ({ el, bands: bandsOfCard(el) })).filter((c) => c.bands > bandMax);
108
+ if (heavy.length) {
109
+ const ex = heavy.map((c) => `${describe(c.el)} ${c.bands} bands`);
110
+ out.push({ code: DRAW_TOO_MANY_BANDS, count: heavy.length, examples: ex.slice(0, 8), detail: `${label}: ${heavy.length} card(s) split into more than ${bandMax} bands - ${ex.slice(0, 4).join('; ')}: merge bands (identity+status, capabilities, numbers+actions)` });
111
+ }
112
+ const loud = all.filter(isEntity).map((el) => ({ el, badges: badgesOfEntity(el) })).filter((e) => e.badges > badgeMax);
113
+ if (loud.length) {
114
+ const ex = loud.map((e) => `${describe(e.el)} ${e.badges} badges`);
115
+ out.push({ code: DRAW_TOO_MANY_BADGES, count: loud.length, examples: ex.slice(0, 8), detail: `${label}: ${loud.length} entit${loud.length > 1 ? 'ies carry' : 'y carries'} more than ${badgeMax} badges - ${ex.slice(0, 4).join('; ')}: a status is one toned word, facts stay plain text` });
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /** The accent colour a render declares: its `--accent` (var() chains resolved within the same scope), else null. */
121
+ function accentOf(html, { name = '--accent' } = {}) {
122
+ const css = [...String(html).matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1]).join('\n');
123
+ const { base } = parseCssCustomProperties(css);
124
+ let value = base.get(name)?.value ?? null;
125
+ for (let hop = 0; value && hop < 8; hop += 1) {
126
+ const trimmed = value.trim();
127
+ const ref = CSS_VAR_WITH_FALLBACK.exec(trimmed) ?? CSS_VAR_WITHOUT_FALLBACK.exec(trimmed);
128
+ if (!ref) break;
129
+ value = base.get(ref[1])?.value ?? ref[2] ?? null;
130
+ }
131
+ return value ? parseColor(value) : null;
132
+ }
133
+
134
+ const accentColorMatches = (color, accent, tolerance) => color ? 100 * Math.hypot(color.oklab.L - accent.L, color.oklab.a - accent.a, color.oklab.b - accent.b) <= tolerance : false;
135
+ function accentMaskOf(data, width, height, accent, tolerance) {
136
+ const mask = new Uint8Array(width * height);
137
+ const cache = new Map();
138
+ for (let y = 0, i = 0; y < height; y += 1) {
139
+ for (let x = 0; x < width; x += 1, i += 1) {
140
+ const offset = i * 4;
141
+ if (data[offset + 3] < 128) continue;
142
+ const key = (data[offset] << 16) | (data[offset + 1] << 8) | data[offset + 2];
143
+ let hit = cache.get(key);
144
+ if (hit === undefined) {
145
+ const color = parseColor(`rgb(${data[offset]} ${data[offset + 1]} ${data[offset + 2]})`);
146
+ hit = accentColorMatches(color, accent, tolerance);
147
+ cache.set(key, hit);
148
+ }
149
+ if (hit) mask[i] = 1;
150
+ }
151
+ }
152
+ return mask;
153
+ }
154
+
155
+ function excludeAccentRects(mask, width, height, exempt) {
156
+ const excluded = new Uint8Array(width * height);
157
+ let excludedCount = 0;
158
+ for (const rect of exempt) {
159
+ const x0 = Math.max(0, Math.floor(rect.x)), y0 = Math.max(0, Math.floor(rect.y)), x1 = Math.min(width, Math.ceil(rect.x + rect.width)), y1 = Math.min(height, Math.ceil(rect.y + rect.height));
160
+ for (let y = y0; y < y1; y += 1) {
161
+ for (let x = x0; x < x1; x += 1) {
162
+ const i = y * width + x;
163
+ if (!excluded[i]) { excluded[i] = 1; excludedCount += 1; }
164
+ mask[i] = 0;
165
+ }
166
+ }
167
+ }
168
+ return excludedCount;
169
+ }
170
+
171
+ function summedAreaTable(mask, width, height) {
172
+ const table = new Uint32Array((width + 1) * (height + 1));
173
+ for (let y = 1; y <= height; y += 1) {
174
+ let row = 0;
175
+ for (let x = 1; x <= width; x += 1) {
176
+ row += mask[(y - 1) * width + (x - 1)];
177
+ table[y * (width + 1) + x] = table[(y - 1) * (width + 1) + x] + row;
178
+ }
179
+ }
180
+ return table;
181
+ }
182
+
183
+ const tableRectSum = (table, stride, x0, y0, x1, y1) => table[y1 * stride + x1] - table[y0 * stride + x1] - table[y1 * stride + x0] + table[y0 * stride + x0];
184
+ function erodedAccentMask(mask, width, height, block, sum, full) {
185
+ const eroded = new Uint8Array(width * height);
186
+ for (let y = 0; y + block <= height; y += 1) {
187
+ for (let x = 0; x + block <= width; x += 1) {
188
+ if (sum(x, y, x + block, y + block) === full) eroded[y * width + x] = 1;
189
+ }
190
+ }
191
+ return eroded;
192
+ }
193
+
194
+ function countSolidAccentPixels(mask, width, height, block, sum) {
195
+ let count = 0;
196
+ for (let y = 0; y < height; y += 1) {
197
+ for (let x = 0; x < width; x += 1) {
198
+ if (!mask[y * width + x]) continue;
199
+ if (sum(Math.max(0, x - block + 1), Math.max(0, y - block + 1), x + 1, y + 1) > 0) count += 1;
200
+ }
201
+ }
202
+ return count;
203
+ }
204
+
205
+ /**
206
+ * The accent share of a decoded RGBA image: {share, accentPixels, measuredPixels}. `exempt` rects are in image pixels.
207
+ * A pixel counts only inside a solid `block` x `block` square of accent pixels (a morphological opening).
208
+ */
209
+ function accentShareOf(img, accent, { exempt = [], tolerance = ACCENT_TOLERANCE, block = ACCENT_BLOCK } = {}) {
210
+ const { width: W, height: H, data } = img;
211
+ const L = accent.oklab;
212
+ const mask = accentMaskOf(data, W, H, L, tolerance);
213
+ const excludedCount = excludeAccentRects(mask, W, H, exempt);
214
+ // Summed-area table of the mask: a block is solid when its sum is block*block.
215
+ const S = summedAreaTable(mask, W, H);
216
+ const sum = (x0, y0, x1, y1) => tableRectSum(S, W + 1, x0, y0, x1, y1);
217
+ const full = block * block;
218
+ // eroded[i] = the block whose top-left is i is solid; a pixel counts when any solid block covers it.
219
+ const eroded = erodedAccentMask(mask, W, H, block, sum, full);
220
+ const E = summedAreaTable(eroded, W, H);
221
+ const esum = (x0, y0, x1, y1) => tableRectSum(E, W + 1, x0, y0, x1, y1);
222
+ const count = countSolidAccentPixels(mask, W, H, block, esum);
223
+ const measured = W * H - excludedCount;
224
+ return { share: measured > 0 ? count / measured : 0, accentPixels: count, measuredPixels: measured };
225
+ }
226
+
227
+ /** The accent-exempt rects of a part in image pixels, from its draw-render record (layout.accentExempt, CSS px). */
228
+ function exemptRectsOf(record) {
229
+ const dpr = Number(record?.viewport?.deviceScaleFactor ?? 1) || 1;
230
+ return (Array.isArray(record?.layout?.accentExempt) ? record.layout.accentExempt : [])
231
+ .map((r) => ({ x: r.x * dpr, y: r.y * dpr, width: r.width * dpr, height: r.height * dpr }))
232
+ .filter((r) => [r.x, r.y, r.width, r.height].every(Number.isFinite) && r.width > 0 && r.height > 0);
233
+ }
234
+
235
+ /** The draw-render record beside a part PNG (same stem .json, schema starci/draw-render@1), or null. */
236
+ function renderRecordOf(png) {
237
+ try {
238
+ const doc = JSON.parse(fs.readFileSync(png.replace(/\.png$/i, '.json'), 'utf8'));
239
+ return doc?.schema === 'starci/draw-render@1' ? doc : null;
240
+ } catch { return null; }
241
+ }
242
+
243
+ /** The accent-budget finding of one part PNG, or null. {finding|null, share, accent} */
244
+ export function accentBudgetOf(png, { html = null, accent = null, settings = drawLoopSettings(), label = null } = {}) {
245
+ const colour = accent ?? (html ? accentOf(html) : null);
246
+ if (!colour) return { finding: null, share: null, accent: null, why: 'no --accent in the render source and no brand primary' };
247
+ let img = null;
248
+ try { img = decodePng(fs.readFileSync(png)); } catch { img = null; }
249
+ if (!img?.data) return { finding: null, share: null, accent: colour.hex, why: 'image unreadable' };
250
+ const r = accentShareOf(img, colour, { exempt: exemptRectsOf(renderRecordOf(png)) });
251
+ const budget = Number(settings.accentBudget);
252
+ const finding = r.share > budget ? { code: DRAW_ACCENT_BUDGET, count: 1, examples: [], detail: `${label ?? path.basename(png)}: the accent ${colour.hex} fills ${(r.share * 100).toFixed(1)}% of the render (budget ${(budget * 100).toFixed(0)}%, the brand art band excluded): spend the accent on the one primary action and the meters, keep surfaces neutral` } : null;
253
+ return { finding, share: r.share, accent: colour.hex };
254
+ }
255
+
256
+ async function main(argv) {
257
+ const at = (n) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : null; };
258
+ const html = at('--html');
259
+ if (!html) { process.stderr.write('use: starci work draw-taste --html <render.html> [--png <part.png>]... [--json]\n'); return 2; }
260
+ const text = fs.readFileSync(html, 'utf8');
261
+ const findings = htmlTasteFindings(text, { label: path.basename(html) });
262
+ const accents = [];
263
+ argv.forEach((a, i) => { if (a === '--png' && argv[i + 1]) { const r = accentBudgetOf(argv[i + 1], { html: text }); accents.push({ png: argv[i + 1], share: r.share, accent: r.accent }); if (r.finding) findings.push(r.finding); } });
264
+ if (argv.includes('--json')) process.stdout.write(`${JSON.stringify({ ok: !findings.length, findings, accents }, null, 2)}\n`);
265
+ else {
266
+ const findingText = findings.map((f) => ` [${f.code}] ${f.detail}`).join('\n');
267
+ const accentText = accents.map((a) => {
268
+ const share = a.share == null ? 'unmeasured' : `${(a.share * 100).toFixed(1)}%`;
269
+ return ` accent ${a.accent ?? '-'} ${share} ${a.png}`;
270
+ }).join('\n');
271
+ process.stdout.write(`${findings.length ? 'REFUSED' : 'ok'}: ${findings.length} finding(s)\n${findingText}\n${accentText}\n`);
272
+ }
273
+ return findings.length ? 1 : 0;
274
+ }
275
+
276
+ if (isMain(import.meta.url)) {
277
+ process.exitCode = await main(process.argv.slice(2));
278
+ }
@@ -0,0 +1,72 @@
1
+ // draw-typecheck.mjs — the typecheck half of the interface.draw source gate (draw-source.mjs): type-check one draw file
2
+ // against a grammar candidate with the product's typescript. Split out so draw-grammar.mjs, which draw-source.mjs loads,
3
+ // never imports the draw-source entry module back.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { findPackage, requirePackage } from '../../lib/package-at.mjs';
8
+ import { isFile } from '../../lib/fs-kind.mjs';
9
+
10
+ export const GRAMMAR_PACKAGE = '@starci/grammar';
11
+ const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
12
+ /** Ambient declarations every draw file type-checks with (a raster import is its URL, as draw-render's esbuild emits it). */
13
+ const DRAW_AMBIENT = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'avif'].map((ext) => `declare module "*.${ext}" { const src: string; export default src }\n`).join('');
14
+
15
+ /** The typescript module: the product's own install, else the runtime's. */
16
+ export function loadTypescript(dirs = []) {
17
+ const found = findPackage([...dirs, SKILL_ROOT], ['typescript']);
18
+ if (!found) return null;
19
+ return { ts: requirePackage(found), version: found.version, root: found.root };
20
+ }
21
+
22
+ const nodeModulesUp = (dir) => {
23
+ const out = [];
24
+ for (let d = path.resolve(dir); ; d = path.dirname(d)) {
25
+ const nm = path.join(d, 'node_modules');
26
+ if (fs.existsSync(nm)) out.push(nm);
27
+ if (path.dirname(d) === d) break;
28
+ }
29
+ return out;
30
+ };
31
+
32
+ /**
33
+ * Type-check one draw file against the grammar at `grammarRoot` (a package root holding dist/<family>/index.d.ts),
34
+ * with the product's typescript and compiler options. Every bare import resolves from the product's node_modules, so
35
+ * the draw file may live anywhere (a temp or ui-record dir). Returns {ok, errors:[{file, line, code, message}], typescript}.
36
+ */
37
+ export function typecheckDraw({ file, productDir, grammarRoot, ts: tsIn = null }) {
38
+ const loaded = tsIn ? { ts: tsIn, version: tsIn.version } : loadTypescript([productDir]);
39
+ if (!loaded) return { ok: false, errors: [{ file, line: null, code: 'no-typescript', message: `no typescript resolvable from ${productDir}` }], typescript: null };
40
+ const { ts } = loaded;
41
+ let options = { strict: true, jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, target: ts.ScriptTarget.ES2020,
42
+ lib: ['lib.dom.d.ts', 'lib.dom.iterable.d.ts', 'lib.esnext.d.ts'], skipLibCheck: true, esModuleInterop: true, isolatedModules: true, resolveJsonModule: true };
43
+ const tsconfig = path.join(productDir, 'tsconfig.json');
44
+ if (isFile(tsconfig)) {
45
+ const read = ts.readConfigFile(tsconfig, ts.sys.readFile);
46
+ if (!read.error) options = ts.parseJsonConfigFileContent(read.config, ts.sys, productDir, undefined, tsconfig).options;
47
+ }
48
+ const modules = nodeModulesUp(productDir);
49
+ const paths = { ...options.paths };
50
+ const base = options.pathsBasePath ?? options.baseUrl ?? productDir;
51
+ for (const [k, v] of Object.entries(paths)) paths[k] = v.map((p) => path.resolve(base, p));
52
+ const gRoot = path.resolve(grammarRoot);
53
+ paths[GRAMMAR_PACKAGE] = [path.join(gRoot, 'dist', 'core', 'index.d.ts')];
54
+ paths[`${GRAMMAR_PACKAGE}/*`] = [path.join(gRoot, 'dist', '*', 'index.d.ts')];
55
+ paths['*'] = [...modules.map((m) => path.join(m, '@types', '*')), ...modules.map((m) => path.join(m, '*'))];
56
+ const opts = { ...options, paths, baseUrl: undefined, pathsBasePath: productDir, noEmit: true, incremental: false, composite: false, tsBuildInfoFile: undefined, plugins: undefined,
57
+ skipLibCheck: true, types: [], typeRoots: modules.map((m) => path.join(m, '@types')) };
58
+ const ambient = path.join(path.dirname(path.resolve(file)), '__starci-draw-ambient.d.ts');
59
+ const host = ts.createCompilerHost(opts);
60
+ const same = (a, b) => path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
61
+ const { getSourceFile, fileExists, readFile } = host;
62
+ host.fileExists = (f) => same(f, ambient) || fileExists.call(host, f);
63
+ host.readFile = (f) => (same(f, ambient) ? DRAW_AMBIENT : readFile.call(host, f));
64
+ host.getSourceFile = (f, lang, ...rest) => (same(f, ambient) ? ts.createSourceFile(f, DRAW_AMBIENT, lang) : getSourceFile.call(host, f, lang, ...rest));
65
+ const program = ts.createProgram({ rootNames: [path.resolve(file), ambient], options: opts, host });
66
+ const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => d.category === ts.DiagnosticCategory.Error);
67
+ const errors = diagnostics.map((d) => {
68
+ const pos = d.file && d.start != null ? d.file.getLineAndCharacterOfPosition(d.start) : null;
69
+ return { file: d.file ? d.file.fileName : null, line: pos ? pos.line + 1 : null, code: `TS${d.code}`, message: ts.flattenDiagnosticMessageText(d.messageText, ' ').slice(0, 600) };
70
+ });
71
+ return { ok: errors.length === 0, errors, typescript: loaded.version ?? ts.version };
72
+ }
@@ -0,0 +1,205 @@
1
+ import { COMPONENT_ATTR, PART_ATTR, componentRootOf, parseHtml, visibleElement, walkElements, classesOf } from './draw-dna.mjs';
2
+ import { list } from '../../lib/list.mjs';
3
+ import { ancestorsOf } from '../../lib/dom-tree.mjs';
4
+
5
+ const PHRASING = new Set(['strong', 'em', 'b', 'i', 'u', 's', 'small', 'sub', 'sup', 'abbr', 'mark', 'time', 'code', 'bdi', 'bdo', 'q', 'cite', 'dfn', 'var', 'data', 'kbd', 'br', 'wbr', 'span']);
6
+ const KINDS_OF_CLASS = Object.freeze({ spacing: ['spacing', 'layout'], radius: ['radius'], fontSize: ['type'], fontWeight: ['type'], lineHeight: ['type'] });
7
+ const CLASS_LABEL = Object.freeze({ spacing: 'gap/padding/inset', radius: 'radius', fontSize: 'font-size', fontWeight: 'font-weight', lineHeight: 'line-height' });
8
+
9
+ const describe = (el) => {
10
+ const cls = classesOf(el).slice(0, 2).join('.');
11
+ return '<' + el.tag + (el.attrs.id ? '#' + el.attrs.id : '') + (cls ? '.' + cls : '') + '>';
12
+ };
13
+ const whysOf = (el, whyAttr, str) => str(el?.attrs?.[whyAttr]).split(/\s+/).filter(Boolean);
14
+ const inRedline = (el, redlineAttr) => [el, ...ancestorsOf(el)].some((a) => a.attrs?.[redlineAttr] != null || a.attrs?.id === 'redlines');
15
+ const refsOf = (record) => list(record?.refs).map((r) => (typeof r === 'string' ? r : r?.id ?? r?.ref ?? null)).filter(Boolean).map(String);
16
+ const hexRgb = (hex) => { const h = String(hex).replace('#', ''); return [0, 2, 4].map((i) => Number.parseInt(h.slice(i, i + 2), 16)); };
17
+ const colourClose = (a, b) => { const x = hexRgb(a), y = hexRgb(b); return x.every((v, i) => Math.abs(v - y[i]) <= 2); };
18
+
19
+ export function findRationaleIssues({ html, entries, errors, measures, resolve, record, dna, label, redlines }, { str, statesValue, round, statedValues, becauseCites, kebab, regionContainerOf, code, measureSchema, whyAttr, redlineAttr }) {
20
+ const groups = new Map();
21
+ const add = (kind, item) => {
22
+ if (!groups.has(kind)) groups.set(kind, []);
23
+ groups.get(kind).push(item);
24
+ };
25
+ for (const e of errors) add('rationale file', e);
26
+ const byId = new Map(entries.filter((e) => e?.id != null).map((e) => [String(e.id), e]));
27
+
28
+ const inspectDecisions = () => {
29
+ const refs = refsOf(record);
30
+ for (const e of entries) {
31
+ for (const r of list(e.rules)) {
32
+ const got = resolve(r);
33
+ if (!got.ok) add('unresolvable rule id', `${e.id}: "${str(r).slice(0, 60)}" (${got.why})`);
34
+ }
35
+ if (!str(e.because).trim()) add('empty because', `${e.id}`);
36
+ else if (!becauseCites(e.because, refs)) add('because cites no FR, content or user job', `${e.id}: "${str(e.because).slice(0, 60)}"`);
37
+ }
38
+ };
39
+ inspectDecisions();
40
+
41
+ const tree = parseHtml(html);
42
+ const visible = walkElements(tree).filter((el) => visibleElement(el) && !inRedline(el, redlineAttr));
43
+ const inspectVisibleElements = () => {
44
+ for (const el of visible) {
45
+ if (ancestorsOf(el).some((a) => a.tag === 'svg')) continue;
46
+ const ids = whysOf(el, whyAttr, str);
47
+ if (!ids.length) {
48
+ if (PHRASING.has(el.tag) && (el.tag === 'br' || el.tag === 'wbr' || (!el.attrs.class && !el.attrs.style && !el.attrs.id)) && el.parent?.tag !== '#root') continue;
49
+ add('element without data-why', describe(el));
50
+ continue;
51
+ }
52
+ for (const id of ids) if (!byId.has(id)) add('data-why naming no decision', `${describe(el)} data-why="${id}"`);
53
+ }
54
+ };
55
+ inspectVisibleElements();
56
+
57
+ const container = regionContainerOf(tree);
58
+ const regions = (container.children ?? []).filter((c) => c.tag && visibleElement(c) && !inRedline(c, redlineAttr) && !['script', 'style', 'template'].includes(c.tag));
59
+ const inspectRegions = () => {
60
+ for (const r of regions) {
61
+ const kinds = whysOf(r, whyAttr, str).map((id) => byId.get(id)?.kind).filter(Boolean);
62
+ if (kinds.length && !kinds.includes('layout')) add('region without a layout decision', `${describe(r)} (${whysOf(r, whyAttr, str).join(' ')} is ${kinds.join('/')})`);
63
+ }
64
+ const orderEntries = entries.filter((e) => e.kind === 'layout' && /order/i.test(`${e.id} ${e.selector} ${e.decision}`));
65
+ const counts = new Set([regions.length, ...measures.map((m) => m?.regions?.count).filter(Number.isFinite)]);
66
+ if (regions.length > 1 && !orderEntries.length) add('no region order decision', `${regions.length} regions and no layout decision states their order (id, selector or decision naming "order")`);
67
+ else if (regions.length > 1 && !orderEntries.some((e) => [...counts].some((n) => statedValues(`${e.value} ${e.decision}`).nums.has(n)))) {
68
+ add('region order without its count', `${orderEntries.map((e) => e.id).join(', ')} names no region count (${[...counts].join(' or ')})`);
69
+ }
70
+ };
71
+ inspectRegions();
72
+
73
+ const mentions = (needle) => {
74
+ const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
75
+ return entries.some((e) => new RegExp(`(^|[^A-Za-z0-9])${escaped}([^A-Za-z0-9]|$)`).test(`${str(e.decision)} ${str(e.value)} ${list(e.rules).map((r) => str(r)).join(' ')}`));
76
+ };
77
+ const variantsOfElement = (el, spec) => {
78
+ const variants = [];
79
+ for (const [k, v] of Object.entries(el.attrs)) {
80
+ if (!k.startsWith('data-') || [COMPONENT_ATTR, PART_ATTR, whyAttr, 'data-grammar-proposal'].includes(k) || !v) continue;
81
+ const prop = k.replace(/^data-(grammar-)?/, '');
82
+ const closed = spec ? [...spec.closed.keys()].find((p) => kebab(p) === prop) : null;
83
+ if (closed || ['variant', 'size', 'tone', 'color'].includes(prop)) variants.push(`${prop}=${v}`);
84
+ }
85
+ return variants;
86
+ };
87
+ const collectUsedVariants = () => {
88
+ const used = new Map();
89
+ for (const el of visible) {
90
+ const name = componentRootOf(el);
91
+ if (!name) continue;
92
+ if (!used.has(name)) used.set(name, new Set());
93
+ const spec = dna?.components?.get(name);
94
+ for (const variant of variantsOfElement(el, spec)) used.get(name).add(variant);
95
+ }
96
+ return used;
97
+ };
98
+ const inspectUsedVariants = (used) => {
99
+ for (const [name, variants] of used) {
100
+ if (!mentions(name)) { add('DNA component without a decision', name); continue; }
101
+ for (const pv of variants) {
102
+ const value = pv.split('=')[1];
103
+ if (!entries.some((e) => {
104
+ const text = `${str(e.decision)} ${str(e.value)} ${list(e.rules).map((r) => str(r)).join(' ')}`;
105
+ return new RegExp(`(^|[^A-Za-z0-9])${name}([^A-Za-z0-9]|$)`).test(text) && text.toLowerCase().includes(value.toLowerCase());
106
+ })) add('DNA variant without a decision', `${name} ${pv}`);
107
+ }
108
+ }
109
+ };
110
+ inspectUsedVariants(collectUsedVariants());
111
+
112
+ const inspectArtSlots = () => {
113
+ const artOf = (el) => whysOf(el, whyAttr, str).some((id) => byId.get(id)?.kind === 'art');
114
+ for (const el of visible) {
115
+ const slot = el.tag === 'img' || el.tag === 'picture' || el.attrs['data-asset-slot'] != null || el.attrs['data-artwork-slot'] != null;
116
+ if (slot && whysOf(el, whyAttr, str).length && !artOf(el)) add('art slot without an art decision', describe(el));
117
+ }
118
+ };
119
+ inspectArtSlots();
120
+
121
+ let need;
122
+ const inspectMeasuredValues = (m) => {
123
+ for (const cls of Object.keys(need)) {
124
+ for (const v of list(m.values?.[cls])) {
125
+ const key = typeof v.value === 'number' ? round(v.value) : String(v.value);
126
+ if (!need[cls].has(key)) need[cls].set(key, { value: key, at: new Set() });
127
+ need[cls].get(key).at.add(`${m.viewport?.width ?? '?'}px ${list(v.at)[0] ?? ''}`.trim());
128
+ }
129
+ }
130
+ };
131
+ const inspectMeasuredGrids = (m) => {
132
+ const gridsSeen = new Set();
133
+ for (const g of list(m.grids)) {
134
+ const gk = `${g.selector}|${g.columns}|${list(g.why).join(' ')}`;
135
+ if (gridsSeen.has(gk)) continue;
136
+ gridsSeen.add(gk);
137
+ const own = list(g.why).map((id) => byId.get(id)).filter(Boolean);
138
+ if (!own.some((e) => e.kind === 'layout' && statedValues(`${e.value} ${e.decision}`).nums.has(g.columns))) {
139
+ add('grid without its column count', `${g.selector} at ${m.viewport?.width ?? '?'}px renders ${g.columns} column(s); its layout decision (${list(g.why).join(' ') || 'none'}) does not state ${g.columns}`);
140
+ }
141
+ }
142
+ };
143
+ const inspectMeasuredColours = (m) => {
144
+ const tokens = m.tokenColours ?? {};
145
+ const colourEntries = entries.filter((e) => e.kind === 'colour');
146
+ const named = (hex) => colourEntries.some((e) => {
147
+ const text = `${str(e.decision)} ${str(e.value)}`.toLowerCase();
148
+ if (text.includes(hex.toLowerCase())) return true;
149
+ for (const t of text.match(/--[a-z0-9-]+/g) ?? []) if (tokens[t] && colourClose(tokens[t], hex)) return true;
150
+ return false;
151
+ });
152
+ for (const c of list(m.colours)) if (!named(c.hex)) add('colour without a decision', `${c.hex} (${c.use} at ${m.viewport?.width ?? '?'}px, ${list(c.at)[0] ?? ''})`);
153
+ };
154
+ const inspectMeasuredArt = (m) => {
155
+ for (const a of list(m.art)) {
156
+ const own = list(a.why).map((id) => byId.get(id)).filter(Boolean);
157
+ if (!own.some((e) => e.kind === 'art')) add('art slot without an art decision', `${a.selector} at ${m.viewport?.width ?? '?'}px (a background image)`);
158
+ }
159
+ };
160
+ const inspectMissingMeasuredValues = () => {
161
+ for (const [cls, values] of Object.entries(need)) {
162
+ const kinds = KINDS_OF_CLASS[cls];
163
+ for (const { value, at } of values.values()) {
164
+ if (!entries.some((e) => kinds.includes(e.kind) && statesValue(e, value))) {
165
+ let displayValue = value;
166
+ if (typeof value === 'number') {
167
+ displayValue = String(value);
168
+ if (cls !== 'fontWeight') displayValue += 'px';
169
+ }
170
+ add(`uncovered ${CLASS_LABEL[cls]} value`, `${displayValue} (${[...at].slice(0, 2).join('; ')}) - no ${kinds.join('/')} decision states it in its value`);
171
+ }
172
+ }
173
+ }
174
+ };
175
+ const inspectMeasures = () => {
176
+ if (!measures.length || measures.some((m) => m?.schema !== measureSchema)) {
177
+ add('render not measured', `${label} has no measured render (draw-render records carry rationale measures): re-render it with starci work draw-render`);
178
+ }
179
+ need = { spacing: new Map(), radius: new Map(), fontSize: new Map(), fontWeight: new Map(), lineHeight: new Map() };
180
+ for (const m of measures.filter((x) => x?.schema === measureSchema)) {
181
+ inspectMeasuredValues(m);
182
+ inspectMeasuredGrids(m);
183
+ inspectMeasuredColours(m);
184
+ inspectMeasuredArt(m);
185
+ }
186
+ inspectMissingMeasuredValues();
187
+ };
188
+ const inspectRedlines = () => {
189
+ if (Array.isArray(redlines)) {
190
+ for (const r of redlines) if (!r.ok) add('no redline render', `${r.part ?? label}: no annotated redline (<part>.redline.png) - draw-render captures it with the rationale beside the source`);
191
+ }
192
+ };
193
+ const summarize = () => {
194
+ const out = [];
195
+ for (const [kind, items] of groups) {
196
+ out.push({ code, kind, count: items.length, examples: items.slice(0, 8),
197
+ detail: `${label}: ${kind} (${items.length}) - ${items.slice(0, 4).join('; ')}${items.length > 4 ? ' (+' + (items.length - 4) + ')' : ''}` });
198
+ }
199
+ return out;
200
+ };
201
+
202
+ inspectMeasures();
203
+ inspectRedlines();
204
+ return summarize();
205
+ }