@treenity/core 3.0.0 → 3.0.1

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 (322) hide show
  1. package/README.md +78 -0
  2. package/dist/chain.d.ts +3 -4
  3. package/dist/chain.d.ts.map +1 -1
  4. package/dist/chain.js.map +1 -1
  5. package/dist/client/trpc.d.ts.map +1 -1
  6. package/dist/client/trpc.js +1 -0
  7. package/dist/client/trpc.js.map +1 -1
  8. package/dist/comp/index.d.ts +3 -4
  9. package/dist/comp/index.d.ts.map +1 -1
  10. package/dist/comp/index.js +5 -4
  11. package/dist/comp/index.js.map +1 -1
  12. package/dist/comp/needs.d.ts.map +1 -1
  13. package/dist/comp/needs.js +3 -3
  14. package/dist/comp/needs.js.map +1 -1
  15. package/dist/core/component.d.ts +10 -8
  16. package/dist/core/component.d.ts.map +1 -1
  17. package/dist/core/component.js +4 -8
  18. package/dist/core/component.js.map +1 -1
  19. package/dist/core/context.d.ts +2 -2
  20. package/dist/core/context.d.ts.map +1 -1
  21. package/dist/core/path.d.ts.map +1 -1
  22. package/dist/core/path.js +7 -3
  23. package/dist/core/path.js.map +1 -1
  24. package/dist/core/registry.d.ts +2 -1
  25. package/dist/core/registry.d.ts.map +1 -1
  26. package/dist/core/registry.js.map +1 -1
  27. package/dist/core.d.ts +1 -1
  28. package/dist/core.d.ts.map +1 -1
  29. package/dist/core.js +1 -1
  30. package/dist/core.js.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/log.d.ts +30 -0
  36. package/dist/log.d.ts.map +1 -1
  37. package/dist/log.js +119 -0
  38. package/dist/log.js.map +1 -1
  39. package/dist/mod/examples/ticker/service.js +2 -3
  40. package/dist/mod/examples/ticker/service.js.map +1 -1
  41. package/dist/mod/index.d.ts +1 -1
  42. package/dist/mod/index.d.ts.map +1 -1
  43. package/dist/mod/index.js +1 -1
  44. package/dist/mod/index.js.map +1 -1
  45. package/dist/mod/loader.d.ts +1 -0
  46. package/dist/mod/loader.d.ts.map +1 -1
  47. package/dist/mod/loader.js +27 -1
  48. package/dist/mod/loader.js.map +1 -1
  49. package/dist/mods/clients.d.ts +2 -0
  50. package/dist/mods/clients.d.ts.map +1 -0
  51. package/dist/mods/clients.js +3 -0
  52. package/dist/mods/clients.js.map +1 -0
  53. package/dist/mods/llm/index.js +1 -1
  54. package/dist/mods/llm/index.js.map +1 -1
  55. package/dist/mods/mcp/server.d.ts +0 -1
  56. package/dist/mods/mcp/server.js +0 -1
  57. package/dist/mods/mcp/service.d.ts +0 -1
  58. package/dist/mods/mcp/service.js +0 -1
  59. package/dist/mods/mcp/types.d.ts +0 -1
  60. package/dist/mods/mcp/types.js +0 -1
  61. package/dist/mods/servers.d.ts +4 -0
  62. package/dist/mods/servers.d.ts.map +1 -0
  63. package/dist/mods/servers.js +5 -0
  64. package/dist/mods/servers.js.map +1 -0
  65. package/dist/mods/treenity/builtins.d.ts +2 -0
  66. package/dist/mods/treenity/builtins.d.ts.map +1 -0
  67. package/dist/mods/treenity/builtins.js +18 -0
  68. package/dist/mods/treenity/builtins.js.map +1 -0
  69. package/dist/mods/treenity/logs.d.ts +18 -0
  70. package/dist/mods/treenity/logs.d.ts.map +1 -0
  71. package/dist/mods/treenity/logs.js +17 -0
  72. package/dist/mods/treenity/logs.js.map +1 -0
  73. package/dist/mods/treenity/seed.js +29 -27
  74. package/dist/mods/treenity/seed.js.map +1 -1
  75. package/dist/mods/treenity/server.d.ts +2 -0
  76. package/dist/mods/treenity/server.d.ts.map +1 -1
  77. package/dist/mods/treenity/server.js +2 -0
  78. package/dist/mods/treenity/server.js.map +1 -1
  79. package/dist/mods/uix/client.js +4 -4
  80. package/dist/mods/uix/client.js.map +1 -1
  81. package/dist/mods/uix/compile.d.ts.map +1 -1
  82. package/dist/mods/uix/compile.js +4 -2
  83. package/dist/mods/uix/compile.js.map +1 -1
  84. package/dist/schema/_test-fixture.d.ts +11 -0
  85. package/dist/schema/_test-fixture.d.ts.map +1 -0
  86. package/dist/schema/_test-fixture.js +8 -0
  87. package/dist/schema/_test-fixture.js.map +1 -0
  88. package/dist/schema/types.d.ts +1 -0
  89. package/dist/schema/types.d.ts.map +1 -1
  90. package/dist/server/actions.js +1 -1
  91. package/dist/server/auth.d.ts.map +1 -1
  92. package/dist/server/auth.js +4 -3
  93. package/dist/server/auth.js.map +1 -1
  94. package/dist/server/client.d.ts +10 -3
  95. package/dist/server/client.d.ts.map +1 -1
  96. package/dist/server/client.js +1 -1
  97. package/dist/server/client.js.map +1 -1
  98. package/dist/server/doc-index.d.ts.map +1 -1
  99. package/dist/server/doc-index.js +13 -12
  100. package/dist/server/doc-index.js.map +1 -1
  101. package/dist/server/factory.d.ts +6 -0
  102. package/dist/server/factory.d.ts.map +1 -1
  103. package/dist/server/factory.js +44 -25
  104. package/dist/server/factory.js.map +1 -1
  105. package/dist/server/main.d.ts +0 -4
  106. package/dist/server/main.d.ts.map +1 -1
  107. package/dist/server/main.js +4 -30
  108. package/dist/server/main.js.map +1 -1
  109. package/dist/server/mcp.d.ts +0 -1
  110. package/dist/server/mcp.js +0 -1
  111. package/dist/server/migrate.js +3 -3
  112. package/dist/server/migrate.js.map +1 -1
  113. package/dist/server/mods-mount.d.ts +0 -1
  114. package/dist/server/mods-mount.d.ts.map +1 -1
  115. package/dist/server/mods-mount.js +0 -1
  116. package/dist/server/mods-mount.js.map +1 -1
  117. package/dist/server/mount-adapters.js +9 -4
  118. package/dist/server/mount-adapters.js.map +1 -1
  119. package/dist/server/prefab.d.ts +2 -2
  120. package/dist/server/prefab.d.ts.map +1 -1
  121. package/dist/server/prefab.js +4 -2
  122. package/dist/server/prefab.js.map +1 -1
  123. package/dist/server/refs.d.ts +3 -0
  124. package/dist/server/refs.d.ts.map +1 -0
  125. package/dist/server/refs.js +59 -0
  126. package/dist/server/refs.js.map +1 -0
  127. package/dist/server/server.d.ts.map +1 -1
  128. package/dist/server/server.js +13 -3
  129. package/dist/server/server.js.map +1 -1
  130. package/dist/server/sub.js.map +1 -1
  131. package/dist/server/trpc.d.ts +7 -0
  132. package/dist/server/trpc.d.ts.map +1 -1
  133. package/dist/server/trpc.js +6 -2
  134. package/dist/server/trpc.js.map +1 -1
  135. package/dist/tree/fs.d.ts.map +1 -1
  136. package/dist/tree/fs.js +20 -18
  137. package/dist/tree/fs.js.map +1 -1
  138. package/dist/tree/index.d.ts.map +1 -1
  139. package/dist/tree/index.js +2 -1
  140. package/dist/tree/index.js.map +1 -1
  141. package/dist/tree-chain.d.ts +2 -1
  142. package/dist/tree-chain.d.ts.map +1 -1
  143. package/dist/tree-chain.js +5 -3
  144. package/dist/tree-chain.js.map +1 -1
  145. package/dist/uri.d.ts.map +1 -1
  146. package/dist/uri.js +8 -3
  147. package/dist/uri.js.map +1 -1
  148. package/package.json +48 -6
  149. package/src/chain.ts +7 -5
  150. package/src/client/trpc.ts +1 -0
  151. package/src/comp/index.test.ts +45 -9
  152. package/src/comp/index.ts +19 -8
  153. package/src/comp/needs.ts +3 -3
  154. package/src/core/component.ts +16 -14
  155. package/src/core/context.ts +4 -4
  156. package/src/core/index.test.ts +22 -1
  157. package/src/core/path.ts +6 -3
  158. package/src/core/registry.ts +4 -7
  159. package/src/core.ts +1 -1
  160. package/src/index.ts +1 -0
  161. package/src/log.ts +172 -1
  162. package/src/mod/docs/07-realtime.md +19 -11
  163. package/src/mod/docs/08-services.md +10 -8
  164. package/src/mod/docs/10-acl.md +1 -1
  165. package/src/mod/docs/12-conventions.md +43 -0
  166. package/src/mod/docs/13-example.md +36 -26
  167. package/src/mod/docs/14-mod-format.md +81 -8
  168. package/src/mod/examples/ticker/service.ts +2 -3
  169. package/src/mod/index.ts +1 -1
  170. package/src/mod/loader.test.ts +53 -1
  171. package/src/mod/loader.ts +34 -1
  172. package/src/mods/clients.ts +2 -0
  173. package/src/mods/llm/index.ts +1 -1
  174. package/src/mods/servers.ts +4 -0
  175. package/src/mods/treenity/builtins.ts +21 -0
  176. package/src/mods/treenity/logs.ts +26 -0
  177. package/src/mods/treenity/seed.ts +30 -28
  178. package/src/mods/treenity/server.ts +2 -0
  179. package/src/mods/uix/client.ts +4 -4
  180. package/src/mods/uix/compile.ts +4 -2
  181. package/src/schema/_test-fixture.ts +12 -0
  182. package/src/schema/generated/ai.agent.json +133 -0
  183. package/src/schema/generated/ai.approval.json +105 -0
  184. package/src/schema/generated/ai.approvals.json +24 -0
  185. package/src/schema/generated/ai.assignment.json +28 -0
  186. package/src/schema/generated/ai.plan.json +84 -0
  187. package/src/schema/generated/ai.policy.json +105 -0
  188. package/src/schema/generated/ai.pool.json +37 -0
  189. package/src/schema/generated/ai.thread.json +64 -0
  190. package/src/schema/generated/board.kanban.json +7 -0
  191. package/src/schema/generated/canary.item.json +40 -0
  192. package/src/schema/generated/claude-search.json +20 -0
  193. package/src/schema/generated/craft.product.json +47 -0
  194. package/src/schema/generated/craft.shop.json +94 -0
  195. package/src/schema/generated/craft.subscription.json +27 -0
  196. package/src/schema/generated/examples.demo.sensor.reading.json +25 -0
  197. package/src/schema/generated/flow.node.action.json +61 -0
  198. package/src/schema/generated/flow.node.code.json +43 -0
  199. package/src/schema/generated/flow.node.condition.json +37 -0
  200. package/src/schema/generated/flow.node.end.json +35 -0
  201. package/src/schema/generated/flow.node.http.json +65 -0
  202. package/src/schema/generated/flow.node.llm.json +67 -0
  203. package/src/schema/generated/flow.node.loop.json +49 -0
  204. package/src/schema/generated/flow.node.start.json +39 -0
  205. package/src/schema/generated/flow.scenario.json +118 -0
  206. package/src/schema/generated/grove.attempt.json +199 -0
  207. package/src/schema/generated/grove.path.json +93 -0
  208. package/src/schema/generated/grove.review.json +27 -0
  209. package/src/schema/generated/grove.task.json +164 -0
  210. package/src/schema/generated/intel.scenario.json +58 -0
  211. package/src/schema/generated/intel.signal.json +113 -0
  212. package/src/schema/generated/intel.world.json +15 -0
  213. package/src/schema/generated/landing.block.json +201 -0
  214. package/src/schema/generated/landing.page.json +84 -0
  215. package/src/schema/generated/metatron.config.json +119 -0
  216. package/src/schema/generated/metatron.permission.json +36 -0
  217. package/src/schema/generated/metatron.skill.json +36 -0
  218. package/src/schema/generated/metatron.task.json +114 -0
  219. package/src/schema/generated/metatron.template.json +26 -0
  220. package/src/schema/generated/metatron.workspace.json +60 -0
  221. package/src/schema/generated/order.status.json +21 -0
  222. package/src/schema/generated/polyhope.backtest.json +161 -0
  223. package/src/schema/generated/polyhope.feed.json +33 -0
  224. package/src/schema/generated/polyhope.run.json +94 -0
  225. package/src/schema/generated/polyhope.strategy.json +152 -0
  226. package/src/schema/generated/polymax.activity.json +65 -0
  227. package/src/schema/generated/polymax.aggr-feed.json +28 -0
  228. package/src/schema/generated/polymax.aggr.json +20 -0
  229. package/src/schema/generated/polymax.alert.json +56 -0
  230. package/src/schema/generated/polymax.bot-config.json +14 -0
  231. package/src/schema/generated/polymax.bot-status.json +35 -0
  232. package/src/schema/generated/polymax.classification.json +55 -0
  233. package/src/schema/generated/polymax.deposits.json +45 -0
  234. package/src/schema/generated/polymax.holding.json +55 -0
  235. package/src/schema/generated/polymax.identity.json +71 -0
  236. package/src/schema/generated/polymax.lb-entry.json +75 -0
  237. package/src/schema/generated/polymax.leaderboard.json +37 -0
  238. package/src/schema/generated/polymax.market-ref.json +25 -0
  239. package/src/schema/generated/polymax.performance.json +65 -0
  240. package/src/schema/generated/polymax.profile.json +16 -0
  241. package/src/schema/generated/polymax.scan-result.json +50 -0
  242. package/src/schema/generated/polymax.status.json +40 -0
  243. package/src/schema/generated/polymax.tags.json +53 -0
  244. package/src/schema/generated/polymax.trader.json +16 -0
  245. package/src/schema/generated/polymax.wallet-market.json +50 -0
  246. package/src/schema/generated/polymax.wallet-pnl.json +35 -0
  247. package/src/schema/generated/pult.concept.json +53 -0
  248. package/src/schema/generated/pult.config.json +227 -0
  249. package/src/schema/generated/pult.connector.json +72 -0
  250. package/src/schema/generated/pult.market.json +113 -0
  251. package/src/schema/generated/pult.order.json +113 -0
  252. package/src/schema/generated/pult.rete.json +68 -0
  253. package/src/schema/generated/pult.sensor.json +74 -0
  254. package/src/schema/generated/pult.signal.json +54 -0
  255. package/src/schema/generated/pult.synapse.json +93 -0
  256. package/src/schema/generated/pult.trade.json +93 -0
  257. package/src/schema/generated/resim.config.json +34 -0
  258. package/src/schema/generated/resim.function.json +62 -0
  259. package/src/schema/generated/resim.goal.json +22 -0
  260. package/src/schema/generated/resim.resource.json +40 -0
  261. package/src/schema/generated/resim.state.json +48 -0
  262. package/src/schema/generated/resim.world.json +40 -0
  263. package/src/schema/generated/saveme.action.save.json +29 -0
  264. package/src/schema/generated/saveme.message.json +36 -0
  265. package/src/schema/generated/saveme.router.json +31 -0
  266. package/src/schema/generated/t.coolify.json +50 -0
  267. package/src/schema/generated/t.event.json +46 -0
  268. package/src/schema/generated/t.logs.json +155 -0
  269. package/src/schema/generated/t.note.json +31 -0
  270. package/src/schema/generated/t.person.json +36 -0
  271. package/src/schema/generated/t.tenant.json +57 -0
  272. package/src/schema/generated/t.tenant.status.json +42 -0
  273. package/src/schema/generated/tagger.config.json +115 -0
  274. package/src/schema/generated/tagger.result.json +57 -0
  275. package/src/schema/generated/tagger.tree.json +36 -0
  276. package/src/schema/generated/ui.table.json +46 -0
  277. package/src/schema/types.ts +1 -0
  278. package/src/server/actions.test.ts +1 -1
  279. package/src/server/actions.ts +1 -1
  280. package/src/server/api.test.ts +9 -0
  281. package/src/server/auth.ts +4 -3
  282. package/src/server/client.ts +1 -1
  283. package/src/server/coverage.test.ts +1 -1
  284. package/src/server/doc-index.ts +13 -12
  285. package/src/server/e2e.test.ts +4 -3
  286. package/src/server/factory.ts +46 -24
  287. package/src/server/main.ts +4 -36
  288. package/src/server/migrate.ts +4 -4
  289. package/src/server/mods-mount.ts +0 -2
  290. package/src/server/mount-adapters.ts +9 -4
  291. package/src/server/mount.test.ts +73 -5
  292. package/src/server/prefab.ts +3 -2
  293. package/src/server/refs.test.ts +82 -0
  294. package/src/server/refs.ts +64 -0
  295. package/src/server/server.ts +14 -3
  296. package/src/server/sub.ts +2 -2
  297. package/src/server/trpc.ts +9 -3
  298. package/src/tree/fs.ts +21 -15
  299. package/src/tree/index.test.ts +26 -0
  300. package/src/tree/index.ts +2 -1
  301. package/src/tree-chain.test.ts +37 -44
  302. package/src/tree-chain.ts +11 -5
  303. package/src/uri.test.ts +32 -5
  304. package/src/uri.ts +4 -2
  305. package/dist/mods/mcp/server.d.ts.map +0 -1
  306. package/dist/mods/mcp/server.js.map +0 -1
  307. package/dist/mods/mcp/service.d.ts.map +0 -1
  308. package/dist/mods/mcp/service.js.map +0 -1
  309. package/dist/mods/mcp/types.d.ts.map +0 -1
  310. package/dist/mods/mcp/types.js.map +0 -1
  311. package/dist/schema/test-opaque.d.ts +0 -3
  312. package/dist/schema/test-opaque.d.ts.map +0 -1
  313. package/dist/schema/test-opaque.js +0 -43
  314. package/dist/schema/test-opaque.js.map +0 -1
  315. package/dist/server/mcp.d.ts.map +0 -1
  316. package/dist/server/mcp.js.map +0 -1
  317. package/src/mods/mcp/CLAUDE.md +0 -6
  318. package/src/mods/mcp/server.ts +0 -2
  319. package/src/mods/mcp/service.ts +0 -19
  320. package/src/mods/mcp/types.ts +0 -7
  321. package/src/schema/test-opaque.ts +0 -42
  322. package/src/server/mcp.ts +0 -326
