synthos 0.10.1 → 0.11.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 (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 +285 -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 +1388 -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 +1888 -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 +283 -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 +1414 -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,242 @@
1
+ import assert from 'assert';
2
+ import * as fs from 'fs/promises';
3
+ import * as os from 'os';
4
+ import path from 'path';
5
+ import {
6
+ deleteSchema,
7
+ isValidSchemaPayload,
8
+ listTables,
9
+ loadSchema,
10
+ mergeSchema,
11
+ newSchemaWrapper,
12
+ saveSchema,
13
+ schemaFile,
14
+ updateSchemaWrapper,
15
+ } from '../src/service/sharedTableSchema';
16
+ import { SynthOSConfig } from '../src/init';
17
+ import { FsStorageProvider } from '../src/storage';
18
+
19
+ function makeConfig(pagesFolder: string): SynthOSConfig {
20
+ return {
21
+ localFolder: '.synthos',
22
+ pagesFolder,
23
+ requiredPagesFolders: [],
24
+ defaultPagesFolders: [],
25
+ defaultScriptsFolders: [],
26
+ defaultThemesFolders: [],
27
+ staticFilesFolders: [],
28
+ serviceConnectorsFolders: [],
29
+ requiredPages: [],
30
+ storageProvider: new FsStorageProvider(),
31
+ debug: false,
32
+ debugPageUpdates: false,
33
+ };
34
+ }
35
+
36
+ const SAMPLE_SCHEMA: Record<string, unknown> = {
37
+ type: 'object',
38
+ properties: {
39
+ first_name: { type: 'string' },
40
+ last_name: { type: 'string' },
41
+ },
42
+ required: ['first_name'],
43
+ };
44
+
45
+ describe('sharedTableSchema — pure helpers', () => {
46
+ describe('schemaFile', () => {
47
+ it('returns <parent>/<table>.schema.json', () => {
48
+ assert.strictEqual(schemaFile('/p/shared', 'employees'), path.join('/p/shared', 'employees.schema.json'));
49
+ });
50
+ });
51
+
52
+ describe('isValidSchemaPayload', () => {
53
+ it('accepts plain objects', () => {
54
+ assert.strictEqual(isValidSchemaPayload({ type: 'object' }), true);
55
+ });
56
+ it('rejects arrays, null, primitives', () => {
57
+ assert.strictEqual(isValidSchemaPayload([]), false);
58
+ assert.strictEqual(isValidSchemaPayload(null), false);
59
+ assert.strictEqual(isValidSchemaPayload('schema'), false);
60
+ assert.strictEqual(isValidSchemaPayload(42), false);
61
+ });
62
+ });
63
+
64
+ describe('newSchemaWrapper', () => {
65
+ it('initializes version, timestamps, schema, definedBy', () => {
66
+ const w = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z', 'builder');
67
+ assert.strictEqual(w.version, 1);
68
+ assert.strictEqual(w.createdAt, '2026-04-25T00:00:00.000Z');
69
+ assert.strictEqual(w.updatedAt, '2026-04-25T00:00:00.000Z');
70
+ assert.strictEqual(w.definedBy, 'builder');
71
+ assert.deepStrictEqual(w.schema, SAMPLE_SCHEMA);
72
+ });
73
+ it('omits definedBy when not provided', () => {
74
+ const w = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z');
75
+ assert.strictEqual(w.definedBy, undefined);
76
+ });
77
+ });
78
+
79
+ describe('updateSchemaWrapper', () => {
80
+ it('preserves createdAt, advances updatedAt', () => {
81
+ const orig = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z', 'page-a');
82
+ const updated = updateSchemaWrapper(orig, { type: 'object' }, '2026-04-26T00:00:00.000Z', 'page-b');
83
+ assert.strictEqual(updated.createdAt, '2026-04-25T00:00:00.000Z');
84
+ assert.strictEqual(updated.updatedAt, '2026-04-26T00:00:00.000Z');
85
+ assert.strictEqual(updated.definedBy, 'page-b');
86
+ });
87
+ it('keeps existing definedBy when no override given', () => {
88
+ const orig = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z', 'page-a');
89
+ const updated = updateSchemaWrapper(orig, { type: 'object' }, '2026-04-26T00:00:00.000Z');
90
+ assert.strictEqual(updated.definedBy, 'page-a');
91
+ });
92
+ });
93
+
94
+ describe('mergeSchema', () => {
95
+ it('replace mode wins outright', () => {
96
+ const { merged, conflicts } = mergeSchema(SAMPLE_SCHEMA, { type: 'object', properties: { name: { type: 'string' } } }, 'replace');
97
+ assert.deepStrictEqual(conflicts, []);
98
+ assert.deepStrictEqual(merged, { type: 'object', properties: { name: { type: 'string' } } });
99
+ });
100
+ it('additive: adds new fields, keeps existing', () => {
101
+ const incoming: Record<string, unknown> = {
102
+ type: 'object',
103
+ properties: {
104
+ phone: { type: 'string' },
105
+ },
106
+ required: ['phone'],
107
+ };
108
+ const { merged, conflicts } = mergeSchema(SAMPLE_SCHEMA, incoming, 'additive');
109
+ assert.deepStrictEqual(conflicts, []);
110
+ const props = (merged.properties as Record<string, unknown>);
111
+ assert.ok(props.first_name);
112
+ assert.ok(props.last_name);
113
+ assert.ok(props.phone);
114
+ assert.deepStrictEqual(merged.required, ['first_name', 'phone']);
115
+ });
116
+ it('additive: existing wins on overlap when types match', () => {
117
+ const incoming: Record<string, unknown> = {
118
+ type: 'object',
119
+ properties: {
120
+ first_name: { type: 'string', description: 'changed' },
121
+ },
122
+ };
123
+ const { merged, conflicts } = mergeSchema(SAMPLE_SCHEMA, incoming, 'additive');
124
+ assert.deepStrictEqual(conflicts, []);
125
+ const fn = (merged.properties as Record<string, any>).first_name;
126
+ assert.strictEqual(fn.description, undefined, 'existing definition should be preserved');
127
+ });
128
+ it('additive: type conflicts produce 409-style result with merged === existing', () => {
129
+ const incoming: Record<string, unknown> = {
130
+ type: 'object',
131
+ properties: {
132
+ first_name: { type: 'number' },
133
+ },
134
+ };
135
+ const { merged, conflicts } = mergeSchema(SAMPLE_SCHEMA, incoming, 'additive');
136
+ assert.strictEqual(conflicts.length, 1);
137
+ assert.strictEqual(conflicts[0].field, 'first_name');
138
+ assert.strictEqual(merged, SAMPLE_SCHEMA);
139
+ });
140
+ it('additive: undefined existing returns incoming as-is', () => {
141
+ const incoming: Record<string, unknown> = { type: 'object', properties: { x: { type: 'string' } } };
142
+ const { merged, conflicts } = mergeSchema(undefined, incoming, 'additive');
143
+ assert.deepStrictEqual(conflicts, []);
144
+ assert.deepStrictEqual(merged, incoming);
145
+ });
146
+ });
147
+ });
148
+
149
+ describe('sharedTableSchema — IO + listing', () => {
150
+ let tmpDir: string;
151
+ let config: SynthOSConfig;
152
+ let parent: string;
153
+
154
+ beforeEach(async () => {
155
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'synthos-schema-test-'));
156
+ parent = path.join(tmpDir, 'shared');
157
+ config = makeConfig(tmpDir);
158
+ await fs.mkdir(parent, { recursive: true });
159
+ });
160
+
161
+ afterEach(async () => {
162
+ await fs.rm(tmpDir, { recursive: true, force: true });
163
+ });
164
+
165
+ it('saveSchema + loadSchema roundtrip', async () => {
166
+ const wrapper = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z', 'builder');
167
+ await saveSchema(config, parent, 'employees', wrapper);
168
+ const loaded = await loadSchema(config, parent, 'employees');
169
+ assert.deepStrictEqual(loaded, wrapper);
170
+ });
171
+
172
+ it('loadSchema returns undefined for missing sidecar', async () => {
173
+ const loaded = await loadSchema(config, parent, 'ghost');
174
+ assert.strictEqual(loaded, undefined);
175
+ });
176
+
177
+ it('loadSchema returns undefined for malformed sidecar', async () => {
178
+ await fs.writeFile(path.join(parent, 'bad.schema.json'), 'not-json');
179
+ const loaded = await loadSchema(config, parent, 'bad');
180
+ assert.strictEqual(loaded, undefined);
181
+ });
182
+
183
+ it('deleteSchema removes the sidecar; idempotent on missing', async () => {
184
+ const wrapper = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z');
185
+ await saveSchema(config, parent, 'employees', wrapper);
186
+ await deleteSchema(config, parent, 'employees');
187
+ assert.strictEqual(await loadSchema(config, parent, 'employees'), undefined);
188
+ // Second delete should not throw
189
+ await deleteSchema(config, parent, 'employees');
190
+ });
191
+
192
+ describe('listTables', () => {
193
+ it('lists subfolder tables with record count', async () => {
194
+ const empFolder = path.join(parent, 'employees');
195
+ await fs.mkdir(empFolder, { recursive: true });
196
+ await fs.writeFile(path.join(empFolder, 'a.json'), '{}');
197
+ await fs.writeFile(path.join(empFolder, 'b.json'), '{}');
198
+
199
+ const tables = await listTables(config, parent);
200
+ assert.deepStrictEqual(tables, [
201
+ { name: 'employees', hasSchema: false, recordCount: 2 },
202
+ ]);
203
+ });
204
+
205
+ it('lists schema-only tables (no records yet)', async () => {
206
+ const wrapper = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z');
207
+ await saveSchema(config, parent, 'locations', wrapper);
208
+ const tables = await listTables(config, parent);
209
+ assert.deepStrictEqual(tables, [
210
+ { name: 'locations', hasSchema: true, recordCount: 0 },
211
+ ]);
212
+ });
213
+
214
+ it('combines records + schema entries', async () => {
215
+ const empFolder = path.join(parent, 'employees');
216
+ await fs.mkdir(empFolder, { recursive: true });
217
+ await fs.writeFile(path.join(empFolder, 'a.json'), '{}');
218
+ const wrapper = newSchemaWrapper(SAMPLE_SCHEMA, '2026-04-25T00:00:00.000Z');
219
+ await saveSchema(config, parent, 'employees', wrapper);
220
+ const tables = await listTables(config, parent);
221
+ assert.deepStrictEqual(tables, [
222
+ { name: 'employees', hasSchema: true, recordCount: 1 },
223
+ ]);
224
+ });
225
+
226
+ it('respects reserved set', async () => {
227
+ const filesFolder = path.join(parent, 'files');
228
+ await fs.mkdir(filesFolder, { recursive: true });
229
+ const empFolder = path.join(parent, 'employees');
230
+ await fs.mkdir(empFolder, { recursive: true });
231
+
232
+ const tables = await listTables(config, parent, new Set(['files']));
233
+ assert.strictEqual(tables.length, 1);
234
+ assert.strictEqual(tables[0].name, 'employees');
235
+ });
236
+
237
+ it('returns [] for missing namespace', async () => {
238
+ const tables = await listTables(config, path.join(tmpDir, 'doesnotexist'));
239
+ assert.deepStrictEqual(tables, []);
240
+ });
241
+ });
242
+ });
@@ -7,7 +7,6 @@ import {
7
7
  stripNodeIds,
8
8
  applyChangeList,
9
9
  parseChangeList,
10
- injectError,
11
10
  deduplicateInlineScripts,
12
11
  normalizedIndexOf,
13
12
  transformPage,
@@ -21,13 +20,13 @@ import { Builder, BuilderResult, ContextSection } from '../src/builders/types';
21
20
  // ---------------------------------------------------------------------------
22
21
 
23
22
  describe('assignNodeIds', () => {
24
- it('assigns sequential data-node-id to every element', () => {
23
+ it('assigns sequential data-nid to every element', () => {
25
24
  const html = '<html><head></head><body><div><p>Hello</p></div></body></html>';
26
25
  const { html: result, nodeCount } = assignNodeIds(html);
27
- assert.ok(result.includes('data-node-id="0"'));
26
+ assert.ok(result.includes('data-nid="0"'));
28
27
  assert.ok(nodeCount > 0);
29
28
  // Every tag should have an id — count occurrences
30
- const ids = result.match(/data-node-id="/g);
29
+ const ids = result.match(/data-nid="/g);
31
30
  assert.strictEqual(ids?.length, nodeCount);
32
31
  });
33
32
 
@@ -38,16 +37,16 @@ describe('assignNodeIds', () => {
38
37
  assert.ok(nodeCount >= 4); // html, head, body, div, span, span = 6
39
38
  });
40
39
 
41
- it('assigns data-node-id to script and style elements', () => {
40
+ it('assigns data-nid to script and style elements', () => {
42
41
  const html = '<html><head><style>.a{color:red}</style><script>var x=1;</script></head><body><script src="/app.js"></script></body></html>';
43
42
  const { html: result, nodeCount } = assignNodeIds(html);
44
43
  // All 6 elements should have ids: html, head, style, script(inline), body, script(src)
45
- const ids = result.match(/data-node-id="/g);
44
+ const ids = result.match(/data-nid="/g);
46
45
  assert.strictEqual(ids?.length, nodeCount);
47
46
  assert.strictEqual(nodeCount, 6);
48
47
  // Verify the style and script tags specifically got ids
49
- assert.ok(result.match(/<style[^>]+data-node-id="/), 'style element should have data-node-id');
50
- assert.ok(result.match(/<script[^>]+data-node-id="/), 'script element should have data-node-id');
48
+ assert.ok(result.match(/<style[^>]+data-nid="/), 'style element should have data-nid');
49
+ assert.ok(result.match(/<script[^>]+data-nid="/), 'script element should have data-nid');
51
50
  });
52
51
  });
53
52
 
@@ -56,10 +55,10 @@ describe('assignNodeIds', () => {
56
55
  // ---------------------------------------------------------------------------
57
56
 
58
57
  describe('stripNodeIds', () => {
59
- it('removes all data-node-id attributes', () => {
60
- const html = '<div data-node-id="0"><p data-node-id="1">Hi</p></div>';
58
+ it('removes all data-nid attributes', () => {
59
+ const html = '<div data-nid="0"><p data-nid="1">Hi</p></div>';
61
60
  const result = stripNodeIds(html);
62
- assert.ok(!result.includes('data-node-id'));
61
+ assert.ok(!result.includes('data-nid'));
63
62
  assert.ok(result.includes('<p>Hi</p>'));
64
63
  });
65
64
  });
@@ -69,12 +68,12 @@ describe('stripNodeIds', () => {
69
68
  // ---------------------------------------------------------------------------
70
69
 
71
70
  describe('assignNodeIds -> stripNodeIds roundtrip', () => {
72
- it('produces HTML without data-node-id attributes', () => {
71
+ it('produces HTML without data-nid attributes', () => {
73
72
  const original = '<html><head></head><body><div><p>Hello</p></div></body></html>';
74
73
  const { html: annotated } = assignNodeIds(original);
75
- assert.ok(annotated.includes('data-node-id'));
74
+ assert.ok(annotated.includes('data-nid'));
76
75
  const stripped = stripNodeIds(annotated);
77
- assert.ok(!stripped.includes('data-node-id'));
76
+ assert.ok(!stripped.includes('data-nid'));
78
77
  assert.ok(stripped.includes('<p>Hello</p>'));
79
78
  });
80
79
  });
@@ -86,7 +85,7 @@ describe('assignNodeIds -> stripNodeIds roundtrip', () => {
86
85
  describe('applyChangeList', () => {
87
86
  // Helper: wrap content in a minimal annotated structure
88
87
  const annotated = '<html><head></head><body>' +
89
- '<div data-node-id="10"><p data-node-id="11">Old text</p></div>' +
88
+ '<div data-nid="10"><p data-nid="11">Old text</p></div>' +
90
89
  '</body></html>';
91
90
 
92
91
  it('applies "update" — replaces innerHTML', () => {
@@ -94,7 +93,7 @@ describe('applyChangeList', () => {
94
93
  { op: 'update', nodeId: '11', html: 'New text' },
95
94
  ];
96
95
  const result = applyChangeList(annotated, changes);
97
- assert.ok(result.includes('<p data-node-id="11">New text</p>'));
96
+ assert.ok(result.includes('<p data-nid="11">New text</p>'));
98
97
  });
99
98
 
100
99
  it('applies "replace" — replaces outerHTML', () => {
@@ -103,7 +102,7 @@ describe('applyChangeList', () => {
103
102
  ];
104
103
  const result = applyChangeList(annotated, changes);
105
104
  assert.ok(result.includes('<span>Replaced</span>'));
106
- assert.ok(!result.includes('data-node-id="11"'));
105
+ assert.ok(!result.includes('data-nid="11"'));
107
106
  });
108
107
 
109
108
  it('applies "delete" — removes element', () => {
@@ -111,7 +110,7 @@ describe('applyChangeList', () => {
111
110
  { op: 'delete', nodeId: '11' },
112
111
  ];
113
112
  const result = applyChangeList(annotated, changes);
114
- assert.ok(!result.includes('data-node-id="11"'));
113
+ assert.ok(!result.includes('data-nid="11"'));
115
114
  assert.ok(!result.includes('Old text'));
116
115
  });
117
116
 
@@ -130,7 +129,7 @@ describe('applyChangeList', () => {
130
129
  const result = applyChangeList(annotated, changes);
131
130
  // Prepended element should appear before the <p>
132
131
  const prependIdx = result.indexOf('<em>Prepended</em>');
133
- const pIdx = result.indexOf('<p data-node-id="11">');
132
+ const pIdx = result.indexOf('<p data-nid="11">');
134
133
  assert.ok(prependIdx < pIdx);
135
134
  });
136
135
 
@@ -140,7 +139,7 @@ describe('applyChangeList', () => {
140
139
  ];
141
140
  const result = applyChangeList(annotated, changes);
142
141
  const beforeIdx = result.indexOf('<em>Before</em>');
143
- const pIdx = result.indexOf('<p data-node-id="11">');
142
+ const pIdx = result.indexOf('<p data-nid="11">');
144
143
  assert.ok(beforeIdx < pIdx);
145
144
  });
146
145
 
@@ -150,7 +149,7 @@ describe('applyChangeList', () => {
150
149
  ];
151
150
  const result = applyChangeList(annotated, changes);
152
151
  const afterIdx = result.indexOf('<em>After</em>');
153
- const pIdx = result.indexOf('<p data-node-id="11">');
152
+ const pIdx = result.indexOf('<p data-nid="11">');
154
153
  assert.ok(afterIdx > pIdx);
155
154
  });
156
155
 
@@ -163,11 +162,14 @@ describe('applyChangeList', () => {
163
162
  assert.ok(!result.includes('Ghost'));
164
163
  });
165
164
 
166
- it('throws on missing parent for insert', () => {
165
+ it('warns but does not throw on missing parent for insert', () => {
167
166
  const changes: ChangeList = [
168
167
  { op: 'insert', parentId: '999', position: 'append', html: '<em>Fail</em>' },
169
168
  ];
170
- assert.throws(() => applyChangeList(annotated, changes), /not found/);
169
+ // Should not throw warns and skips
170
+ const result = applyChangeList(annotated, changes);
171
+ assert.ok(!result.includes('Fail'));
172
+ assert.ok(result.includes('Old text')); // original content preserved
171
173
  });
172
174
 
173
175
  it('applies "style-element" — sets style attribute on unlocked element', () => {
@@ -176,12 +178,12 @@ describe('applyChangeList', () => {
176
178
  ];
177
179
  const result = applyChangeList(annotated, changes);
178
180
  assert.ok(result.includes('style="color: red; font-size: 16px"'));
179
- assert.ok(result.includes('data-node-id="11"'));
181
+ assert.ok(result.includes('data-nid="11"'));
180
182
  });
181
183
 
182
184
  it('skips "style-element" on a data-locked element', () => {
183
185
  const lockedHtml = '<html><head></head><body>' +
184
- '<div data-node-id="10"><p data-node-id="11" data-locked>Locked text</p></div>' +
186
+ '<div data-nid="10"><p data-nid="11" data-locked>Locked text</p></div>' +
185
187
  '</body></html>';
186
188
  const changes: ChangeList = [
187
189
  { op: 'style-element', nodeId: '11', style: 'color: red' },
@@ -200,9 +202,9 @@ describe('applyChangeList', () => {
200
202
 
201
203
  it('allows delete of unlocked child inside a data-locked parent', () => {
202
204
  const lockedParentHtml = '<html><head></head><body>' +
203
- '<div data-node-id="10" data-locked="true">' +
204
- '<p data-node-id="11">Child message</p>' +
205
- '<p data-node-id="12">Another child</p>' +
205
+ '<div data-nid="10" data-locked="true">' +
206
+ '<p data-nid="11">Child message</p>' +
207
+ '<p data-nid="12">Another child</p>' +
206
208
  '</div></body></html>';
207
209
  const changes: ChangeList = [
208
210
  { op: 'delete', nodeId: '11' },
@@ -214,7 +216,7 @@ describe('applyChangeList', () => {
214
216
 
215
217
  it('blocks delete of element that itself has data-locked', () => {
216
218
  const lockedHtml = '<html><head></head><body>' +
217
- '<div data-node-id="10"><p data-node-id="11" data-locked="true">Locked</p></div>' +
219
+ '<div data-nid="10"><p data-nid="11" data-locked="true">Locked</p></div>' +
218
220
  '</body></html>';
219
221
  const changes: ChangeList = [
220
222
  { op: 'delete', nodeId: '11' },
@@ -225,8 +227,8 @@ describe('applyChangeList', () => {
225
227
 
226
228
  it('allows replace of unlocked child inside a data-locked parent', () => {
227
229
  const lockedParentHtml = '<html><head></head><body>' +
228
- '<div data-node-id="10" data-locked="true">' +
229
- '<p data-node-id="11">Old child</p>' +
230
+ '<div data-nid="10" data-locked="true">' +
231
+ '<p data-nid="11">Old child</p>' +
230
232
  '</div></body></html>';
231
233
  const changes: ChangeList = [
232
234
  { op: 'replace', nodeId: '11', html: '<span>New child</span>' },
@@ -246,7 +248,7 @@ describe('applyChangeList', () => {
246
248
 
247
249
  it('skips replace on a data-locked element', () => {
248
250
  const lockedHtml = '<html><head></head><body>' +
249
- '<div data-node-id="10"><p data-node-id="11" data-locked>Locked</p></div>' +
251
+ '<div data-nid="10"><p data-nid="11" data-locked>Locked</p></div>' +
250
252
  '</body></html>';
251
253
  const changes: ChangeList = [
252
254
  { op: 'replace', nodeId: '11', html: '<span>Replaced</span>' },
@@ -265,11 +267,14 @@ describe('applyChangeList', () => {
265
267
  assert.ok(result.includes('Old text'));
266
268
  });
267
269
 
268
- it('throws on unknown insert position', () => {
270
+ it('warns but does not throw on unknown insert position', () => {
269
271
  const changes = [
270
272
  { op: 'insert', parentId: '10', position: 'sideways', html: '<em>Oops</em>' },
271
273
  ] as unknown as ChangeList;
272
- assert.throws(() => applyChangeList(annotated, changes), /unknown position/);
274
+ // Should not throw warns and skips
275
+ const result = applyChangeList(annotated, changes);
276
+ assert.ok(!result.includes('Oops'));
277
+ assert.ok(result.includes('Old text')); // original content preserved
273
278
  });
274
279
 
275
280
  it('throws on unknown op', () => {
@@ -313,34 +318,9 @@ describe('parseChangeList', () => {
313
318
  });
314
319
 
315
320
  // ---------------------------------------------------------------------------
316
- // injectError
321
+ // injectError — REMOVED (iframe model: errors reported via shell postMessage)
317
322
  // ---------------------------------------------------------------------------
318
323
 
319
- describe('injectError', () => {
320
- it('injects error script block into body', () => {
321
- const html = '<html><head></head><body><p>Page</p></body></html>';
322
- const result = injectError(html, 'Oops', 'details here');
323
- assert.ok(result.includes('<script id="error" type="application/json">'));
324
- assert.ok(result.includes('"message":"Oops"'));
325
- assert.ok(result.includes('"details":"details here"'));
326
- });
327
-
328
- it('replaces existing error block', () => {
329
- const html = '<html><head></head><body><script id="error" type="application/json">{"message":"old"}</script></body></html>';
330
- const result = injectError(html, 'New', 'new detail');
331
- // Should have exactly one error script
332
- const matches = result.match(/<script id="error"/g);
333
- assert.strictEqual(matches?.length, 1);
334
- assert.ok(result.includes('"message":"New"'));
335
- });
336
-
337
- it('appends to end if no body tag', () => {
338
- const html = '<div>No body</div>';
339
- const result = injectError(html, 'Err', 'det');
340
- assert.ok(result.includes('<script id="error"'));
341
- });
342
- });
343
-
344
324
  // ---------------------------------------------------------------------------
345
325
  // deduplicateInlineScripts
346
326
  // ---------------------------------------------------------------------------
@@ -497,7 +477,7 @@ describe('normalizedIndexOf', () => {
497
477
 
498
478
  describe('applyChangeList — search-replace / search-insert ops', () => {
499
479
  const scriptHtml = '<html><head></head><body>' +
500
- '<script data-node-id="5">let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\nlet e = 5;</script>' +
480
+ '<script data-nid="5">let a = 1;\nlet b = 2;\nlet c = 3;\nlet d = 4;\nlet e = 5;</script>' +
501
481
  '</body></html>';
502
482
 
503
483
  it('search-replace replaces exact text match', () => {
@@ -524,7 +504,7 @@ describe('applyChangeList — search-replace / search-insert ops', () => {
524
504
  it('search-replace falls back to normalized match', () => {
525
505
  // Script has single spaces, search has different whitespace
526
506
  const html = '<html><head></head><body>' +
527
- '<script data-node-id="5">function foo() {\n return 1;\n}</script>' +
507
+ '<script data-nid="5">function foo() {\n return 1;\n}</script>' +
528
508
  '</body></html>';
529
509
  const changes: ChangeList = [
530
510
  { op: 'search-replace', nodeId: '5', search: 'function foo() { return 1; }', replace: 'function foo() { return 2; }' },
@@ -545,7 +525,7 @@ describe('applyChangeList — search-replace / search-insert ops', () => {
545
525
  });
546
526
 
547
527
  it('search-replace works on style blocks', () => {
548
- const styleHtml = '<html><head><style data-node-id="3">.a { color: red; }\n.b { color: blue; }\n.c { color: green; }</style></head><body></body></html>';
528
+ const styleHtml = '<html><head><style data-nid="3">.a { color: red; }\n.b { color: blue; }\n.c { color: green; }</style></head><body></body></html>';
549
529
  const changes: ChangeList = [
550
530
  { op: 'search-replace', nodeId: '3', search: '.b { color: blue; }', replace: '.b { color: purple; }' },
551
531
  ];
@@ -602,11 +582,11 @@ describe('transformPage', () => {
602
582
  <div id="thoughts" style="display: none;"></div>
603
583
  </body></html>`;
604
584
 
605
- /** Extract the data-node-id for an element identified by a CSS-style attribute (e.g. id="content") from annotated HTML. */
585
+ /** Extract the data-nid for an element identified by a CSS-style attribute (e.g. id="content") from annotated HTML. */
606
586
  function findNodeId(annotatedHtml: string, idAttr: string): string {
607
- // Match a tag that contains both data-node-id="X" and the target id, in either order
608
- const pattern1 = new RegExp(`data-node-id="(\\d+)"[^>]*${idAttr}`);
609
- const pattern2 = new RegExp(`${idAttr}[^>]*data-node-id="(\\d+)"`);
587
+ // Match a tag that contains both data-nid="X" and the target id, in either order
588
+ const pattern1 = new RegExp(`data-nid="(\\d+)"[^>]*${idAttr}`);
589
+ const pattern2 = new RegExp(`${idAttr}[^>]*data-nid="(\\d+)"`);
610
590
  const m = annotatedHtml.match(pattern1) || annotatedHtml.match(pattern2);
611
591
  return m ? m[1] : '99999';
612
592
  }
@@ -639,11 +619,11 @@ describe('transformPage', () => {
639
619
  assert.ok(result.value);
640
620
  assert.ok(result.value.html.includes('Updated content'));
641
621
  assert.strictEqual(result.value.changeCount, 1);
642
- // Should not contain data-node-id attributes
643
- assert.ok(!result.value.html.includes('data-node-id'));
622
+ // Should not contain data-nid attributes
623
+ assert.ok(!result.value.html.includes('data-nid'));
644
624
  });
645
625
 
646
- it('returns error HTML when builder returns error', async () => {
626
+ it('returns errorText when builder returns error (shell displays it)', async () => {
647
627
  const builder = makeBuilder(async () => ({
648
628
  kind: 'error',
649
629
  error: new Error('API quota exceeded'),
@@ -652,11 +632,12 @@ describe('transformPage', () => {
652
632
  const result = await transformPage(makeArgs(builder));
653
633
  assert.strictEqual(result.completed, true);
654
634
  assert.ok(result.value);
655
- assert.ok(result.value.html.includes('id="error"'));
656
- assert.strictEqual(result.value.changeCount, 0);
635
+ assert.strictEqual(result.value.errorText, 'API quota exceeded');
636
+ assert.ok(!result.value.html.includes('id="error"'), 'error should not be injected into HTML');
637
+ assert.strictEqual(result.value.changeCount, -1);
657
638
  });
658
639
 
659
- it('handles reply result by appending chat messages', async () => {
640
+ it('returns replyText when builder returns reply (shell displays it)', async () => {
660
641
  const builder = makeBuilder(async () => ({
661
642
  kind: 'reply',
662
643
  text: 'I cannot help with that.',
@@ -668,10 +649,9 @@ describe('transformPage', () => {
668
649
  });
669
650
  assert.strictEqual(result.completed, true);
670
651
  assert.ok(result.value);
671
- assert.ok(result.value.html.includes('User:'));
672
- assert.ok(result.value.html.includes('SynthOS:'));
673
- assert.ok(result.value.html.includes('I cannot help with that.'));
674
- assert.strictEqual(result.value.changeCount, 0);
652
+ assert.strictEqual(result.value.replyText, 'I cannot help with that.');
653
+ assert.ok(!result.value.html.includes('User:'), 'chat messages should not be injected into HTML');
654
+ assert.strictEqual(result.value.changeCount, -1);
675
655
  });
676
656
 
677
657
  it('handles missing nodes gracefully (no repair pass)', async () => {
@@ -783,7 +763,7 @@ describe('transformPage', () => {
783
763
  assert.ok(!result.value!.html.includes('color: red'));
784
764
  });
785
765
 
786
- it('catches exceptions from builder and injects error', async () => {
766
+ it('catches exceptions from builder and returns errorText (shell displays it)', async () => {
787
767
  const builder = makeBuilder(async () => {
788
768
  throw new Error('Unexpected builder crash');
789
769
  });
@@ -791,8 +771,9 @@ describe('transformPage', () => {
791
771
  const result = await transformPage(makeArgs(builder));
792
772
  assert.strictEqual(result.completed, true);
793
773
  assert.ok(result.value);
794
- assert.ok(result.value.html.includes('id="error"'));
795
- assert.strictEqual(result.value.changeCount, 0);
774
+ assert.strictEqual(result.value.errorText, 'Unexpected builder crash');
775
+ assert.ok(!result.value.html.includes('id="error"'), 'error should not be injected into HTML');
776
+ assert.strictEqual(result.value.changeCount, -1);
796
777
  });
797
778
 
798
779
  it('detects newBuild when isBuilder is true and only one chat message', async () => {
@@ -864,7 +845,7 @@ describe('transformPage', () => {
864
845
  // Edit should be applied
865
846
  assert.ok(result.value.html.includes('let count = 42;'));
866
847
  // Node ids should be stripped
867
- assert.ok(!result.value.html.includes('data-node-id'));
848
+ assert.ok(!result.value.html.includes('data-nid'));
868
849
  assert.strictEqual(result.value.changeCount, 1);
869
850
  });
870
851
  });
@@ -1,10 +0,0 @@
1
- {
2
- "title": "Application",
3
- "categories": [
4
- "Starters"
5
- ],
6
- "pinned": false,
7
- "showInAll": false,
8
- "pageVersion": 3,
9
- "mode": "unlocked"
10
- }
@@ -1,12 +0,0 @@
1
- {
2
- "title": "Retro Game Starter",
3
- "categories": [
4
- "Starters"
5
- ],
6
- "pinned": false,
7
- "showInAll": true,
8
- "createdDate": "2026-03-02T12:00:00.000Z",
9
- "lastModified": "2026-03-02T12:00:00.000Z",
10
- "pageVersion": 3,
11
- "mode": "unlocked"
12
- }