synthos 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +5 -5
  2. package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
  3. package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
  4. package/default-pages/elevenlabs_effects_studio/page.json +13 -11
  5. package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +782 -801
  7. package/default-pages/elevenlabs_voice_studio/page.json +13 -11
  8. package/default-pages/json_tools/chat-history.json +1 -0
  9. package/default-pages/json_tools/page.html +70 -90
  10. package/default-pages/json_tools/page.json +12 -10
  11. package/default-pages/my_notes/chat-history.json +1 -0
  12. package/default-pages/my_notes/page.html +115 -131
  13. package/default-pages/my_notes/page.json +14 -12
  14. package/default-pages/neon_asteroids/chat-history.json +1 -0
  15. package/default-pages/neon_asteroids/page.html +1777 -1803
  16. package/default-pages/neon_asteroids/page.json +14 -12
  17. package/default-pages/oregon_trail/chat-history.json +1 -0
  18. package/default-pages/oregon_trail/page.html +290 -307
  19. package/default-pages/oregon_trail/page.json +14 -12
  20. package/default-pages/solar_explorer/chat-history.json +1 -0
  21. package/default-pages/solar_explorer/page.html +1929 -1951
  22. package/default-pages/solar_explorer/page.json +14 -12
  23. package/default-pages/solar_tutorial/chat-history.json +1 -0
  24. package/default-pages/solar_tutorial/page.html +464 -478
  25. package/default-pages/solar_tutorial/page.json +12 -10
  26. package/default-pages/us_map/chat-history.json +1 -0
  27. package/default-pages/us_map/page.html +170 -193
  28. package/default-pages/us_map/page.json +14 -12
  29. package/default-pages/us_map/page.light.png +0 -0
  30. package/default-pages/us_map_1850/chat-history.json +1 -0
  31. package/default-pages/us_map_1850/page.html +302 -326
  32. package/default-pages/us_map_1850/page.json +14 -12
  33. package/default-pages/western_cities_1850/chat-history.json +1 -0
  34. package/default-pages/western_cities_1850/page.html +503 -527
  35. package/default-pages/western_cities_1850/page.json +14 -12
  36. package/default-themes/aurora-dawn.v3.css +15 -14
  37. package/default-themes/aurora-dusk.v3.css +26 -26
  38. package/default-themes/cosmos-dawn.v3.css +15 -14
  39. package/default-themes/cosmos-dusk.v3.css +26 -26
  40. package/default-themes/elemental-dawn.v3.css +200 -0
  41. package/default-themes/nebula-dawn.v3.css +15 -14
  42. package/default-themes/nebula-dusk.v3.css +24 -24
  43. package/default-themes/solar-flare-dawn.v3.css +15 -14
  44. package/default-themes/solar-flare-dusk.v3.css +26 -26
  45. package/dist/builders/anthropic.d.ts +26 -2
  46. package/dist/builders/anthropic.d.ts.map +1 -1
  47. package/dist/builders/anthropic.js +132 -31
  48. package/dist/builders/anthropic.js.map +1 -1
  49. package/dist/builders/claudecode.d.ts +13 -0
  50. package/dist/builders/claudecode.d.ts.map +1 -0
  51. package/dist/builders/claudecode.js +253 -0
  52. package/dist/builders/claudecode.js.map +1 -0
  53. package/dist/builders/index.d.ts +2 -1
  54. package/dist/builders/index.d.ts.map +1 -1
  55. package/dist/builders/index.js +8 -1
  56. package/dist/builders/index.js.map +1 -1
  57. package/dist/builders/openai.js +2 -1
  58. package/dist/builders/openai.js.map +1 -1
  59. package/dist/builders/types.d.ts +31 -7
  60. package/dist/builders/types.d.ts.map +1 -1
  61. package/dist/builders/types.js +60 -28
  62. package/dist/builders/types.js.map +1 -1
  63. package/dist/connectors/types.d.ts +8 -0
  64. package/dist/connectors/types.d.ts.map +1 -1
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +13 -6
  67. package/dist/init.js.map +1 -1
  68. package/dist/migrations.d.ts.map +1 -1
  69. package/dist/migrations.js +161 -14
  70. package/dist/migrations.js.map +1 -1
  71. package/dist/models/anthropic.d.ts +1 -0
  72. package/dist/models/anthropic.d.ts.map +1 -1
  73. package/dist/models/anthropic.js +129 -29
  74. package/dist/models/anthropic.js.map +1 -1
  75. package/dist/models/chainOfThought.d.ts.map +1 -1
  76. package/dist/models/chainOfThought.js +32 -19
  77. package/dist/models/chainOfThought.js.map +1 -1
  78. package/dist/models/index.d.ts +2 -2
  79. package/dist/models/index.d.ts.map +1 -1
  80. package/dist/models/index.js +2 -1
  81. package/dist/models/index.js.map +1 -1
  82. package/dist/models/providers.d.ts +1 -0
  83. package/dist/models/providers.d.ts.map +1 -1
  84. package/dist/models/providers.js +12 -4
  85. package/dist/models/providers.js.map +1 -1
  86. package/dist/models/types.d.ts +15 -1
  87. package/dist/models/types.d.ts.map +1 -1
  88. package/dist/models/types.js.map +1 -1
  89. package/dist/pages.d.ts +57 -8
  90. package/dist/pages.d.ts.map +1 -1
  91. package/dist/pages.js +258 -45
  92. package/dist/pages.js.map +1 -1
  93. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  94. package/dist/service/createCompletePrompt.js +5 -0
  95. package/dist/service/createCompletePrompt.js.map +1 -1
  96. package/dist/service/mediaCache.d.ts +36 -0
  97. package/dist/service/mediaCache.d.ts.map +1 -0
  98. package/dist/service/mediaCache.js +182 -0
  99. package/dist/service/mediaCache.js.map +1 -0
  100. package/dist/service/pageValidator.d.ts +25 -0
  101. package/dist/service/pageValidator.d.ts.map +1 -0
  102. package/dist/service/pageValidator.js +315 -0
  103. package/dist/service/pageValidator.js.map +1 -0
  104. package/dist/service/server.d.ts.map +1 -1
  105. package/dist/service/server.js +4 -0
  106. package/dist/service/server.js.map +1 -1
  107. package/dist/service/sharedTableSchema.d.ts +73 -0
  108. package/dist/service/sharedTableSchema.d.ts.map +1 -0
  109. package/dist/service/sharedTableSchema.js +206 -0
  110. package/dist/service/sharedTableSchema.js.map +1 -0
  111. package/dist/service/transformPage.d.ts +49 -11
  112. package/dist/service/transformPage.d.ts.map +1 -1
  113. package/dist/service/transformPage.js +354 -241
  114. package/dist/service/transformPage.js.map +1 -1
  115. package/dist/service/useApiRoutes.d.ts.map +1 -1
  116. package/dist/service/useApiRoutes.js +288 -34
  117. package/dist/service/useApiRoutes.js.map +1 -1
  118. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  119. package/dist/service/useConnectorRoutes.js +170 -32
  120. package/dist/service/useConnectorRoutes.js.map +1 -1
  121. package/dist/service/useDataRoutes.d.ts.map +1 -1
  122. package/dist/service/useDataRoutes.js +59 -2
  123. package/dist/service/useDataRoutes.js.map +1 -1
  124. package/dist/service/useExtractRoutes.d.ts +4 -0
  125. package/dist/service/useExtractRoutes.d.ts.map +1 -0
  126. package/dist/service/useExtractRoutes.js +304 -0
  127. package/dist/service/useExtractRoutes.js.map +1 -0
  128. package/dist/service/usePageRoutes.d.ts +17 -0
  129. package/dist/service/usePageRoutes.d.ts.map +1 -1
  130. package/dist/service/usePageRoutes.js +1385 -483
  131. package/dist/service/usePageRoutes.js.map +1 -1
  132. package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
  133. package/dist/service/useSharedDataRoutes.js +54 -2
  134. package/dist/service/useSharedDataRoutes.js.map +1 -1
  135. package/dist/settings.d.ts +27 -0
  136. package/dist/settings.d.ts.map +1 -1
  137. package/dist/settings.js +40 -1
  138. package/dist/settings.js.map +1 -1
  139. package/dist/themes.d.ts +0 -5
  140. package/dist/themes.d.ts.map +1 -1
  141. package/dist/themes.js +3 -95
  142. package/dist/themes.js.map +1 -1
  143. package/migration-rules/v2-to-v3.md +277 -119
  144. package/package.json +5 -1
  145. package/{default-pages/application → required-pages/_shell}/page.html +56 -42
  146. package/required-pages/_shell/page.json +14 -0
  147. package/required-pages/_starters/page.html +534 -0
  148. package/required-pages/_starters/page.json +12 -0
  149. package/required-pages/builder/page.html +353 -43
  150. package/required-pages/builder/page.json +12 -10
  151. package/required-pages/pages/page.html +697 -924
  152. package/required-pages/pages/page.json +12 -10
  153. package/required-pages/settings/page.html +1879 -1753
  154. package/required-pages/settings/page.json +12 -10
  155. package/required-pages/synthos_apis/page.html +834 -845
  156. package/required-pages/synthos_apis/page.json +12 -10
  157. package/required-pages/synthos_scripts/page.html +74 -88
  158. package/required-pages/synthos_scripts/page.json +12 -10
  159. package/scripts/append-instructions.py +90 -0
  160. package/scripts/audit-instructions.py +76 -0
  161. package/scripts/cleanup-shell-markup.mjs +112 -0
  162. package/service-connectors/buffer/connector.json +46 -0
  163. package/service-connectors/canva/connector.json +67 -0
  164. package/service-connectors/elevenlabs/connector.json +1 -1
  165. package/src/builders/anthropic.ts +150 -25
  166. package/src/builders/claudecode.ts +310 -0
  167. package/src/builders/index.ts +7 -1
  168. package/src/builders/openai.ts +2 -1
  169. package/src/builders/types.ts +93 -32
  170. package/src/connectors/types.ts +8 -0
  171. package/src/init.ts +13 -7
  172. package/src/migrations.ts +187 -16
  173. package/src/models/anthropic.ts +140 -30
  174. package/src/models/chainOfThought.ts +33 -18
  175. package/src/models/index.ts +2 -2
  176. package/src/models/providers.ts +10 -1
  177. package/src/models/types.ts +21 -1
  178. package/src/pages.ts +271 -35
  179. package/src/service/createCompletePrompt.ts +6 -0
  180. package/src/service/mediaCache.ts +206 -0
  181. package/src/service/pageValidator.ts +337 -0
  182. package/src/service/server.ts +4 -0
  183. package/src/service/sharedTableSchema.ts +236 -0
  184. package/src/service/transformPage.ts +370 -260
  185. package/src/service/useApiRoutes.ts +282 -32
  186. package/src/service/useConnectorRoutes.ts +189 -34
  187. package/src/service/useDataRoutes.ts +198 -116
  188. package/src/service/useExtractRoutes.ts +331 -0
  189. package/src/service/usePageRoutes.ts +1411 -394
  190. package/src/service/useSharedDataRoutes.ts +184 -109
  191. package/src/settings.ts +65 -0
  192. package/src/themes.ts +78 -180
  193. package/starters/blank_starter/chat-history.json +1 -0
  194. package/starters/blank_starter/page.dark.png +0 -0
  195. package/starters/blank_starter/page.html +47 -0
  196. package/starters/blank_starter/page.json +13 -0
  197. package/starters/blank_starter/page.light.png +0 -0
  198. package/starters/calculator_starter/chat-history.json +1 -0
  199. package/starters/calculator_starter/page.dark.png +0 -0
  200. package/starters/calculator_starter/page.html +232 -0
  201. package/starters/calculator_starter/page.json +13 -0
  202. package/starters/calculator_starter/page.light.png +0 -0
  203. package/starters/calendar_starter/chat-history.json +1 -0
  204. package/starters/calendar_starter/page.dark.png +0 -0
  205. package/starters/calendar_starter/page.html +495 -0
  206. package/starters/calendar_starter/page.json +13 -0
  207. package/starters/calendar_starter/page.light.png +0 -0
  208. package/starters/chat_starter/chat-history.json +1 -0
  209. package/starters/chat_starter/page.dark.png +0 -0
  210. package/starters/chat_starter/page.html +351 -0
  211. package/starters/chat_starter/page.json +13 -0
  212. package/starters/chat_starter/page.light.png +0 -0
  213. package/starters/checklist_starter/chat-history.json +1 -0
  214. package/starters/checklist_starter/page.dark.png +0 -0
  215. package/starters/checklist_starter/page.html +437 -0
  216. package/starters/checklist_starter/page.json +13 -0
  217. package/starters/checklist_starter/page.light.png +0 -0
  218. package/starters/dashboard_starter/chat-history.json +1 -0
  219. package/starters/dashboard_starter/page.dark.png +0 -0
  220. package/starters/dashboard_starter/page.html +195 -0
  221. package/starters/dashboard_starter/page.json +13 -0
  222. package/starters/dashboard_starter/page.light.png +0 -0
  223. package/starters/form_starter/chat-history.json +1 -0
  224. package/starters/form_starter/page.dark.png +0 -0
  225. package/starters/form_starter/page.html +313 -0
  226. package/starters/form_starter/page.json +13 -0
  227. package/starters/form_starter/page.light.png +0 -0
  228. package/starters/gallery_starter/chat-history.json +1 -0
  229. package/starters/gallery_starter/page.dark.png +0 -0
  230. package/starters/gallery_starter/page.html +418 -0
  231. package/starters/gallery_starter/page.json +13 -0
  232. package/starters/gallery_starter/page.light.png +0 -0
  233. package/starters/generator_starter/chat-history.json +1 -0
  234. package/starters/generator_starter/page.dark.png +0 -0
  235. package/starters/generator_starter/page.html +261 -0
  236. package/starters/generator_starter/page.json +13 -0
  237. package/starters/generator_starter/page.light.png +0 -0
  238. package/starters/index.html +538 -0
  239. package/starters/kanban_starter/chat-history.json +1 -0
  240. package/starters/kanban_starter/page.dark.png +0 -0
  241. package/starters/kanban_starter/page.html +432 -0
  242. package/starters/kanban_starter/page.json +13 -0
  243. package/starters/kanban_starter/page.light.png +0 -0
  244. package/starters/presentation_builder/chat-history.json +1 -0
  245. package/starters/presentation_builder/page.dark.png +0 -0
  246. package/starters/presentation_builder/page.html +970 -0
  247. package/starters/presentation_builder/page.json +15 -0
  248. package/starters/presentation_builder/page.light.png +0 -0
  249. package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
  250. package/starters/pulse_starter/chat-history.json +1 -0
  251. package/starters/pulse_starter/page.dark.png +0 -0
  252. package/starters/pulse_starter/page.html +698 -0
  253. package/starters/pulse_starter/page.json +13 -0
  254. package/starters/pulse_starter/page.light.png +0 -0
  255. package/starters/quiz_starter/chat-history.json +1 -0
  256. package/starters/quiz_starter/page.dark.png +0 -0
  257. package/starters/quiz_starter/page.html +292 -0
  258. package/starters/quiz_starter/page.json +13 -0
  259. package/starters/quiz_starter/page.light.png +0 -0
  260. package/starters/reference_starter/chat-history.json +1 -0
  261. package/starters/reference_starter/page.dark.png +0 -0
  262. package/starters/reference_starter/page.html +250 -0
  263. package/starters/reference_starter/page.json +13 -0
  264. package/starters/reference_starter/page.light.png +0 -0
  265. package/starters/retro_game_starter/chat-history.json +1 -0
  266. package/starters/retro_game_starter/page.dark.png +0 -0
  267. package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
  268. package/starters/retro_game_starter/page.json +15 -0
  269. package/starters/retro_game_starter/page.light.png +0 -0
  270. package/starters/roster_starter/chat-history.json +1 -0
  271. package/starters/roster_starter/page.dark.png +0 -0
  272. package/starters/roster_starter/page.html +600 -0
  273. package/starters/roster_starter/page.json +13 -0
  274. package/starters/roster_starter/page.light.png +0 -0
  275. package/starters/server.js +182 -0
  276. package/starters/start.cmd +1 -0
  277. package/starters/timeline_starter/chat-history.json +1 -0
  278. package/starters/timeline_starter/page.dark.png +0 -0
  279. package/starters/timeline_starter/page.html +446 -0
  280. package/starters/timeline_starter/page.json +13 -0
  281. package/starters/timeline_starter/page.light.png +0 -0
  282. package/starters/tutorial_starter/chat-history.json +1 -0
  283. package/starters/tutorial_starter/page.dark.png +0 -0
  284. package/starters/tutorial_starter/page.html +283 -0
  285. package/starters/tutorial_starter/page.json +13 -0
  286. package/starters/tutorial_starter/page.light.png +0 -0
  287. package/static-files/agent.v3.js +122 -0
  288. package/static-files/connector.v3.js +48 -0
  289. package/static-files/extract.v3.js +188 -0
  290. package/static-files/helpers.v3.js +50 -6
  291. package/static-files/page-bridge.js +114 -0
  292. package/static-files/page.v3.js +1292 -1290
  293. package/static-files/script.v3.js +32 -0
  294. package/static-files/server.v3.js +89 -0
  295. package/static-files/shell-bridge.v3.js +174 -0
  296. package/static-files/shell-modals.v3.js +521 -0
  297. package/static-files/{shell.css → shell.v3.css} +271 -22
  298. package/static-files/shell.v3.js +1865 -0
  299. package/static-files/storage.v3.js +176 -0
  300. package/tests/anthropic.spec.ts +42 -7
  301. package/tests/builders.spec.ts +70 -2
  302. package/tests/pageValidator.spec.ts +548 -0
  303. package/tests/profiles.spec.ts +122 -0
  304. package/tests/sharedTableSchema.spec.ts +242 -0
  305. package/tests/transformPage.spec.ts +62 -81
  306. package/default-pages/application/page.json +0 -10
  307. package/default-pages/retro_game_starter/page.json +0 -12
  308. package/default-pages/sidebar_page/page.html +0 -51
  309. package/default-pages/sidebar_page/page.json +0 -10
  310. package/default-pages/two-panel_page/page.html +0 -68
  311. package/default-pages/two-panel_page/page.json +0 -10