@@ -13,25 +13,28 @@ export type ComponentData<T = Record<string, unknown>> = T & {
13
13
  $acl?: GroupPerm[];
14
14
  };
15
15
 
16
+ export type RefEntry = { t: string; f?: string; d?: ComponentData };
17
+
16
18
  export type NodeData<T = Record<string, unknown>> = ComponentData<T> & {
17
19
  $path: string;
18
20
  $owner?: string;
19
21
  $rev?: number;
22
+ $refs?: RefEntry[];
20
23
  };
21
24
 
22
25
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
23
26
  export type Class<T = unknown> = new (...args: any[]) => T;
24
27
 
25
- // Accept string, object with $type, or registered class (registerType stamps $type on constructor)
26
- export type TypeId<T = unknown> = string | { $type: string } | Class<T>;
28
+ // Accept string or registered class (registerType stamps $type on constructor)
29
+ export type TypeId<T = unknown> = string | Class<T>;
27
30
 
28
31
  // ── Type normalization ──
29
32
  // Dot-less types belong to treenity namespace: 'dir' → 't.dir', 'ref' → 't.ref'
30
33
  // Types with dots are already namespaced and returned as-is
31
34
  export function normalizeType(type: TypeId): string {
32
35
  if (typeof type === 'string') return type.includes('.') ? type : `t.${type}`;
33
- if ('$type' in type && typeof (type as any).$type === 'string') return normalizeType((type as any).$type);
34
- throw new Error('TypeId: object has no $type property');
36
+ if (typeof (type as any).$type === 'string') return normalizeType((type as any).$type);
37
+ throw new Error('TypeId: class not registered (missing $type)');
35
38
  }
