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.
- package/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +288 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1385 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1879 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +150 -25
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +10 -1
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +282 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1411 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +70 -2
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- 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
|
+
}
|
package/src/service/server.ts
CHANGED
|
@@ -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
|
+
}
|