@owlmeans/viable-common 0.0.4

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 (453) hide show
  1. package/README.md +68 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/viable-common/SKILL.md +179 -0
  4. package/build/agent/index.d.ts +4 -0
  5. package/build/agent/index.d.ts.map +1 -0
  6. package/build/agent/index.js +3 -0
  7. package/build/agent/index.js.map +1 -0
  8. package/build/agent/memory.d.ts +12 -0
  9. package/build/agent/memory.d.ts.map +1 -0
  10. package/build/agent/memory.js +11 -0
  11. package/build/agent/memory.js.map +1 -0
  12. package/build/agent/spectator/consts.d.ts +9 -0
  13. package/build/agent/spectator/consts.d.ts.map +1 -0
  14. package/build/agent/spectator/consts.js +10 -0
  15. package/build/agent/spectator/consts.js.map +1 -0
  16. package/build/agent/spectator/index.d.ts +3 -0
  17. package/build/agent/spectator/index.d.ts.map +1 -0
  18. package/build/agent/spectator/index.js +2 -0
  19. package/build/agent/spectator/index.js.map +1 -0
  20. package/build/agent/spectator/types.d.ts +13 -0
  21. package/build/agent/spectator/types.d.ts.map +1 -0
  22. package/build/agent/spectator/types.js +2 -0
  23. package/build/agent/spectator/types.js.map +1 -0
  24. package/build/agent/types.d.ts +10 -0
  25. package/build/agent/types.d.ts.map +1 -0
  26. package/build/agent/types.js +2 -0
  27. package/build/agent/types.js.map +1 -0
  28. package/build/areas/consts.d.ts +88 -0
  29. package/build/areas/consts.d.ts.map +1 -0
  30. package/build/areas/consts.js +106 -0
  31. package/build/areas/consts.js.map +1 -0
  32. package/build/areas/index.d.ts +3 -0
  33. package/build/areas/index.d.ts.map +1 -0
  34. package/build/areas/index.js +2 -0
  35. package/build/areas/index.js.map +1 -0
  36. package/build/areas/types.d.ts +28 -0
  37. package/build/areas/types.d.ts.map +1 -0
  38. package/build/areas/types.js +2 -0
  39. package/build/areas/types.js.map +1 -0
  40. package/build/ba/consts.d.ts +38 -0
  41. package/build/ba/consts.d.ts.map +1 -0
  42. package/build/ba/consts.js +41 -0
  43. package/build/ba/consts.js.map +1 -0
  44. package/build/ba/helpers.d.ts +41 -0
  45. package/build/ba/helpers.d.ts.map +1 -0
  46. package/build/ba/helpers.js +79 -0
  47. package/build/ba/helpers.js.map +1 -0
  48. package/build/ba/index.d.ts +5 -0
  49. package/build/ba/index.d.ts.map +1 -0
  50. package/build/ba/index.js +4 -0
  51. package/build/ba/index.js.map +1 -0
  52. package/build/ba/schemas.d.ts +13 -0
  53. package/build/ba/schemas.d.ts.map +1 -0
  54. package/build/ba/schemas.js +181 -0
  55. package/build/ba/schemas.js.map +1 -0
  56. package/build/ba/types.d.ts +113 -0
  57. package/build/ba/types.d.ts.map +1 -0
  58. package/build/ba/types.js +2 -0
  59. package/build/ba/types.js.map +1 -0
  60. package/build/blueprint/case.d.ts +73 -0
  61. package/build/blueprint/case.d.ts.map +1 -0
  62. package/build/blueprint/case.js +244 -0
  63. package/build/blueprint/case.js.map +1 -0
  64. package/build/blueprint/consts.d.ts +11 -0
  65. package/build/blueprint/consts.d.ts.map +1 -0
  66. package/build/blueprint/consts.js +11 -0
  67. package/build/blueprint/consts.js.map +1 -0
  68. package/build/blueprint/index.d.ts +6 -0
  69. package/build/blueprint/index.d.ts.map +1 -0
  70. package/build/blueprint/index.js +6 -0
  71. package/build/blueprint/index.js.map +1 -0
  72. package/build/blueprint/resolve.d.ts +51 -0
  73. package/build/blueprint/resolve.d.ts.map +1 -0
  74. package/build/blueprint/resolve.js +108 -0
  75. package/build/blueprint/resolve.js.map +1 -0
  76. package/build/blueprint/schemas.d.ts +41 -0
  77. package/build/blueprint/schemas.d.ts.map +1 -0
  78. package/build/blueprint/schemas.js +27 -0
  79. package/build/blueprint/schemas.js.map +1 -0
  80. package/build/blueprint/types.d.ts +186 -0
  81. package/build/blueprint/types.d.ts.map +1 -0
  82. package/build/blueprint/types.js +32 -0
  83. package/build/blueprint/types.js.map +1 -0
  84. package/build/branding.d.ts +19 -0
  85. package/build/branding.d.ts.map +1 -0
  86. package/build/branding.js +32 -0
  87. package/build/branding.js.map +1 -0
  88. package/build/connect/consts.d.ts +378 -0
  89. package/build/connect/consts.d.ts.map +1 -0
  90. package/build/connect/consts.js +425 -0
  91. package/build/connect/consts.js.map +1 -0
  92. package/build/connect/entrypoints.d.ts +41 -0
  93. package/build/connect/entrypoints.d.ts.map +1 -0
  94. package/build/connect/entrypoints.js +141 -0
  95. package/build/connect/entrypoints.js.map +1 -0
  96. package/build/connect/errors.d.ts +50 -0
  97. package/build/connect/errors.d.ts.map +1 -0
  98. package/build/connect/errors.js +77 -0
  99. package/build/connect/errors.js.map +1 -0
  100. package/build/connect/index.d.ts +8 -0
  101. package/build/connect/index.d.ts.map +1 -0
  102. package/build/connect/index.js +6 -0
  103. package/build/connect/index.js.map +1 -0
  104. package/build/connect/ops.d.ts +192 -0
  105. package/build/connect/ops.d.ts.map +1 -0
  106. package/build/connect/ops.js +2 -0
  107. package/build/connect/ops.js.map +1 -0
  108. package/build/connect/references.d.ts +194 -0
  109. package/build/connect/references.d.ts.map +1 -0
  110. package/build/connect/references.js +62 -0
  111. package/build/connect/references.js.map +1 -0
  112. package/build/connect/schemas.d.ts +59 -0
  113. package/build/connect/schemas.d.ts.map +1 -0
  114. package/build/connect/schemas.js +346 -0
  115. package/build/connect/schemas.js.map +1 -0
  116. package/build/connect/types.d.ts +428 -0
  117. package/build/connect/types.d.ts.map +1 -0
  118. package/build/connect/types.js +2 -0
  119. package/build/connect/types.js.map +1 -0
  120. package/build/consts.d.ts +9 -0
  121. package/build/consts.d.ts.map +1 -0
  122. package/build/consts.js +31 -0
  123. package/build/consts.js.map +1 -0
  124. package/build/convert/consts.d.ts +539 -0
  125. package/build/convert/consts.d.ts.map +1 -0
  126. package/build/convert/consts.js +630 -0
  127. package/build/convert/consts.js.map +1 -0
  128. package/build/convert/helpers.d.ts +91 -0
  129. package/build/convert/helpers.d.ts.map +1 -0
  130. package/build/convert/helpers.js +301 -0
  131. package/build/convert/helpers.js.map +1 -0
  132. package/build/convert/index.d.ts +11 -0
  133. package/build/convert/index.d.ts.map +1 -0
  134. package/build/convert/index.js +4 -0
  135. package/build/convert/index.js.map +1 -0
  136. package/build/convert/schemas.d.ts +62 -0
  137. package/build/convert/schemas.d.ts.map +1 -0
  138. package/build/convert/schemas.js +626 -0
  139. package/build/convert/schemas.js.map +1 -0
  140. package/build/convert/types.d.ts +683 -0
  141. package/build/convert/types.d.ts.map +1 -0
  142. package/build/convert/types.js +2 -0
  143. package/build/convert/types.js.map +1 -0
  144. package/build/design/consts.d.ts +25 -0
  145. package/build/design/consts.d.ts.map +1 -0
  146. package/build/design/consts.js +26 -0
  147. package/build/design/consts.js.map +1 -0
  148. package/build/design/helpers.d.ts +60 -0
  149. package/build/design/helpers.d.ts.map +1 -0
  150. package/build/design/helpers.js +221 -0
  151. package/build/design/helpers.js.map +1 -0
  152. package/build/design/index.d.ts +6 -0
  153. package/build/design/index.d.ts.map +1 -0
  154. package/build/design/index.js +5 -0
  155. package/build/design/index.js.map +1 -0
  156. package/build/design/runtime.d.ts +126 -0
  157. package/build/design/runtime.d.ts.map +1 -0
  158. package/build/design/runtime.js +65 -0
  159. package/build/design/runtime.js.map +1 -0
  160. package/build/design/schemas.d.ts +21 -0
  161. package/build/design/schemas.d.ts.map +1 -0
  162. package/build/design/schemas.js +369 -0
  163. package/build/design/schemas.js.map +1 -0
  164. package/build/design/types.d.ts +199 -0
  165. package/build/design/types.d.ts.map +1 -0
  166. package/build/design/types.js +2 -0
  167. package/build/design/types.js.map +1 -0
  168. package/build/dev/consts.d.ts +32 -0
  169. package/build/dev/consts.d.ts.map +1 -0
  170. package/build/dev/consts.js +33 -0
  171. package/build/dev/consts.js.map +1 -0
  172. package/build/dev/index.d.ts +4 -0
  173. package/build/dev/index.d.ts.map +1 -0
  174. package/build/dev/index.js +3 -0
  175. package/build/dev/index.js.map +1 -0
  176. package/build/dev/schemas.d.ts +15 -0
  177. package/build/dev/schemas.d.ts.map +1 -0
  178. package/build/dev/schemas.js +116 -0
  179. package/build/dev/schemas.js.map +1 -0
  180. package/build/dev/types.d.ts +42 -0
  181. package/build/dev/types.d.ts.map +1 -0
  182. package/build/dev/types.js +2 -0
  183. package/build/dev/types.js.map +1 -0
  184. package/build/execution/consts.d.ts +36 -0
  185. package/build/execution/consts.d.ts.map +1 -0
  186. package/build/execution/consts.js +37 -0
  187. package/build/execution/consts.js.map +1 -0
  188. package/build/execution/index.d.ts +3 -0
  189. package/build/execution/index.d.ts.map +1 -0
  190. package/build/execution/index.js +2 -0
  191. package/build/execution/index.js.map +1 -0
  192. package/build/execution/types.d.ts +50 -0
  193. package/build/execution/types.d.ts.map +1 -0
  194. package/build/execution/types.js +2 -0
  195. package/build/execution/types.js.map +1 -0
  196. package/build/index.d.ts +22 -0
  197. package/build/index.d.ts.map +1 -0
  198. package/build/index.js +21 -0
  199. package/build/index.js.map +1 -0
  200. package/build/integrity/consts.d.ts +226 -0
  201. package/build/integrity/consts.d.ts.map +1 -0
  202. package/build/integrity/consts.js +401 -0
  203. package/build/integrity/consts.js.map +1 -0
  204. package/build/integrity/index.d.ts +4 -0
  205. package/build/integrity/index.d.ts.map +1 -0
  206. package/build/integrity/index.js +3 -0
  207. package/build/integrity/index.js.map +1 -0
  208. package/build/integrity/types.d.ts +35 -0
  209. package/build/integrity/types.d.ts.map +1 -0
  210. package/build/integrity/types.js +2 -0
  211. package/build/integrity/types.js.map +1 -0
  212. package/build/integrity/verify.d.ts +23 -0
  213. package/build/integrity/verify.d.ts.map +1 -0
  214. package/build/integrity/verify.js +198 -0
  215. package/build/integrity/verify.js.map +1 -0
  216. package/build/metadata/consts.d.ts +101 -0
  217. package/build/metadata/consts.d.ts.map +1 -0
  218. package/build/metadata/consts.js +104 -0
  219. package/build/metadata/consts.js.map +1 -0
  220. package/build/metadata/index.d.ts +3 -0
  221. package/build/metadata/index.d.ts.map +1 -0
  222. package/build/metadata/index.js +3 -0
  223. package/build/metadata/index.js.map +1 -0
  224. package/build/metadata/types.d.ts +286 -0
  225. package/build/metadata/types.d.ts.map +1 -0
  226. package/build/metadata/types.js +39 -0
  227. package/build/metadata/types.js.map +1 -0
  228. package/build/moderation/consts.d.ts +58 -0
  229. package/build/moderation/consts.d.ts.map +1 -0
  230. package/build/moderation/consts.js +60 -0
  231. package/build/moderation/consts.js.map +1 -0
  232. package/build/moderation/index.d.ts +5 -0
  233. package/build/moderation/index.d.ts.map +1 -0
  234. package/build/moderation/index.js +4 -0
  235. package/build/moderation/index.js.map +1 -0
  236. package/build/moderation/schemas.d.ts +19 -0
  237. package/build/moderation/schemas.d.ts.map +1 -0
  238. package/build/moderation/schemas.js +26 -0
  239. package/build/moderation/schemas.js.map +1 -0
  240. package/build/moderation/types.d.ts +64 -0
  241. package/build/moderation/types.d.ts.map +1 -0
  242. package/build/moderation/types.js +2 -0
  243. package/build/moderation/types.js.map +1 -0
  244. package/build/moderation/utils.d.ts +21 -0
  245. package/build/moderation/utils.d.ts.map +1 -0
  246. package/build/moderation/utils.js +31 -0
  247. package/build/moderation/utils.js.map +1 -0
  248. package/build/preview/consts.d.ts +10 -0
  249. package/build/preview/consts.d.ts.map +1 -0
  250. package/build/preview/consts.js +11 -0
  251. package/build/preview/consts.js.map +1 -0
  252. package/build/preview/index.d.ts +3 -0
  253. package/build/preview/index.d.ts.map +1 -0
  254. package/build/preview/index.js +2 -0
  255. package/build/preview/index.js.map +1 -0
  256. package/build/preview/types.d.ts +60 -0
  257. package/build/preview/types.d.ts.map +1 -0
  258. package/build/preview/types.js +2 -0
  259. package/build/preview/types.js.map +1 -0
  260. package/build/scaffold/consts.d.ts +25 -0
  261. package/build/scaffold/consts.d.ts.map +1 -0
  262. package/build/scaffold/consts.js +26 -0
  263. package/build/scaffold/consts.js.map +1 -0
  264. package/build/scaffold/helpers.d.ts +35 -0
  265. package/build/scaffold/helpers.d.ts.map +1 -0
  266. package/build/scaffold/helpers.js +54 -0
  267. package/build/scaffold/helpers.js.map +1 -0
  268. package/build/scaffold/index.d.ts +5 -0
  269. package/build/scaffold/index.d.ts.map +1 -0
  270. package/build/scaffold/index.js +4 -0
  271. package/build/scaffold/index.js.map +1 -0
  272. package/build/scaffold/schemas.d.ts +5 -0
  273. package/build/scaffold/schemas.d.ts.map +1 -0
  274. package/build/scaffold/schemas.js +273 -0
  275. package/build/scaffold/schemas.js.map +1 -0
  276. package/build/scaffold/types.d.ts +144 -0
  277. package/build/scaffold/types.d.ts.map +1 -0
  278. package/build/scaffold/types.js +2 -0
  279. package/build/scaffold/types.js.map +1 -0
  280. package/build/skills/catalogue.d.ts +47 -0
  281. package/build/skills/catalogue.d.ts.map +1 -0
  282. package/build/skills/catalogue.js +1566 -0
  283. package/build/skills/catalogue.js.map +1 -0
  284. package/build/skills/consts.d.ts +104 -0
  285. package/build/skills/consts.d.ts.map +1 -0
  286. package/build/skills/consts.js +170 -0
  287. package/build/skills/consts.js.map +1 -0
  288. package/build/skills/index.d.ts +4 -0
  289. package/build/skills/index.d.ts.map +1 -0
  290. package/build/skills/index.js +4 -0
  291. package/build/skills/index.js.map +1 -0
  292. package/build/skills/roles.d.ts +52 -0
  293. package/build/skills/roles.d.ts.map +1 -0
  294. package/build/skills/roles.js +256 -0
  295. package/build/skills/roles.js.map +1 -0
  296. package/build/slot/consts.d.ts +305 -0
  297. package/build/slot/consts.d.ts.map +1 -0
  298. package/build/slot/consts.js +376 -0
  299. package/build/slot/consts.js.map +1 -0
  300. package/build/slot/helpers.d.ts +25 -0
  301. package/build/slot/helpers.d.ts.map +1 -0
  302. package/build/slot/helpers.js +27 -0
  303. package/build/slot/helpers.js.map +1 -0
  304. package/build/slot/index.d.ts +6 -0
  305. package/build/slot/index.d.ts.map +1 -0
  306. package/build/slot/index.js +5 -0
  307. package/build/slot/index.js.map +1 -0
  308. package/build/slot/layout.d.ts +72 -0
  309. package/build/slot/layout.d.ts.map +1 -0
  310. package/build/slot/layout.js +82 -0
  311. package/build/slot/layout.js.map +1 -0
  312. package/build/slot/schemas.d.ts +12 -0
  313. package/build/slot/schemas.d.ts.map +1 -0
  314. package/build/slot/schemas.js +43 -0
  315. package/build/slot/schemas.js.map +1 -0
  316. package/build/slot/types.d.ts +147 -0
  317. package/build/slot/types.d.ts.map +1 -0
  318. package/build/slot/types.js +2 -0
  319. package/build/slot/types.js.map +1 -0
  320. package/build/topology/card.d.ts +23 -0
  321. package/build/topology/card.d.ts.map +1 -0
  322. package/build/topology/card.js +95 -0
  323. package/build/topology/card.js.map +1 -0
  324. package/build/topology/consts.d.ts +32 -0
  325. package/build/topology/consts.d.ts.map +1 -0
  326. package/build/topology/consts.js +55 -0
  327. package/build/topology/consts.js.map +1 -0
  328. package/build/topology/helpers.d.ts +44 -0
  329. package/build/topology/helpers.d.ts.map +1 -0
  330. package/build/topology/helpers.js +112 -0
  331. package/build/topology/helpers.js.map +1 -0
  332. package/build/topology/index.d.ts +5 -0
  333. package/build/topology/index.d.ts.map +1 -0
  334. package/build/topology/index.js +4 -0
  335. package/build/topology/index.js.map +1 -0
  336. package/build/topology/types.d.ts +83 -0
  337. package/build/topology/types.d.ts.map +1 -0
  338. package/build/topology/types.js +2 -0
  339. package/build/topology/types.js.map +1 -0
  340. package/build/types.d.ts +88 -0
  341. package/build/types.d.ts.map +1 -0
  342. package/build/types.js +2 -0
  343. package/build/types.js.map +1 -0
  344. package/build/ux/consts.d.ts +5 -0
  345. package/build/ux/consts.d.ts.map +1 -0
  346. package/build/ux/consts.js +6 -0
  347. package/build/ux/consts.js.map +1 -0
  348. package/build/ux/index.d.ts +4 -0
  349. package/build/ux/index.d.ts.map +1 -0
  350. package/build/ux/index.js +3 -0
  351. package/build/ux/index.js.map +1 -0
  352. package/build/ux/schemas.d.ts +8 -0
  353. package/build/ux/schemas.d.ts.map +1 -0
  354. package/build/ux/schemas.js +91 -0
  355. package/build/ux/schemas.js.map +1 -0
  356. package/build/ux/types.d.ts +21 -0
  357. package/build/ux/types.d.ts.map +1 -0
  358. package/build/ux/types.js +2 -0
  359. package/build/ux/types.js.map +1 -0
  360. package/package.json +71 -0
  361. package/src/agent/index.ts +4 -0
  362. package/src/agent/memory.ts +16 -0
  363. package/src/agent/spectator/consts.ts +10 -0
  364. package/src/agent/spectator/index.ts +3 -0
  365. package/src/agent/spectator/types.ts +14 -0
  366. package/src/agent/types.ts +10 -0
  367. package/src/areas/consts.ts +112 -0
  368. package/src/areas/index.ts +3 -0
  369. package/src/areas/types.ts +29 -0
  370. package/src/ba/consts.ts +40 -0
  371. package/src/ba/helpers.ts +93 -0
  372. package/src/ba/index.ts +5 -0
  373. package/src/ba/schemas.ts +196 -0
  374. package/src/ba/types.ts +125 -0
  375. package/src/blueprint/case.ts +272 -0
  376. package/src/blueprint/consts.ts +11 -0
  377. package/src/blueprint/index.ts +6 -0
  378. package/src/blueprint/resolve.ts +133 -0
  379. package/src/blueprint/schemas.ts +44 -0
  380. package/src/blueprint/types.ts +204 -0
  381. package/src/branding.ts +34 -0
  382. package/src/connect/consts.ts +441 -0
  383. package/src/connect/entrypoints.ts +291 -0
  384. package/src/connect/errors.ts +91 -0
  385. package/src/connect/index.ts +7 -0
  386. package/src/connect/ops.ts +208 -0
  387. package/src/connect/references.ts +89 -0
  388. package/src/connect/schemas.ts +384 -0
  389. package/src/connect/types.ts +456 -0
  390. package/src/consts.ts +34 -0
  391. package/src/convert/consts.ts +672 -0
  392. package/src/convert/helpers.ts +340 -0
  393. package/src/convert/index.ts +10 -0
  394. package/src/convert/schemas.ts +663 -0
  395. package/src/convert/types.ts +741 -0
  396. package/src/design/consts.ts +26 -0
  397. package/src/design/helpers.ts +255 -0
  398. package/src/design/index.ts +5 -0
  399. package/src/design/runtime.ts +142 -0
  400. package/src/design/schemas.ts +384 -0
  401. package/src/design/types.ts +191 -0
  402. package/src/dev/consts.ts +35 -0
  403. package/src/dev/index.ts +4 -0
  404. package/src/dev/schemas.ts +124 -0
  405. package/src/dev/types.ts +47 -0
  406. package/src/execution/consts.ts +37 -0
  407. package/src/execution/index.ts +3 -0
  408. package/src/execution/types.ts +55 -0
  409. package/src/index.ts +22 -0
  410. package/src/integrity/consts.ts +477 -0
  411. package/src/integrity/index.ts +3 -0
  412. package/src/integrity/types.ts +37 -0
  413. package/src/integrity/verify.ts +221 -0
  414. package/src/metadata/consts.ts +118 -0
  415. package/src/metadata/index.ts +3 -0
  416. package/src/metadata/types.ts +310 -0
  417. package/src/moderation/consts.ts +59 -0
  418. package/src/moderation/index.ts +4 -0
  419. package/src/moderation/schemas.ts +28 -0
  420. package/src/moderation/types.ts +68 -0
  421. package/src/moderation/utils.ts +35 -0
  422. package/src/preview/consts.ts +9 -0
  423. package/src/preview/index.ts +3 -0
  424. package/src/preview/types.ts +79 -0
  425. package/src/scaffold/consts.ts +26 -0
  426. package/src/scaffold/helpers.ts +84 -0
  427. package/src/scaffold/index.ts +4 -0
  428. package/src/scaffold/schemas.ts +283 -0
  429. package/src/scaffold/types.ts +118 -0
  430. package/src/skills/catalogue.ts +1617 -0
  431. package/src/skills/consts.ts +183 -0
  432. package/src/skills/index.ts +4 -0
  433. package/src/skills/roles.ts +299 -0
  434. package/src/slot/consts.ts +393 -0
  435. package/src/slot/helpers.ts +34 -0
  436. package/src/slot/index.ts +5 -0
  437. package/src/slot/layout.ts +141 -0
  438. package/src/slot/schemas.ts +46 -0
  439. package/src/slot/types.ts +164 -0
  440. package/src/topology/card.ts +106 -0
  441. package/src/topology/consts.ts +59 -0
  442. package/src/topology/helpers.ts +138 -0
  443. package/src/topology/index.ts +5 -0
  444. package/src/topology/types.ts +86 -0
  445. package/src/types.ts +104 -0
  446. package/src/ux/consts.ts +5 -0
  447. package/src/ux/index.ts +4 -0
  448. package/src/ux/schemas.ts +97 -0
  449. package/src/ux/types.ts +25 -0
  450. package/tests/connect-convert.spec.ts +161 -0
  451. package/tests/convert.spec.ts +318 -0
  452. package/tests/design.spec.ts +136 -0
  453. package/tsconfig.json +16 -0
