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