agemon 0.1.0 → 6.3.0

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 (269) hide show
  1. package/README.md +983 -30
  2. package/assets/skills/architecture/agemon-architecture/LICENSE +21 -0
  3. package/assets/skills/architecture/agemon-architecture/NOTICE.md +9 -0
  4. package/assets/skills/architecture/agemon-architecture/SKILL.md +275 -0
  5. package/assets/skills/code-quality/agemon-clean-code/LICENSE +21 -0
  6. package/assets/skills/code-quality/agemon-clean-code/NOTICE.md +9 -0
  7. package/assets/skills/code-quality/agemon-clean-code/SKILL.md +78 -0
  8. package/assets/skills/code-quality/agemon-house-rules/SKILL.md +103 -0
  9. package/assets/skills/commit/agemon-commit/SKILL.md +87 -0
  10. package/assets/skills/design/agemon-design/LICENSE +21 -0
  11. package/assets/skills/design/agemon-design/NOTICE.md +43 -0
  12. package/assets/skills/design/agemon-design/SKILL.md +128 -0
  13. package/assets/skills/design/agemon-design/references/accessibility.md +182 -0
  14. package/assets/skills/design/agemon-design/references/actions.md +215 -0
  15. package/assets/skills/design/agemon-design/references/ai-chat.md +158 -0
  16. package/assets/skills/design/agemon-design/references/brief.md +190 -0
  17. package/assets/skills/design/agemon-design/references/color.md +216 -0
  18. package/assets/skills/design/agemon-design/references/components.md +91 -0
  19. package/assets/skills/design/agemon-design/references/copy.md +300 -0
  20. package/assets/skills/design/agemon-design/references/core-rules.md +166 -0
  21. package/assets/skills/design/agemon-design/references/craft-intent.md +162 -0
  22. package/assets/skills/design/agemon-design/references/dashboard.md +131 -0
  23. package/assets/skills/design/agemon-design/references/dataviz.md +197 -0
  24. package/assets/skills/design/agemon-design/references/finish-bar.md +243 -0
  25. package/assets/skills/design/agemon-design/references/forms.md +192 -0
  26. package/assets/skills/design/agemon-design/references/heuristics.md +319 -0
  27. package/assets/skills/design/agemon-design/references/inspiration.md +287 -0
  28. package/assets/skills/design/agemon-design/references/layout.md +236 -0
  29. package/assets/skills/design/agemon-design/references/loops.md +155 -0
  30. package/assets/skills/design/agemon-design/references/metadata.md +98 -0
  31. package/assets/skills/design/agemon-design/references/modern-css.md +448 -0
  32. package/assets/skills/design/agemon-design/references/motion.md +434 -0
  33. package/assets/skills/design/agemon-design/references/personas.md +206 -0
  34. package/assets/skills/design/agemon-design/references/preset-dashboard.md +109 -0
  35. package/assets/skills/design/agemon-design/references/preset-editorial.md +94 -0
  36. package/assets/skills/design/agemon-design/references/preset-minimal.md +93 -0
  37. package/assets/skills/design/agemon-design/references/principles-catalog.md +299 -0
  38. package/assets/skills/design/agemon-design/references/recipe-auth.md +88 -0
  39. package/assets/skills/design/agemon-design/references/recipe-dashboard.md +130 -0
  40. package/assets/skills/design/agemon-design/references/recipe-landing.md +120 -0
  41. package/assets/skills/design/agemon-design/references/responsive.md +168 -0
  42. package/assets/skills/design/agemon-design/references/review.md +340 -0
  43. package/assets/skills/design/agemon-design/references/routing.md +143 -0
  44. package/assets/skills/design/agemon-design/references/sound.md +150 -0
  45. package/assets/skills/design/agemon-design/references/spec.md +123 -0
  46. package/assets/skills/design/agemon-design/references/stack.md +522 -0
  47. package/assets/skills/design/agemon-design/references/stage-assets.md +106 -0
  48. package/assets/skills/design/agemon-design/references/stage-build.md +46 -0
  49. package/assets/skills/design/agemon-design/references/stage-critique.md +34 -0
  50. package/assets/skills/design/agemon-design/references/stage-directions.md +44 -0
  51. package/assets/skills/design/agemon-design/references/stage-intake.md +34 -0
  52. package/assets/skills/design/agemon-design/references/stage-verify.md +44 -0
  53. package/assets/skills/design/agemon-design/references/state-design.md +188 -0
  54. package/assets/skills/design/agemon-design/references/tells-2026-11.md +32 -0
  55. package/assets/skills/design/agemon-design/references/themes.md +233 -0
  56. package/assets/skills/design/agemon-design/references/third-party-licenses.md +323 -0
  57. package/assets/skills/design/agemon-design/references/tokens.md +350 -0
  58. package/assets/skills/design/agemon-design/references/typography.md +248 -0
  59. package/assets/skills/design/agemon-design/scripts/audit-page.js +532 -0
  60. package/assets/skills/design/agemon-design/scripts/check-source.mjs +358 -0
  61. package/assets/skills/design/agemon-design/scripts/deal.mjs +231 -0
  62. package/assets/skills/design/agemon-design/scripts/inventory.mjs +676 -0
  63. package/assets/skills/essentials/web-design-guidelines/LICENSE +21 -0
  64. package/assets/skills/essentials/web-design-guidelines/NOTICE.md +14 -0
  65. package/assets/skills/essentials/web-design-guidelines/SKILL.md +18 -0
  66. package/assets/skills/essentials/web-design-guidelines/references/guidelines.md +184 -0
  67. package/assets/skills/essentials/writing-guidelines/LICENSE +21 -0
  68. package/assets/skills/essentials/writing-guidelines/NOTICE.md +14 -0
  69. package/assets/skills/essentials/writing-guidelines/SKILL.md +18 -0
  70. package/assets/skills/essentials/writing-guidelines/references/guidelines.md +250 -0
  71. package/assets/skills/founder/agemon-founder-board/LICENSE +21 -0
  72. package/assets/skills/founder/agemon-founder-board/NOTICE.md +11 -0
  73. package/assets/skills/founder/agemon-founder-board/SKILL.md +111 -0
  74. package/assets/skills/founder/agemon-founder-board/lenses.md +84 -0
  75. package/assets/skills/founder/agemon-founder-brand/LICENSE +21 -0
  76. package/assets/skills/founder/agemon-founder-brand/NOTICE.md +12 -0
  77. package/assets/skills/founder/agemon-founder-brand/SKILL.md +77 -0
  78. package/assets/skills/founder/agemon-founder-cfo/LICENSE +21 -0
  79. package/assets/skills/founder/agemon-founder-cfo/NOTICE.md +12 -0
  80. package/assets/skills/founder/agemon-founder-cfo/SKILL.md +109 -0
  81. package/assets/skills/founder/agemon-founder-cfo/example.json +27 -0
  82. package/assets/skills/founder/agemon-founder-cfo/unit_economics.py +270 -0
  83. package/assets/skills/founder/agemon-founder-competitors/LICENSE +21 -0
  84. package/assets/skills/founder/agemon-founder-competitors/NOTICE.md +12 -0
  85. package/assets/skills/founder/agemon-founder-competitors/SKILL.md +84 -0
  86. package/assets/skills/founder/agemon-founder-consumer/LICENSE +21 -0
  87. package/assets/skills/founder/agemon-founder-consumer/NOTICE.md +11 -0
  88. package/assets/skills/founder/agemon-founder-consumer/SKILL.md +126 -0
  89. package/assets/skills/founder/agemon-founder-consumer/customer.example.json +34 -0
  90. package/assets/skills/founder/agemon-founder-consumer/panel.py +424 -0
  91. package/assets/skills/founder/agemon-founder-consumer/pitch.example.md +3 -0
  92. package/assets/skills/founder/agemon-founder-launch/LICENSE +21 -0
  93. package/assets/skills/founder/agemon-founder-launch/NOTICE.md +11 -0
  94. package/assets/skills/founder/agemon-founder-launch/SKILL.md +65 -0
  95. package/assets/skills/founder/agemon-founder-marketing/LICENSE +21 -0
  96. package/assets/skills/founder/agemon-founder-marketing/NOTICE.md +11 -0
  97. package/assets/skills/founder/agemon-founder-marketing/SKILL.md +80 -0
  98. package/assets/skills/founder/agemon-founder-offer/LICENSE +21 -0
  99. package/assets/skills/founder/agemon-founder-offer/NOTICE.md +11 -0
  100. package/assets/skills/founder/agemon-founder-offer/SKILL.md +75 -0
  101. package/assets/skills/founder/agemon-founder-ops/LICENSE +21 -0
  102. package/assets/skills/founder/agemon-founder-ops/NOTICE.md +12 -0
  103. package/assets/skills/founder/agemon-founder-ops/SKILL.md +77 -0
  104. package/assets/skills/founder/agemon-founder-plan/LICENSE +21 -0
  105. package/assets/skills/founder/agemon-founder-plan/NOTICE.md +11 -0
  106. package/assets/skills/founder/agemon-founder-plan/SKILL.md +88 -0
  107. package/assets/skills/founder/agemon-founder-plan/compile.py +178 -0
  108. package/assets/skills/founder/agemon-founder-pricing/LICENSE +21 -0
  109. package/assets/skills/founder/agemon-founder-pricing/NOTICE.md +12 -0
  110. package/assets/skills/founder/agemon-founder-pricing/SKILL.md +92 -0
  111. package/assets/skills/founder/agemon-founder-pricing/van_westendorp.py +172 -0
  112. package/assets/skills/geo-seo/geo/LICENSE +21 -0
  113. package/assets/skills/geo-seo/geo/NOTICE.md +18 -0
  114. package/assets/skills/geo-seo/geo/SKILL.md +145 -0
  115. package/assets/skills/geo-seo/geo-audit/LICENSE +21 -0
  116. package/assets/skills/geo-seo/geo-audit/NOTICE.md +17 -0
  117. package/assets/skills/geo-seo/geo-audit/SKILL.md +371 -0
  118. package/assets/skills/geo-seo/geo-audit/references/ai-visibility.md +233 -0
  119. package/assets/skills/geo-seo/geo-audit/references/content.md +329 -0
  120. package/assets/skills/geo-seo/geo-audit/references/platform-analysis.md +294 -0
  121. package/assets/skills/geo-seo/geo-audit/references/schema.md +383 -0
  122. package/assets/skills/geo-seo/geo-audit/references/technical.md +368 -0
  123. package/assets/skills/geo-seo/geo-brand-mentions/LICENSE +21 -0
  124. package/assets/skills/geo-seo/geo-brand-mentions/NOTICE.md +15 -0
  125. package/assets/skills/geo-seo/geo-brand-mentions/SKILL.md +459 -0
  126. package/assets/skills/geo-seo/geo-citability/LICENSE +21 -0
  127. package/assets/skills/geo-seo/geo-citability/NOTICE.md +14 -0
  128. package/assets/skills/geo-seo/geo-citability/SKILL.md +320 -0
  129. package/assets/skills/geo-seo/geo-content/LICENSE +21 -0
  130. package/assets/skills/geo-seo/geo-content/NOTICE.md +14 -0
  131. package/assets/skills/geo-seo/geo-content/SKILL.md +352 -0
  132. package/assets/skills/geo-seo/geo-crawlers/LICENSE +21 -0
  133. package/assets/skills/geo-seo/geo-crawlers/NOTICE.md +15 -0
  134. package/assets/skills/geo-seo/geo-crawlers/SKILL.md +388 -0
  135. package/assets/skills/geo-seo/geo-llmstxt/LICENSE +21 -0
  136. package/assets/skills/geo-seo/geo-llmstxt/NOTICE.md +14 -0
  137. package/assets/skills/geo-seo/geo-llmstxt/SKILL.md +433 -0
  138. package/assets/skills/geo-seo/geo-platform-optimizer/LICENSE +21 -0
  139. package/assets/skills/geo-seo/geo-platform-optimizer/NOTICE.md +14 -0
  140. package/assets/skills/geo-seo/geo-platform-optimizer/SKILL.md +282 -0
  141. package/assets/skills/geo-seo/geo-report/LICENSE +21 -0
  142. package/assets/skills/geo-seo/geo-report/NOTICE.md +14 -0
  143. package/assets/skills/geo-seo/geo-report/SKILL.md +406 -0
  144. package/assets/skills/geo-seo/geo-report-pdf/LICENSE +21 -0
  145. package/assets/skills/geo-seo/geo-report-pdf/NOTICE.md +20 -0
  146. package/assets/skills/geo-seo/geo-report-pdf/SKILL.md +153 -0
  147. package/assets/skills/geo-seo/geo-report-pdf/templates/geo-report-template.html +548 -0
  148. package/assets/skills/geo-seo/geo-schema/LICENSE +21 -0
  149. package/assets/skills/geo-seo/geo-schema/NOTICE.md +16 -0
  150. package/assets/skills/geo-seo/geo-schema/SKILL.md +396 -0
  151. package/assets/skills/geo-seo/geo-technical/LICENSE +21 -0
  152. package/assets/skills/geo-seo/geo-technical/NOTICE.md +15 -0
  153. package/assets/skills/geo-seo/geo-technical/SKILL.md +566 -0
  154. package/assets/skills/output-style/agemon-adhd/LICENSE +21 -0
  155. package/assets/skills/output-style/agemon-adhd/NOTICE.md +13 -0
  156. package/assets/skills/output-style/agemon-adhd/SKILL.md +138 -0
  157. package/assets/skills/performance/agemon-performance/NOTICE.md +1 -0
  158. package/assets/skills/performance/agemon-performance/SKILL.md +76 -0
  159. package/assets/skills/security/agemon-security/LICENSE +21 -0
  160. package/assets/skills/security/agemon-security/NOTICE.md +8 -0
  161. package/assets/skills/security/agemon-security/SKILL.md +356 -0
  162. package/assets/skills/security/agemon-security-audit/AI-AND-LLM.md +83 -0
  163. package/assets/skills/security/agemon-security-audit/ATTACK-CLASSES.md +130 -0
  164. package/assets/skills/security/agemon-security-audit/CLIENT-SIDE.md +83 -0
  165. package/assets/skills/security/agemon-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  166. package/assets/skills/security/agemon-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  167. package/assets/skills/security/agemon-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  168. package/assets/skills/security/agemon-security-audit/HUNTING.md +251 -0
  169. package/assets/skills/security/agemon-security-audit/LICENSE +21 -0
  170. package/assets/skills/security/agemon-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  171. package/assets/skills/security/agemon-security-audit/NOTICE.md +12 -0
  172. package/assets/skills/security/agemon-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  173. package/assets/skills/security/agemon-security-audit/RECONNAISSANCE.md +156 -0
  174. package/assets/skills/security/agemon-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  175. package/assets/skills/security/agemon-security-audit/SKILL.md +192 -0
  176. package/assets/skills/security/agemon-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  177. package/assets/skills/security/agemon-security-audit/VALIDATION-AND-REPORTING.md +186 -0
  178. package/assets/skills/security/agemon-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  179. package/assets/skills/security/agemon-security-audit/report-schema.json +461 -0
  180. package/assets/skills/security/agemon-security-audit/validate-coverage-ledger.cjs +872 -0
  181. package/assets/skills/security/agemon-security-audit/validate-findings.cjs +773 -0
  182. package/assets/skills/self-review/agemon-review-intake/LICENSE +21 -0
  183. package/assets/skills/self-review/agemon-review-intake/NOTICE.md +9 -0
  184. package/assets/skills/self-review/agemon-review-intake/SKILL.md +205 -0
  185. package/assets/skills/self-review/agemon-review-request/LICENSE +21 -0
  186. package/assets/skills/self-review/agemon-review-request/NOTICE.md +9 -0
  187. package/assets/skills/self-review/agemon-review-request/SKILL.md +104 -0
  188. package/assets/skills/self-review/agemon-review-request/code-reviewer.md +188 -0
  189. package/assets/skills/self-review/agemon-root-cause/LICENSE +21 -0
  190. package/assets/skills/self-review/agemon-root-cause/NOTICE.md +10 -0
  191. package/assets/skills/self-review/agemon-root-cause/SKILL.md +283 -0
  192. package/assets/skills/self-review/agemon-root-cause/condition-based-waiting-example.ts +158 -0
  193. package/assets/skills/self-review/agemon-root-cause/condition-based-waiting.md +115 -0
  194. package/assets/skills/self-review/agemon-root-cause/defense-in-depth.md +122 -0
  195. package/assets/skills/self-review/agemon-root-cause/find-polluter.sh +72 -0
  196. package/assets/skills/self-review/agemon-root-cause/root-cause-tracing.md +169 -0
  197. package/assets/skills/self-review/agemon-root-cause/test-academic.md +14 -0
  198. package/assets/skills/self-review/agemon-root-cause/test-pressure-1.md +58 -0
  199. package/assets/skills/self-review/agemon-root-cause/test-pressure-2.md +68 -0
  200. package/assets/skills/self-review/agemon-root-cause/test-pressure-3.md +69 -0
  201. package/assets/skills/self-review/agemon-verify-before-done/LICENSE +21 -0
  202. package/assets/skills/self-review/agemon-verify-before-done/NOTICE.md +9 -0
  203. package/assets/skills/self-review/agemon-verify-before-done/SKILL.md +120 -0
  204. package/assets/skills/team-lead/agemon-plan-rfc/SKILL.md +317 -0
  205. package/assets/skills/team-lead/prd-generator/SKILL.md +437 -0
  206. package/assets/skills/team-lead/skill-headofit-audit/SKILL.md +72 -0
  207. package/assets/skills/team-lead/skill-incident-rca/SKILL.md +66 -0
  208. package/assets/skills/team-lead/skill-meeting-lifecycle/SKILL.md +63 -0
  209. package/assets/skills/team-lead/skill-obsidian-vault-sync/SKILL.md +69 -0
  210. package/assets/skills/team-lead/skill-prd-decomposer/SKILL.md +71 -0
  211. package/assets/skills/team-lead/skill-sec-check/SKILL.md +70 -0
  212. package/assets/skills/team-lead/skill-task-extractor/SKILL.md +79 -0
  213. package/assets/skills/team-lead/skill-tech-spike/SKILL.md +57 -0
  214. package/assets/skills/team-lead/skill-techlead-reviewer/SKILL.md +81 -0
  215. package/assets/skills/workflow/agemon-brainstorming/LICENSE +21 -0
  216. package/assets/skills/workflow/agemon-brainstorming/NOTICE.md +14 -0
  217. package/assets/skills/workflow/agemon-brainstorming/SKILL.md +287 -0
  218. package/assets/skills/workflow/agemon-brainstorming/builder-check-prompt.md +23 -0
  219. package/assets/skills/workflow/agemon-dispatching-parallel-agents/LICENSE +21 -0
  220. package/assets/skills/workflow/agemon-dispatching-parallel-agents/NOTICE.md +13 -0
  221. package/assets/skills/workflow/agemon-dispatching-parallel-agents/SKILL.md +167 -0
  222. package/assets/skills/workflow/agemon-executing-plans/LICENSE +21 -0
  223. package/assets/skills/workflow/agemon-executing-plans/NOTICE.md +15 -0
  224. package/assets/skills/workflow/agemon-executing-plans/SKILL.md +322 -0
  225. package/assets/skills/workflow/agemon-executing-plans/scripts/task-done +55 -0
  226. package/assets/skills/workflow/agemon-executing-plans/scripts/task-start +32 -0
  227. package/assets/skills/workflow/agemon-finishing-a-development-branch/LICENSE +21 -0
  228. package/assets/skills/workflow/agemon-finishing-a-development-branch/NOTICE.md +13 -0
  229. package/assets/skills/workflow/agemon-finishing-a-development-branch/SKILL.md +225 -0
  230. package/assets/skills/workflow/agemon-subagent-driven-development/LICENSE +21 -0
  231. package/assets/skills/workflow/agemon-subagent-driven-development/NOTICE.md +15 -0
  232. package/assets/skills/workflow/agemon-subagent-driven-development/SKILL.md +339 -0
  233. package/assets/skills/workflow/agemon-subagent-driven-development/code-reviewer.md +198 -0
  234. package/assets/skills/workflow/agemon-subagent-driven-development/implementer-prompt.md +154 -0
  235. package/assets/skills/workflow/agemon-subagent-driven-development/re-review-prompt.md +116 -0
  236. package/assets/skills/workflow/agemon-subagent-driven-development/references/batching.md +11 -0
  237. package/assets/skills/workflow/agemon-subagent-driven-development/references/cannot-verify.md +10 -0
  238. package/assets/skills/workflow/agemon-subagent-driven-development/references/final-review.md +49 -0
  239. package/assets/skills/workflow/agemon-subagent-driven-development/references/fix-loop.md +77 -0
  240. package/assets/skills/workflow/agemon-subagent-driven-development/references/handling-reports.md +15 -0
  241. package/assets/skills/workflow/agemon-subagent-driven-development/references/preflight.md +26 -0
  242. package/assets/skills/workflow/agemon-subagent-driven-development/references/resuming.md +12 -0
  243. package/assets/skills/workflow/agemon-subagent-driven-development/references/role-scout.md +18 -0
  244. package/assets/skills/workflow/agemon-subagent-driven-development/references/role-worker.md +19 -0
  245. package/assets/skills/workflow/agemon-subagent-driven-development/scripts/review-package +64 -0
  246. package/assets/skills/workflow/agemon-subagent-driven-development/scripts/sdd-repo +54 -0
  247. package/assets/skills/workflow/agemon-subagent-driven-development/scripts/sdd-workspace +110 -0
  248. package/assets/skills/workflow/agemon-subagent-driven-development/scripts/task-brief +108 -0
  249. package/assets/skills/workflow/agemon-subagent-driven-development/task-reviewer-prompt.md +209 -0
  250. package/assets/skills/workflow/agemon-test-driven-development/LICENSE +21 -0
  251. package/assets/skills/workflow/agemon-test-driven-development/NOTICE.md +14 -0
  252. package/assets/skills/workflow/agemon-test-driven-development/SKILL.md +330 -0
  253. package/assets/skills/workflow/agemon-test-driven-development/writing-good-tests.md +198 -0
  254. package/assets/skills/workflow/agemon-using-git-worktrees/LICENSE +21 -0
  255. package/assets/skills/workflow/agemon-using-git-worktrees/NOTICE.md +13 -0
  256. package/assets/skills/workflow/agemon-using-git-worktrees/SKILL.md +167 -0
  257. package/assets/skills/workflow/agemon-writing-plans/LICENSE +21 -0
  258. package/assets/skills/workflow/agemon-writing-plans/NOTICE.md +13 -0
  259. package/assets/skills/workflow/agemon-writing-plans/SKILL.md +204 -0
  260. package/assets/tools/code-review-graph-constraints.txt +1418 -0
  261. package/assets/tools/package-lock.json +111 -0
  262. package/assets/tools/package.json +6 -0
  263. package/dist/chunk-ZZBF3GOV.js +3451 -0
  264. package/dist/chunk-ZZBF3GOV.js.map +1 -0
  265. package/dist/fake-subprocess-IV4ML77S.js +636 -0
  266. package/dist/fake-subprocess-IV4ML77S.js.map +1 -0
  267. package/dist/index.js +23836 -1945
  268. package/dist/index.js.map +1 -1
  269. package/package.json +37 -17
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Read-only source checks. Usage: node check-source.mjs [--tokens <inventory.json>] <paths...>
4
+ // Prints JSON to stdout and writes nothing. Exits 1 only for placeholder copy, 2 for bad input.
5
+ // Messages hold fixed text and typed values only, because the repo it reads is untrusted.
6
+
7
+ import { lstatSync, readdirSync, readFileSync } from "node:fs";
8
+ import { join, relative, resolve } from "node:path";
9
+ import { parseArgs } from "node:util";
10
+
11
+ const MAX_FILES = 5000;
12
+ const MAX_FILE_BYTES = 1024 * 1024;
13
+ const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
14
+ const MAX_FINDINGS_PER_RULE = 100;
15
+ const MAX_VALUE_LENGTH = 120;
16
+ const MAX_PATH_SPACES = 3;
17
+ const MAX_PATH_SEGMENT_LENGTH = 64;
18
+ const PLACEHOLDER_HINT_LOOKBACK = 200;
19
+ const EXIT_PLACEHOLDER = 1;
20
+ const EXIT_BAD_INPUT = 2;
21
+
22
+ // Source can suggest these but only a rendered page can prove them: one upstream
23
+ // detector logged 73 false contrast findings statically against 0 when rendered.
24
+ const RENDERED_ONLY_RULES = ["contrast", "target-size", "overflow", "console-errors"];
25
+
26
+ const SKIPPED_DIRECTORIES = new Set([
27
+ "node_modules", "dist", "build", "out", "coverage", "vendor", "storybook-static",
28
+ ]);
29
+
30
+ const SOURCE_FILE = /\.(?:css|scss|sass|less|pcss|tsx?|jsx?|mjs|cjs|html|hbs|ejs|vue|svelte|astro|mdx)$/;
31
+ const NON_PRODUCT_FILE =
32
+ /(?:^|\/)(?:tests?|e2e|cypress|fixtures|mocks|__tests__|__mocks__|__fixtures__)\/|\.(?:test|spec|stories|cy)\.[a-z]+$/;
33
+ const SAFE_PATH = /^[\p{L}\p{N}_ ./@()+[\]-]{1,200}$/u;
34
+ const SAFE_TOKEN_NAME = /^[A-Za-z-][\w-]{0,63}$/;
35
+ const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/;
36
+ const COLOR_FUNCTION =
37
+ /^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\((?:[\d.,%\s/+-]|none|deg|rad|turn|srgb|display-p3)+\)$/;
38
+
39
+ // The lookbehinds keep fragment links out: href="#add" is not a colour.
40
+ const COLOR_IN_TEXT =
41
+ /(?<![\w&/])(?<!href=["'])#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^()]{1,80}\)/g;
42
+ const TOKEN_DECLARATION_LINE = /^\s*--[\w-]{1,64}\s*:/;
43
+ const TOKEN_DECLARATION_MAX_LENGTH = 200;
44
+
45
+ // The lookbehind keeps globs and quoted strings such as "src/**/*.ts" from opening a comment.
46
+ const COMMENT_OPENER = /<!--|(?<![\w*"'`/.-])\/\*|(?<=^|\s)\/\//gm;
47
+ const COMMENT_CLOSERS = { "<!--": "-->", "/*": "*/", "//": "\n" };
48
+
49
+ const PLACEHOLDER_COPY = /\b(?:lorem ipsum|dolor sit amet|john doe|jane doe)\b/gi;
50
+ const PLACEHOLDER_HINT = /placeholder["'`]?\s*[=:,]\s*[{(]?\s*(?:[\w.]+\()?\s*["'`][^"'`\n]*$/i;
51
+ const SPREAD_PROPS = /\{\s*\.\.\./;
52
+ const OVERSHOOTING_CURVE =
53
+ /cubic-bezier\(\s*[\d.]+\s*,\s*(-?[\d.]+)\s*,\s*[\d.]+\s*,\s*(-?[\d.]+)\s*\)/;
54
+
55
+ function overshoots(match) {
56
+ const curve = OVERSHOOTING_CURVE.exec(match);
57
+ return !curve || [curve[1], curve[2]].some((y) => Number(y) < 0 || Number(y) > 1);
58
+ }
59
+
60
+ const ADVISORY_RULES = [
61
+ {
62
+ rule: "transition-all",
63
+ tier: "reflex",
64
+ pattern: /\btransition(?:-property)?\s*:[^;}\n]{0,80}\ball\b|\btransition-all\b/g,
65
+ message: "transition: all animates every property; name what moves",
66
+ },
67
+ {
68
+ rule: "gradient-text",
69
+ tier: "reflex",
70
+ pattern: /\bbackground-clip\s*:\s*text\b|\bbg-clip-text\b/g,
71
+ message: "gradient-filled text is a common generated look, and rendered contrast cannot be read from it",
72
+ },
73
+ {
74
+ rule: "bounce-easing",
75
+ tier: "reflex",
76
+ pattern: /\banimate-bounce\b|cubic-bezier\([^()]{1,60}\)/g,
77
+ applies: overshoots,
78
+ message: "bounce or overshooting easing; use it only if the brief asks",
79
+ },
80
+ {
81
+ rule: "placeholder-image",
82
+ tier: "reflex",
83
+ pattern:
84
+ /\b(?:i\.pravatar\.cc|randomuser\.me|ui-avatars\.com|api\.dicebear\.com|placekitten\.com|placehold\.co|via\.placeholder\.com|placeimg\.com|dummyimage\.com|fakeimg\.pl|lorempixel\.com|loremflickr\.com|picsum\.photos|source\.unsplash\.com)\b/g,
85
+ message: "image from a placeholder service; use real material or flag the gap",
86
+ },
87
+ {
88
+ rule: "missing-alt",
89
+ tier: "reflex",
90
+ pattern: /<img\b[^>]{0,2000}>/g,
91
+ applies: (tag) => !/\balt\s*=/.test(tag) && !SPREAD_PROPS.test(tag),
92
+ message: "image with no alt attribute",
93
+ },
94
+ {
95
+ rule: "missing-lang",
96
+ tier: "reflex",
97
+ pattern: /<html\b[^>]{0,2000}>/g,
98
+ applies: (tag) => !/\blang\s*=/.test(tag) && !SPREAD_PROPS.test(tag),
99
+ message: "html element with no lang attribute",
100
+ },
101
+ {
102
+ rule: "justified-text",
103
+ tier: "finish",
104
+ pattern: /\btext-align\s*:\s*justify\b|\btext-justify\b(?!\s*:)/g,
105
+ message: "justified text without hyphenation leaves uneven gaps",
106
+ },
107
+ ];
108
+
109
+ function fail(problem) {
110
+ process.stderr.write(`check-source: ${problem}\n`);
111
+ process.exit(EXIT_BAD_INPUT);
112
+ }
113
+
114
+ function readArguments() {
115
+ try {
116
+ return parseArgs({ allowPositionals: true, options: { tokens: { type: "string" } } });
117
+ } catch {
118
+ fail("unknown or incomplete flag");
119
+ }
120
+ }
121
+
122
+ function normalizedColor(raw) {
123
+ const value = raw.trim().toLowerCase().replace(/\s+/g, " ");
124
+ if (value.length > MAX_VALUE_LENGTH) return null;
125
+ if (HEX_COLOR.test(value)) return value.length > 5 ? value : expandShortHex(value);
126
+ return COLOR_FUNCTION.test(value) ? value : null;
127
+ }
128
+
129
+ function expandShortHex(hex) {
130
+ return `#${[...hex.slice(1)].map((digit) => digit + digit).join("")}`;
131
+ }
132
+
133
+ function readTokensByColor(path) {
134
+ let inventory;
135
+ try {
136
+ const status = lstatSync(path);
137
+ if (!status.isFile() || status.size > MAX_TOTAL_BYTES) fail("the tokens path is not a regular file of a sane size");
138
+ inventory = JSON.parse(readFileSync(path, "utf8"));
139
+ } catch {
140
+ fail("cannot read the tokens file as JSON");
141
+ }
142
+ if (!Array.isArray(inventory?.tokens)) fail("the tokens file has no tokens list");
143
+ const tokensByColor = new Map();
144
+ for (const { name, value, source, line } of inventory.tokens) {
145
+ const color = typeof value === "string" ? normalizedColor(value) : null;
146
+ if (!color || typeof name !== "string" || !SAFE_TOKEN_NAME.test(name)) continue;
147
+ const token = tokensByColor.get(color) ?? { name, definedAt: new Set() };
148
+ token.definedAt.add(`${source}:${line}`);
149
+ tokensByColor.set(color, token);
150
+ }
151
+ return tokensByColor;
152
+ }
153
+
154
+ function isSafePath(path) {
155
+ return (
156
+ SAFE_PATH.test(path) &&
157
+ path.split(" ").length - 1 <= MAX_PATH_SPACES &&
158
+ path.split("/").every((segment) => segment.length <= MAX_PATH_SEGMENT_LENGTH)
159
+ );
160
+ }
161
+
162
+ function isCheckedFile(path) {
163
+ return SOURCE_FILE.test(path) && !NON_PRODUCT_FILE.test(path);
164
+ }
165
+
166
+ function isWalkedDirectory(name) {
167
+ return !name.startsWith(".") && !SKIPPED_DIRECTORIES.has(name);
168
+ }
169
+
170
+ function listDirectory(path) {
171
+ try {
172
+ return readdirSync(path, { withFileTypes: true })
173
+ .filter((entry) => !entry.isSymbolicLink())
174
+ .filter((entry) => !entry.isDirectory() || isWalkedDirectory(entry.name))
175
+ .map((entry) => join(path, entry.name))
176
+ .sort();
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ function readWithinLimits(file, bytesReadSoFar) {
183
+ try {
184
+ const { size } = lstatSync(file);
185
+ if (size > MAX_FILE_BYTES || bytesReadSoFar + size > MAX_TOTAL_BYTES) {
186
+ return { truncated: true };
187
+ }
188
+ return { text: readFileSync(file, "utf8"), size };
189
+ } catch {
190
+ return { truncated: false };
191
+ }
192
+ }
193
+
194
+ function collectFiles(paths) {
195
+ const scan = { files: [], truncated: false, unread: 0, skipped: 0 };
196
+ const queue = paths.map((path) => relative(".", resolve(path)) || ".");
197
+ const namedPaths = new Set(queue);
198
+ while (queue.length > 0) {
199
+ const path = queue.shift();
200
+ if (!isSafePath(path)) {
201
+ scan.unread += 1;
202
+ continue;
203
+ }
204
+ let status;
205
+ try {
206
+ status = lstatSync(path);
207
+ } catch {
208
+ scan.unread += 1;
209
+ continue;
210
+ }
211
+ if (status.isDirectory()) {
212
+ const entries = listDirectory(path);
213
+ if (entries) for (const entry of entries) queue.push(entry);
214
+ else scan.unread += 1;
215
+ } else if (!status.isFile()) {
216
+ scan.unread += 1;
217
+ } else if (isCheckedFile(path)) {
218
+ if (scan.files.length === MAX_FILES) {
219
+ scan.truncated = true;
220
+ return scan;
221
+ }
222
+ scan.files.push(path);
223
+ } else if (namedPaths.has(path)) {
224
+ scan.skipped += 1;
225
+ }
226
+ }
227
+ return scan;
228
+ }
229
+
230
+ function blankOut(text) {
231
+ return text.replace(/[^\n]/g, " ");
232
+ }
233
+
234
+ function blankComments(text) {
235
+ let result = "";
236
+ let cursor = 0;
237
+ for (const opener of text.matchAll(COMMENT_OPENER)) {
238
+ if (opener.index < cursor) continue;
239
+ const closer = COMMENT_CLOSERS[opener[0]];
240
+ const closerIndex = text.indexOf(closer, opener.index + opener[0].length);
241
+ const end = closerIndex === -1 ? text.length : closerIndex + (closer === "\n" ? 0 : closer.length);
242
+ result += text.slice(cursor, opener.index) + blankOut(text.slice(opener.index, end));
243
+ cursor = end;
244
+ }
245
+ return result + text.slice(cursor);
246
+ }
247
+
248
+ function lineStartsOf(text) {
249
+ const starts = [0];
250
+ for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) {
251
+ starts.push(index + 1);
252
+ }
253
+ return starts;
254
+ }
255
+
256
+ function lineNumberAt(lineStarts, offset) {
257
+ let low = 0;
258
+ let high = lineStarts.length - 1;
259
+ while (low < high) {
260
+ const middle = Math.ceil((low + high) / 2);
261
+ if (lineStarts[middle] <= offset) low = middle;
262
+ else high = middle - 1;
263
+ }
264
+ return low + 1;
265
+ }
266
+
267
+ function isInputHint(text, offset) {
268
+ return PLACEHOLDER_HINT.test(text.slice(Math.max(0, offset - PLACEHOLDER_HINT_LOOKBACK), offset));
269
+ }
270
+
271
+ function checkText(rawText, file, tokensByColor, report) {
272
+ const text = blankComments(rawText);
273
+ const lineStarts = lineStartsOf(text);
274
+ const lineOf = (offset) => lineNumberAt(lineStarts, offset);
275
+
276
+ // Scans the raw text, because comment stripping cannot tell a regex or a string from a
277
+ // real opener: copy it blanks must stay visible as a finding, never pass in silence.
278
+ for (const match of rawText.matchAll(PLACEHOLDER_COPY)) {
279
+ if (isInputHint(rawText, match.index)) continue;
280
+ const copy = match[0].toLowerCase();
281
+ const looksCommented = text[match.index] !== rawText[match.index];
282
+ report({
283
+ tier: looksCommented ? "reflex" : "floor",
284
+ rule: looksCommented ? "placeholder-in-comment" : "placeholder-copy",
285
+ file,
286
+ line: lineOf(match.index),
287
+ message: looksCommented
288
+ ? `placeholder copy "${copy}" in what looks like a comment; check it is never shown`
289
+ : `placeholder copy "${copy}"; use real material or flag the gap`,
290
+ });
291
+ }
292
+ for (const { rule, tier, pattern, applies, message } of ADVISORY_RULES) {
293
+ for (const match of text.matchAll(pattern)) {
294
+ if (applies && !applies(match[0])) continue;
295
+ report({ tier, rule, file, line: lineOf(match.index), message });
296
+ }
297
+ }
298
+ for (const match of text.matchAll(COLOR_IN_TEXT)) {
299
+ const color = normalizedColor(match[0]);
300
+ const token = color && tokensByColor.get(color);
301
+ if (!token) continue;
302
+ const line = lineOf(match.index);
303
+ const lineStart = lineStarts[line - 1];
304
+ const lineText = text.slice(lineStart, lineStart + TOKEN_DECLARATION_MAX_LENGTH);
305
+ if (token.definedAt.has(`${file}:${line}`) || TOKEN_DECLARATION_LINE.test(lineText)) continue;
306
+ report({
307
+ tier: "reflex",
308
+ rule: "raw-value-beside-token",
309
+ file,
310
+ line,
311
+ message: `raw ${color} matches the token ${token.name}`,
312
+ });
313
+ }
314
+ }
315
+
316
+ function runChecks(paths, tokensByColor) {
317
+ const scan = collectFiles(paths);
318
+ const findings = [];
319
+ const countByRule = new Map();
320
+ const report = (finding) => {
321
+ const count = countByRule.get(finding.rule) ?? 0;
322
+ countByRule.set(finding.rule, count + 1);
323
+ if (count < MAX_FINDINGS_PER_RULE) findings.push(finding);
324
+ else scan.truncated = true;
325
+ };
326
+
327
+ let bytesRead = 0;
328
+ let filesRead = 0;
329
+ for (const file of scan.files) {
330
+ const { text, size, truncated } = readWithinLimits(file, bytesRead);
331
+ if (text === undefined) {
332
+ scan.unread += 1;
333
+ scan.truncated ||= truncated;
334
+ continue;
335
+ }
336
+ bytesRead += size;
337
+ filesRead += 1;
338
+ checkText(text, file, tokensByColor, report);
339
+ }
340
+ return {
341
+ findings,
342
+ blocked: countByRule.has("placeholder-copy"),
343
+ scan: { files: filesRead, truncated: scan.truncated, unread: scan.unread, skipped: scan.skipped },
344
+ };
345
+ }
346
+
347
+ // Node exits 1 on an uncaught error, which callers would read as placeholder copy.
348
+ try {
349
+ const { values: options, positionals: paths } = readArguments();
350
+ const tokensByColor = options.tokens ? readTokensByColor(options.tokens) : new Map();
351
+ const { findings, blocked, scan } = runChecks(paths, tokensByColor);
352
+ if (scan.files === 0) fail("read no source file; report these checks as not run");
353
+
354
+ process.stdout.write(`${JSON.stringify({ findings, notVerified: RENDERED_ONLY_RULES, scan }, null, 2)}\n`);
355
+ if (blocked) process.exitCode = EXIT_PLACEHOLDER;
356
+ } catch {
357
+ fail("could not finish; report these checks as not run");
358
+ }
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Picks one design direction outside the model, because a model's own pick falls
4
+ // back on the typical option.
5
+ // Usage: node deal.mjs --specs <path> --log <path> --seed <string> [--tau 1.75] [--pick <id>] [--check]
6
+ // Prints the result as JSON and appends one line to the log.
7
+ // --check writes nothing and hides the pick, so a preview cannot be re-rolled by editing specs.
8
+ // Only ids and axis buckets reach the log, because later sessions read it back.
9
+
10
+ import { createHash } from "node:crypto";
11
+ import { appendFileSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
12
+ import { dirname } from "node:path";
13
+ import { parseArgs } from "node:util";
14
+
15
+ const AXES = ["palette", "type", "layout", "density", "signature"];
16
+ const SPEC_COUNT = 3;
17
+ const DEFAULT_TAU = "1.75";
18
+ const RECENT_LOG_ENTRIES = 5;
19
+ const COLLISION_PENALTY = 0.25;
20
+ // Keeps a spec of zero typicality selectable; a zero weight could never be dealt.
21
+ const MIN_TYPICALITY = 0.01;
22
+ const EXIT_BAD_INPUT = 2;
23
+ const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
24
+ const MAX_SLUG_LENGTH = 64;
25
+
26
+ function fail(problem) {
27
+ process.stderr.write(`deal: ${problem}\n`);
28
+ process.exit(EXIT_BAD_INPUT);
29
+ }
30
+
31
+ function readArguments() {
32
+ try {
33
+ return parseArgs({
34
+ options: {
35
+ specs: { type: "string" },
36
+ log: { type: "string" },
37
+ seed: { type: "string" },
38
+ tau: { type: "string", default: DEFAULT_TAU },
39
+ pick: { type: "string" },
40
+ check: { type: "boolean", default: false },
41
+ },
42
+ }).values;
43
+ } catch {
44
+ fail("unknown or incomplete flag");
45
+ }
46
+ }
47
+
48
+ function isSlug(value) {
49
+ return typeof value === "string" && value.length <= MAX_SLUG_LENGTH && SLUG.test(value);
50
+ }
51
+
52
+ function isObject(value) {
53
+ return typeof value === "object" && value !== null && !Array.isArray(value);
54
+ }
55
+
56
+ function specProblem(spec) {
57
+ if (!isObject(spec)) return "a spec is not an object";
58
+ if (!isSlug(spec.id)) return "a spec id is not a lowercase slug";
59
+ if (typeof spec.summary !== "string" || typeof spec.subjectBasis !== "string") {
60
+ return `spec ${spec.id} needs a summary and a subjectBasis`;
61
+ }
62
+ if (typeof spec.typicality !== "number" || !(spec.typicality >= 0 && spec.typicality <= 1)) {
63
+ return `spec ${spec.id} needs a typicality from 0 to 1`;
64
+ }
65
+ if (!isObject(spec.axes) || !AXES.every((axis) => isSlug(spec.axes[axis]))) {
66
+ return `spec ${spec.id} needs ${AXES.join(", ")} as lowercase slugs`;
67
+ }
68
+ }
69
+
70
+ function directionsProblem(directions) {
71
+ if (!isObject(directions)) return "the specs file is not an object";
72
+ if (!isSlug(directions.briefSlug)) return "briefSlug is not a lowercase slug";
73
+ if (directions.locks !== undefined && !isObject(directions.locks)) return "locks is not an object";
74
+ if (!Array.isArray(directions.specs) || directions.specs.length !== SPEC_COUNT) {
75
+ return `the specs file needs exactly ${SPEC_COUNT} specs`;
76
+ }
77
+ const problem = directions.specs.map(specProblem).find(Boolean);
78
+ if (problem) return problem;
79
+ if (new Set(directions.specs.map((spec) => spec.id)).size !== SPEC_COUNT) {
80
+ return "spec ids repeat";
81
+ }
82
+ }
83
+
84
+ function readDirections(path) {
85
+ let directions;
86
+ try {
87
+ directions = JSON.parse(readFileSync(path, "utf8"));
88
+ } catch {
89
+ fail("cannot read the specs file as JSON");
90
+ }
91
+ const problem = directionsProblem(directions);
92
+ if (problem) fail(problem);
93
+ return directions;
94
+ }
95
+
96
+ function refuseSymlinkedLog(path) {
97
+ try {
98
+ if (lstatSync(path).isSymbolicLink()) fail("the log is a symlink");
99
+ } catch (error) {
100
+ if (error.code !== "ENOENT") fail("cannot read the log");
101
+ }
102
+ }
103
+
104
+ function readLogText(path) {
105
+ try {
106
+ return readFileSync(path, "utf8");
107
+ } catch (error) {
108
+ if (error.code === "ENOENT") return "";
109
+ fail("cannot read the log");
110
+ }
111
+ }
112
+
113
+ function parseLogLine(line) {
114
+ try {
115
+ const entry = JSON.parse(line);
116
+ return isObject(entry) && isObject(entry.axes) ? entry : undefined;
117
+ } catch {
118
+ return undefined;
119
+ }
120
+ }
121
+
122
+ function parseLogEntries(logText) {
123
+ const entries = [];
124
+ logText.split("\n").forEach((line, index) => {
125
+ if (!line.trim()) return;
126
+ const entry = parseLogLine(line);
127
+ if (entry) entries.push(entry);
128
+ else process.stderr.write(`deal: skipped corrupt log line ${index + 1}\n`);
129
+ });
130
+ return entries;
131
+ }
132
+
133
+ function hasSameAxes(entry, spec) {
134
+ return AXES.every((axis) => entry.axes[axis] === spec.axes[axis]);
135
+ }
136
+
137
+ function loggedPickId(logEntries, specs, briefSlug) {
138
+ const entry = logEntries.findLast(
139
+ (candidate) =>
140
+ candidate.briefSlug === briefSlug &&
141
+ specs.some((spec) => spec.id === candidate.picked && hasSameAxes(candidate, spec)),
142
+ );
143
+ return entry?.picked ?? null;
144
+ }
145
+
146
+ function typedAxes(spec) {
147
+ return Object.fromEntries(AXES.map((axis) => [axis, spec.axes[axis]]));
148
+ }
149
+
150
+ function collidingAxes(spec, recentEntries, locks) {
151
+ return AXES.filter(
152
+ (axis) =>
153
+ // A brand-fixed value repeats on every run, so it says nothing about variety.
154
+ (locks?.[axis] ?? null) === null &&
155
+ recentEntries.some((entry) => entry.axes[axis] === spec.axes[axis]),
156
+ );
157
+ }
158
+
159
+ function seededDraw(seed) {
160
+ return createHash("sha256").update(seed).digest().readUInt32BE(0) / 2 ** 32;
161
+ }
162
+
163
+ function dealtSpec(specs, collisionsById, seed, tau) {
164
+ const weights = specs.map(
165
+ (spec) =>
166
+ Math.max(spec.typicality, MIN_TYPICALITY) ** (1 / tau) *
167
+ COLLISION_PENALTY ** collisionsById.get(spec.id).length,
168
+ );
169
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
170
+ let remaining = seededDraw(seed) * totalWeight;
171
+ for (const [index, weight] of weights.entries()) {
172
+ remaining -= weight;
173
+ if (remaining < 0) return specs[index];
174
+ }
175
+ return specs.at(-1);
176
+ }
177
+
178
+ function appendLogEntry(path, logText, entry) {
179
+ const separator = logText && !logText.endsWith("\n") ? "\n" : "";
180
+ try {
181
+ mkdirSync(dirname(path), { recursive: true });
182
+ appendFileSync(path, `${separator}${JSON.stringify(entry)}\n`);
183
+ } catch {
184
+ fail("cannot write the log");
185
+ }
186
+ }
187
+
188
+ const options = readArguments();
189
+ if (!options.specs || !options.log || !options.seed) {
190
+ fail("--specs, --log and --seed are required");
191
+ }
192
+ const tau = Number(options.tau);
193
+ if (!(tau > 0 && Number.isFinite(tau))) fail("--tau must be a positive number");
194
+
195
+ const directions = readDirections(options.specs);
196
+ const { specs, locks, briefSlug } = directions;
197
+ const userPick = specs.find((spec) => spec.id === options.pick);
198
+ if (options.pick !== undefined && !userPick) fail("--pick names no spec");
199
+
200
+ refuseSymlinkedLog(options.log);
201
+ const logText = readLogText(options.log);
202
+ const logEntries = parseLogEntries(logText);
203
+ const recentEntries = logEntries.slice(-RECENT_LOG_ENTRIES);
204
+ const collisionsById = new Map(
205
+ specs.map((spec) => [spec.id, collidingAxes(spec, recentEntries, locks)]),
206
+ );
207
+
208
+ const allCollide = specs.every((spec) => collisionsById.get(spec.id).length > 0);
209
+
210
+ function printResult(result) {
211
+ process.stdout.write(`${JSON.stringify(result)}\n`);
212
+ }
213
+
214
+ if (options.check) {
215
+ printResult({
216
+ logged: loggedPickId(logEntries, specs, briefSlug),
217
+ collisions: Object.fromEntries(collisionsById),
218
+ allCollide,
219
+ });
220
+ } else {
221
+ const picked = userPick ?? dealtSpec(specs, collisionsById, options.seed, tau);
222
+ const pickedBy = userPick ? "user" : "deal";
223
+ appendLogEntry(options.log, logText, {
224
+ ts: new Date().toISOString(),
225
+ briefSlug,
226
+ picked: picked.id,
227
+ pickedBy,
228
+ axes: typedAxes(picked),
229
+ });
230
+ printResult({ picked: picked.id, pickedBy, collisions: collisionsById.get(picked.id), allCollide });
231
+ }