dsh-llm-fallbacks 0.1.0-alpha.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.
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Client bundle build: emits the closure-factory CJS artifact the dsh web
3
+ * loader consumes — `window.__ModuleLoader__.load({ id: 'dsh-llm-fallbacks',
4
+ * factory: (require) => { … return module.exports; } })`. Externals resolve
5
+ * through the loader module table (platform seed entries + the documented
6
+ * `@deepseek-ai/dsh-client-runtime/client` exemption); everything else
7
+ * inlines.
8
+ *
9
+ * Build tool: `tsdown` (rolldown) — the same tool the dsh host itself uses
10
+ * for its client bundles (packages/client/tsdown.client.ts pattern), run via
11
+ * `tsx`. Replaces the previous `bun build` recipe; no bun anywhere in the
12
+ * pnpm stack. `dist/client/index.js` is a build artifact (gitignored like the
13
+ * rest of `dist/`); the matching `dist/client/index.d.ts` is emitted by `tsc`
14
+ * (`emitDeclarationOnly`) from `src/client/index.ts`.
15
+ *
16
+ * One build plugin enforces the bundle contract (mirroring the dsh tsdown
17
+ * client recipe):
18
+ * - CSS Modules (`*.module.css`) are compiled to a hashed class map plus an
19
+ * inline `<style data-plugin>` injection that runs when the factory
20
+ * materializes (the loader removes plugin-owned tags on unload). The
21
+ * transform itself (FNV-1a `_<8hex>_<local>` hash, comment-stripped css)
22
+ * mirrors the host bundle's `_1zfRHq_section` shape — the `_` prefix keeps
23
+ * every hashed class a legal CSS identifier (a bare `8hex_local` starts
24
+ * with a digit and the browser silently drops those rules) — and the
25
+ * bundle-contract assertions at the bottom pin that invariant. Only the
26
+ * plugin API moved from bun's `onLoad` to rolldown's `resolveId`/`load`
27
+ * over a virtual id (the virtual id must not end in `.css` so tsdown's own
28
+ * css pipeline never sees it).
29
+ *
30
+ * The `@deepseek-ai/*` purity gate (mirror of the host's
31
+ * `dsh-client-bundle-purity` plugin) is enforced at resolution time: any
32
+ * value import of an `@deepseek-ai/*` module outside CLIENT_EXTERNALS
33
+ * throws, so the 20260812 type-only peers (`dsh-client-ui-settings-plugins`,
34
+ * `dsh-client-locale`, `dsh-api-remotes`, …) can never leak into the
35
+ * browser-loadable artifact. The emitted text may reference only
36
+ * CLIENT_EXTERNALS entries (asserted below).
37
+ */
38
+
39
+ import { build } from 'tsdown'
40
+ import { basename, dirname, join, resolve } from 'node:path'
41
+ import { existsSync, readFileSync } from 'node:fs'
42
+
43
+ const ID = 'dsh-llm-fallbacks'
44
+ const ENTRY = 'src/client/index.ts'
45
+ const OUT_DIR = 'dist/client'
46
+ const OUT_FILE = 'index.js'
47
+
48
+ /**
49
+ * Frozen loader module table (dsh mechanism-guide): the plan-documented
50
+ * composition = PLATFORM_MODULES (10 entries, `web/src/platform.ts:8-15`)
51
+ * + the RUNTIME_STORE_EXEMPTION (`@deepseek-ai/dsh-client-runtime/client`,
52
+ * `tsdown.client.ts:62-65`). `@deepseek-ai/cordis` is the in-box cordis
53
+ * framework (dsh-advisor alignment): the client graph resolves it through
54
+ * the generated shim, so it stays on the table as a real loader-table
55
+ * specifier for the frozen-composition claim.
56
+ */
57
+ export const CLIENT_EXTERNALS: readonly string[] = [
58
+ 'react',
59
+ 'react/jsx-runtime',
60
+ 'react-dom',
61
+ 'react-dom/client',
62
+ '@deepseek-ai/cordis',
63
+ '@deepseek-ai/dsh-client-ui-slots',
64
+ '@deepseek-ai/dsh-client-web-react',
65
+ '@deepseek-ai/dsh-client-ui-primitives',
66
+ '@deepseek-ai/dsh-client-ui-attachment',
67
+ '@deepseek-ai/dsh-client-schema-form',
68
+ '@deepseek-ai/dsh-client-runtime/client',
69
+ ]
70
+
71
+ /**
72
+ * Deterministic CSS-module class hash: FNV-1a 32-bit over the local name →
73
+ * `_<8hex>_<local>`. The `_` prefix is load-bearing: an 8-hex FNV-1a hash
74
+ * starts with a digit ~62.5% of the time, and `.8b697c55_fieldRow` is not a
75
+ * legal CSS identifier — the browser silently drops the whole rule. The
76
+ * underscore form mirrors the host bundle's `_1zfRHq_section` shape (the dsh
77
+ * recipe's `[hash]_[local]` pattern). The hash only needs to be stable within
78
+ * the bundle — the class map and the rewritten css text come from the same
79
+ * transform.
80
+ */
81
+ function hashClass(local: string): string {
82
+ let h = 0x811c9dc5
83
+ for (let i = 0; i < local.length; i++) {
84
+ h ^= local.charCodeAt(i)
85
+ h = Math.imul(h, 0x01000193) >>> 0
86
+ }
87
+ return `_${h.toString(16).padStart(8, '0')}_${local}`
88
+ }
89
+
90
+ /**
91
+ * Inline `<style data-plugin>` injection source for a css text blob: the tag
92
+ * is created once per factory materialization (the loader removes plugin-owned
93
+ * tags on unload).
94
+ */
95
+ function styleInjectionContents(cssText: string, tagId: string): string {
96
+ return [
97
+ `const css = ${JSON.stringify(cssText)};`,
98
+ `const tagId = ${JSON.stringify(tagId)};`,
99
+ `if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {`,
100
+ ` const tag = document.createElement('style');`,
101
+ ` tag.dataset.plugin = ${JSON.stringify(ID)};`,
102
+ ` tag.dataset.pluginCss = tagId;`,
103
+ ` tag.textContent = css;`,
104
+ ` document.head.appendChild(tag);`,
105
+ `}`,
106
+ ].join('\n')
107
+ }
108
+
109
+ /**
110
+ * Rewrite css-module class tokens (`.local` -> `._<8hex>_<local>`) in
111
+ * **selector position only** — the text between the previous `}`/start and
112
+ * each `{`, at every nesting depth. Declaration values (data-uris, decimals,
113
+ * `content:` strings) are copied verbatim, so a `url("…www.w3.org…")` never
114
+ * gets its dots rewritten into spurious class tokens (the data-uri
115
+ * corruption class). Same brace-walking shape as `collectCssSelectors` —
116
+ * and the same documented limitation: a literal `{` inside a string/url
117
+ * value would mis-walk; the current css keeps all braces out of strings.
118
+ */
119
+ function hashSelectorTokens(cssText: string, classMap: Record<string, string>): string {
120
+ let out = ''
121
+ let i = 0
122
+ let selectorStart = 0
123
+ while (i < cssText.length) {
124
+ if (cssText[i] === '{') {
125
+ out += cssText.slice(selectorStart, i).replace(/\.[A-Za-z_][A-Za-z0-9_-]*/g, (match) => {
126
+ const local = match.slice(1)
127
+ const hashed = hashClass(local)
128
+ classMap[local] = hashed
129
+ return `.${hashed}`
130
+ })
131
+ let depth = 1
132
+ const blockStart = i + 1
133
+ i++
134
+ while (i < cssText.length && depth > 0) {
135
+ if (cssText[i] === '{') depth++
136
+ else if (cssText[i] === '}') depth--
137
+ i++
138
+ }
139
+ const blockEnd = depth === 0 ? i - 1 : i
140
+ out += `{${hashSelectorTokens(cssText.slice(blockStart, blockEnd), classMap)}}`
141
+ selectorStart = i
142
+ } else {
143
+ i++
144
+ }
145
+ }
146
+ out += cssText.slice(selectorStart)
147
+ return out
148
+ }
149
+
150
+ /**
151
+ * Compile one `*.module.css` file into a JS module: the css text (comments
152
+ * stripped, class tokens hashed) plus a `<style data-plugin>` injection that
153
+ * runs at factory materialization, and the hashed class map as the default
154
+ * export (mirror of the dsh CSS plugin). Class tokens hash to
155
+ * `_<8hex>_<local>` — legal CSS identifiers, mirroring the host bundle's
156
+ * `_1zfRHq_section` shape (see `hashClass`). The class map and the rewritten
157
+ * css text come from this single transform, so they can never drift. The
158
+ * rewrite is selector-aware (see `hashSelectorTokens`): declaration values —
159
+ * including `url()` data-uris — pass through byte-identical, so the emitted
160
+ * chevron SVG keeps `www.w3.org/2000/svg` intact.
161
+ *
162
+ * simplify: the css text is emitted as-is (comment-stripped, not minified);
163
+ * the dsh recipe runs lightningcss. If bundle size ever matters, switch the
164
+ * transform to lightningcss or add a minifier here.
165
+ */
166
+ function cssModuleContents(fileId: string): string {
167
+ const source = readFileSync(fileId, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
168
+ const classMap: Record<string, string> = {}
169
+ const css = hashSelectorTokens(source, classMap)
170
+ const tagId = `${ID}/${basename(fileId)}`
171
+ return [
172
+ styleInjectionContents(css, tagId),
173
+ `export default ${JSON.stringify(classMap)};`,
174
+ ].join('\n')
175
+ }
176
+
177
+ // Virtual-id wrapper keeping module CSS away from tsdown's own css pipeline
178
+ // (mirror of the dsh tsdown.client.ts pattern): the suffix matters — tsdown's
179
+ // guard matches ids ending in `.css`, so the virtual id must not.
180
+ const CSS_VIRTUAL_PREFIX = '\0dsh-css:'
181
+ const CSS_VIRTUAL_SUFFIX = '.mjs'
182
+
183
+ await build({
184
+ // `config: false` — run this config standalone, never merge the root
185
+ // tsdown.config.ts (which owns the host bundle + `clean: true`).
186
+ config: false,
187
+ name: `${ID}/client`,
188
+ entry: { index: ENTRY },
189
+ outDir: OUT_DIR,
190
+ format: 'cjs',
191
+ platform: 'browser',
192
+ target: 'es2022',
193
+ dts: false,
194
+ clean: false,
195
+ deps: {
196
+ // The loader module table is the rule: table entries stay external
197
+ // (tsdown also auto-externalizes peerDependencies), everything else must
198
+ // inline — the browser loader cannot answer a require() it does not
199
+ // know. `onlyBundle: false` marks the inlining intentional.
200
+ neverBundle: [...CLIENT_EXTERNALS],
201
+ alwaysBundle: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
202
+ onlyBundle: false,
203
+ },
204
+ // react/zustand-style deps read process.env.NODE_ENV; honor the build env
205
+ // like the dsh recipe (artifacts default to production). No `import.meta`
206
+ // define needed: nothing inlined reads it (the store engine is external),
207
+ // and a literal `import.meta` would be a SyntaxError under the classic
208
+ // <script> loader anyway.
209
+ define: {
210
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
211
+ },
212
+ outputOptions: {
213
+ entryFileNames: OUT_FILE,
214
+ // Closure-factory handoff (mirror of the dsh tsdown recipe): `intro`
215
+ // lands inside the factory body (after `banner`) and declares
216
+ // `module`/`exports` because the bundler's cjs emission assigns
217
+ // module.exports itself; the factory returns that surface to the loader.
218
+ banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(ID)}, factory: (require) => {`,
219
+ intro: 'var module = { exports: {} }; var exports = module.exports;',
220
+ footer: 'return module.exports; } });',
221
+ },
222
+ plugins: [
223
+ {
224
+ // Bundle purity gate (mirror of the host's `dsh-client-bundle-purity`
225
+ // plugin, tsdown.client.ts:227-237): a value import of any
226
+ // `@deepseek-ai/*` module outside CLIENT_EXTERNALS throws at
227
+ // resolution time. Type-only imports are erased before the module
228
+ // graph is built, so reaching this hook means a real value import —
229
+ // which must not be inlined into the browser-loadable artifact. The
230
+ // emitted-surface scan below is the second line of defense; this hook
231
+ // closes the inlining path, where a stray value import would
232
+ // otherwise be bundled silently (the old require-only scan only saw
233
+ // externals, never inlined modules).
234
+ name: 'dsh-client-bundle-purity',
235
+ resolveId(source: string) {
236
+ if (source.startsWith('@deepseek-ai/') && !CLIENT_EXTERNALS.includes(source)) {
237
+ throw new Error(
238
+ `client bundle purity: value import of ${source} is not in CLIENT_EXTERNALS — type-only peers must stay type-only (use \`import type\`)`,
239
+ )
240
+ }
241
+ return null
242
+ },
243
+ },
244
+ {
245
+ name: 'dsh-css-modules-inline',
246
+ resolveId(source: string, importer: string | undefined) {
247
+ if (!source.endsWith('.module.css')) return null
248
+ const abs = importer !== undefined ? resolve(dirname(importer), source) : source
249
+ return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
250
+ },
251
+ load(id: string) {
252
+ if (!id.startsWith(CSS_VIRTUAL_PREFIX)) return null
253
+ const fileId = id.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length)
254
+ return cssModuleContents(fileId)
255
+ },
256
+ },
257
+ ],
258
+ })
259
+
260
+ const outFile = join(OUT_DIR, OUT_FILE)
261
+ if (!existsSync(outFile)) {
262
+ throw new Error(`client bundle build: expected ${outFile} — check the tsdown config (entry/entryFileNames/outDir)`)
263
+ }
264
+
265
+ /**
266
+ * Collect every selector text from a css blob: the text between the previous
267
+ * `}`/start and each `{`, at every nesting depth (so `@media` blocks are
268
+ * walked too). The bundle contract uses this to check class-selector tokens
269
+ * in selector position only — declaration values like `opacity: 0.6`,
270
+ * `transition: .12s`, or the data-uri decimals never sit in a selector, so
271
+ * they cannot be mistaken for a `.`-prefixed class.
272
+ */
273
+ function collectCssSelectors(cssText: string): string[] {
274
+ const selectors: string[] = []
275
+ const walk = (text: string) => {
276
+ let i = 0
277
+ let selectorStart = 0
278
+ while (i < text.length) {
279
+ if (text[i] === '{') {
280
+ selectors.push(text.slice(selectorStart, i))
281
+ let depth = 1
282
+ const blockStart = i + 1
283
+ i++
284
+ while (i < text.length && depth > 0) {
285
+ if (text[i] === '{') depth++
286
+ else if (text[i] === '}') depth--
287
+ i++
288
+ }
289
+ walk(text.slice(blockStart, depth === 0 ? i - 1 : i))
290
+ selectorStart = i
291
+ } else {
292
+ i++
293
+ }
294
+ }
295
+ }
296
+ walk(cssText)
297
+ return selectors
298
+ }
299
+
300
+ // Inline bundle-contract assertions: the emitted text must carry the inlined
301
+ // css-module class map (the transform ran), must NOT contain `import.meta` /
302
+ // ESM statements (the classic-script loader would fail to parse them), every
303
+ // `@deepseek-ai/*` reference in the emitted text must be a documented
304
+ // CLIENT_EXTERNALS entry (the 20260812 type-only peers —
305
+ // `dsh-client-ui-settings-plugins` / `dsh-client-locale` / `dsh-api-remotes` and
306
+ // the other type-only imports — must never appear at runtime, whether as
307
+ // `require(...)` calls or as any other surviving text reference), and every
308
+ // css-module class must be a legal CSS identifier — `hashClass` emits
309
+ // `_<8hex>_<local>` (mirroring the host bundle's `_1zfRHq_section` shape)
310
+ // because a digit-leading hash would be a valid JS key but an illegal
311
+ // selector the browser silently drops.
312
+ const bundleText = readFileSync(outFile, 'utf8')
313
+ if (!bundleText.includes('data-plugin-css')) {
314
+ throw new Error('client bundle contract: no inlined css-module style injection found — check the dsh-css-modules-inline plugin')
315
+ }
316
+ if (bundleText.includes('import.meta') || /(^|\n)\s*(import|export)\s/.test(bundleText)) {
317
+ throw new Error('client bundle contract: emitted bundle contains import.meta / ESM statements — the classic-script loader would fail to parse it')
318
+ }
319
+ // Emitted-surface contract: subsumes the require-only check (externals are
320
+ // the only legitimate `require(...)` targets) and also catches any other
321
+ // surviving text reference to a non-external package. The resolution-time
322
+ // purity plugin above is the structural gate; this scan pins the contract on
323
+ // the artifact itself.
324
+ const deepseekTokens = [...bundleText.matchAll(/@deepseek-ai\/[\w./-]+/g)].map(match => match[0])
325
+ const unexpected = deepseekTokens.filter(specifier => !CLIENT_EXTERNALS.includes(specifier))
326
+ if (unexpected.length > 0) {
327
+ throw new Error(`client bundle contract: non-external @deepseek-ai/* reference(s) survived in the emitted bundle: ${unexpected.join(', ')}`)
328
+ }
329
+
330
+ // CSS-identifier contract: no class selector in the injected css text starts
331
+ // with a digit (`.\d`), and every hashed class name in the inlined class map
332
+ // matches `^[A-Za-z_][A-Za-z0-9_-]*$`.
333
+ const injectedCssMatches = [...bundleText.matchAll(/\bconst css = ("(?:[^"\\]|\\.)*");/g)]
334
+ if (injectedCssMatches.length === 0) {
335
+ throw new Error('client bundle contract: no inlined css-module style text found — check the dsh-css-modules-inline plugin')
336
+ }
337
+ for (const match of injectedCssMatches) {
338
+ const cssText = JSON.parse(match[1]) as string
339
+ for (const selector of collectCssSelectors(cssText)) {
340
+ for (const token of selector.matchAll(/\.[A-Za-z_0-9][A-Za-z0-9_-]*/g)) {
341
+ if (!/^\.[A-Za-z_]/.test(token[0])) {
342
+ throw new Error(`client bundle contract: class selector ${token[0]} starts with a non-identifier char — hashClass must emit _<8hex>_<local> (mirror the host _1zfRHq_section shape)`)
343
+ }
344
+ }
345
+ }
346
+ // Declaration-position contract: a hashed class token (`_<8hex>_`) inside
347
+ // a `url(...)` means the css-module transform leaked into a declaration
348
+ // value — the data-uri corruption class (`www.w3.org` ->
349
+ // `www._0e4734df_w3._9c1df059_org`). The transform rewrites selector
350
+ // position only, so this must never fire; it guards against a future
351
+ // regression to a naive global replace. The matcher accepts quoted urls
352
+ // whose content contains the other quote type (the chevron data uri is
353
+ // double-quoted and full of `xmlns='...'`), or bare unquoted urls.
354
+ for (const urlMatch of cssText.matchAll(/url\(\s*(?:"([^"]*)"|'([^']*)'|([^"'()\s]+))\s*\)/g)) {
355
+ const urlContent = urlMatch[1] ?? urlMatch[2] ?? urlMatch[3]
356
+ if (/_[0-9a-fA-F]{8}_/.test(urlContent)) {
357
+ throw new Error(`client bundle contract: hashed class token leaked into url() declaration value: ${urlContent} — the css-module transform must rewrite selector position only`)
358
+ }
359
+ }
360
+ }
361
+ const classMapMatches = [...bundleText.matchAll(/\b\w+_module_css_default\s*=\s*(\{[\s\S]*?\n\s*\});/g)]
362
+ if (classMapMatches.length === 0) {
363
+ throw new Error('client bundle contract: no inlined css-module class map found — check the dsh-css-modules-inline plugin')
364
+ }
365
+ for (const mapMatch of classMapMatches) {
366
+ const classMap = JSON.parse(mapMatch[1]) as Record<string, string>
367
+ for (const [local, hashed] of Object.entries(classMap)) {
368
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(hashed)) {
369
+ throw new Error(`client bundle contract: hashed class "${local}" -> "${hashed}" is not a valid CSS identifier — hashClass must emit _<8hex>_<local> (mirror the host _1zfRHq_section shape)`)
370
+ }
371
+ }
372
+ }
373
+
374
+ console.log(`build-client: ${ENTRY} -> ${outFile} (closure-factory CJS, tsdown)`)
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Parse-only guard for dist artifacts — never executes module code, never
3
+ * resolves imports. Catches the boot failure mode where the bundler leaves
4
+ * standard-decorator syntax (e.g. `@Remote(...)`) verbatim in emitted ESM,
5
+ * which Node cannot parse.
6
+ *
7
+ * - host bundle: `dist/index.js` is ESM -> `node --check --input-type=module`
8
+ * (syntax check only, no execution, no dependency resolution). The original
9
+ * design called for `new vm.SourceTextModule(...)`, but Node 24 removed the
10
+ * experimental `vm.Module` API; `--check --input-type=module` is Node's
11
+ * built-in parse-only ESM path and matches the old guard's semantics.
12
+ * - client bundle: `dist/client/index.js` is a CJS closure -> `vm.Script`
13
+ * (classic-script compile at construction).
14
+ *
15
+ * On SyntaxError, prints file + message and exits 1; otherwise prints sizes
16
+ * and exits 0.
17
+ */
18
+ import { spawnSync } from 'node:child_process'
19
+ import { readFileSync } from 'node:fs'
20
+ import { Script } from 'node:vm'
21
+
22
+ const targets = [
23
+ { name: 'host', file: 'dist/index.js', kind: 'esm' },
24
+ { name: 'client', file: 'dist/client/index.js', kind: 'cjs' },
25
+ ]
26
+
27
+ let failed = false
28
+ for (const { name, file, kind } of targets) {
29
+ const code = readFileSync(file, 'utf8')
30
+ const bytes = Buffer.byteLength(code)
31
+ const lines = code.split('\n').length
32
+ let error = null
33
+ if (kind === 'esm') {
34
+ const result = spawnSync(process.execPath, ['--check', '--input-type=module'], {
35
+ input: code,
36
+ encoding: 'utf8',
37
+ })
38
+ if (result.status !== 0) {
39
+ error = new SyntaxError(
40
+ (result.stderr.split('\n').find(line => line.startsWith('SyntaxError')) ??
41
+ result.stderr.trim() ??
42
+ result.error?.message ??
43
+ 'syntax check failed'),
44
+ )
45
+ }
46
+ } else {
47
+ try {
48
+ new Script(code)
49
+ } catch (cause) {
50
+ error = cause
51
+ }
52
+ }
53
+ if (error === null) {
54
+ console.log(`verify-dist: ${name} OK (${file}, ${bytes} bytes, ${lines} lines)`)
55
+ } else {
56
+ failed = true
57
+ if (error instanceof SyntaxError) {
58
+ console.error(`verify-dist: ${name} SyntaxError in ${file}: ${error.message}`)
59
+ } else {
60
+ throw error
61
+ }
62
+ }
63
+ }
64
+
65
+ if (failed) {
66
+ console.error('verify-dist: dist parse check failed')
67
+ process.exit(1)
68
+ }