@@ -0,0 +1,1617 @@
1
+ import type { SkillDefinition } from '@owlmeans/llm-common'
2
+ import { SKILL_ORDER, ViableSkill } from './consts.js'
3
+
4
+ /**
5
+ * Every skill body is written for a WEAK model, which means three things throughout:
6
+ * state the rule as an imperative, show the wrong form next to the right one, and name
7
+ * the exact error the wrong form produces. A model that has seen the error text
8
+ * recognises its own output; one that has only seen an abstract rule does not.
9
+ *
10
+ * Bodies are pure constants — they land in the cached region of the system prompt, and
11
+ * one interpolated byte would invalidate the prefix for every call that shares them.
12
+ */
13
+
14
+ const skill = (alias: ViableSkill, title: string, body: string): SkillDefinition => ({
15
+ alias, title, body: body.trim(), order: SKILL_ORDER[alias],
16
+ })
17
+
18
+ /** The one rendering of a library allow-list — an override must match it byte for byte. */
19
+ export const renderLibraries = (list: string[]): string =>
20
+ `Use these libraries; do not introduce others:\n${list.map(l => `- ${l}`).join('\n')}`
21
+
22
+ const libraries = (alias: ViableSkill, title: string, list: string[]): SkillDefinition =>
23
+ skill(alias, title, renderLibraries(list))
24
+
25
+ /**
26
+ * What a file in the shared package imports. Deliberately just `ajv`: this list is carried by
27
+ * `DomainTypesArchitect`/`AccessArchitect` too, and a domain model type file imports nothing
28
+ * else. `@owlmeans/entrypoint` and `@owlmeans/route` are named by `OwlMeansEntrypoints`
29
+ * instead — the personas that actually author `entrypoints.ts` carry that skill, and naming a
30
+ * package pulls its whole published documentation into the prompt.
31
+ */
32
+ export const commonLibraryList = ['ajv']
33
+ export const uiStateLibraryList = [
34
+ 'react', '@owlmeans/client', '@owlmeans/state', '@owlmeans/web-client',
35
+ ]
36
+ export const uiComponentLibraryList = [
37
+ 'react', 'tailwindcss', 'shadcn', '@owlmeans/client', '@owlmeans/web-client', 'lucide-react',
38
+ // `toast` comes from here, and this list is the last thing a UI persona reads: a package
39
+ // missing from it reads as a package that is not installed.
40
+ 'sonner',
41
+ ]
42
+ /**
43
+ * Deliberately WITHOUT `drizzle-orm`. This list renders last of all the skills (order 90), so a
44
+ * package named here is the final instruction a backend persona reads — and it used to say
45
+ * "use drizzle" directly under `ResourceLayer` saying "never import it". `postgres-resource`
46
+ * owns drizzle internally (its own dependency; the peers are `pg` and `ajv`), and a target
47
+ * reaches the builder through the resource, never through an import. Do not re-add it.
48
+ */
49
+ export const backendLibraryList = [
50
+ '@owlmeans/server-app', '@owlmeans/postgres-resource', 'ajv',
51
+ ]
52
+ export const fullUiLibraryList = [...new Set([...uiStateLibraryList, ...uiComponentLibraryList])]
53
+
54
+ /**
55
+ * The backend list plus the two packages that make a model call possible.
56
+ *
57
+ * Naming them is what pulls their own published documentation into the prompt, so this list
58
+ * carries the API contract without this catalogue restating a line of it. It is reachable only
59
+ * through a blueprint case that also installs them — a coder told about a package the manifest
60
+ * does not declare writes `TS2307`.
61
+ */
62
+ export const aiLibraryList = [
63
+ ...backendLibraryList, '@owlmeans/llm', '@owlmeans/agent',
64
+ ]
65
+
66
+ /**
67
+ * The UI list plus three.js. Scene code and interface code are the same package and the same
68
+ * bundle here; what separates them is the component, not the dependency list.
69
+ */
70
+ export const gameLibraryList = [...new Set([...fullUiLibraryList, 'three'])]
71
+
72
+ /**
73
+ * How an entrypoint alias is spelled. Shared verbatim by the entrypoints skill and by every
74
+ * helper prompt that asks a model to invent one — two renderings would drift, and the pipeline
75
+ * splits screen from API access rules on the prefix alone.
76
+ */
77
+ export const ALIAS_CONVENTION = `
78
+ - A screen alias is \`web:<screen-definition>\` — \`web:task-list\`, \`web:task-details\`.
79
+ - An AREA alias is \`web:area:<area>\`, and there are exactly four, all already declared:
80
+ \`web:area:guest\`, \`web:area:user\`, \`web:area:admin\`, \`web:area:operator\`, referenced as
81
+ \`app.web.area.<area>\`. Never invent a fifth, never respell one. There is no
82
+ \`web:layout:\` alias and no \`app.web.layout\` — writing either addresses nothing.
83
+ - An endpoint alias is \`api:<entity>:<action>\` — \`api:task:list\`, \`api:task:create\`. The
84
+ group alias that carries the shared path and guard is \`api:<entity>\` — \`api:task\`.
85
+ - A queued job alias is \`job:<entity>:<action>\` — \`job:report:build\`, \`job:contract:analyze\`.
86
+ A job is never given a \`web:\` or \`api:\` alias, and an endpoint is never given a \`job:\` one:
87
+ the prefix is what says which side serves it.
88
+ - Lowercase kebab-case in every segment. The prefix is NOT optional and NOT decorative: access
89
+ rules are applied per side by it.
90
+ `.trim()
91
+
92
+ export const VIABLE_SKILLS: SkillDefinition[] = [
93
+
94
+ skill(ViableSkill.ProjectLayout, 'Where files live', `
95
+ The generated project is a \`@owlmeans/create-app\` monorepo with five workspace packages under
96
+ \`sources/\`. Always prefix a relative path with the package it belongs to:
97
+
98
+ - \`sources/common\` — types, entrypoint declarations and the alias tree, shared by every side.
99
+ - \`sources/backend\` — the shared backend LIBRARY: the context factory, the config, the
100
+ resources, the services and the domain models. It builds with \`tsc -b\` and serves nothing.
101
+ - \`sources/api\` — the HTTP server. It builds its context from \`sources/backend\` and holds the
102
+ endpoint handlers.
103
+ - \`sources/web\` — the react application.
104
+ - \`sources/worker\` — the queue consumer. Same context, no HTTP routes, one processor per job.
105
+
106
+ These files are the CONTRACT between the packages. Every screen, endpoint and job passes through
107
+ them, and each has a sentinel comment marking where a new line goes:
108
+
109
+ - \`sources/common/src/consts.ts\` — the \`app\` alias tree.
110
+ - \`sources/common/src/entrypoints.ts\` — the shared \`entrypoints\` declaration list.
111
+ - \`sources/api/src/entrypoints.ts\` — server elevations (\`appEntrypoints\`).
112
+ - \`sources/web/src/entrypoints.ts\` — client elevations (\`appEntrypoints\`).
113
+ - \`sources/worker/src/entrypoints.ts\` — job elevations.
114
+ - \`sources/web/src/nav.ts\` — the navigation registry: one line per screen, which
115
+ is the only thing that puts it in the menus.
116
+
117
+ A file's KIND is its directory, never its name: \`models/task/task.ts\` and \`state/task.ts\` are
118
+ both plain \`.ts\` files and differ only in where they sit. The one exception is a view model,
119
+ \`<component>.vm.ts\`, because a view and its view model share a directory and a base name.
120
+
121
+ The web chrome is the FOUR area layouts in \`sources/web/src/layout/area.tsx\` —
122
+ the project's only layouts, already written, never added to. Screens are components under
123
+ \`sources/web/src/screens/\`.
124
+
125
+ An entity owns exactly ONE directory segment, shared by its shared types
126
+ (\`common/src/models/<entity>/\`), its resources (\`backend/src/resources/<entity>/\`), its
127
+ backend models (\`backend/src/models/<entity>/\`) and its endpoint handlers
128
+ (\`api/src/app/<entity>/\`). Spell it in lowercase kebab-case and spell it
129
+ the same way everywhere — the resource alias, and therefore the physical table name, is derived
130
+ from that directory. When the task carries a \`NAME REGISTRY\` block it is the authority on
131
+ every such name; use its paths verbatim rather than inventing a variant.
132
+ `),
133
+
134
+ skill(ViableSkill.AgentTooling, 'Working with the project', `
135
+ - Start by LISTING the files you need. Never assume the project structure — ask for it.
136
+ - Reading a source file and getting empty content means the file does not exist; the path
137
+ is almost certainly wrong. Do not create it blindly, look for the real one.
138
+ - Writing a file is a separate step. Generating code does not persist it — call the write
139
+ tool explicitly for every file you created or changed.
140
+ - Resolve the working directory before globbing: a source-list pattern has to be prefixed
141
+ with the project root.
142
+ - A clean validation is not a working application. \`validate\` type-checks and
143
+ \`validate_with_renderer\` builds — neither one loads the page, and the faults that matter most
144
+ here are the ones that only happen while a module is being evaluated. Never report a change as
145
+ verified because a build came back clean.
146
+ `),
147
+
148
+ skill(ViableSkill.OutputSourceOnly, 'Output format — source code only', `
149
+ Return working TypeScript source and nothing else. No prose before or after it, no
150
+ explanation, no markdown code fences.
151
+ `),
152
+
153
+ skill(ViableSkill.OutputTextOnly, 'Output format — text only', `
154
+ Return only the requested text. No commentary, no preamble, no explanation of what you
155
+ did, no markdown code fences.
156
+ `),
157
+
158
+ skill(ViableSkill.MainFlowFocus, 'Main flow focus', `
159
+ An application delivers value through ONE straight path, described at its natural length — long
160
+ enough to cover every step the flow really has, and no longer. That path is the main flow,
161
+ and it is the only thing you describe.
162
+
163
+ - The path has one BENEFICIARY — the kind of user who ends up holding the value — and however
164
+ many PARTICIPANTS the delivery cannot happen without. A social network needs a creator AND a
165
+ reader; a marketplace needs a seller AND a buyer; most applications need nobody besides the
166
+ beneficiary. Name each as a kind of user ("operator", "author", "reader"), never a market
167
+ segment or an ICP label, and never add a role the value can be delivered without.
168
+ - The main flow is 3 to 12 ordered steps, each performed by one named kind of user, running from
169
+ the enabling first action to the moment the beneficiary holds the value. The FIRST step is what
170
+ someone declares, creates or brings in before the flow can run at all.
171
+ - WRONG first step: "the user sees a dashboard of their results". RIGHT first step: "the user
172
+ declares the process their leads will pass through". A screen that shows, lists or reports on
173
+ something the flow has not produced yet is never the beginning of the flow.
174
+ - Cover the WHOLE path, not its opening. Every participant acts in at least one step, at most one
175
+ step is about setting things up, and the last step delivers the value. A description that stops
176
+ after the setup — a social network where someone publishes but nobody ever reads — has
177
+ described no value at all, however detailed its first steps are.
178
+ - Never describe branches, alternatives, optional steps, error paths, or anything about signing
179
+ up, signing in, roles, permissions, settings, configuration, onboarding, notifications, billing
180
+ or administration. They are not the flow. This bans them as STEPS — it does not ban thinking
181
+ about them: whether an actor needs an account is always judged, because it is what decides the
182
+ area their story belongs to.
183
+ - If a step can be dropped and the beneficiary still gets the value, drop it, and spend it on the
184
+ part of the path that is still missing. Breadth is one failure mode; a truncated path is the
185
+ other, and it is the more common one — a path that never shows the public half of the product,
186
+ or stops before anyone consumes what was produced, is truncated however tidy it looks.
187
+
188
+ Both bans above are about the FLOW ITSELF — about what may be one of its numbered steps. They are
189
+ not a ban on the screens that CONNECT the steps to each other: the list of records waiting for
190
+ someone, the directory that finds one of them, the page of a single record, the list of what an
191
+ actor submitted. Those are never steps, and you never volunteer one while you are describing the
192
+ flow. When a task asks for them IN SO MANY WORDS — handing you the flow steps as finished context
193
+ and asking what has to exist BETWEEN them — that task is not asking about the flow, and answering
194
+ it is not breadth. Describe them only then.
195
+
196
+ The application serves exactly FOUR audiences, called AREAS, and every actor in a story belongs
197
+ to one of them:
198
+
199
+ - \`guest\` — the actor is NOT signed in: public visitors, prospects, anonymous readers.
200
+ - \`user\` — a signed-in end user consuming the product's value; the front office.
201
+ - \`operator\` — staff running the product's business process from the inside; the back office.
202
+ - \`admin\` — the owner configuring or managing the application itself.
203
+
204
+ Decide an actor's area by asking, IN ORDER, and stopping at the first yes: can they do this step
205
+ with NO account (\`guest\`) — does the step manage the application itself (\`admin\`) — do they act
206
+ on the business process from the inside (\`operator\`) — otherwise \`user\`.
207
+
208
+ The authentication question comes first and it outranks who benefits: an actor who needs no
209
+ account is \`guest\` even when the product exists for them. A public product therefore normally has
210
+ both \`guest\` and \`user\` steps, and a flow whose stories all landed in one area is usually a
211
+ misread of that first question.
212
+
213
+ A product's own role names — a seller, an author, a reviewer, a reader — are not areas. The same
214
+ name can sit in different areas in different steps, so judge the STEP, never the title. An area is
215
+ never a step of its own: classifying an actor is not licence to add one, so an \`admin\` or a
216
+ moderator still appears only when the flow genuinely needs their action.
217
+ `),
218
+
219
+ skill(ViableSkill.ScopeDiscipline, 'Scope', `
220
+ Implement only what the task describes. Do not add features, files, abstractions, or
221
+ error handling for situations the task does not mention.
222
+ `),
223
+
224
+ skill(ViableSkill.NonTypescriptOutput, 'This file is not TypeScript', `
225
+ The file you are asked for is NOT TypeScript — it is CSS, JSON, Markdown or another
226
+ format. The TypeScript style and import rules above do not apply to it. Produce the file
227
+ in its own language, with no TypeScript syntax and no import statements it does not need.
228
+ `),
229
+
230
+ skill(ViableSkill.TsStyle, 'TypeScript style', `
231
+ - No semicolons at the end of statements.
232
+ - Named exports only — never \`export default\`.
233
+ - Declare every function before it is used.
234
+ - Add an explicit props type to every React component.
235
+ - When a value is a free-form dictionary (for example collected Zod errors), type it
236
+ explicitly as \`Record<string, any>\`.
237
+ - Never import a runtime value with \`import type\` or \`import { type X }\`. Those are for
238
+ types and interfaces ONLY. Importing a value that way compiles and then fails at
239
+ runtime with "X is not defined" or "does not provide an export named X", because the
240
+ import is erased.
241
+ - Keep comments short and only for what the code cannot show. Never restate parameter or
242
+ return types in a comment — TypeScript already carries them.
243
+ - View-model files end in \`.vm.ts\`, not \`.vm.tsx\`: they contain no JSX.
244
+ `),
245
+
246
+ skill(ViableSkill.TsImports, 'Imports and path aliases', `
247
+ - Import shared code as \`project-common/<path>\`, never as \`sources/common/src/<path>\`,
248
+ and never with a \`.js\` extension.
249
+ - Inside a BUNDLED package — \`api\`, \`web\`, \`worker\` — \`@/\` is an alias for that package's own
250
+ \`src\`; use it instead of chains of \`../\`.
251
+ - **The shared backend library has NO \`@/\` alias.** It is built by \`tsc -b\`, not bundled, and
252
+ declares no tsconfig \`paths\`. Inside \`sources/backend/\` every internal import is RELATIVE and
253
+ keeps its \`.js\` suffix:
254
+ \`import { taskResource } from '../../resources/task/task.js'\`.
255
+ \`@/resources/task/task.js\` there is \`TS2307: Cannot find module\`, and changing the suffix does
256
+ not help — the alias is what is missing, not the extension.
257
+ - \`@/\` is the ONLY alias that exists. If you find yourself writing any other \`@\`-prefixed
258
+ import that is not a real npm package, you are inventing a module that is not there;
259
+ use a relative path or an existing package instead.
260
+ `),
261
+
262
+ skill(ViableSkill.ReactComponents, 'React components', `
263
+ - Icons come from \`lucide-react\`. If a named icon does not exist in that package, do not
264
+ guess another name — inline a small SVG instead.
265
+ - Every component declares a props type.
266
+ - Do not fetch data directly in a component. A component reads its data and its handlers
267
+ from its view-model hook.
268
+ - Never use a dynamic import for a component or an icon — in particular never
269
+ \`lucide-react/dynamicIconImports\`. Import what you need statically by name.
270
+ `),
271
+
272
+ skill(ViableSkill.OwlMeansEntrypoints, 'Entrypoints — screens and endpoints', `
273
+ Everything this app addresses — a screen the browser renders, an endpoint the server answers
274
+ — is an OwlMeans **entrypoint**: declared with \`@owlmeans/entrypoint\` and \`@owlmeans/route\`
275
+ in the shared package, elevated with \`@owlmeans/web-client\` in the browser and
276
+ \`@owlmeans/server-app\` on the backend. \`@owlmeans/web-client\` builds the route table from the
277
+ entrypoints and renders it over the History API, so there is no router file, no \`<Routes>\` and
278
+ no \`app.get(...)\`. No third-party routing or HTTP-server package is installed — importing one
279
+ fails to resolve.
280
+
281
+ An entrypoint is DECLARED once in the shared package, then ELEVATED on each side that uses it.
282
+
283
+ **1. The alias.** Aliases live in ONE place, the \`app\` tree in
284
+ \`sources/common/src/consts.ts\`. NEVER write an alias string inline — always reference
285
+ \`app.web.*\` / \`app.api.*\`. An inline string compiles and then silently addresses nothing.
286
+
287
+ ${ALIAS_CONVENTION}
288
+
289
+ **2. The declaration** — \`sources/common/src/entrypoints.ts\`, one line in the \`entrypoints\`
290
+ array:
291
+
292
+ import { entrypoint, guard, gate, filter, body } from '@owlmeans/entrypoint'
293
+ import { route, frontend, RouteMethod } from '@owlmeans/route'
294
+
295
+ entrypoint(route(app.api.task.list, '/list', { parent: app.api.task }), guard(DEFAULT_GUARD))
296
+
297
+ - \`route(alias, path)\` is a BACKEND route by default. \`route(alias, path, frontend())\` makes
298
+ it a screen; \`frontend({ default: true, parent: app.web.base })\` makes it the index screen.
299
+ - Nest with \`{ parent: <group alias> }\` — the child path is APPENDED to the parent's, so the
300
+ child path is the tail only (\`'/list'\`, not \`'/tasks/list'\`).
301
+ - \`{ method: RouteMethod.POST }\` (from \`@owlmeans/route\`) for anything that is not a GET.
302
+ - \`filter(body<T>(TSchema))\` whenever the body's type has a schema beside it in the shared
303
+ package — every generated type exports one, named after the type with a \`Schema\` suffix
304
+ (\`Task\` → \`TaskSchema\`). Import both from the shared package and use them; that is what makes
305
+ the framework reject a malformed request before your handler runs, so the handler never has to
306
+ check whether a field arrived. Still never INVENT a schema inline: if the type has none, leave
307
+ the endpoint unfiltered rather than writing a literal here that nothing else agrees with.
308
+ - Access is declarative: no \`guard()\` = public, \`guard(DEFAULT_GUARD)\` = any signed-in user,
309
+ \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['<permission>']))\` = a permission is required.
310
+ \`<permission>\` is a SHAPE, never a value: build the real name from the domain being
311
+ implemented — the resource, TWO hyphens, the action, lowercase kebab-case, singular resource.
312
+ Do not write a bracketed word into an application, and do not copy a name out of this
313
+ document; both produce a gate nobody can pass.
314
+ Add \`@<routeParam>\` to scope the check to one record — the name after \`@\` MUST be a \`:\`
315
+ segment of that same entrypoint's own path, since one the path does not carry resolves to
316
+ nothing and the gate then refuses every request. Guards and gates are INHERITED by children and
317
+ enforced by the framework — a handler or a screen never re-checks them.
318
+
319
+ **3. The elevation.** The declaration alone renders and answers nothing.
320
+
321
+ - Server, \`sources/api/src/entrypoints.ts\`:
322
+ \`elevate(appEntrypoints, app.api.task.list, handleRequest(...))\`
323
+ - Client screen, \`sources/web/src/entrypoints.ts\`:
324
+ \`elevate(appEntrypoints, app.web.taskList, handler(TaskListScreen))\`
325
+ - Client CALL, same file: a BARE elevation, no component —
326
+ \`elevate(appEntrypoints, app.api.task.list)\`. This is what makes the alias callable from the
327
+ browser. Passing a component to a backend alias is a hard error, not a warning.
328
+
329
+ **A missing client elevation is invisible until the app runs.** The whole shared list is
330
+ registered on the browser context, so \`ctx.entrypoint(alias)\` finds the un-elevated declaration
331
+ and returns it; the call site casts it to \`ClientEntrypoint\`, so TypeScript sees a type that has
332
+ \`call\` and the build is clean. The only symptom is in the browser:
333
+
334
+ TypeError: entrypoint.call is not a function
335
+
336
+ That message means EXACTLY one thing — the alias in that \`ctx.entrypoint(...)\` has no
337
+ \`elevate(list, <alias>)\` line in \`sources/web/src/entrypoints.ts\`. Add the bare
338
+ elevation above the \`// owlmeans: add new backend elevations above this line\` sentinel. It is
339
+ never a broken component, never a bad import, and never a reason to rewrite the view model or to
340
+ replace the call with \`fetch\`. If the alias is not declared in
341
+ \`sources/common/src/entrypoints.ts\` either, declare it there FIRST: elevating an alias the
342
+ shared list does not carry throws \`Entrypoint with alias X not present\` while the module is
343
+ still loading, which blanks the whole app instead of failing one call.
344
+
345
+ **4. An AREA is the PARENT entrypoint.** The chrome of the app — header, navigation, footer —
346
+ is never imported by a screen. It comes from an entrypoint of its own, and the screens that use
347
+ it are declared as its CHILDREN. There are exactly FOUR areas, they already exist in the
348
+ project, and an area contributes chrome AND a URL prefix:
349
+
350
+ - \`app.web.area.guest\` at \`/\` — visitors who are not signed in.
351
+ - \`app.web.area.user\` at \`/frontoffice\` — signed-in end users; the front office.
352
+ - \`app.web.area.admin\` at \`/admin\` — the owner of the application.
353
+ - \`app.web.area.operator\` at \`/backoffice\` — staff running the business process; the back office.
354
+
355
+ Those four declarations and their elevations are FIXED. NEVER add an area, NEVER change one's
356
+ path, NEVER touch its \`guard()\`/\`gate()\`, NEVER re-declare one — put the screen in the area
357
+ whose access and audience it needs instead. A product's own roles are not areas: they are users
358
+ or operators holding different permissions.
359
+
360
+ // sources/common/src/entrypoints.ts — a screen: TAIL path, area as parent, no access
361
+ entrypoint(route(app.web.taskList, '/tasks', frontend({ parent: app.web.area.user }))),
362
+
363
+ // sources/web/src/entrypoints.ts
364
+ elevate(list, app.web.taskList, handler(TaskListScreen))
365
+
366
+ That screen answers at \`/frontoffice/tasks\`. The path you write is the TAIL ONLY — repeating the
367
+ area prefix (\`'/frontoffice/tasks'\`) publishes it at \`/frontoffice/frontoffice/tasks\`, which
368
+ nothing in the menu links to and no navigation call reaches.
369
+
370
+ The framework passes the matched child to the area as \`children\`, so an area layout is
371
+ \`FC<PropsWithChildren>\` and renders \`{children}\` in its content region. A screen therefore
372
+ renders ONLY its own content: importing a layout inside a screen renders the header, the menu
373
+ and the footer a second time, nested inside the first.
374
+
375
+ - \`app.web.base\` stays BARE — \`elevate(list, app.web.base)\`, no \`handler()\`. It is the
376
+ pass-through shell that hosts the four areas and contributes no chrome.
377
+ - Every area needs a child declared \`frontend({ default: true, parent: app.web.area.<area> })\`.
378
+ An entrypoint with children but no default child matches nothing and renders a BLANK PAGE.
379
+ Each area already ships one; never remove it and never add a second.
380
+
381
+ **Access is INHERITED from the area.** Guards and gates cascade to children, so choosing the area
382
+ IS the access decision and a screen declares none of its own:
383
+
384
+ - guest area — no guard at all; its screens add nothing.
385
+ - user area — \`guard(DEFAULT_GUARD)\`; its screens add nothing.
386
+ - admin area — \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['project--admin']))\`; its screens add
387
+ nothing, the marker is the whole rule.
388
+ - operator area — \`guard(DEFAULT_GUARD)\`, and EVERY screen under it additionally declares
389
+ \`gate(OIDC_GATE, ['<permission>'])\` naming the permission THAT screen needs, built from the
390
+ screen's own resource and action (add \`@<routeParam>\` when the screen shows one specific
391
+ record). This is the ONLY screen that declares access.
392
+
393
+ An ENDPOINT has no area to inherit from, so it states its level itself: no \`guard()\` for guest,
394
+ \`guard(DEFAULT_GUARD)\` for any signed-in user,
395
+ \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['<permission>']))\` when a permission is required — built
396
+ from ITS OWN resource and action — and
397
+ \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['project--admin']))\` for owner-only.
398
+
399
+ \`project--admin\` is the OWNER's marker — holding it passes EVERY gate, so the owner is not a
400
+ role inside the app. It is never one of the permissions this app declares for itself, and it is
401
+ never granted to an ordinary user. Never hand-write an admin bypass in a handler, never weaken an
402
+ existing guard to make something reachable, and never model a "limited admin" with this marker:
403
+ that is an ordinary user holding some of the app's own permissions.
404
+
405
+ Insert every new line ABOVE the matching \`// owlmeans: add new ... above this line\` sentinel,
406
+ one declaration or elevation per line. Adding an endpoint touches five files (alias,
407
+ declaration, handler, server elevation, client elevation); a screen touches five too (alias,
408
+ declaration, screen component, client elevation, navigation entry). Skipping one leaves it
409
+ unreachable.
410
+
411
+ **Calling an endpoint** from the frontend — \`call()\` resolves to the VALUE, and a non-2xx THROWS:
412
+
413
+ const tasks = await owlCtx.entrypoint<ClientEntrypoint<Task[]>>(app.api.task.list).call()
414
+ const task = await owlCtx.entrypoint<ClientEntrypoint<Task>>(app.api.task.create)
415
+ .call({ body: input })
416
+ const task = await owlCtx.entrypoint<ClientEntrypoint<Task>>(app.api.task.get)
417
+ .call({ params: { taskId } })
418
+
419
+ An entrypoint carries three verbs and each answers a different question:
420
+
421
+ - \`call(req?)\` — performs the call and resolves to the VALUE. This is the one nearly every
422
+ view model wants.
423
+ - \`invoke(req?)\` — the same round trip, resolving to \`{ value, outcome }\`, for the rare place
424
+ where the OUTCOME decides what happens next.
425
+ - \`url(req?, opts?)\` — builds the URL this entrypoint addresses; \`url(req, { absolute: true })\`
426
+ for a fully qualified one. A SCREEN entrypoint answers only this verb — calling \`call()\` or
427
+ \`invoke()\` on one THROWS.
428
+
429
+ \`ClientEntrypoint\` comes from \`@owlmeans/web-client\` and from nowhere else:
430
+
431
+ import type { ClientEntrypoint } from '@owlmeans/web-client'
432
+
433
+ \`@owlmeans/client\` does NOT export it — importing it from there fails the build with
434
+ \`error TS2305: Module '"@owlmeans/client"' has no exported member 'ClientEntrypoint'\`.
435
+ And the type argument goes on \`ClientEntrypoint\`, never on \`entrypoint\` itself:
436
+ \`owlCtx.entrypoint<Task>(alias)\` fails with \`error TS2739: Type 'Task' is missing the
437
+ following properties from type 'BasicEntrypoint'\`.
438
+
439
+ Inside a component or a hook — which is where nearly every call belongs — read the context with
440
+ \`useContext()\` from \`@owlmeans/web-client\`. \`owlCtx\` is for module-level code that has no
441
+ hook to run in: it is a MODULE EXPORT — \`import { owlCtx } from '@/owlmeans'\` — never a global;
442
+ \`window.owlCtx\` and \`(window as any).__owlCtx\` do not exist. NEVER set an
443
+ \`Authorization\` header and never read a token — the framework attaches it to every guarded
444
+ call. There is no \`fetchApi\` helper and no \`fetch\` call to the backend.
445
+ `),
446
+
447
+ skill(ViableSkill.OwlMeansNav, 'Navigating between screens', `
448
+ Navigation addresses an ALIAS, never a URL. The path an alias resolves to is owned by
449
+ \`sources/common/src/entrypoints.ts\`; a hard-coded \`/tasks/\${id}\` desynchronizes silently the
450
+ moment the declaration changes.
451
+
452
+ import { useNavigate } from '@owlmeans/client'
453
+
454
+ const nav = useNavigate()
455
+ nav.go(app.web.taskDetails, { params: { taskId } }) // navigate now
456
+ <button onClick={nav.press(app.web.taskList)}> // press() RETURNS the handler
457
+ nav.back()
458
+
459
+ - \`useNavigate\` comes from \`@owlmeans/client\`. It is NOT exported by
460
+ \`@owlmeans/web-client\` — importing it from there fails to resolve.
461
+ - \`nav.press(alias)\` returns a click handler. Write \`onClick={nav.press(alias)}\`, never
462
+ \`onClick={() => nav.press(alias)}\` — the second form navigates nowhere because it only
463
+ builds a handler and drops it.
464
+ - There is no \`<Link>\` and no \`<Navigate>\` component. Use an \`<a>\` (or a button) with
465
+ \`onClick\`.
466
+
467
+ Route params are PROPS, not a hook. The framework renders a screen with
468
+ \`{ context, params, alias, path }\`, so a screen that needs \`:taskId\` declares it:
469
+
470
+ interface TaskDetailsScreenProps { params: { taskId: string } }
471
+
472
+ export const TaskDetailsScreen: FC<TaskDetailsScreenProps> = ({ params }) => { ... }
473
+
474
+ Never import \`useParams\` — no package the app depends on exports it, and the import fails to
475
+ resolve.
476
+
477
+ **A screen reaches the MENUS only through the navigation registry** — one line per screen in
478
+ \`sources/web/src/nav.ts\`, above the
479
+ \`// owlmeans: add new navigation entries above this line\` sentinel:
480
+
481
+ { area: 'user', section: 'Tasks', alias: app.web.taskList, label: 'All tasks' },
482
+
483
+ - \`area\` is \`'guest' | 'user' | 'admin' | 'operator'\` and must be the SAME area the screen is
484
+ declared under in \`sources/common/src/entrypoints.ts\`.
485
+ - \`section\` groups screens into the TOP menu (the first level). The screens of the active
486
+ section are the SIDE menu (the second level), and a section holding a single screen shows no
487
+ side menu at all.
488
+ - A section is a LABEL, never a URL segment. Regrouping screens changes the menus and never an
489
+ address.
490
+ - NEVER hand-write a menu, a \`<nav>\`, a sidebar, a breadcrumb or a list of links in a screen or
491
+ a layout. The chrome comes from \`NavLayout\` (\`@owlmeans/web-panel\`), which the four area
492
+ layouts already render off this registry.
493
+
494
+ A screen with no line here compiles, elevates and renders — it is simply reachable by direct URL
495
+ only, and nothing reports it.
496
+ `),
497
+
498
+ skill(ViableSkill.PermissionModel, 'Permissions — the name, the gate and the grant', `
499
+ \`enquiry\` below belongs to a DIFFERENT application, used here only to show the shape. Every
500
+ permission you write is built from the resource and action of the app you are working on; a name
501
+ copied out of this document is a gate nobody can ever pass.
502
+
503
+ A permission is ONE string that has to line up in three places:
504
+
505
+ 1. the GATE on the entrypoint — \`gate(OIDC_GATE, ['enquiry--view@enquiryId'])\`
506
+ 2. the permission DEFINITION registered for the project — name \`enquiry--view\`,
507
+ resource \`enquiry\`, action \`view\`, resource-scoped \`true\`
508
+ 3. the GRANT an administrator makes, which addresses that definition by name.
509
+
510
+ **The \`@\` suffix belongs to the GATE ONLY. It is never part of a permission's name.**
511
+
512
+ // right — the gate scopes the check; the definition and the grant use the bare name
513
+ entrypoint(
514
+ route(app.api.enquiry.get, '/:enquiryId', { parent: app.api.enquiry }),
515
+ guard(DEFAULT_GUARD, gate(OIDC_GATE, ['enquiry--view@enquiryId']))
516
+ )
517
+ // definition: enquiry--view grant: enquiry--view
518
+
519
+ // wrong — the suffix carried into the registered or granted NAME
520
+ // definition: enquiry--view@enquiryId
521
+ // grant: enquiry--view@enquiryId
522
+
523
+ The gate splits its parameter at the FIRST \`@\`: what comes before is the permission it looks up,
524
+ what comes after says where to read the resource id. So a definition or a grant whose name
525
+ contains an \`@\` is a key nothing ever looks up. The symptom is a user who HAS been granted the
526
+ permission and still gets 403, with nothing logged. The cause is the \`@\` in the stored name —
527
+ never the gate line, which is usually already correct. Repair the definition and the grant; do
528
+ NOT delete the \`@\` from the gate to make the two strings match.
529
+
530
+ **Repair order matters.** Normalize FIRST, then judge what is left. Normalizing renames a
531
+ malformed definition and carries its grants across in the same write; deleting first throws those
532
+ grants away, and nothing can reconstruct who held them.
533
+
534
+ **The name.** \`<resource>--<action>\`:
535
+ - Both halves are names from THIS application's own domain. \`<resource>\`, \`<action>\` and
536
+ \`<permission>\` are placeholders describing the shape — writing one of them, or any other
537
+ bracketed word, produces a permission no screen can grant and no user can ever hold.
538
+ - \`--\` is TWO hyphens. One hyphen is not a separator: \`enquiry-view\` is a resource named
539
+ \`enquiry-view\` with no action at all.
540
+ - lowercase kebab-case in both segments, singular resource.
541
+ - Actions are collapsed: create, update, edit and delete are all \`modify\`; reading one record is
542
+ \`view\`; reading many is \`list\`.
543
+ - \`project--admin\` is the OWNER's marker. No story ever registers it, and holding it passes every
544
+ gate. It is enforced by a decoration wrapped around the gate in \`sources/backend/src/context.ts\`,
545
+ NOT by any gate parameter — so its absence from the entrypoint declarations is not evidence that
546
+ it is unused, and it must never be deleted as "a definition nothing names".
547
+
548
+ **The \`@\` selector.** \`@<routeParam>\`: the name after \`@\` must be a \`:\` segment of THAT
549
+ entrypoint's own declared path. \`'enquiry--view@enquiryId'\` on a route declared \`'/enquiries/:id'\`
550
+ resolves to nothing, so the gate refuses every request and no error explains why. A permission used
551
+ with a selector is resource-scoped: a grant may carry the specific ids it covers, and a grant
552
+ carrying no ids at all covers every resource.
553
+
554
+ **Every way this goes wrong, and what each one costs.** The application is a clinic in these
555
+ examples; yours is not, so read the right-hand column as a shape and build the name from your own
556
+ domain.
557
+
558
+ // WRONG — a bracketed word from an instruction, written as if it were a name
559
+ gate(OIDC_GATE, ['<permission>'])
560
+ gate(OIDC_GATE, ['<resource>--<action>'])
561
+ // RIGHT — this application's own resource and action
562
+ gate(OIDC_GATE, ['appointment--modify'])
563
+ // Costs: nothing registers a bracketed name, so the gate refuses every request forever.
564
+
565
+ // WRONG — a name copied out of a skill, a comment or another app's example
566
+ gate(OIDC_GATE, ['article--modify']) // in an application that has no articles
567
+ // RIGHT — a name that exists in THIS domain
568
+ gate(OIDC_GATE, ['appointment--modify'])
569
+ // Costs: the permission is asserted but never declared, so no administrator can grant it.
570
+
571
+ // WRONG — the selector carried into the stored name
572
+ ensurePermission(entity, client, 'appointment', { permission: 'modify@appointmentId' })
573
+ // RIGHT — the selector stays in the gate; the stored name is bare
574
+ ensurePermission(entity, client, 'appointment', { permission: 'modify' })
575
+ // Costs: a grant lands on a key no gate reads — the screen says granted, the request says 403.
576
+
577
+ // WRONG — a selector naming a param this route does not declare
578
+ route(app.api.appointment.get, '/:appointmentId')
579
+ gate(OIDC_GATE, ['appointment--view@id'])
580
+ // RIGHT — the name after @ is a ":" segment of this very route
581
+ gate(OIDC_GATE, ['appointment--view@appointmentId'])
582
+ // Costs: the id resolves to nothing, so the endpoint refuses every request with nothing logged.
583
+
584
+ // WRONG — one hyphen
585
+ gate(OIDC_GATE, ['appointment-modify'])
586
+ // RIGHT — two
587
+ gate(OIDC_GATE, ['appointment--modify'])
588
+ // Costs: it registers as a resource with no action, and never lines up with the real one.
589
+
590
+ **Never spell one permission two ways.** Reuse the exact string an existing declaration already
591
+ carries. Introducing a second spelling orphans every grant made against the first.
592
+ `),
593
+
594
+ skill(ViableSkill.ShadcnUi, 'shadcn/ui and Tailwind', `
595
+ - The \`cn\` helper is a NAMED export of \`@/lib/utils\` — not \`@/lib/utils/cn\`, which is not a
596
+ file and fails to resolve.
597
+ - The shadcn primitives are VENDORED into this project at \`@/components/ui/*\`, and
598
+ \`@owlmeans/web-panel\`'s own components resolve theirs through that same path — against these
599
+ files, not a copy of their own. They are already there: import them, never delete, rename or
600
+ move one, and never re-add a primitive the directory already carries.
601
+ - Styling is Tailwind utility classes. There are no CSS modules and no styled-components.
602
+ - Colour comes in PAIRS. Every surface token in this theme has a foreground partner —
603
+ \`--background\`/\`--foreground\`, \`--card\`/\`--card-foreground\`, \`--primary\`/\`--primary-foreground\`,
604
+ and the same for \`secondary\`, \`muted\`, \`accent\`, \`popover\`, \`destructive\` and \`sidebar\`. Painting a
605
+ surface is therefore never a single decision: give the element its partner in the SAME
606
+ \`className\`, or the text keeps the colour meant for the surface underneath and vanishes wherever
607
+ the design made that surface dark.
608
+
609
+ <section className="bg-primary p-6">Join us</section> // WRONG
610
+ <section className="bg-primary text-primary-foreground p-6">Join us</section> // right
611
+
612
+ Nothing catches the wrong form — it compiles and it renders. It is unreadable only on the
613
+ surfaces the design happened to make dark, which is why it survives to the finished screen.
614
+ - Never paint a surface with a raw or palette colour — \`bg-slate-900\`, \`bg-[#101820]\`,
615
+ \`style={{ background: '#101820' }}\`. Those have no foreground partner, so nothing keeps the text
616
+ on them legible, and they ignore the theme. Every surface is one of the tokens above.
617
+ - Depth and emphasis come from the theme's DECORATION VOCABULARY, and using it is expected rather
618
+ than exceptional — a screen built only from flat cards is an unfinished screen. That vocabulary
619
+ is: the component classes \`gradient-heading\` (a page or section headline), \`glass-card\` (a
620
+ panel with depth), \`eyebrow\` (the small chip above a headline), \`cta-btn\` (the primary call to
621
+ action, on an \`<a>\` or a bare \`<button>\`, never on a shadcn \`Button\` whose own \`bg-\`
622
+ utility would win), \`glow-orb\` (a soft colour field behind a section, always
623
+ \`pointer-events-none\` and behind the content); the elevation utilities \`shadow-soft\`,
624
+ \`shadow-raised\`, \`shadow-floating\`, \`shadow-glow\`; the idle-motion classes \`reveal\`,
625
+ \`float-slow\`, \`drift-slow\`, \`pulse-soft\`, \`spin-slow\`; and gradients whose stops are THEME
626
+ tokens with opacity. None of these is a raw colour.
627
+
628
+ <div className="bg-[radial-gradient(#818cf8,transparent)] blur-[120px]" /> // WRONG
629
+ <span className="glow-orb h-80 w-96 -z-10" aria-hidden /> // right
630
+ <div className="bg-gradient-to-br from-primary/20 to-transparent p-6" /> // right
631
+
632
+ Keep a gradient under a text element light — \`/20\` or less — unless the element also carries a
633
+ \`-foreground\` partner class. These class names are defined in the project's stylesheet; do not
634
+ invent others in the same family.
635
+ - Text that is NOT on a coloured surface takes NO colour class: it already inherits the readable
636
+ one. Never add \`text-white\`, \`text-black\` or a \`-foreground\` class "to be safe" — on an ordinary
637
+ panel that is the same fault inverted. \`text-muted-foreground\` for secondary text is the
638
+ exception, and it is the partner of the standard background.
639
+ - Tailwind is version 4: every styling configuration — theme variables, custom utilities,
640
+ layers — lives in the css files. There is NO \`tailwind.config.js\` in the project and it is
641
+ never required: do not create it, do not look for it, do not reference it.
642
+ - A \`SelectItem\` value must never be the empty string. Radix throws
643
+ "A <Select.Item /> must have a value prop that is not an empty string" at render time,
644
+ because the empty string is reserved for clearing the selection. Model an
645
+ empty/unset/"all" option with a sentinel value of \`"__empty__"\` and translate it back to
646
+ an empty value in the change handler:
647
+
648
+ <SelectItem value="__empty__">Any</SelectItem>
649
+ ...
650
+ onValueChange={value => onChange(value === '__empty__' ? undefined : value)}
651
+ `),
652
+
653
+ skill(ViableSkill.FormFeedback, 'Telling the user what happened', `
654
+ Every action a user takes says what happened, and a successful one leaves them somewhere sensible.
655
+ An action that silently succeeds and an action that silently fails look identical from the other
656
+ side of the screen, and the user's only recourse is to do it again.
657
+
658
+ import { toast } from 'sonner'
659
+
660
+ toast.success('Request submitted')
661
+ toast.error('Could not submit the request — please try again')
662
+
663
+ - The \`<Toaster />\` is ALREADY MOUNTED by the area layouts. Never import it, never render one in
664
+ a screen or a component: a second one renders every message twice.
665
+ - ON SUCCESS of a submit, a create, an update or a delete: raise \`toast.success\` with what
666
+ happened in a few words, then navigate. Where to is decided in this order:
667
+ 1. the destination the user story itself names;
668
+ 2. otherwise the SECTION DASHBOARD of the section this screen belongs to — its alias is the
669
+ first entry of that section in \`sources/web/src/nav.ts\`.
670
+ Leaving the user on a form they have just submitted, with the fields still filled in, reads as
671
+ though nothing happened.
672
+ - ON FAILURE: raise \`toast.error\` with a message that says what to do about it, and STAY on the
673
+ screen with the entered values intact. Never navigate away from a failure.
674
+ - The toast belongs in the VIEW MODEL's action handler, beside the call that succeeded or threw —
675
+ never in JSX, and never inside a render.
676
+ - Never \`alert()\`, never \`window.confirm()\`, never a hand-built floating \`<div>\` announcing an
677
+ outcome, and never a bare \`console.error\` as the user-facing report.
678
+
679
+ const submit = async (values: RequestInput) => { // right
680
+ try {
681
+ await model.create(values)
682
+ toast.success('Request submitted')
683
+ nav.go(app.web.requestsDashboard)
684
+ } catch (e) {
685
+ toast.error(e instanceof Error ? e.message : 'Could not submit the request')
686
+ }
687
+ }
688
+
689
+ await model.create(values); alert('Saved'); location.href = '/requests' // WRONG
690
+ `),
691
+
692
+ skill(ViableSkill.OwlmeansState, 'Client state', `
693
+ There is NO redux in this project. \`@reduxjs/toolkit\` and \`react-redux\` are NOT installed,
694
+ there is no \`store.ts\`, and \`createSlice\`, \`createAsyncThunk\`, \`configureStore\`,
695
+ \`useSelector\`, \`useDispatch\`, \`PayloadAction\`, \`RootState\` and \`AppDispatch\` do not exist.
696
+ Importing any of them fails to resolve.
697
+
698
+ State lives in a state RESOURCE on the OwlMeans context — the same container the entrypoints and
699
+ services live in.
700
+
701
+ **1. One state module per domain type**, at \`sources/web/src/state/<entity>/<type>.ts\`.
702
+ It declares the alias and the hooks that READ it, and nothing else:
703
+
704
+ import { useStoreList, useStoreModel } from '@owlmeans/client'
705
+ import { useContext } from '@owlmeans/web-client'
706
+ import type { Criteria } from '@owlmeans/resource'
707
+ import type { Task } from 'project-common/models/task/task.type.js'
708
+
709
+ export const TASK_STATE = 'task-state'
710
+
711
+ export const useTaskResource = () => useContext().getStateResource<Task>(TASK_STATE)
712
+ export const useTask = (id?: string) => useStoreModel<Task>(id, TASK_STATE)
713
+ export const useTaskList = (query: Criteria<Task> = {}) =>
714
+ useStoreList<Task>({ query, resource: TASK_STATE })
715
+
716
+ \`useStoreModel\` / \`useStoreList\` come from \`@owlmeans/client\` — \`@owlmeans/web-client\` does
717
+ NOT re-export them. \`useContext\` in a STATE module comes from \`@owlmeans/web-client\`, never
718
+ from \`@/context.js\`: that module imports this one to register the alias, so importing it back
719
+ is a cycle.
720
+
721
+ **2. The alias is registered on the context**, one line in \`sources/web/src/context.ts\`
722
+ above its sentinel:
723
+
724
+ import { TASK_STATE } from '@/state/task/task.js'
725
+ ...
726
+ appendStateResource<C, T>(context, TASK_STATE)
727
+ // owlmeans: add new state resources above this line
728
+
729
+ Without that line every hook for the alias throws \`Resource task-state not found\` at runtime,
730
+ and no type check can see it.
731
+
732
+ **3. Reading.** \`useStoreList({ query })\` is a LIVE list: it re-renders whenever a write changes
733
+ which records match, so a screen never recomputes ids and never re-subscribes.
734
+
735
+ const open = useTaskList({ status: 'open' }) // StateModel<Task>[]
736
+ open.map(model => model.record.title)
737
+
738
+ \`useStoreModel(id)\` is one record — the model always exists so a screen has something to bind
739
+ to, but nothing is invented to fill it. \`model.empty\` is what "not loaded yet" looks like:
740
+
741
+ const task = useTask(id)
742
+ if (task.empty) return <Spinner /> // no record for that id (yet)
743
+
744
+ **4. Writing.** A \`StateModel.record\` is a COPY. Assigning to it changes nothing:
745
+
746
+ model.record.title = 'renamed' // WRONG — silent no-op, nothing re-renders
747
+ model.update({ title: 'renamed' }) // RIGHT — merges and commits
748
+ model.clear() // deletes the record
749
+
750
+ Through the resource: \`await tasks.save(record)\` creates-or-replaces, \`tasks.delete(id)\`
751
+ removes, \`tasks.list(criteria)\` returns \`{ items, total }\` and \`tasks.count(criteria)\` the
752
+ number alone.
753
+
754
+ When an endpoint answers with a WHOLE set — every task of a project, the session's items — install
755
+ it in one step:
756
+
757
+ await tasks.replace(fromServer) // the store now holds exactly these
758
+
759
+ \`replace\` is what makes the store agree with the server: a record deleted elsewhere leaves in the
760
+ same write, and the subscribers wake once instead of once per record. A loop of \`save\` calls only
761
+ ever ADDS, so anything stale stays visible forever.
762
+
763
+ **5. Criteria** are the same language the backend resources take: a bare value is equality, a
764
+ bare ARRAY means "any of these", plus \`$eq $ne $gt $gte $lt $lte $in $nin $exists $null $like
765
+ $ilike $regex $startsWith $endsWith $between\` and \`$and\` / \`$or\` / \`$not\`. A dotted key
766
+ reaches into the record. A value of \`undefined\` is skipped, so an untouched filter does not
767
+ empty the list.
768
+ `),
769
+
770
+ skill(ViableSkill.StoreAccess, 'Reading and writing the store', `
771
+ - The store is NOT granular: it holds whole records. There is no method per field, and adding
772
+ one is not how a single property is changed — \`model.update({ field })\` merges.
773
+ - A subscribed record is reached through \`model.record\`, never as the model itself:
774
+ \`task.record.title\`, not \`task.title\`.
775
+ - A record may not have arrived yet, and nothing invents one to stand in. \`model.empty\` says so;
776
+ guard on it before rendering, and guard before reading THROUGH the record —
777
+ \`model.record?.items?.[id]?.name\` — rather than assuming the shape is populated.
778
+ - A list read is \`model.list()\` on the resource, returning \`{ items, total }\` — take the count
779
+ from \`total\`, and \`count(criteria)\` when the number is all that is wanted.
780
+ - The store carries RECORDS, not request status. Loading flags and error messages are ordinary
781
+ \`useState\` in the view model; a field named \`isLoading\` or \`error\` does not belong in a
782
+ stored record unless the domain type genuinely has one.
783
+ `),
784
+
785
+ skill(ViableSkill.ViewModelNaming, 'View-model naming convention', `
786
+ A component's view logic lives in its sibling \`.vm.ts\` file. Derive its public
787
+ symbols from the component name (PascalCase of the component definition — for example
788
+ "main-task-list" becomes "MainTaskList"):
789
+
790
+ - The hook is \`use<ComponentName>ViewModel\` (\`useMainTaskListViewModel\`).
791
+ - Its options/props type, when it takes arguments, is \`<ComponentName>ViewModelOptions\`.
792
+ - The primary loader the hook returns is \`load<ComponentName>\`; a refresh action is
793
+ \`reload<ComponentName>\`.
794
+ - Action handlers are verb-first camelCase (\`createTask\`, \`removeTask\`).
795
+
796
+ Both sides must use these exact names. This is a DEFAULT: when a "View-model contract"
797
+ block is present in the task, it lists the symbols that actually exist and overrides this
798
+ convention entirely.
799
+ `),
800
+
801
+ skill(ViableSkill.ResourceLayer, 'OwlMeans resources own the schema', `
802
+ A "resource" is ONE file — \`src/resources/{entity}/{type}.ts\` — carrying all data access
803
+ for one domain entity: its AJV schema, its \`makePostgresResource()\` maker, and an accessor
804
+ that returns the instance already registered on the OwlMeans context.
805
+
806
+ \`src/resources\` sits directly under \`src\`, not under \`models\`.
807
+
808
+ THE RESOURCE OWNS THE DDL. You never define a table. The AJV schema you write on the
809
+ resource IS the table: the OwlMeans layer creates it, and on every start reconciles an
810
+ existing table against the schema — adding, retyping and dropping columns and reconciling
811
+ indexes. A resource whose schema changed simply converges the next time the backend starts.
812
+
813
+ Never do any of the following — each one is the removed Drizzle-DDL layout. Some of these
814
+ imports still RESOLVE, because the framework uses the driver internally, so a clean build
815
+ proves nothing: the table you define becomes a SECOND owner of one the resource layer
816
+ already owns, and the next start reconciles yours away.
817
+
818
+ - \`import ... from 'drizzle-orm'\` or \`'drizzle-orm/pg-core'\`. \`drizzle-orm\` is NOT a
819
+ dependency of this project. If you ever truly need the builder, the resource hands it to
820
+ you — see the resource-method rules below.
821
+ - \`import ... from 'postgres'\`, \`'pg'\`, \`'@/lib/db.js'\` or \`'@/db/schema/...'\`. \`pg\` is
822
+ installed only because the framework's driver requires it; it is never yours to import.
823
+ - calling \`drizzle()\`, \`pgSchema()\` or \`pgTable()\` yourself.
824
+ - writing a table definition, a migration file, a \`drizzle.config.ts\`, or a \`src/db\`
825
+ directory. There is no \`db:generate\` and no \`db:migrate\`.
826
+
827
+ A foreign key is declared by the referenced resource's ALIAS, never by importing another
828
+ file:
829
+
830
+ pg: { references: { resource: 'other-alias' } }
831
+
832
+ If the referenced resource does not exist, use a plain string property instead.
833
+ `),
834
+
835
+ skill(ViableSkill.ResourceResults, 'How resource methods are called, and what they return', `
836
+ Reach data through the accessor from \`@/resources/{entity}/{type}.js\` and call the
837
+ resource's own methods. Never write raw SQL in a route handler, never import a database
838
+ connection, and never construct a resource yourself.
839
+
840
+ A WRITE takes ONE argument — the record — and the id travels INSIDE it. There is no
841
+ \`(id, changes)\` overload on any of them:
842
+
843
+ await resource.create(payload) // refuses a caller-supplied id
844
+ await resource.update({ ...current, ...changes }) // replaces the WHOLE record
845
+ await resource.patch({ id: current.id, progress }) // merges only the listed fields
846
+ await resource.save(record) // create-or-replace
847
+
848
+ THE DATABASE ASSIGNS THE ID. \`id\` is a generated column — the table is created with a
849
+ \`gen_random_uuid()\` default — so a NEW record does not have one yet and it is not yours to
850
+ invent. NEVER generate an id in application code: no \`randomUUID()\`, no \`crypto.randomUUID()\`,
851
+ no \`uuid()\`, no counter, no \`Date.now()\` string, and never \`import { randomUUID } from
852
+ 'crypto'\` for this. Build the record WITHOUT an \`id\` key at all and read the id off the record
853
+ \`create\` HANDS BACK:
854
+
855
+ const created = await resource.create({ title, ownerId }) // no id in the payload
856
+ return created.id // the id the database assigned
857
+
858
+ \`create\` REJECTS a record that carries an \`id\` — including one you just generated — by
859
+ throwing \`resource:record-exists:id-present\`. Despite its name that error does NOT mean a
860
+ duplicate record was found and it is NOT a race, a double submit or a uniqueness collision: it
861
+ means the payload had an id in it. The only fix is to stop putting one there. Do not "repair" it
862
+ with a retry, an in-flight guard, an idempotency check, or a \`load(id)\`-then-return-existing
863
+ branch — none of those touch the cause, and looking up an id you just minted can only ever miss.
864
+
865
+ When you genuinely have an id already — the record exists and you are writing it back — that is
866
+ \`update\`/\`patch\`/\`save\`, never \`create\`. A record type declaring \`id: string\` as required
867
+ describes a STORED record; the create payload is that type without its id (\`Omit<T, 'id'>\`).
868
+
869
+ Passing an id as a WRITE's first argument fails to compile with \`error TS2559: Type 'string'
870
+ has no properties in common with type 'Partial<...>'\` — a bare id is a READ's argument, never
871
+ a write's. Load the record first when you only have its id, then write the merged object back
872
+ — or use \`patch\`.
873
+
874
+ A READ takes either an ID or a CRITERIA OBJECT as its first argument. There is no
875
+ "value plus field name" overload — the field is a key of the object:
876
+
877
+ await resource.load(id) // the record, or null
878
+ await resource.load({ projectId }) // by any other field, or several at once
879
+ await resource.get(id) // throws UnknownRecordError instead of null
880
+ await resource.get({ ownerId, slug })
881
+ await resource.list({ status: 'open' }) // { items, total, page?, size? }
882
+ await resource.count({ status: 'open' }) // number
883
+
884
+ Writing \`load(projectId, 'projectId')\` fails to compile — the second parameter is
885
+ \`{ sort }\`, not a field name. Fetching ONE record is \`load\`/\`get\`, never a \`list\` whose
886
+ first element you take: \`const { items: [x] } = await resource.list({ a, b })\` is
887
+ \`const x = await resource.load({ a, b })\`.
888
+
889
+ Inside the criteria object a bare value means equality, a bare ARRAY means "any of these"
890
+ (\`{ status: ['open', 'held'] }\`), \`null\` asks for IS NULL, and \`undefined\` is SKIPPED —
891
+ so an untouched filter cannot empty a list. A field may instead carry an operator object —
892
+ \`$eq $ne $gt $gte $lt $lte $in $nin $exists $null $like $ilike $regex $startsWith $endsWith
893
+ $between $contains $contained $overlaps\` — and \`$and\` / \`$or\` / \`$not\` combine whole
894
+ criteria:
895
+
896
+ await resource.list({ createdAt: { $gte: since }, $or: [{ ownerId }, { shared: true }] })
897
+
898
+ \`delete(id)\` returns the removed record or \`null\`. \`take(id)\` DELETES the record it hands
899
+ back and throws \`UnknownRecordError\` when there is none — it is never a way to read one; use
900
+ \`load\`/\`get\` for that. \`purge(where)\` deletes every match and returns how many went, so
901
+ clearing a set is one call and never a page loop around \`delete\`; it refuses an empty \`{}\`
902
+ rather than emptying the table.
903
+
904
+ These methods return the RECORDS THEMSELVES, never a driver result object. There is NO
905
+ \`rowsAffected\`, \`affectedRows\`, \`changes\` or \`rowCount\` anywhere — reading one
906
+ yields \`undefined\`, which then silently compares as false. Use \`count(where)\` for a
907
+ number and \`await resource.load(id) != null\` for an existence check.
908
+
909
+ PAGING AND SORT ARE FLAT IN THE SECOND ARGUMENT of \`list\`. There is no \`limit\`, no
910
+ \`offset\`, and no \`pager\` or \`criteria\` wrapper — those two names do not exist:
911
+
912
+ resource.list({ status: 'open' }, { page: 0, size: 20, sort: ['createdAt'] })
913
+
914
+ - \`sort\` is an array of field names (ascending) or \`{ field, order: 'desc' }\` objects.
915
+ - Writing \`list({}, { page, limit: 10 })\` fails with \`'limit' does not exist in type
916
+ 'ListOptions<...>'\`, and nesting the same pair under \`pager\` only moves the error to
917
+ \`'pager' does not exist in type 'ListOptions<...>'\`. The declared name is \`size\`.
918
+ - The backend pages by itself: Postgres returns at most \`DEFAULT_PAGE_SIZE\` (100) rows when
919
+ \`size\` is absent, so a listing that genuinely needs every match asks for
920
+ \`{ size: 0 }\` — explicitly, once, where a reader can see it.
921
+ - The result is \`{ items, total, page?, size? }\`. \`total\` is the full count of matching
922
+ records, never \`items.length\` — build pagination UI from it, and never recompute it.
923
+
924
+ For a join or aggregate the CRUD surface cannot express, use \`resource.select()\`/\`query()\`
925
+ with \`{{}}\` for this resource's table and \`{{other-alias}}\` for another registered
926
+ resource's.
927
+
928
+ A Drizzle handle exists as a LAST RESORT, and only through the resource:
929
+ \`(await resource.db()).drizzle\` over \`resource.entity\`, reached with NO import. Nothing on
930
+ that path is typed — \`resource.entity\` is \`Record<string, any>\` — and the moment the query
931
+ needs \`eq\`, \`and\` or \`sql\` it needs an import you may not write: that query goes in
932
+ \`resource.query()\` instead. Read \`resource.entity\` inside a function, never at module
933
+ scope; it is built during \`init()\` and is \`undefined\` before then.
934
+ `),
935
+
936
+ skill(ViableSkill.ResourceMigrations, 'Data migrations', `
937
+ Schema shape needs no migration — reconciliation handles it. Only a DATA change
938
+ reconciliation cannot make on its own (a backfill, a value rewrite) needs one, and it is
939
+ registered on the resource as code, inside the maker, beside \`resource.index(...)\`. The
940
+ signature is \`resource.migration(name, fn, stage)\`:
941
+
942
+ resource.migration(name, async tx => { ... }, stage)
943
+
944
+ Registering is all there is to do: the resource runs its pending migrations itself during
945
+ \`init()\`, so nothing ever calls one. The call returns the resource, so several chain, and
946
+ re-registering the same name with the same body is a no-op — which is what makes it safe in a
947
+ maker that runs more than once.
948
+
949
+ Each one is applied once and recorded under its name and a checksum of its body. Editing a
950
+ migration that has already run throws \`resource:migration-conflict\` at the next boot; a new
951
+ fact needs a NEW migration under a new name. A migration that throws aborts the boot with
952
+ \`resource:migration-failed\` rather than leaving the app on a half-shaped database.
953
+ `),
954
+
955
+ skill(ViableSkill.OwlmeansContext, 'The OwlMeans context', `
956
+ The context is the application's container. It holds exactly two kinds of thing: RESOURCES (data)
957
+ and SERVICES. Nothing else lives on it.
958
+
959
+ **The context is built by a LIBRARY and there is no module-level \`owlCtx\` on the server side.**
960
+ \`makeBackendContext\` lives in \`sources/backend/src/context.ts\`, and the api and the worker each
961
+ build their own from it — so a module in \`sources/backend\` that imported a container singleton
962
+ would bind whichever process happened to load it first. The browser is the exception: the web app
963
+ IS one process, and \`sources/web/src/owlmeans.ts\` exports \`owlCtx\` for module-level code.
964
+
965
+ **Data comes from the resource accessor, and the accessor takes the context.** Every handler and
966
+ every job processor is handed its own; pass it down.
967
+
968
+ import { taskResource } from 'project-backend/resources/task/task'
969
+ const { items } = await taskResource(ctx).list({ status: 'open' })
970
+
971
+ **Business logic in \`src/models/**\` is PLAIN EXPORTED FUNCTIONS. It is never a service.**
972
+ Never write this:
973
+
974
+ // WRONG — this object is not a service, nothing registers it, and the lookup throws
975
+ export const DASHBOARD_SERVICE = 'dashboard-activity'
976
+ export const makeDashboardService = (ctx) => ({
977
+ alias: DASHBOARD_SERVICE, registerContext: ..., assertCtx: ..., initialized: true,
978
+ getSummary: async () => ...
979
+ })
980
+
981
+ Write this instead, and let the route handler import the function directly:
982
+
983
+ // RIGHT — sources/backend/src/models/dashboard/activity.ts
984
+ export const getSummary = async (ctx: BasicContext<BasicConfig>, guestId: string) => { ... }
985
+
986
+ // RIGHT — sources/api/src/app/dashboard/activity.ts
987
+ import { getSummary } from 'project-backend/models/dashboard/activity.js'
988
+ const summary = await getSummary(ctx, guestId)
989
+
990
+ Do not create a service just to hold functions.
991
+
992
+ **A resource is NEVER reached through \`service()\`.** Resources and services are two different
993
+ registries. Asking for a resource alias on the service registry throws even though the resource
994
+ exists and is registered:
995
+
996
+ // WRONG — 'reservation' is a RESOURCE; this throws
997
+ // SyntaxError: Service reservation not found
998
+ const reservations = ctx.service<PostgresResource<any>>('reservation')
999
+
1000
+ // RIGHT — import the accessor the resource file exports, and hand it the context
1001
+ import { reservationResource } from 'project-backend/resources/reservation/reservation'
1002
+ const { items } = await reservationResource(ctx).list({ status: 'open' })
1003
+
1004
+ A function in \`src/models/**\` takes the context first and its own domain arguments after.
1005
+
1006
+ **\`ctx.service(alias)\` THROWS when the alias is unknown** — it does NOT return
1007
+ \`undefined\`:
1008
+
1009
+ SyntaxError: Service dashboard-activity not found
1010
+
1011
+ So a null check around it is dead code that never runs:
1012
+
1013
+ // WRONG — the throw already happened on the line above
1014
+ const svc = ctx.service(ALIAS)
1015
+ if (!svc) { throw { status: 503 } }
1016
+
1017
+ \`ctx.resource(alias)\` and \`ctx.entrypoint(alias)\` throw the same way
1018
+ (\`Resource X not found\`, \`Entrypoint X not found\`).
1019
+
1020
+ **Only when a real lifecycle-owning singleton is needed** — a client holding a connection, a
1021
+ cache, a poller. Business logic never qualifies, and neither does "the handler needs to call
1022
+ this": export a function. When it genuinely does qualify, put it in
1023
+ \`sources/backend/src/services/{name}.ts\`, one service per file,
1024
+ flat in that directory, built with \`createService\` and never as a hand-written object:
1025
+
1026
+ import { createService } from '@owlmeans/context'
1027
+ import type { InitializedService } from '@owlmeans/context'
1028
+ import type { BasicConfig, BasicContext } from '@owlmeans/context'
1029
+
1030
+ export const SERVICE_ALIAS = 'dashboard-activity'
1031
+
1032
+ export interface DashboardActivityService extends InitializedService {
1033
+ getSummary: (guestId: string) => Promise<Summary | null>
1034
+ }
1035
+
1036
+ // The maker name is fixed — the generated service registry imports exactly this symbol.
1037
+ export const makeService = (): DashboardActivityService =>
1038
+ createService<DashboardActivityService>(SERVICE_ALIAS, {
1039
+ getSummary: async guestId => { ... }
1040
+ })
1041
+
1042
+ /** The registered instance — import this from API handlers and job processors. */
1043
+ export const dashboardActivityService = (ctx: BasicContext<BasicConfig>): DashboardActivityService =>
1044
+ ctx.service<DashboardActivityService>(SERVICE_ALIAS)
1045
+
1046
+ Never call \`registerService()\` yourself and never edit \`src/services/index.ts\`: that registry
1047
+ is generated from the directory, and the context factory registers everything in it before
1048
+ \`init()\` runs.
1049
+
1050
+ **Never resolve anything at module scope.** Module bodies run before the context is initialized,
1051
+ so a top-level \`const svc = ctx.service(X)\` throws while the file is being imported and kills
1052
+ the process before it can listen. Resolve inside the function that uses it.
1053
+ `),
1054
+
1055
+ skill(ViableSkill.OwlMeansServices, 'Services vs entrypoints — two registries', `
1056
+ This project has TWO registries of names. They share nothing, and confusing them is the single
1057
+ most expensive mistake that still compiles.
1058
+
1059
+ **SERVICES** are the two sides of the application as NETWORK ENDPOINTS — a host, a port, a base
1060
+ path. They live in \`cfg.services\`, and there are exactly two:
1061
+
1062
+ - \`APP_WEB\` — the frontend
1063
+ - \`APP_API\` — the backend
1064
+
1065
+ Both are declared in \`sources/common/src/consts.ts\` and registered by a \`service({ … })\` /
1066
+ \`sservice({ … })\` call in \`sources/web/src/config.ts\` and
1067
+ \`sources/api/src/owlmeans.ts\`. **\`serviceRoute()\` reads this registry and nothing else.**
1068
+
1069
+ **ENTRYPOINTS** are the \`app.web.*\` and \`app.api.*\` tree — screens and endpoints, i.e. ROUTES.
1070
+ \`ctx.entrypoint()\` reads that one.
1071
+
1072
+ // RIGHT — sources/web/src/owlmeans.ts, exactly as the project ships
1073
+ import { APP_API, APP_WEB } from 'project-common/consts'
1074
+ owlCtx.serviceRoute(APP_WEB, true)
1075
+ owlCtx.serviceRoute(APP_API, true)
1076
+
1077
+ // WRONG — every one of these type-checks, builds clean, and blanks the whole application
1078
+ owlCtx.serviceRoute(app.web.base, true)
1079
+ owlCtx.serviceRoute(app.web.area.guest, true)
1080
+ owlCtx.serviceRoute(app.api.post.base, true)
1081
+
1082
+ Both sides are strings, so nothing in TypeScript can tell them apart. The failure appears only
1083
+ in the browser, while the module is still being imported:
1084
+
1085
+ Uncaught SyntaxError: Service not found web:base
1086
+
1087
+ The alias in that message is not a key of \`cfg.services\`. Pass one of the SERVICE constants
1088
+ above. Do NOT substitute a different \`app.*\` value: the error simply returns wearing the new
1089
+ name, which is how one project spent twenty repair attempts on one line — every one of them
1090
+ reporting success.
1091
+
1092
+ **\`config.ts\` and \`owlmeans.ts\` on both sides are framework wiring this project was generated
1093
+ with.** They carry no sentinel and no application code, nothing in the pipeline authors them, and
1094
+ the write tools refuse them. If one of them is the cause, call \`restore_wiring_file\` to put back
1095
+ the version the project was generated from — never re-derive it, and never "fix" it by changing
1096
+ which name it uses. A service alias is renamed in \`sources/common/src/consts.ts\` and nowhere
1097
+ else, and both sides must keep using the same constant or the frontend calls a backend that
1098
+ publishes itself under another name.
1099
+ `),
1100
+
1101
+ skill(ViableSkill.OwlMeansServer, 'Writing an endpoint handler', `
1102
+ There is NO express here. \`express\`, \`cors\` and \`@types/express\` are not installed, there is
1103
+ no \`app.get(...)\`, no \`req\`/\`res\` of a web server, no \`next\`, and no middleware. Anything
1104
+ written for express fails to resolve.
1105
+
1106
+ A handler is a plain async function that RETURNS its result, wrapped in one of three helpers
1107
+ from \`@owlmeans/server-app\`. Pick by what the handler reads:
1108
+
1109
+ import { handleBody, handleParams, handleRequest } from '@owlmeans/server-app'
1110
+
1111
+ // the request BODY — the payload type is the type argument
1112
+ export const createTask = handleBody<TaskInput>(async (payload, ctx) => {
1113
+ const tasks = getTaskResource(ctx)
1114
+ return await tasks.create(payload)
1115
+ })
1116
+
1117
+ // the route PARAMS — names match the ':' segments of the declared path
1118
+ export const getTask = handleParams<{ taskId: string }>(async ({ taskId }, ctx) => {
1119
+ return await tasks(ctx).load(taskId)
1120
+ })
1121
+
1122
+ // anything else (query, headers, nothing at all) — the whole request
1123
+ export const listTasks = handleRequest(async (req, ctx) => {
1124
+ const query = req.query as { search?: string }
1125
+ return await tasks(ctx).list(query.search != null ? { search: query.search } : {})
1126
+ })
1127
+
1128
+ - The SECOND argument is the OwlMeans context. Reach every resource through it — never
1129
+ import a database connection and never write raw SQL in a handler.
1130
+ - RETURN the value. \`res.json(...)\`, \`res.status(...)\`, \`res.send(...)\` do not exist; a
1131
+ returned value IS the 200 response body.
1132
+ - Do not wrap the body in try/catch to convert an error into a response. A thrown
1133
+ \`ResilientError\` subclass is mapped to its status by the framework; catching it produces a
1134
+ 200 carrying an error object instead.
1135
+ - Never read a token, never check a role, never look at an \`Authorization\` header. Access is
1136
+ declared on the entrypoint (\`guard()\` / \`gate()\`) and enforced before the handler runs.
1137
+ - A handler is inert until an \`elevate(appEntrypoints, alias, handler)\` line in
1138
+ \`sources/api/src/entrypoints.ts\` binds it to its alias. Without that line the endpoint
1139
+ answers 404 and nothing reports an error.
1140
+ - Handlers are ENTITY-SCOPED: \`sources/api/src/app/<entity>/<action>.ts\`, named exports only.
1141
+ The directory is what keeps two entities' \`list\` apart — the file name carries no marker.
1142
+ `),
1143
+
1144
+ skill(ViableSkill.FixerHeuristics, 'Reading a build error', `
1145
+ Change as little as the reported error requires, and read the error for what it actually
1146
+ says before rewriting anything.
1147
+
1148
+ - When the task carries a \`NAME REGISTRY\` block, it is AUTHORITATIVE for every directory,
1149
+ path, alias and exported symbol it lists. NEVER rename an identifier that appears in it. An
1150
+ error saying one "does not exist" means the thing is MISSING and must be DECLARED or CREATED
1151
+ where the registry says it lives — add the alias line above the sentinel, add the import,
1152
+ write the file at that path. Renaming the reference to a different guess makes the error move
1153
+ rather than go away: the next build reports it again under the new spelling, and the loop
1154
+ never converges. "Change as little as required" is about the SIZE of the edit, never a licence
1155
+ to rename instead of declare.
1156
+ - Two files disagreeing about where something lives is not two problems. Take the registry's
1157
+ spelling in both, and never invent a third.
1158
+
1159
+ - **"Cannot find module '@/…'" in a file under \`sources/backend/\` is the ALIAS, not the
1160
+ suffix.** That package is the shared library: it is built by \`tsc -b\`, declares no tsconfig
1161
+ \`paths\`, and \`@/\` resolves to nothing in it. Rewrite the specifier as a RELATIVE path keeping
1162
+ its \`.js\` suffix — \`'../../resources/task/task.js'\`. Adding, removing or changing the
1163
+ extension on the alias fixes nothing and the same error comes back under the other spelling;
1164
+ two attempts that differ only in the suffix mean you are in this case.
1165
+ - "Cannot find module" for \`postgres\`, \`@/lib/db.js\` or \`@/db/schema/...\` inside a
1166
+ \`resources/**\` file means the file was written against the REMOVED Drizzle-DDL layout.
1167
+ Rewrite it as an OwlMeans resource: an AJV schema on \`resource.schema\`, built with
1168
+ \`makePostgresResource\`, with no table definition at all. Do not create the missing module.
1169
+ - An \`import\` of \`drizzle-orm\`, \`drizzle-orm/pg-core\` or \`pg\` reports NO error — the
1170
+ driver is installed for the framework's own use — and is still wrong. It is never the cause
1171
+ of the error you were given, so do not go hunting for one; but when the reported error puts
1172
+ you in that file anyway, drop the import as part of the fix. The builder is
1173
+ \`(await resource.db()).drizzle\`, and anything that needed \`eq\`/\`and\`/\`sql\` becomes
1174
+ \`resource.query()\` with \`{{}}\` and \`$1\` parameters.
1175
+ - A missing foreign-key target means the referenced resource does not exist. Declare the
1176
+ reference by alias (\`pg: { references: { resource: '<alias>' } }\`) or fall back to a
1177
+ plain string property — never import another resource file. The alias must be one the
1178
+ registry (or the listed existing resources) actually names: an invented one compiles and then
1179
+ kills the backend at startup with \`fk-unknown-resource\`, which no type check can catch.
1180
+ - An error about \`rowsAffected\`/\`rowCount\`/\`changes\` not existing is the resource result
1181
+ shape — see the result rules above. \`result.pager\` is the same fault: a listing returns
1182
+ \`{ items, total, page?, size? }\`, so \`pager?.total\` becomes \`total\`.
1183
+ - \`'limit' does not exist in type 'ListOptions<...>'\` (or the same for \`offset\` or
1184
+ \`pager\`) is one call written against a shape that has none of them. Paging is FLAT in
1185
+ \`list\`'s second argument: \`list(where, { page, size, sort })\`. \`size\` is the declared
1186
+ name — moving the pair under a \`pager\` key only changes which of the two names the next
1187
+ build rejects, and that swap is the loop that never converges.
1188
+ - \`Property 'pick' does not exist\` on a resource means \`take(id)\` — delete-and-return, which
1189
+ throws \`UnknownRecordError\` instead of returning null.
1190
+ - \`Argument of type 'string' is not assignable to parameter of type 'Criteria<...>'\` on a
1191
+ \`load\`/\`get\` call is the removed \`(value, 'field')\` overload. The field is a key of the
1192
+ criteria object: \`load({ projectId })\`. The second argument now carries only \`sort\`.
1193
+ - \`delete\` takes an id and nothing else. A \`delete(criteria)\` call is \`purge(where)\`, which
1194
+ returns the number of records removed rather than a record.
1195
+ - "Cannot find module" for a third-party router or HTTP-server package (\`express\`, \`cors\`),
1196
+ \`@reduxjs/toolkit\`, \`react-redux\`, \`@/state/store\` or \`@/lib/fetch\` means the file was
1197
+ written against the REMOVED stack. None of those packages are installed and none will be.
1198
+ Rewrite the file against entrypoints: \`useNavigate\` from \`@owlmeans/client\` for navigation,
1199
+ \`ctx.entrypoint(alias).call(...)\` for a backend call, a
1200
+ \`handleRequest\`/\`handleBody\`/\`handleParams\` function for an endpoint, and the state hooks
1201
+ (\`useStoreModel\`/\`useStoreList\` over a state resource) for client state. Do NOT install the
1202
+ package and do NOT create the missing module.
1203
+ - A missing export from a \`*.ts\` module — a selector, an action creator, a thunk, a
1204
+ reducer — is the same removed stack in a different disguise. That module exports an ALIAS
1205
+ constant and read hooks only. Replace a selector with the matching hook, a dispatched action
1206
+ with \`resource.save(record)\` / \`model.update({ ... })\`, and a thunk with an
1207
+ \`entrypoint(alias).call(...)\` in the VIEW MODEL followed by a \`save\`. Do not add the missing
1208
+ export to the state module.
1209
+ - A runtime \`Resource <alias> not found\` for a state alias means the state module is written but
1210
+ nothing registered it. Add the import and ONE
1211
+ \`appendStateResource<C, T>(context, ALIAS)\` line above the sentinel in
1212
+ \`sources/web/src/context.ts\` — never a second alias string, and never a new store file.
1213
+ - \`DEFAULT_ID\` has no export from \`@owlmeans/state\`. A record is either held or it is not, and
1214
+ \`model.empty\` is the test — \`record.id === DEFAULT_ID\` was a placeholder that no longer gets
1215
+ invented. Never reintroduce the constant, and never compare against \`'_default'\` in its place.
1216
+ - \`Property 'erase' does not exist\` on a state resource is \`clear()\`; \`all()\` and
1217
+ \`match(criteria)\` are both \`list(criteria)\`, which returns \`{ items, total }\` rather than a
1218
+ bare array. An \`erase()\` followed by a \`save\` per record is \`replace(records)\` — one write,
1219
+ and the store ends up holding exactly what the server sent.
1220
+ - A list screen that recomputes ids to keep itself in step is fighting the store. One
1221
+ \`useStoreList({ query, resource })\` subscribes to the CRITERIA and re-renders on every write
1222
+ that changes which records match, so the \`useValue(() => resource.list())\` that fed it a list
1223
+ of ids goes away entirely rather than being repaired.
1224
+ - A runtime \`TypeError: entrypoint.call is not a function\` (or \`<name>.call is not a function\`
1225
+ on the result of \`ctx.entrypoint(...)\`) is a MISSING CLIENT ELEVATION and nothing else. The
1226
+ alias resolves — the browser context carries every shared declaration — but only an elevated
1227
+ one has \`call\`. Add the bare \`elevate(list, <the alias in that call>)\` above the
1228
+ \`// owlmeans: add new backend elevations above this line\` sentinel in
1229
+ \`sources/web/src/entrypoints.ts\`. Do NOT rewrite the view model, do NOT replace the
1230
+ call with \`fetch\`, and do NOT add a component to a backend alias. The build was clean
1231
+ because the call site casts to \`ClientEntrypoint\`, so \`tsc\` will not confirm the fix —
1232
+ the elevation line is the fix.
1233
+ - A screen that renders blank is the same class of fault on the other side: the screen alias has
1234
+ no \`elevate(list, alias, handler(Screen))\` line. Adding a declaration without its elevation
1235
+ is the usual cause of both.
1236
+ - A header, menu or footer rendered TWICE is a screen importing its own layout. The AREA is the
1237
+ screen's parent entrypoint and the framework already wraps it — delete the import and the
1238
+ wrapper element from the screen, never the area from the entrypoint tree.
1239
+ - An error naming an AREA — \`app.web.area.<area>\`, its \`route(...)\`, its \`guard()\`/\`gate()\`,
1240
+ or one of the four layouts in \`sources/web/src/layout/area.tsx\` — means generated code
1241
+ EDITED something that ships with the project. RESTORE the shipped form: four areas under
1242
+ \`app.web.base\` at \`/\`, \`/frontoffice\`, \`/admin\` and \`/backoffice\`, each with its own guard
1243
+ and one \`default: true\` child. Never re-declare an area to satisfy the error, never add a
1244
+ fifth, and never move a screen's access up onto the area — a screen that needs different
1245
+ access belongs in a different area.
1246
+ - A user who WAS granted a permission and still gets 403 is almost always an \`@\` in the stored
1247
+ permission NAME. The gate splits its parameter at the first \`@\`: \`'enquiry--view@enquiryId'\`
1248
+ in \`gate(OIDC_GATE, [...])\` is CORRECT and means "look up \`enquiry--view\`, read the resource
1249
+ id from the \`:enquiryId\` route param". The same string registered or granted as a permission
1250
+ NAME is a key nothing ever looks up, so every grant against it is a silent no-op. Fix the
1251
+ registration and the grant, never the gate line — and never delete the \`@\` from the gate to
1252
+ "make the names match". A gate whose \`@name\` is not a \`:\` segment of that entrypoint's own
1253
+ declared path is the other half of the same fault: it refuses every request.
1254
+ - A PERMISSION that no definition backs is a gate nobody can pass, and there are two ways generated
1255
+ code gets one. A bracketed word — \`gate(OIDC_GATE, ['<permission>'])\` — is an instruction's
1256
+ placeholder written out as if it were a name. A foreign name — \`article--modify\` in an
1257
+ application that has no articles — is an example copied from a comment or a skill. Both look
1258
+ perfectly valid to the compiler and to the boot check, so nothing reports them; the app simply
1259
+ refuses those requests forever. Replace the string with a permission built from THIS project's own
1260
+ resource and action, spelling it exactly as the project's other declarations spell that capability
1261
+ — never delete the gate to make the error go away, and never invent a definition to match a name
1262
+ that should not have been written.
1263
+ - A whole area rendering BLANK is its missing \`default: true\` child: an entrypoint with
1264
+ children and no default child matches nothing. Restore the default child; do not give the area
1265
+ a component of its own.
1266
+ - \`'X' does not exist in type 'Y'\` means the SHAPE you wrote is wrong, not that the
1267
+ feature is impossible. When a TYPE_DECLARATIONS block is present in the task, it is the
1268
+ ground truth — rewrite the object or call to match it exactly, instead of renaming the
1269
+ property to another guess or deleting the code.
1270
+ - If the same error survives a fix, the fix was wrong. Re-read the message rather than
1271
+ applying a larger version of the same change.
1272
+ - NEVER make a handler or a function stop doing its job to make the build pass. Returning
1273
+ \`{ items: [], total: 0 }\`, an empty object, a hard-coded literal or \`res.sendStatus(204)\`
1274
+ in place of the real call is not a fix — it turns a loud build error into a silent wrong
1275
+ answer nobody will notice. If a call does not type-check, correct the ARGUMENTS to match
1276
+ the function's real signature; the imports and the call must survive the fix.
1277
+ - A runtime \`SyntaxError: Service not found <alias>\` from \`context.serviceRoute\` is a
1278
+ DIFFERENT error from the one below, and the difference decides the repair. It means an
1279
+ ENTRYPOINT alias (\`app.web.*\`, \`app.api.*\`) was passed where a SERVICE alias is required.
1280
+ The registered ones are \`APP_WEB\` and \`APP_API\` from \`sources/common/src/consts.ts\`, and
1281
+ the REGISTERED SERVICE ALIASES block in this task lists what this project actually declares.
1282
+ **Substituting another \`app.*\` value is what makes this loop circle** — it has already cost
1283
+ one project twenty attempts, each one reporting success and changing only which name the error
1284
+ prints. The two files that can produce it, \`config.ts\` and \`owlmeans.ts\`, are framework
1285
+ wiring: they are refused to the write tools, and \`restore_wiring_file\` is the repair.
1286
+ - A runtime \`Service <alias> not found\` means generated code called
1287
+ \`owlCtx.service('<alias>')\` for something nothing registers. Two causes, both common:
1288
+ a hand-written service object in \`src/models/**\`, or a RESOURCE alias asked for on the
1289
+ service registry (\`ctx.service('reservation')\` — use the resource accessor instead,
1290
+ \`reservationResource()\` from \`@/resources/reservation/reservation.js\`, and drop the
1291
+ \`ctx\` parameter the function only needed for the lookup). **Do NOT create a service to fix it, and do NOT add a
1292
+ file under \`src/services/\`.** Delete the fake service object and the lookup, EXPORT the
1293
+ functions it wrapped from the same model file (dropping the \`ctx\` parameter they never
1294
+ needed), and import those functions directly in the handler. TypeScript cannot see the
1295
+ original error — the code compiles — but it does see the follow-ups, so expect
1296
+ \`declares 'X' locally, but it is not exported\` next and fix it by exporting, never by
1297
+ reintroducing a service.
1298
+ - A runtime \`Resource <alias> not found\` means the resource module is missing from the
1299
+ generated \`src/resources/index.ts\`. Report it; never hand-register a resource and never
1300
+ edit a generated registry.
1301
+ `),
1302
+
1303
+ skill(ViableSkill.LayoutDefinition, 'What "layout" means here', `
1304
+ A layout is one of the FOUR AREA shells this project already ships, all in one file —
1305
+ \`sources/web/src/layout/area.tsx\`: \`GuestLayout\`, \`UserLayout\`, \`AdminLayout\`,
1306
+ \`OperatorLayout\`. They carry the chrome of the application — header, section menu, side menu,
1307
+ content region, footer — and each is a thin wrapper over \`NavLayout\` from
1308
+ \`@owlmeans/web-panel\`, which builds both menu levels from \`sources/web/src/nav.ts\`.
1309
+
1310
+ A layout is mounted as the PARENT entrypoint of its area, never imported by a screen. The
1311
+ framework renders the matched screen INTO it as \`children\`, so a layout is
1312
+ \`FC<PropsWithChildren>\` and puts \`{children}\` where the screen belongs. It knows nothing about
1313
+ which screen that is.
1314
+
1315
+ A generated project never creates a layout. There are four and there will only ever be four:
1316
+ customise one by RESTYLING it — its title, its classes, its header actions — never by writing a
1317
+ fifth shell and never by rebuilding the navigation inside it by hand.
1318
+ `),
1319
+
1320
+ skill(ViableSkill.ScreenDefinition, 'What "screen" means here', `
1321
+ A screen is the React component that arranges other components and their layout inside
1322
+ the page. It may also switch between the components it contains. It never contains
1323
+ general chrome — no header, no footer, no navigation, no menu, no sidebar.
1324
+
1325
+ A screen NEVER imports and NEVER renders a layout component. Its layout is the AREA it is
1326
+ declared under — \`frontend({ parent: app.web.area.<area> })\` — and the framework wraps the
1327
+ screen in it. A screen that wraps itself renders the whole chrome twice, nested.
1328
+
1329
+ A screen's declared path is the TAIL ONLY: \`'/tasks'\`, never \`'/frontoffice/tasks'\`. The area
1330
+ contributes the prefix, so repeating it publishes the screen at a doubled URL nothing links to.
1331
+ `),
1332
+
1333
+ skill(ViableSkill.QueueDiscipline, 'When work belongs off the request path', `
1334
+ Almost never. Start from "this is a request handler and a table" and stay there unless one of
1335
+ the reasons below is TRUE of this feature. A queue turns one request into two processes and a
1336
+ message that can be delivered twice; a worker is a second thing that can be down; an LLM agent
1337
+ turns a click into a bill. None of that is free, and none of it is undone easily.
1338
+
1339
+ ## Reasons that are real
1340
+ - The work calls an LLM, or any third-party API that is slow or rate-limited.
1341
+ - The work walks an unbounded set — every row a user owns, every file in an upload.
1342
+ - The work must survive the user closing the tab: a long import, a generated report.
1343
+ - The work is scheduled or repeated rather than requested.
1344
+ - The work must be retried on failure without the user doing anything.
1345
+
1346
+ ## Reasons that are NOT real
1347
+ - "It might be slow one day." Measure first; moving it later is a small change.
1348
+ - "It writes to several tables." That is a transaction, not a job.
1349
+ - "It sends one email." One outbound call in a request is fine.
1350
+ - "It feels like background work." Feelings are not a reason; name the property.
1351
+ - "It is complicated." Complexity belongs in a function, not in another process.
1352
+
1353
+ ## If the answer is yes
1354
+ Say WHY in one sentence naming the property above, and say how the processor is safe to run
1355
+ twice — a worker can die mid-job, the lock expires and the step re-runs. There is no way to
1356
+ make that automatic. "Skip rows already marked done" and "delete what a previous attempt
1357
+ created before recreating it" are answers; "it should be fine" is not.
1358
+
1359
+ ## And say who watches it
1360
+ A queued job has no screen and no session. Name the story whose screen shows its progress, or
1361
+ the work is invisible and the user is left pressing a button that appears to do nothing.
1362
+ `),
1363
+
1364
+ skill(ViableSkill.WorkerJobs, 'Queues, jobs and processors', `
1365
+ Three files have to agree, and a job that exists in two of the three is worse than one that
1366
+ exists in none — a declared name nothing processes is a message that piles up, and a processor
1367
+ with no declaration is dead code the barrel still imports.
1368
+
1369
+ ## 1. The queue — \`sources/backend/src/jobs/index.ts\`
1370
+ A queue is an ADDRESS: it says what exists and which job names it accepts. Both the api (which
1371
+ enqueues) and the worker (which consumes) read this one list, and a job name the queue does not
1372
+ declare is refused at enqueue time.
1373
+
1374
+ \`\`\`ts
1375
+ export const queues: QueueDeclaration[] = [
1376
+ { name: APP_QUEUE, jobs: [app.job.test, app.job.<name>],
1377
+ worker: { concurrency: 4, lockDuration: 60_000 } },
1378
+ ]
1379
+ \`\`\`
1380
+
1381
+ ## 2. The alias and the entrypoint — \`sources/common/src\`
1382
+ The job's name IS its entrypoint alias. Declare \`app.job.<name>\` in \`consts.ts\` above the
1383
+ sentinel, and the entrypoint in \`entrypoints.ts\` with \`job()\` from \`@owlmeans/route\`.
1384
+
1385
+ ## 3. The processor — \`sources/worker/src/jobs/<name>.ts\`
1386
+ A plain async function wrapped in \`handleRequest\` / \`handleBody<T>\` / \`handleParams<T>\`,
1387
+ exactly like an endpoint handler. It RETURNS its result; throwing a \`ResilientError\` subclass
1388
+ is how a refusal is reported, and the class survives the broker.
1389
+
1390
+ Two rules with no equivalent on the HTTP side:
1391
+ - **Call \`job.touch()\` inside every long loop.** The broker judges liveness by the lock, and
1392
+ silence for longer than \`lockDuration\` is indistinguishable from a dead worker — the job is
1393
+ handed to somebody else and the work runs twice.
1394
+ - **A processor must be safe to run twice.** Skip what a previous attempt recorded, or delete
1395
+ what it created, and say in a comment which of the two this one does.
1396
+
1397
+ Elevate it in \`sources/worker/src/entrypoints.ts\` above the sentinel. Enqueue from an endpoint
1398
+ with \`context.jobs().create({ name: app.job.<name>, data })\`.
1399
+ `),
1400
+
1401
+ skill(ViableSkill.TargetAgents, 'LLM agents inside the application', `
1402
+ An agent generated into the application is an \`@owlmeans/agent\` model, built in the shared
1403
+ backend package and RUN FROM A JOB — never on the request path. A model call takes seconds to
1404
+ minutes and costs money per attempt; holding a request open for it gives the user a timeout and
1405
+ the operator a bill with no result attached.
1406
+
1407
+ - \`makeAgentModel({ exec, tools, ... })\` for a tool loop that converses.
1408
+ - \`makePipeline(spec, steps)\` for ordered, resumable steps whose position must survive a crash.
1409
+ Pipeline state holds KEYS, never artifacts — a step writes its output somewhere and puts the
1410
+ id in the state.
1411
+
1412
+ The agent module goes in \`sources/backend/src/agents/<alias>.ts\` and is registered in that
1413
+ directory's generated barrel. The endpoint the user presses enqueues the job; the job invokes
1414
+ the agent; the screen watches the job. Never import an agent from \`sources/api\`.
1415
+ `),
1416
+
1417
+ skill(ViableSkill.AgenticChoice, 'A call, a pipeline, or an agent', `
1418
+ Three shapes can perform work with a model, and they are not interchangeable. Pick the
1419
+ SIMPLEST one that does the job, and increase complexity only when the simpler shape provably
1420
+ cannot. Every step up costs latency, money and a failure mode.
1421
+
1422
+ **A call** — one prompt, one answer. The default, and the right answer far more often than it
1423
+ is chosen. Summarise, classify, extract, rewrite, draft, translate, answer a question about
1424
+ text you already have. If you can write down the prompt, it is a call.
1425
+
1426
+ **A pipeline** — fixed, ordered steps, decided by you and not by the model. Choose it when the
1427
+ work decomposes cleanly into subtasks you can NAME IN ADVANCE, and each step's output is the
1428
+ next step's input: transcribe then summarise then file; extract then validate then store. You
1429
+ are trading latency for accuracy, and you know the number of steps before you start.
1430
+
1431
+ **An agent** — the model chooses its own path through tools, and you cannot say in advance how
1432
+ many steps it will take. Choose it ONLY when all of these are true:
1433
+ - the number of steps genuinely cannot be predicted;
1434
+ - which tool to use next is a judgement, not a rule you could write down;
1435
+ - the input is unstructured and the decision is contextual;
1436
+ - and the work runs in a place where an unpredictable number of tool calls is acceptable.
1437
+
1438
+ Not reasons to choose an agent: the task is "complex"; it uses more than one piece of data; it
1439
+ sounds impressive; a tool exists. A rules engine with a model in it is a pipeline. An app that
1440
+ calls a model but does not let the model direct the work is not an agent at all.
1441
+
1442
+ If you are unsure between two of them, take the simpler one. A call that turns out to need a
1443
+ second step becomes a pipeline with one edit; an agent that never needed to be one is a bill
1444
+ nobody can explain.
1445
+
1446
+ Whichever shape it is, it runs off the request path — a model call is seconds to minutes and
1447
+ costs money per attempt, and holding a request open for it gives the user a timeout and the
1448
+ operator a bill with no result attached. So it is ALWAYS reached through something a person does:
1449
+ somebody starts it, and somebody watches it finish. Work with no human half is work nobody can
1450
+ see, cancel or be told about, and it reaches the user as a button that appears to do nothing.
1451
+
1452
+ Describe that work INSIDE the story of the person who starts it — "I request a write-up and come
1453
+ back to read it" — rather than as a story of its own told in the machine's voice. A machine has
1454
+ no account to sign in with, no screen of its own and no permission that can be granted to it, so
1455
+ "As an AI assistant, I want to open the queue…" is a story whose actor can never use what gets
1456
+ built for it.
1457
+ `),
1458
+
1459
+ skill(ViableSkill.TargetLlm, 'Calling a model from the application', `
1460
+ Model calls happen in \`sources/backend\` and are invoked from a job in \`sources/worker\`.
1461
+ NEVER from \`sources/web\` — a browser bundle cannot hold a provider key — and never inline in
1462
+ a request handler in \`sources/api\`.
1463
+
1464
+ The key is the user's, not the platform's, and it may be absent. Follow the shape
1465
+ \`sources/backend/src/config.ts\` already uses for the database and the queue:
1466
+
1467
+ \`\`\`ts
1468
+ export const anthropicKey = process.env.ANTHROPIC_API_KEY ?? ''
1469
+ export const llmConfigured = anthropicKey !== ''
1470
+ \`\`\`
1471
+
1472
+ - Never throw at module scope when it is missing. The application must still boot, exactly as
1473
+ it does without \`VALKEY_URL\`.
1474
+ - The endpoint that would use it answers a refusal explaining that a model API key has not been
1475
+ configured yet — not a 500, and not silence.
1476
+ - Declare the variable so the platform can ask the owner for it; do not invent a default.
1477
+
1478
+ Give every call a bounded output and treat a refusal or an empty answer as an outcome the code
1479
+ handles, never as an exception that reaches the user.
1480
+
1481
+ Where the answer has a SHAPE, describe it with the same AJV schema style the rest of this
1482
+ application uses — the one already exported beside each type. Do not reach for \`zod\`: it is not
1483
+ a dependency of this project, and importing it fails the build with \`TS2307\`.
1484
+ `),
1485
+
1486
+ skill(ViableSkill.TargetAgentTools, 'Tools and skills for the application’s agent', `
1487
+ An agent is only as good as the tools it is given, and a bloated tool set is the most common
1488
+ way to make one worse.
1489
+
1490
+ - Give it the FEWEST tools that can complete the work. If you cannot say which of two tools the
1491
+ agent should reach for in a given situation, neither can it.
1492
+ - Name each tool for what it accomplishes, not for the endpoint behind it, and describe it in
1493
+ one sentence that says WHEN to use it.
1494
+ - Return human-readable results. An agent reasons better over a name than over a row id, and a
1495
+ large result should be filtered or truncated by the tool rather than by the model.
1496
+ - A tool must never reject. Catch inside it and return the failure as text the agent can act
1497
+ on; a thrown tool call aborts the whole turn.
1498
+
1499
+ The application's own \`.agents/skills/\` directory is loadable into its agent's prompt. Where
1500
+ guidance is long or situational, write it as a skill and let the agent read it when it needs
1501
+ it, rather than pasting it into the system prompt where it is paid for on every call.
1502
+ `),
1503
+
1504
+ skill(ViableSkill.GameDesign, 'What a game brief is made of', `
1505
+ A game is not a business flow, and analysing it as one produces a menu with nothing behind it.
1506
+
1507
+ Describe, in this order:
1508
+ 1. **The core loop** — the one thing the player does over and over, in a sentence. "Steer, dodge,
1509
+ collect." "Place a tile, score the line."
1510
+ 2. **The win and lose conditions** — how a session ends, both ways. A game with no end is a toy.
1511
+ 3. **The controls** — what input does what. Keyboard, pointer, touch.
1512
+ 4. **Progression** — what changes between the first minute and the tenth: speed, levels, score,
1513
+ unlocks. One axis is enough.
1514
+ 5. **What is persisted** — usually a score, a run history, a player profile. Almost never the
1515
+ frame-by-frame state of a session.
1516
+
1517
+ Steps that are NOT part of a game brief: sign-up, settings, billing, admin. They exist, they are
1518
+ ordinary screens, and they are not the product.
1519
+
1520
+ Keep it to one game. A brief that describes a platform of several games describes none of them.
1521
+ `),
1522
+
1523
+ skill(ViableSkill.GameScene, 'The three.js scene component', `
1524
+ Exactly ONE component owns the 3D scene. It creates the renderer, the camera and the scene,
1525
+ runs the animation loop, and tears all of it down again.
1526
+
1527
+ \`\`\`tsx
1528
+ // right — the loop and the renderer live in one effect, and it cleans up after itself
1529
+ useEffect(() => {
1530
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
1531
+ mount.current!.appendChild(renderer.domElement)
1532
+ let frame = 0
1533
+ const tick = () => { frame = requestAnimationFrame(tick); renderer.render(scene, camera) }
1534
+ tick()
1535
+ return () => {
1536
+ cancelAnimationFrame(frame)
1537
+ renderer.dispose()
1538
+ renderer.domElement.remove()
1539
+ }
1540
+ }, [])
1541
+ \`\`\`
1542
+
1543
+ Rules that are not optional:
1544
+ - **Never drive the animation loop from React state.** A \`setState\` per frame re-renders the
1545
+ tree sixty times a second and the game stutters. Mutate the object3D directly in the loop and
1546
+ publish to React only what the interface shows — a score, a life count — and only when it
1547
+ changes.
1548
+ - **Dispose what you create.** Geometries, materials and the renderer all hold GPU memory that
1549
+ unmounting does not release. A scene mounted and unmounted a few times without disposal
1550
+ exhausts the context and the canvas goes black.
1551
+ - **Resize is an event, not a render.** Update \`camera.aspect\`, call
1552
+ \`camera.updateProjectionMatrix()\` and \`renderer.setSize(...)\` from a resize listener.
1553
+ - **Geometry is generated, never loaded.** Compose it from the built-in geometries and simple
1554
+ materials — box, sphere, cylinder, plane, lathe, extrude. No textures, no image files, no
1555
+ external model formats. Colour, light and shape carry the whole look.
1556
+ `),
1557
+
1558
+ skill(ViableSkill.GameUi, 'The interface is React, above the canvas', `
1559
+ Everything a player reads or presses — score, menus, dialogs, buttons, settings — is an
1560
+ ordinary React component rendered ABOVE the canvas, using the same shadcn primitives as the rest
1561
+ of the application. Nothing is drawn as text inside the 3D scene.
1562
+
1563
+ \`\`\`tsx
1564
+ // right — the canvas fills the frame, the interface floats over it
1565
+ <div className="relative h-full w-full">
1566
+ <div ref={mount} className="absolute inset-0" />
1567
+ <div className="pointer-events-none absolute inset-0 p-4">
1568
+ <ScoreBadge value={score} />
1569
+ <div className="pointer-events-auto"><Button onClick={pause}>Pause</Button></div>
1570
+ </div>
1571
+ </div>
1572
+ \`\`\`
1573
+
1574
+ - The overlay container carries \`pointer-events-none\` so clicks reach the canvas; each
1575
+ interactive control turns them back on with \`pointer-events-auto\`. Forgetting this makes the
1576
+ game unplayable — every drag lands on an invisible div.
1577
+ - Text rendered into the scene cannot be selected, translated, scaled by the browser or read by
1578
+ a screen reader. Use it for nothing that matters.
1579
+ - SVG is the second half of the art: icons, badges, backgrounds and 2D games are inline SVG
1580
+ written by hand. No raster images anywhere.
1581
+ `),
1582
+
1583
+ skill(ViableSkill.GameNetworking, 'Who owns the state', `
1584
+ Three kinds of game, and the difference is entirely about where the state lives.
1585
+
1586
+ **Casual** — one player, one browser. The session runs entirely on the client; the server sees
1587
+ only what is worth keeping: a final score, a run record, a profile. No queue, no worker, no
1588
+ socket. This is most games, and it is the right answer unless the brief asks otherwise.
1589
+
1590
+ **Online, turn-based** — several players acting one after another, minutes or days apart. The
1591
+ match is an ordinary record and every move is an ordinary endpoint that validates it and writes
1592
+ the next state. Anything slow that follows a move — scoring a finished match, notifying the next
1593
+ player, rebuilding a leaderboard — is a job. No realtime anything.
1594
+
1595
+ **Online, live** — several players acting at once, and the server simulates.
1596
+ - The client sends INTENT ("move forward", "fire"), never an outcome ("I am at x=12", "I hit
1597
+ them"). A client that reports outcomes is a client that decides them, and one player's browser
1598
+ then decides everybody's game.
1599
+ - The server holds the authoritative state, advances it on a fixed tick, and broadcasts it.
1600
+ - The client may predict its own movement locally so it feels immediate, and corrects when the
1601
+ server's answer disagrees. Other players' entities are interpolated between the last two
1602
+ updates rather than snapped.
1603
+ - Anything the outcome depends on — damage, scoring, currency, who won — is decided on the
1604
+ server and nowhere else.
1605
+ `),
1606
+
1607
+ libraries(ViableSkill.LibrariesCommon, 'Libraries — shared code', commonLibraryList),
1608
+ libraries(ViableSkill.LibrariesUiState, 'Libraries — UI state', uiStateLibraryList),
1609
+ libraries(ViableSkill.LibrariesUi, 'Libraries — UI', fullUiLibraryList),
1610
+ libraries(ViableSkill.LibrariesBackend, 'Libraries — backend', backendLibraryList),
1611
+ libraries(ViableSkill.LibrariesAi, 'Libraries — model calls and agents', aiLibraryList),
1612
+ libraries(ViableSkill.LibrariesGame, 'Libraries — game', gameLibraryList),
1613
+ ]
1614
+
1615
+ /** Look one up by alias — for the places that still splice a rule into a task prompt. */
1616
+ export const viableSkill = (alias: ViableSkill): SkillDefinition =>
1617
+ VIABLE_SKILLS.find(entry => entry.alias === alias)!