@@ -0,0 +1,337 @@
1
+ /**
2
+ * pageValidator.ts — Lightweight page validation for LLM-generated HTML.
3
+ *
4
+ * Catches common errors (syntax, missing DOM elements, bad synthos.* calls)
5
+ * before serving pages. No browser binary — static analysis only.
6
+ *
7
+ * Layers:
8
+ * 1. Script extraction + Acorn parse → syntax errors
9
+ * 2. DOM element ID inventory → missing element references
10
+ * 3. SynthOS API pattern checks → hallucinated method calls
11
+ */
12
+
13
+ import * as acorn from 'acorn';
14
+ import * as cheerio from 'cheerio';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export interface PageValidationError {
21
+ type: 'syntax-error' | 'missing-element' | 'unknown-api';
22
+ message: string;
23
+ source?: string; // 'inline-script-N'
24
+ line?: number;
25
+ col?: number;
26
+ }
27
+
28
+ export interface PageValidationResult {
29
+ valid: boolean;
30
+ errors: PageValidationError[];
31
+ durationMs: number;
32
+ }
33
+
34
+ interface ScriptBlock {
35
+ index: number;
36
+ source: string;
37
+ startLine: number;
38
+ isModule: boolean;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Known injected script IDs — skip these during validation
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const INJECTED_SCRIPT_IDS = new Set([
46
+ 'page-bridge', // legacy (replaced by shell-v3)
47
+ 'page-helpers', // legacy (replaced by v3 modules)
48
+ 'page-info',
49
+ 'page-script', // legacy shell script
50
+ 'synthos-error-capture', // legacy error capture
51
+ // V3 module scripts
52
+ 'shell-v3',
53
+ 'server-v3',
54
+ 'storage-v3',
55
+ 'script-v3',
56
+ 'connector-v3',
57
+ 'agent-v3',
58
+ ]);
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Known SynthOS API surface (from helpers.v3.js)
62
+ // ---------------------------------------------------------------------------
63
+
64
+ const KNOWN_SYNTHOS_METHODS = new Set([
65
+ // data
66
+ 'synthos.data',
67
+ 'synthos.data.list',
68
+ 'synthos.data.get',
69
+ 'synthos.data.save',
70
+ 'synthos.data.remove',
71
+ // files
72
+ 'synthos.files',
73
+ 'synthos.files.list',
74
+ 'synthos.files.upload',
75
+ 'synthos.files.url',
76
+ 'synthos.files.remove',
77
+ // shared.data
78
+ 'synthos.shared',
79
+ 'synthos.shared.data',
80
+ 'synthos.shared.data.list',
81
+ 'synthos.shared.data.get',
82
+ 'synthos.shared.data.save',
83
+ 'synthos.shared.data.remove',
84
+ // shared.files
85
+ 'synthos.shared.files',
86
+ 'synthos.shared.files.list',
87
+ 'synthos.shared.files.upload',
88
+ 'synthos.shared.files.url',
89
+ 'synthos.shared.files.remove',
90
+ // generate
91
+ 'synthos.generate',
92
+ 'synthos.generate.image',
93
+ 'synthos.generate.completion',
94
+ // script
95
+ 'synthos.script',
96
+ 'synthos.script.run',
97
+ // page
98
+ 'synthos.page',
99
+ 'synthos.page.list',
100
+ 'synthos.page.get',
101
+ 'synthos.page.update',
102
+ 'synthos.page.remove',
103
+ 'synthos.page.ask',
104
+ // search
105
+ 'synthos.search',
106
+ 'synthos.search.web',
107
+ // connector
108
+ 'synthos.connector',
109
+ 'synthos.connector.call',
110
+ 'synthos.connector.list',
111
+ // agent
112
+ 'synthos.agent',
113
+ 'synthos.agent.list',
114
+ 'synthos.agent.send',
115
+ 'synthos.agent.sendStream',
116
+ 'synthos.agent.chat',
117
+ 'synthos.agent.chat.send',
118
+ 'synthos.agent.chat.sendStream',
119
+ 'synthos.agent.chat.history',
120
+ 'synthos.agent.chat.abort',
121
+ 'synthos.agent.chat.clear',
122
+ 'synthos.agent.isEnabled',
123
+ 'synthos.agent.getCapabilities',
124
+ // shell
125
+ 'synthos.shell',
126
+ 'synthos.shell.navigate',
127
+ 'synthos.shell.submitChat',
128
+ 'synthos.shell.setDirty',
129
+ 'synthos.shell.showLoading',
130
+ 'synthos.shell.hideLoading',
131
+ 'synthos.shell.focusChat',
132
+ 'synthos.shell.openSaveModal',
133
+ 'synthos.shell.toggleBuilder',
134
+ 'synthos.shell.openBuilder',
135
+ 'synthos.shell.closeBuilder',
136
+ 'synthos.shell.on',
137
+ ]);
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Layer 1: Script Extraction + Acorn Parse
141
+ // ---------------------------------------------------------------------------
142
+
143
+ function estimateLineOffset(html: string, scriptHtml: string): number {
144
+ const idx = html.indexOf(scriptHtml);
145
+ if (idx === -1) return 0;
146
+ let line = 1;
147
+ for (let i = 0; i < idx; i++) {
148
+ if (html[i] === '\n') line++;
149
+ }
150
+ return line;
151
+ }
152
+
153
+ function extractScripts(html: string): ScriptBlock[] {
154
+ const $ = cheerio.load(html);
155
+ const scripts: ScriptBlock[] = [];
156
+ let index = 0;
157
+
158
+ $('script').each((_, el) => {
159
+ const $el = $(el);
160
+
161
+ // Skip external scripts (have src attribute)
162
+ if ($el.attr('src')) return;
163
+
164
+ // Skip known injected scripts
165
+ const id = $el.attr('id');
166
+ if (id && INJECTED_SCRIPT_IDS.has(id)) return;
167
+
168
+ const source = $el.text();
169
+ if (!source.trim()) return;
170
+
171
+ scripts.push({
172
+ index: index++,
173
+ source,
174
+ startLine: estimateLineOffset(html, $.html(el)!),
175
+ isModule: $el.attr('type') === 'module',
176
+ });
177
+ });
178
+
179
+ return scripts;
180
+ }
181
+
182
+ function parseSyntax(script: ScriptBlock): PageValidationError | null {
183
+ try {
184
+ acorn.parse(script.source, {
185
+ ecmaVersion: 'latest' as any,
186
+ sourceType: script.isModule ? 'module' : 'script',
187
+ allowAwaitOutsideFunction: true,
188
+ });
189
+ return null;
190
+ } catch (err: unknown) {
191
+ const e = err as { message?: string; loc?: { line: number; column: number } };
192
+ return {
193
+ type: 'syntax-error',
194
+ message: e.message ?? 'Unknown syntax error',
195
+ source: `inline-script-${script.index}`,
196
+ line: e.loc ? script.startLine + e.loc.line : undefined,
197
+ col: e.loc?.column,
198
+ };
199
+ }
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Layer 2: DOM Element ID Inventory
204
+ // ---------------------------------------------------------------------------
205
+
206
+ function buildIdInventory(html: string): Set<string> {
207
+ const $ = cheerio.load(html);
208
+ const ids = new Set<string>();
209
+ $('[id]').each((_, el) => {
210
+ const id = $(el).attr('id');
211
+ if (id) ids.add(id);
212
+ });
213
+ return ids;
214
+ }
215
+
216
+ /**
217
+ * Patterns that CREATE element IDs inside script source text. Scripts that
218
+ * build DOM via `innerHTML = \`...<input id="btnSaveAll">...\`` or assign
219
+ * `el.id = 'foo'` produce IDs at runtime that `buildIdInventory` misses.
220
+ * Seeding those IDs as "known" prevents false-positive missing-element
221
+ * errors for dynamically constructed UI.
222
+ */
223
+ const ID_CREATE_PATTERNS = [
224
+ // id="foo" / id='foo' in string literals (innerHTML templates, etc.)
225
+ /\bid\s*=\s*["']([a-zA-Z_][\w-]*)["']/g,
226
+ // el.id = 'foo' / obj.id = "foo"
227
+ /\.id\s*=\s*["']([a-zA-Z_][\w-]*)["']/g,
228
+ ];
229
+
230
+ function collectDynamicIds(scripts: ScriptBlock[]): Set<string> {
231
+ const ids = new Set<string>();
232
+ for (const script of scripts) {
233
+ for (const pattern of ID_CREATE_PATTERNS) {
234
+ pattern.lastIndex = 0;
235
+ let match;
236
+ while ((match = pattern.exec(script.source)) !== null) {
237
+ ids.add(match[1]);
238
+ }
239
+ }
240
+ }
241
+ return ids;
242
+ }
243
+
244
+ /** Patterns that reference element IDs in JS code. */
245
+ const ID_REF_PATTERNS = [
246
+ /getElementById\(\s*['"]([^'"]+)['"]\s*\)/g,
247
+ /querySelector(?:All)?\(\s*['"]#([^'"]+)['"]\s*\)/g,
248
+ /\$\(\s*['"]#([^'"]+)['"]\s*\)/g,
249
+ ];
250
+
251
+ function checkIdReferences(scripts: ScriptBlock[], ids: Set<string>): PageValidationError[] {
252
+ const errors: PageValidationError[] = [];
253
+
254
+ for (const script of scripts) {
255
+ for (const pattern of ID_REF_PATTERNS) {
256
+ pattern.lastIndex = 0;
257
+ let match;
258
+ while ((match = pattern.exec(script.source)) !== null) {
259
+ const refId = match[1];
260
+ if (!ids.has(refId)) {
261
+ errors.push({
262
+ type: 'missing-element',
263
+ message: `Script references element '#${refId}' which does not exist in the page`,
264
+ source: `inline-script-${script.index}`,
265
+ });
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ return errors;
272
+ }
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // Layer 3: SynthOS API Pattern Checks
276
+ // ---------------------------------------------------------------------------
277
+
278
+ const SYNTHOS_CALL_PATTERN = /synthos(?:\.[\w]+)+/g;
279
+
280
+ function checkSynthosApi(scripts: ScriptBlock[]): PageValidationError[] {
281
+ const errors: PageValidationError[] = [];
282
+ const seen = new Set<string>();
283
+
284
+ for (const script of scripts) {
285
+ SYNTHOS_CALL_PATTERN.lastIndex = 0;
286
+ let match;
287
+ while ((match = SYNTHOS_CALL_PATTERN.exec(script.source)) !== null) {
288
+ const call = match[0];
289
+ // Deduplicate within a single validation run
290
+ if (seen.has(call)) continue;
291
+
292
+ if (!KNOWN_SYNTHOS_METHODS.has(call)) {
293
+ seen.add(call);
294
+ errors.push({
295
+ type: 'unknown-api',
296
+ message: `Unknown SynthOS API call: ${call}`,
297
+ source: `inline-script-${script.index}`,
298
+ });
299
+ }
300
+ }
301
+ }
302
+
303
+ return errors;
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Public API
308
+ // ---------------------------------------------------------------------------
309
+
310
+ export function validatePage(html: string): PageValidationResult {
311
+ const start = Date.now();
312
+ const errors: PageValidationError[] = [];
313
+
314
+ // Layer 1: Extract and parse scripts
315
+ const scripts = extractScripts(html);
316
+ for (const script of scripts) {
317
+ const err = parseSyntax(script);
318
+ if (err) errors.push(err);
319
+ }
320
+
321
+ // Layer 2: DOM ID reference checks (only if no syntax errors)
322
+ if (errors.length === 0) {
323
+ const ids = buildIdInventory(html);
324
+ const dynamicIds = collectDynamicIds(scripts);
325
+ for (const id of dynamicIds) ids.add(id);
326
+ errors.push(...checkIdReferences(scripts, ids));
327
+ }
328
+
329
+ // Layer 3: SynthOS API pattern checks (always runs)
330
+ errors.push(...checkSynthosApi(scripts));
331
+
332
+ return {
333
+ valid: errors.length === 0,
334
+ errors,
335
+ durationMs: Date.now() - start,
336
+ };
337
+ }
@@ -4,6 +4,7 @@ import { useApiRoutes } from './useApiRoutes';
4
4
  import { SynthOSConfig } from '../init';
5
5
  import { useDataRoutes } from './useDataRoutes';
6
6
  import { useFileRoutes } from './useFileRoutes';
7
+ import { useExtractRoutes } from './useExtractRoutes';
7
8
  import { useSharedDataRoutes } from './useSharedDataRoutes';
8
9
  import { useSharedFileRoutes } from './useSharedFileRoutes';
9
10
  import { useConnectorRoutes } from './useConnectorRoutes';
@@ -59,6 +60,9 @@ export function server(config: SynthOSConfig, customizer: Customizer = defaultCu
59
60
  // File routes
60
61
  if (customizer.isEnabled('files')) useFileRoutes(config, app);
61
62
 
63
+ // Extract routes — schema-typed file extraction via active chat model
64
+ if (customizer.isEnabled('extract')) useExtractRoutes(config, app);
65
+
62
66
  // Shared data routes
63
67
  if (customizer.isEnabled('shared-data')) useSharedDataRoutes(config, app);
64
68
 
@@ -0,0 +1,236 @@
1
+ import path from 'path';
2
+ import { SynthOSConfig } from '../init';
3
+
4
+ /**
5
+ * Schema sidecar IO + additive-merge logic. Used by both shared-table and
6
+ * per-page-table routes — the only difference is the parent folder where the
7
+ * `<table>.schema.json` file lives.
8
+ *
9
+ * Spec: docs/specs/shared-tables-schema-sidecar.md
10
+ */
11
+
12
+ export interface SchemaWrapper {
13
+ version: number;
14
+ schema: Record<string, unknown>;
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ definedBy?: string;
18
+ }
19
+
20
+ export type MergeMode = 'additive' | 'replace';
21
+
22
+ export interface MergeConflict {
23
+ field: string;
24
+ existing: unknown;
25
+ incoming: unknown;
26
+ }
27
+
28
+ const SCHEMA_VERSION = 1;
29
+
30
+ /** Sidecar file path: `<parent>/<table>.schema.json`. */
31
+ export function schemaFile(parent: string, table: string): string {
32
+ return path.join(parent, `${table}.schema.json`);
33
+ }
34
+
35
+ /** Load the wrapper. Returns undefined if missing or unparsable. */
36
+ export async function loadSchema(config: SynthOSConfig, parent: string, table: string): Promise<SchemaWrapper | undefined> {
37
+ const sp = config.storageProvider;
38
+ const file = schemaFile(parent, table);
39
+ if (!await sp.checkIfExists(file)) return undefined;
40
+ try {
41
+ const parsed = JSON.parse(await sp.loadFile(file));
42
+ if (parsed && typeof parsed === 'object' && parsed.schema && typeof parsed.schema === 'object') {
43
+ return parsed as SchemaWrapper;
44
+ }
45
+ } catch {
46
+ // fall through
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ /** Persist a wrapper to disk. */
52
+ export async function saveSchema(config: SynthOSConfig, parent: string, table: string, wrapper: SchemaWrapper): Promise<void> {
53
+ const sp = config.storageProvider;
54
+ await sp.ensureFolderExists(parent);
55
+ await sp.saveFile(schemaFile(parent, table), JSON.stringify(wrapper, null, 4));
56
+ }
57
+
58
+ /** Delete the schema sidecar. No-op if it doesn't exist. */
59
+ export async function deleteSchema(config: SynthOSConfig, parent: string, table: string): Promise<void> {
60
+ const sp = config.storageProvider;
61
+ const file = schemaFile(parent, table);
62
+ if (await sp.checkIfExists(file)) {
63
+ await sp.deleteFile(file);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Apply `incoming` against `existing` per `mode`:
69
+ * - 'replace' → drop existing entirely; incoming wins.
70
+ * - 'additive' → union of `properties` (existing wins on overlap if types match;
71
+ * conflicting types reported via `conflicts`); union of `required`.
72
+ *
73
+ * Returns the merged schema and any conflicts. On conflict, callers should
74
+ * surface them as a 409 and NOT persist the result.
75
+ */
76
+ export function mergeSchema(
77
+ existing: Record<string, unknown> | undefined,
78
+ incoming: Record<string, unknown>,
79
+ mode: MergeMode,
80
+ ): { merged: Record<string, unknown>; conflicts: MergeConflict[] } {
81
+ if (mode === 'replace' || !existing) {
82
+ return { merged: incoming, conflicts: [] };
83
+ }
84
+
85
+ const conflicts: MergeConflict[] = [];
86
+
87
+ const existingProps = isPlainObject(existing.properties) ? existing.properties as Record<string, unknown> : {};
88
+ const incomingProps = isPlainObject(incoming.properties) ? incoming.properties as Record<string, unknown> : {};
89
+ const mergedProps: Record<string, unknown> = { ...existingProps };
90
+
91
+ for (const [field, def] of Object.entries(incomingProps)) {
92
+ if (!(field in existingProps)) {
93
+ mergedProps[field] = def;
94
+ continue;
95
+ }
96
+ const existingDef = existingProps[field];
97
+ if (typesAreCompatible(existingDef, def)) {
98
+ // Existing wins on overlap (preserves enum/required/format choices).
99
+ continue;
100
+ }
101
+ conflicts.push({ field, existing: existingDef, incoming: def });
102
+ }
103
+
104
+ if (conflicts.length > 0) {
105
+ return { merged: existing, conflicts };
106
+ }
107
+
108
+ const existingRequired = Array.isArray(existing.required) ? existing.required as unknown[] : [];
109
+ const incomingRequired = Array.isArray(incoming.required) ? incoming.required as unknown[] : [];
110
+ const mergedRequired = Array.from(new Set([...existingRequired, ...incomingRequired]
111
+ .filter(r => typeof r === 'string')));
112
+
113
+ const merged: Record<string, unknown> = {
114
+ ...existing,
115
+ properties: mergedProps,
116
+ };
117
+ if (mergedRequired.length > 0) {
118
+ merged.required = mergedRequired;
119
+ }
120
+ return { merged, conflicts: [] };
121
+ }
122
+
123
+ /**
124
+ * Validate the structural shape of an incoming JSON Schema. Strict-mode
125
+ * validation against the meta-schema is out of scope; we only check that the
126
+ * payload is a plain object so we don't persist garbage.
127
+ */
128
+ export function isValidSchemaPayload(s: unknown): s is Record<string, unknown> {
129
+ return isPlainObject(s);
130
+ }
131
+
132
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
133
+ return !!v && typeof v === 'object' && !Array.isArray(v);
134
+ }
135
+
136
+ /**
137
+ * Type-compatibility check used by additive merge. Two definitions are
138
+ * compatible when their `type` fields match (or both are absent). We
139
+ * intentionally tolerate other field differences (description, enum widening,
140
+ * format changes) — incoming-vs-existing field-level diffs aren't surfaced
141
+ * as conflicts in v1; only top-level type mismatches block the merge.
142
+ */
143
+ function typesAreCompatible(existing: unknown, incoming: unknown): boolean {
144
+ if (!isPlainObject(existing) || !isPlainObject(incoming)) return existing === incoming;
145
+ const et = existing.type;
146
+ const it = incoming.type;
147
+ if (et === undefined && it === undefined) return true;
148
+ if (et === undefined || it === undefined) return true;
149
+ if (Array.isArray(et) || Array.isArray(it)) {
150
+ const ea = Array.isArray(et) ? et : [et];
151
+ const ia = Array.isArray(it) ? it : [it];
152
+ return ea.some(x => ia.includes(x));
153
+ }
154
+ return et === it;
155
+ }
156
+
157
+ /**
158
+ * Build a SchemaWrapper from a plain JSON Schema, using `now` for both
159
+ * timestamps when no existing wrapper is being preserved.
160
+ */
161
+ export function newSchemaWrapper(schema: Record<string, unknown>, now: string, definedBy?: string): SchemaWrapper {
162
+ const wrapper: SchemaWrapper = {
163
+ version: SCHEMA_VERSION,
164
+ schema,
165
+ createdAt: now,
166
+ updatedAt: now,
167
+ };
168
+ if (definedBy) wrapper.definedBy = definedBy;
169
+ return wrapper;
170
+ }
171
+
172
+ /**
173
+ * Update an existing wrapper, preserving createdAt and bumping updatedAt.
174
+ */
175
+ export function updateSchemaWrapper(
176
+ existing: SchemaWrapper,
177
+ schema: Record<string, unknown>,
178
+ now: string,
179
+ definedBy?: string,
180
+ ): SchemaWrapper {
181
+ const wrapper: SchemaWrapper = {
182
+ version: existing.version || SCHEMA_VERSION,
183
+ schema,
184
+ createdAt: existing.createdAt,
185
+ updatedAt: now,
186
+ };
187
+ if (definedBy) wrapper.definedBy = definedBy;
188
+ else if (existing.definedBy) wrapper.definedBy = existing.definedBy;
189
+ return wrapper;
190
+ }
191
+
192
+ /**
193
+ * Enumerate tables in a namespace folder. A "table" is either:
194
+ * - a subfolder containing record files, OR
195
+ * - a `<name>.schema.json` sidecar (table may be schemaless until first save).
196
+ *
197
+ * Returns one entry per unique table name, with hasSchema + recordCount.
198
+ *
199
+ * `reserved` filters out names that occupy the namespace for non-table use
200
+ * (e.g. `files`, the per-page uploads folder).
201
+ */
202
+ export async function listTables(
203
+ config: SynthOSConfig,
204
+ parent: string,
205
+ reserved: ReadonlySet<string> = new Set(),
206
+ ): Promise<Array<{ name: string; hasSchema: boolean; recordCount: number }>> {
207
+ const sp = config.storageProvider;
208
+ if (!await sp.checkIfExists(parent)) return [];
209
+
210
+ const folders = await sp.listFolders(parent);
211
+ const files = await sp.listFiles(parent);
212
+
213
+ const known = new Map<string, { hasSchema: boolean; recordCount: number }>();
214
+ for (const folder of folders) {
215
+ if (reserved.has(folder)) continue;
216
+ const recs = (await sp.listFiles(path.join(parent, folder)))
217
+ .filter(f => f.endsWith('.json')).length;
218
+ known.set(folder, { hasSchema: false, recordCount: recs });
219
+ }
220
+ for (const file of files) {
221
+ const m = file.match(/^(.+)\.schema\.json$/);
222
+ if (!m) continue;
223
+ const name = m[1];
224
+ if (reserved.has(name)) continue;
225
+ const existing = known.get(name);
226
+ if (existing) {
227
+ existing.hasSchema = true;
228
+ } else {
229
+ known.set(name, { hasSchema: true, recordCount: 0 });
230
+ }
231
+ }
232
+
233
+ return Array.from(known.entries())
234
+ .map(([name, info]) => ({ name, ...info }))
235
+ .sort((a, b) => a.name.localeCompare(b.name));
236
+ }