36
39
 
37
40
  // ── Utils ──
@@ -40,17 +43,12 @@ export function isComponent(value: unknown): value is ComponentData {
40
43
  return typeof value === 'object' && value !== null && '$type' in value;
41
44
  }
42
45
 
43
- export function getCompByKey(node: NodeData, key: string): ComponentData | undefined {
44
- const v = key === '' ? node : node[key];
45
- return isComponent(v) ? v : undefined;
46
- }
47
-
48
- export const AnyType = { $type: 'any' };
46
+ export const AnyType = 't.any';
49
47
 
50
48
  export function isOfType<T>(value: unknown, type: TypeId): value is ComponentData<T> {
51
49
  if (!isComponent(value)) return false;
52
50
  const t = normalizeType(type);
53
- return t === 't.any' || normalizeType(value.$type) === t;
51
+ return t === AnyType || t === normalizeType(value.$type);
54
52
  }
55
53
 
56
54
  // ── Ref ──
@@ -72,13 +70,17 @@ export function assertNonSystemName(name: string) {
72
70
  if (name.startsWith('$')) throw new Error(`Component name cannot start with $: ${name}`);
73
71
  }
74
72
 
73
+ export function createNode<T, C = Record<string, ComponentData<any>>>(
74
+ path: string, type: Class<T>, data?: Partial<T>, components?: C): NodeData<T & C>;
75
75
  export function createNode<T = any, C = Record<string, ComponentData<any>>>(
76
+ path: string, type: string, data?: T, components?: C): NodeData<T & C>;
77
+ export function createNode(
76
78
  path: string,
77
79
  type: TypeId,
78
- data?: T,
79
- components?: C): NodeData<T & C> {
80
+ data?: any,
81
+ components?: any): NodeData {
80
82
 
81
- const node: NodeData<T & C> = { $path: path, $type: normalizeType(type) } as NodeData<T & C>;
83
+ const node: NodeData = { $path: path, $type: normalizeType(type) } as NodeData;
82
84
  if (components) Object.keys(components).forEach(assertNonSystemName);
83
85
  if (data) Object.keys(data).forEach(assertNonSystemName);
84
86
 
@@ -4,11 +4,11 @@ export type Handler = (...args: any[]) => any;
4
4
 
5
5
  // Typed context handlers — augmented by layers via declaration merging
6
6
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
7
- export interface ContextHandlers {
7
+ export interface ContextHandlers<T = any> {
8
8
  }
9
9
 
10
- export type ContextHandler<C extends string> = C extends keyof ContextHandlers
11
- ? ContextHandlers[C]
10
+ export type ContextHandler<C extends string, T = any> = C extends keyof ContextHandlers<T>
11
+ ? ContextHandlers<T>[C]
12
12
  : C extends `${infer Base}:${string}`
13
- ? ContextHandler<Base>
13
+ ? ContextHandler<Base, T>
14
14
  : Handler;
@@ -15,9 +15,21 @@ import {
15
15
  resolve,
16
16
  unregister,
17
17
  } from './index';
18
+ import { registerBuiltins } from '#mods/treenity/builtins';
19
+
20
+ const testTypes = ['test.doc', 'test.item', 'test.session', 'test.task'];
21
+
22
+ export function registerTestTypes() {
23
+ for (const t of testTypes)
24
+ register(t, 'schema', () => ({ $id: t, type: 'object' as const, title: t, properties: {} }));
25
+ }
26
+
27
+ registerTestTypes();
18
28
 
19
29
  export function clearRegistry(): void {
20
30
  mapRegistry((t, c) => unregister(t, c));
31
+ registerBuiltins();
32
+ registerTestTypes();
21
33
  }
22
34
 
23
35
  describe('Node', () => {
@@ -81,7 +93,7 @@ describe('Component access', () => {
81
93
 
82
94
  it('isComponent type guard', () => {
83
95
  assert.ok(isComponent(node['budget']));
84
- assert.ok(!isComponent(node['missing']));
96
+ assert.ok(!isComponent((node as any)['missing']));
85
97
  assert.ok(!isComponent('string'));
86
98
  assert.ok(!isComponent(null));
87
99
  });
@@ -141,6 +153,15 @@ describe('Path utils', () => {
141
153
  assert.equal(isChildPath('/tasks', '/tasks/123/sub', false), true);
142
154
  assert.equal(isChildPath('/', '/tasks/123', false), true);
143
155
  });
156
+
157
+ // Regression: /board must not match /boards (prefix overlap without separator)
158
+ it('isChildPath rejects prefix overlap without separator', () => {
159
+ assert.equal(isChildPath('/board', '/boards'), false);
160
+ assert.equal(isChildPath('/board', '/boards/test'), false);
161
+ assert.equal(isChildPath('/board', '/boards/test', false), false);
162
+ assert.equal(isChildPath('/board', '/board/real'), true);
163
+ assert.equal(isChildPath('/board', '/board/real/deep', false), true);
164
+ });
144
165
  });
145
166
 
146
167
  describe('Context', () => {
package/src/core/path.ts CHANGED
@@ -17,10 +17,13 @@ export function join(parent: string, name: string): string {
17
17
  }
18
18
 
19
19
  export function isChildPath(parent: string, candidate: string, directOnly = true): boolean {
20
- if (!candidate.startsWith(parent) || candidate === parent) return false;
20
+ if (candidate === parent) return false;
21
+ // Must match parent + '/' to avoid /board matching /boards
22
+ const prefix = parent === '/' ? '/' : parent + '/';
23
+ if (!candidate.startsWith(prefix)) return false;
21
24
  if (directOnly) {
22
- const rest = parent === '/' ? candidate.slice(1) : candidate.slice(parent.length + 1);
23
- return !rest.includes('/');
25
+ const rest = candidate.slice(prefix.length);
26
+ return rest.length > 0 && !rest.includes('/');
24
27
  }
25
28
 
26
29
  return true;
@@ -1,4 +1,4 @@
1
- import { ComponentData, normalizeType, type TypeId } from './component';
1
+ import { type Class, ComponentData, normalizeType, type TypeId } from './component';
2
2
  import { ContextHandler, Handler } from './context';
3
3
 
4
4
  const registry = new Map<string, Handler>();
@@ -21,12 +21,9 @@ function key(type: string, context: string): string {
21
21
  return `${type}@${context}`;
22
22
  }
23
23
 
24
- export function register<C extends string>(
25
- type: string,
26
- context: C,
27
- handler: ContextHandler<C>,
28
- meta?: Record<string, unknown>,
29
- ): void {
24
+ export function register<C extends string>(type: string, context: C, handler: ContextHandler<C>, meta?: Record<string, unknown>): void;
25
+ export function register<T, C extends string>(type: Class<T>, context: C, handler: ContextHandler<C, T>, meta?: Record<string, unknown>): void;
26
+ export function register(type: TypeId, context: string, handler: Handler, meta?: Record<string, unknown>): void {
30
27
  const k = key(normalizeType(type), context);
31
28
  // Sealed: no overrides. In dev (HMR) modules re-execute, so we allow silent replace.
32
29
  // In production, duplicate register = bug, caught by tests.
package/src/core.ts CHANGED
@@ -1 +1 @@
1
- export * from './core/index.ts';
1
+ export * from './core/index';
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './core/index';
package/src/log.ts CHANGED
@@ -1,3 +1,150 @@
1
+ // Unified logging: tree-persisted nodes + ring buffer fallback + debug filter + console intercept
2
+
3
+ import dayjs from 'dayjs'
4
+
5
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
6
+
7
+ export interface LogEntry {
8
+ t: number
9
+ level: LogLevel
10
+ msg: string
11
+ code?: string
12
+ sub?: string
13
+ userId?: string
14
+ method?: string
15
+ path?: string
16
+ }
17
+
18
+ // ── Execution context provider — set by comp/index.ts to avoid circular imports ──
19
+
20
+ type CtxProvider = () => Record<string, unknown> | null
21
+ let _getCtx: CtxProvider = () => null
22
+
23
+ export function setCtxProvider(fn: CtxProvider) { _getCtx = fn }
24
+
25
+ // ── Log listeners ──
26
+
27
+ type OnLog = (entry: LogEntry) => void
28
+ const listeners: OnLog[] = []
29
+
30
+ export function addOnLog(fn: OnLog) { listeners.push(fn) }
31
+
32
+ // ── Timestamp ID: YYMMDD-HHmmss-mmm-NNN ──
33
+
34
+ let lastMs = 0
35
+ let seq = 0
36
+
37
+ export function makeLogPath(): string {
38
+ const now = Date.now()
39
+ if (now === lastMs) {
40
+ seq++
41
+ } else {
42
+ lastMs = now
43
+ seq = 0
44
+ }
45
+
46
+ const stamp = dayjs(now).format('YYMMDD-HHmmss-SSS')
47
+ const sq = String(seq).padStart(3, '0')
48
+
49
+ return `/sys/logs/${stamp}-${sq}`
50
+ }
51
+
52
+ function notify(entry: LogEntry) {
53
+ for (const fn of listeners) fn(entry)
54
+ }
55
+
56
+ // ── Ring buffer (fallback before tree init) ──
57
+
58
+ const MAX = 2000
59
+ const buffer: LogEntry[] = []
60
+ let cursor = 0
61
+ let total = 0
62
+
63
+ function push(level: LogLevel, args: unknown[]) {
64
+ // Extract [tag] → sub
65
+ let sub: string | undefined
66
+ if (typeof args[0] === 'string') {
67
+ const m = args[0].match(/^\[([^\]]+)\]$/)
68
+ if (m) {
69
+ sub = m[1]
70
+ args = args.slice(1)
71
+ }
72
+ }
73
+
74
+ // Extract UPPER_SNAKE code
75
+ let code: string | undefined
76
+ if (args.length > 1 && typeof args[0] === 'string' && /^[A-Z][A-Z0-9_]+$/.test(args[0])) {
77
+ code = args[0]
78
+ args = args.slice(1)
79
+ }
80
+
81
+ const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')
82
+ const ctx = _getCtx()
83
+
84
+ const entry: LogEntry = {
85
+ t: Date.now(),
86
+ level,
87
+ msg,
88
+ code,
89
+ sub,
90
+ userId: ctx?.userId as string | undefined,
91
+ method: ctx?.method as string | undefined,
92
+ path: ctx?.path as string | undefined,
93
+ }
94
+
95
+ if (listeners.length) {
96
+ notify(entry)
97
+ } else {
98
+ if (total < MAX) {
99
+ buffer.push(entry)
100
+ } else {
101
+ buffer[cursor] = entry
102
+ }
103
+ cursor = (cursor + 1) % MAX
104
+ total++
105
+ }
106
+ }
107
+
108
+ /** Get ordered log entries from ring buffer (oldest first) */
109
+ function getOrdered(): LogEntry[] {
110
+ if (total <= MAX) return buffer.slice()
111
+ return [...buffer.slice(cursor), ...buffer.slice(0, cursor)]
112
+ }
113
+
114
+ // ── Query (ring buffer fallback — when tree available, use sift via getChildren) ──
115
+
116
+ export interface LogQuery {
117
+ grep?: string
118
+ level?: LogLevel | LogLevel[]
119
+ head?: number
120
+ tail?: number
121
+ }
122
+
123
+ export function queryLogs(opts: LogQuery = {}): LogEntry[] {
124
+ let entries = getOrdered()
125
+
126
+ if (opts.level) {
127
+ const levels = Array.isArray(opts.level) ? opts.level : [opts.level]
128
+ entries = entries.filter(e => levels.includes(e.level))
129
+ }
130
+
131
+ if (opts.grep) {
132
+ const re = new RegExp(opts.grep, 'i')
133
+ entries = entries.filter(e => re.test(e.msg))
134
+ }
135
+
136
+ if (opts.tail) entries = entries.slice(-opts.tail)
137
+ if (opts.head) entries = entries.slice(0, opts.head)
138
+
139
+ return entries
140
+ }
141
+
142
+ export function logStats() {
143
+ return { buffered: Math.min(total, MAX), total, max: MAX }
144
+ }
145
+
146
+ // ── Debug filter ──
147
+
1
148
  const enabled = new Set<string>()
2
149
  let all = false
3
150
 
@@ -15,7 +162,7 @@ if (typeof process !== 'undefined' && process.env?.DEBUG) {
15
162
  setDebug(process.env.DEBUG)
16
163
  }
17
164
 
18
- ;(globalThis as any).setDebug = setDebug
165
+ ;(globalThis as Record<string, unknown>).setDebug = setDebug
19
166
 
20
167
  export function createLogger(name: string) {
21
168
  const tag = `[${name}]`
@@ -26,3 +173,27 @@ export function createLogger(name: string) {
26
173
  error(...args: unknown[]) { console.error(tag, ...args) },
27
174
  }
28
175
  }
176
+
177
+ // ── Console intercept — call once at startup ──
178
+
179
+ let intercepted = false
180
+
181
+ export function interceptConsole() {
182
+ if (intercepted) return
183
+ intercepted = true
184
+
185
+ for (const level of ['debug', 'info', 'warn', 'error'] as const) {
186
+ const orig = console[level]
187
+ console[level] = (...args: unknown[]) => {
188
+ push(level, args)
189
+ orig.apply(console, args)
190
+ }
191
+ }
192
+
193
+ // console.log → info level
194
+ const origLog = console.log
195
+ console.log = (...args: unknown[]) => {
196
+ push('info', args)
197
+ origLog.apply(console, args)
198
+ }
199
+ }
@@ -66,19 +66,23 @@ function MyRenderer({ value, onChange }) {
66
66
  ## react:list — самодостаточные list-item компоненты
67
67
 
68
68
  ```tsx
69
- function DefaultListItem({ value }: { value: NodeData }) {
69
+ import type { View } from '@treenity/react/context';
70
+
71
+ // View для default — value: ComponentData (базовый тип, есть $type)
72
+ const DefaultListItem: View<ComponentData> = ({ value, ctx }) => {
73
+ const node = ctx!.node;
70
74
  return (
71
- <div className="child-card" onClick={() => navigate(value.$path)}>
72
- <span className="child-icon">{typeIcon(value.$type)}</span>
75
+ <div className="child-card" onClick={() => navigate(node.$path)}>
76
+ <span className="child-icon">{typeIcon(node.$type)}</span>
73
77
  <div className="child-info">
74
- <span className="child-name">{pathName(value.$path)}</span>
75
- <span className="child-type">{value.$type}</span>
78
+ <span className="child-name">{pathName(node.$path)}</span>
79
+ <span className="child-type">{node.$type}</span>
76
80
  </div>
77
81
  <span className="child-chevron">&#8250;</span>
78
82
  </div>
79
83
  );
80
- }
81
- register('default', 'react:list', DefaultListItem as any);
84
+ };
85
+ register('default', 'react:list', DefaultListItem);
82
86
  ```
83
87
 
84
88
  Fallback: `react:list` → `default@react:list` → strip `:list` → `react`.
@@ -88,11 +92,15 @@ Fallback: `react:list` → `default@react:list` → strip `:list` → `react`.
88
92
  Когда `node.$type` совпадает с `$type` компонента, нода **сама является** компонентом. Поля лежат плоско:
89
93
 
90
94
  ```ts
91
- // Правильно
92
- await store.set({ $path: '/bot', $type: 'brahman.bot', token: '...', alias: '@bot' } as NodeData);
95
+ import { createNode } from '#core';
96
+
97
+ // Правильно — createNode создаёт типизированную ноду
98
+ await store.set(createNode('/bot', 'brahman.bot', { token: '...', alias: '@bot' }));
93
99
 
94
- // Неправильно
95
- await store.set({ $path: '/bot', $type: 'brahman.bot', config: { $type: 'brahman.bot', token: '...' } });
100
+ // Неправильно — вложенный компонент с тем же $type что и нода
101
+ await store.set(createNode('/bot', 'brahman.bot', {
102
+ config: { $type: 'brahman.bot', token: '...' }, // WRONG: дублирует $type
103
+ }));
96
104
  ```
97
105
 
98
106
  ## Views — read-only
@@ -3,16 +3,16 @@
3
3
  Фоновые процессы: боты, сенсоры, воркеры. `register(type, "service", handler)` → возвращает `{ stop() }`.
4
4
 
5
5
  ```ts
6
- import { register, type NodeData } from '#core';
6
+ import { register, createNode, type NodeData } from '#core';
7
7
  import { type ServiceHandle, type ServiceCtx } from '#contexts/service';
8
8
 
9
9
  register('my-worker', 'service', async (node: NodeData, ctx: ServiceCtx) => {
10
10
  const timer = setInterval(async () => {
11
- await ctx.store.set({
12
- $path: `${node.$path}/${Date.now()}`,
13
- $type: 'tick',
14
- ts: Date.now(),
15
- } as NodeData);
11
+ await ctx.store.set(createNode(
12
+ `${node.$path}/${Date.now()}`,
13
+ 'tick',
14
+ { ts: Date.now() },
15
+ ));
16
16
  }, 5000);
17
17
 
18
18
  return {
@@ -26,8 +26,10 @@ register('my-worker', 'service', async (node: NodeData, ctx: ServiceCtx) => {
26
26
  Создай ref-ноду в `/sys/autostart`:
27
27
 
28
28
  ```ts
29
- await store.set({ $path: '/my-worker', $type: 'my-worker' } as NodeData);
30
- await store.set({ $path: '/sys/autostart/my-worker', $type: 'ref', $ref: '/my-worker' } as NodeData);
29
+ import { createNode } from '#core';
30
+
31
+ await store.set(createNode('/my-worker', 'my-worker'));
32
+ await store.set(createNode('/sys/autostart/my-worker', 'ref', { $ref: '/my-worker' }));
31
33
  ```
32
34
 
33
35
  При старте сервера `startServices(store)` обходит `/sys/autostart`, резолвит refs, вызывает service-хэндлеры.
@@ -46,7 +46,7 @@ const validated = withValidation(store);
46
46
  import { withVolatile } from '#server/volatile';
47
47
 
48
48
  const store = withVolatile(backingStore);
49
- await store.set({ $path: '/temp', $type: 'session', $volatile: true } as NodeData);
49
+ await store.set(createNode('/temp', 'session', { $volatile: true }));
50
50
  ```
51
51
 
52
52
  ## Server Pipeline
@@ -40,6 +40,49 @@ t.* → treenity infrastructure: t.mount.fs, t.mount.mongo, t.llm
40
40
  | `class` | `Class<T>` | Registered component class |
41
41
  | `telegram` | `(node, tgCtx) => void` | Telegram-хэндлер |
42
42
 
43
+ ## TypeScript — дисциплина типов
44
+
45
+ **`as any` ЗАПРЕЩЁН.** Не "только в emergency" — НИКОГДА. Если типы не сходятся, чини типы. `as any` прячет баги, ломает inference, расползается как рак.
46
+
47
+ Допустимые касты:
48
+ - `as const` — сужение литерала
49
+ - `as 'idle'` — сужение к известному union-варианту
50
+ - `as SensorReading` — сужение к конкретному типу (когда ты знаешь что это он)
51
+
52
+ **Создание нод — ВСЕГДА через `createNode()`:**
53
+ ```ts
54
+ // Со строковым типом — T выводится из data:
55
+ await store.set(createNode('/path', 'my.type', { field: 'value' }));
56
+
57
+ // С Class<T> — data проверяется по полям класса (Partial<T>):
58
+ await store.set(createNode('/sensors/temp', SensorConfig, { interval: 10, source: 'api' }));
59
+ // ^^^^^^^^ autocomplete + type check
60
+
61
+ // С компонентами:
62
+ await store.set(createNode('/path', 'sensor', { interval: 5 }, {
63
+ config: { $type: 'sensor.config', source: 'api' },
64
+ }));
65
+
66
+ // ЗАПРЕЩЕНО — as NodeData убивает типизацию
67
+ await store.set({ $path: '/path', $type: 'my.type', field: 'value' } as NodeData);
68
+ ```
69
+
70
+ **Views — типизированные через `View<T>`:**
71
+ ```tsx
72
+ import type { View } from '@treenity/react/context';
73
+
74
+ // value — данные компонента. path — через ctx!.node.$path
75
+ const MyView: View<MyType> = ({ value, ctx }) => { ... };
76
+
77
+ // register принимает Class<T> — T пробрасывается автоматически
78
+ register(MyType, 'react', MyView);
79
+ ```
80
+
81
+ **Запрещённые паттерны:**
82
+ - `value as NodeData & MyType` — value УЖЕ типизирован через View<T>
83
+ - `(node as any).field` — используй `getComp(node, Class)` для типобезопасного доступа
84
+ - `register('type', 'react', handler as any)` — используй `register(Class, 'react', handler)`
85
+
43
86
  ## Ошибки
44
87
 
45
88
  ```ts
@@ -4,8 +4,7 @@
4
4
 
5
5
  ```ts
6
6
  // src/mods/sensor/types.ts
7
- import { getCtx, registerComp } from '#comp';
8
- import { join, type NodeData } from '#core';
7
+ import { getCtx, registerComp, getComp } from '#comp';
9
8
 
10
9
  export class SensorConfig {
11
10
  /** @title Интервал @description Секунды между замерами */
@@ -20,7 +19,10 @@ export class SensorConfig {
20
19
  async history() {
21
20
  const { node, store } = getCtx();
22
21
  const { items } = await store.getChildren(node.$path, { limit: 100 });
23
- return { items: items.map(n => ({ value: (n as any).value, ts: (n as any).ts })) };
22
+ return { items: items.map(n => {
23
+ const r = getComp(n, SensorReading);
24
+ return r ? { value: r.value, ts: r.ts } : null;
25
+ }).filter(Boolean) };
24
26
  }
25
27
  }
26
28
 
@@ -38,7 +40,7 @@ registerComp('sensor.reading', SensorReading);
38
40
 
39
41
  ```ts
40
42
  // src/mods/sensor/service.ts
41
- import { register, type NodeData } from '#core';
43
+ import { register, createNode, type NodeData } from '#core';
42
44
  import { getComp } from '#comp';
43
45
  import { type ServiceHandle, type ServiceCtx } from '#contexts/service';
44
46
  import { SensorConfig } from './types';
@@ -48,12 +50,11 @@ register('sensor', 'service', async (node: NodeData, ctx: ServiceCtx) => {
48
50
  const interval = (config?.interval ?? 5) * 1000;
49
51
 
50
52
  const timer = setInterval(async () => {
51
- await ctx.store.set({
52
- $path: `${node.$path}/${Date.now()}`,
53
- $type: 'sensor.reading',
54
- value: Math.random() * 100,
55
- ts: Date.now(),
56
- } as NodeData);
53
+ await ctx.store.set(createNode(
54
+ `${node.$path}/${Date.now()}`,
55
+ 'sensor.reading',
56
+ { value: Math.random() * 100, ts: Date.now() },
57
+ ));
57
58
  }, interval);
58
59
 
59
60
  return { stop: async () => clearInterval(timer) } satisfies ServiceHandle;
@@ -64,34 +65,39 @@ register('sensor', 'service', async (node: NodeData, ctx: ServiceCtx) => {
64
65
 
65
66
  ```tsx
66
67
  // src/mods/sensor/view.tsx
67
- import { register, type ComponentData } from '#core';
68
- import { useCurrentNode } from '#contexts/react';
69
- import { useChildren } from '#front/hooks';
68
+ import { register } from '#core';
69
+ import type { View } from '@treenity/react/context';
70
+ import { useChildren } from '@treenity/react/hooks';
71
+ import { SensorConfig, SensorReading } from './types';
70
72
 
71
- register('sensor.config', 'react', ({ value, onChange }) => {
73
+ // View<T> типизированный компонент. value: T, ctx: ViewCtx
74
+ const ConfigView: View<SensorConfig> = ({ value }) => {
72
75
  return (
73
76
  <div>
74
- <label>Interval: {(value as any).interval}s</label>
75
- <label>Source: {(value as any).source}</label>
77
+ <label>Interval: {value.interval}s</label>
78
+ <label>Source: {value.source}</label>
76
79
  </div>
77
80
  );
78
- });
81
+ };
79
82
 
80
- register('sensor', 'react', ({ value }) => {
81
- const node = useCurrentNode();
82
- const readings = useChildren(node.$path, { limit: 10, watchNew: true });
83
+ const SensorView: View<SensorConfig> = ({ value, ctx }) => {
84
+ const readings = useChildren(ctx!.node.$path, { limit: 10, watchNew: true });
83
85
 
84
86
  return (
85
87
  <div>
86
- <h3>Sensor: {node.$path}</h3>
88
+ <h3>Sensor: {ctx!.node.$path}</h3>
87
89
  {readings.map(r => (
88
90
  <div key={r.$path}>
89
- {(r as any).value?.toFixed(1)} @ {new Date((r as any).ts).toLocaleTimeString()}
91
+ {(r as SensorReading).value?.toFixed(1)} @ {new Date((r as SensorReading).ts).toLocaleTimeString()}
90
92
  </div>
91
93
  ))}
92
94
  </div>
93
95
  );
94
- });
96
+ };
97
+
98
+ // register принимает Class<T> — типы пробрасываются автоматически
99
+ register(SensorConfig, 'react', ConfigView);
100
+ register(SensorConfig, 'react:list', SensorView);
95
101
  ```
96
102
 
97
103
  ## 4. schemas.ts — JSON Schema
@@ -124,9 +130,13 @@ import './sensor/schemas';
124
130
  ## 6. Seed-данные
125
131
 
126
132
  ```ts
127
- await store.set({ $path: '/sensors/temp', $type: 'sensor',
133
+ import { createNode } from '#core';
134
+
135
+ await store.set(createNode('/sensors/temp', 'sensor', {
128
136
  config: { $type: 'sensor.config', interval: 10, source: 'internal' },
129
- } as NodeData);
137
+ }));
130
138
 
131
- await store.set({ $path: '/sys/autostart/temp-sensor', $type: 'ref', $ref: '/sensors/temp' } as NodeData);
139
+ await store.set(createNode('/sys/autostart/temp-sensor', 'ref', {
140
+ $ref: '/sensors/temp',
141
+ }));
132
142
  ```