@bushizhanghan/dsh-publish-gate 0.9.0 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ - `tool-no-description` (error), `tool-description-thin` (warn), `tool-no-parameters` (error): the host
6
+ renders name + description + parameters into the prompt and nothing else, so those three fields are the
7
+ tool's entire specification. A tool with no description is a name in a list; a tool that reads args with no
8
+ schema never receives them. All nine of my plugins were clean, so this guards the shape rather than a
9
+ specific crash — the first two fixtures that had `description: 'd'` did fail, which is the point.
10
+ ## 0.10.0
11
+
12
+ - `client-double-load` (warn): the client probe now evaluates the bundle a second time against the same
13
+ `window`, with a loader that throws on a repeated registration. A bundle inserted by two patches (its own,
14
+ plus any sidebar plugin that injects `dsh-client-modules`) aborted web boot this way. A window-level guard
15
+ makes it a no-op, which is what the recommended bundle shape now shows.
3
16
  ## 0.9.0
4
17
 
5
18
  - `checkChangelog()`: `changelog-missing-version` (warn), `changelog-no-versions` (warn), `no-changelog` (info).
package/README.md CHANGED
@@ -104,6 +104,9 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
104
104
  | `tool-present-call-throws` | `presentCall` 抛——调用记录渲染时炸 |
105
105
  | `tool-bad-card-kind` | `card: 'generic'` 的 `kind` 不在宿主认识的那一组里(`other` / `execute` / `search` / `read` / `edit` / `delete` / `fetch`)。注册不报错,卡片渲染坏掉——我五个插件一起踩过 |
106
106
  | `tool-bad-parameter-type` | `parameters` 里某个 `type` 写错(`"strin"`)。宿主把 `parameters` 渲染进 prompt、但**从不校验**调用值,所以这个 typo 只是悄悄把类型提示弄丢 |
107
+ | `tool-no-description` | 工具没有 description。模型是靠 name + description + parameters 挑工具的,没有描述就只剩名字——工具发出去了,但基本选不中 |
108
+ | `tool-description-thin` | description 少于 30 字符,或者就是把工具名重复一遍。等于没写 |
109
+ | `tool-no-parameters` | execute 读 `args.x`,但整个 `parameters` 都没有。模型拿不到参数清单,不知道该传什么 |
107
110
  | `tool-undeclared-param` | `execute` 读了 `args.x`,但 `parameters.properties` 里没有 `x`。模型只能传 schema 声明过的参数,所以这个值在真实调用里永远是 `undefined`——代码在,永远不跑,也不报错。`args.x`、`args['x']`、`const { x } = args` 三种写法都认 |
108
111
  | `tool-output-invalid` | 工具返回的东西**自己的 output schema 不收**。宿主每个结果都拿 schema 校验,不匹配就抛 `ToolOutputError`——工具从第一次调用起就不可用,而报错只给一个 JSON 路径,不给是哪行代码返回的。只有点名 `call` 真的调了才检查。这个插件自己踩过:schema 收紧成 `additionalProperties: false`,但有一条错误分支还在返回 `{ ok, error }` |
109
112
  | `tool-input-throws` | 把每个声明过的参数**塞一个错类型的值**去调,工具直接抛 `TypeError` 而不是回答。宿主从不拿 `parameters` 校验入参,所以这个值真的会到你的代码里——调用方拿到的是 `The "path" argument must be of type string`,插件日志里没有一行说是哪个工具。这条规则是在自己的工作区里跑出 **12 个真实崩溃** 之后才加的(`session`/`profile`/`casePath`/`workspace` 各一个入口),现在九个插件全绿。默认关:它会把每个工具都调一遍,用 `skip` 点名排除那些会开程序、写文件的 |
@@ -124,6 +127,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
124
127
  | `client-apply-throws` | `apply(ctx)` 抛 |
125
128
  | `client-no-registration` | `apply` 跑完一次服务都没调用——面板不会出现 |
126
129
  | `client-inject-missing-<service>` | `apply` 用了 `ctx.sidebarRightTabs` 但 `inject` 数组里没写它。cordis 不会把没声明的服务给你,于是 `ctx.sidebarRightTabs` 是 `undefined`,`apply` 一碰就抛(或静默什么都没注册) |
130
+ | `client-double-load` | 同一个 bundle 被注入了两次(它自己的 patch 一次,注入 `dsh-client-modules` 的侧边栏插件再一次)。loader 按 id 建工厂,第二次注册直接抛错,整个 Web 启动被中断——不是这个面板没了,是所有面板都没了。用 `if (window.__xLoaded) return; window.__xLoaded = true` 挡一下 |
127
131
  | `client-parse-failed` | 文件有语法错误。**单独判**:`SyntaxError` 说明 bundle 本身是坏的,别的报错说明是沙箱不够真,两者别混为一谈 |
128
132
  | `client-unavailable` | 子进程没跑起来(warn)——这一层没跑,别把它当干净 |
129
133
 
@@ -216,7 +220,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
216
220
  ## 开发
217
221
 
218
222
  ```sh
219
- node --test # 97 个引擎单测
223
+ node --test # 102 个引擎单测
220
224
  node test/integration.test.mjs # 27 条集成断言(9 个真实插件作 fixture)
221
225
  ```
222
226
 
package/lib/engine.js CHANGED
@@ -1201,11 +1201,8 @@ const walkParams = (schema, path) => {
1201
1201
  const ARG_DOT = /\\bargs\\s*\\??\\.\\s*([A-Za-z_$][\\w$]*)/g
1202
1202
  const ARG_INDEX = /\\bargs\\s*\\[\\s*['"]([^'"]+)['"]\\s*\\]/g
1203
1203
  const ARG_DESTRUCTURE = /\\{\\s*([^{}]+?)\\s*\\}\\s*=\\s*args\\b/g
1204
- const undeclaredArgs = (def) => {
1205
- const props = def?.parameters?.properties
1206
- if (!props || typeof props !== 'object') return []
1207
- const declared = Object.keys(props)
1208
- if (typeof def.execute !== 'function') return []
1204
+ const argReads = (def) => {
1205
+ if (typeof def?.execute !== 'function') return []
1209
1206
  let src = ''
1210
1207
  try { src = def.execute.toString() } catch { return [] }
1211
1208
  const reads = []
@@ -1217,7 +1214,13 @@ const undeclaredArgs = (def) => {
1217
1214
  if (/^[A-Za-z_$][\\w$]*$/.test(key)) reads.push(key)
1218
1215
  }
1219
1216
  }
1220
- return [...new Set(reads)].filter((r) => !declared.includes(r))
1217
+ return [...new Set(reads)]
1218
+ }
1219
+ const undeclaredArgs = (def) => {
1220
+ const props = def?.parameters?.properties
1221
+ if (!props || typeof props !== 'object') return []
1222
+ const declared = Object.keys(props)
1223
+ return argReads(def).filter((r) => !declared.includes(r))
1221
1224
  }
1222
1225
  const noopService = () => new Proxy(function () {}, {
1223
1226
  get: (target, prop) => {
@@ -1249,6 +1252,8 @@ ctx.tools = {
1249
1252
  hasOutputSchema: Boolean(def && def.output && def.output.schema),
1250
1253
  schema: (def && def.output && def.output.schema) || null,
1251
1254
  parameters: (def && def.parameters) || null,
1255
+ description: typeof def?.description === 'string' ? def.description : '',
1256
+ argReads: argReads(def),
1252
1257
  cardKind: card,
1253
1258
  undeclaredParams: undeclaredArgs(def),
1254
1259
  run: typeof def?.execute === 'function' ? def.execute : null,
@@ -1477,7 +1482,47 @@ export async function checkToolRegistrations(dir, opts = {}) {
1477
1482
  else seen.add(t.name)
1478
1483
 
1479
1484
  if (!t.hasExecute) out.push(F('tool-missing-execute', 'error', `tool "${label}" has no execute function`, "It appears in the model's tool list and throws the moment it is called.", 'Add execute(args, exec).'))
1480
- if (!t.hasOutputSchema) out.push(F('tool-missing-output-schema', 'warn', `tool "${label}" declares no output.schema`, 'The host validates tool output against output.schema; without one the contract is implicit and a malformed result reaches the caller.', "Add output: { schema: { type: 'json' }, render }."))
1485
+
1486
+ // The description is the only thing that tells the model what the tool is
1487
+ // for. Without one the tool is a name in a list, and a name is not a
1488
+ // specification: it gets called for the wrong thing or not at all.
1489
+ const desc = typeof t.description === 'string' ? t.description.trim() : ''
1490
+ if (!desc) {
1491
+ out.push(
1492
+ F(
1493
+ 'tool-no-description',
1494
+ 'error',
1495
+ `tool "${label}" has no description`,
1496
+ "The model chooses tools from name + description + parameters. With no description it has to guess from the name alone, so the tool is shipped but effectively unselectable.",
1497
+ 'Write one sentence: what it reads, and what it returns.',
1498
+ ),
1499
+ )
1500
+ } else if (desc.length < 30 || desc.toLowerCase() === String(label).toLowerCase()) {
1501
+ out.push(
1502
+ F(
1503
+ 'tool-description-thin',
1504
+ 'warn',
1505
+ `tool "${label}" has a ${desc.length}-character description`,
1506
+ 'A description that only repeats the name adds nothing the model did not already have, so it picks between tools on vibes.',
1507
+ 'Say what it takes and what it gives back, in one sentence.',
1508
+ ),
1509
+ )
1510
+ }
1511
+
1512
+ // A tool that reads args but declares no parameters schema: the model is
1513
+ // given no argument list at all, so every field it must send is invisible.
1514
+ if (!t.parameters && Array.isArray(t.argReads) && t.argReads.length > 0) {
1515
+ out.push(
1516
+ F(
1517
+ 'tool-no-parameters',
1518
+ 'error',
1519
+ `tool "${label}" reads args.${t.argReads[0]} but declares no parameters schema`,
1520
+ 'The host renders parameters into the prompt. With none, the model is never told which arguments exist, so it cannot send them.',
1521
+ 'Declare parameters: { type: "object", properties: { … } }.',
1522
+ ),
1523
+ )
1524
+ }
1525
+ if (!t.hasOutputSchema) out.push(F('tool-missing-output-schema', 'tool-no-description', 'tool-description-thin', 'tool-no-parameters', 'warn', `tool "${label}" declares no output.schema`, 'The host validates tool output against output.schema; without one the contract is implicit and a malformed result reaches the caller.', "Add output: { schema: { type: 'json' }, render }."))
1481
1526
  if (t.cardKind && typeof t.cardKind === 'object') {
1482
1527
  out.push(F('tool-present-call-throws', 'error', `tool "${label}" presentCall() throws`, `presentCall renders the transcript card and must be total: ${t.cardKind.threw}`, 'Do not read args fields that may be absent.'))
1483
1528
  } else if (typeof t.cardKind === 'string' && !CARD_KINDS.has(t.cardKind)) {
@@ -1786,6 +1831,24 @@ if (!records.parseError) {
1786
1831
  else records.evalError = text
1787
1832
  }
1788
1833
  }
1834
+ // The same bundle can reach the page twice: its own patch inserts it, and any
1835
+ // sidebar-style plugin that injects dsh-client-modules inserts it again. The
1836
+ // loader keys factories by id and throws on the second registration, which
1837
+ // aborts web boot. Evaluate a second time against the same window and see
1838
+ // whether load() is called again; a window-level guard makes it a no-op.
1839
+ records.doubleLoad = null
1840
+ if (!records.parseError && records.loaded > 0) {
1841
+ let second = 0
1842
+ const first = globalThis.window.__ModuleLoader__.load
1843
+ globalThis.window.__ModuleLoader__.load = (spec) => {
1844
+ second += 1
1845
+ throw new Error('duplicate factory registration: ' + String(spec && spec.id))
1846
+ }
1847
+ try { new Function(source)() } catch (e) {
1848
+ records.doubleLoad = second > 0 ? String(e && e.message || e) : null
1849
+ }
1850
+ globalThis.window.__ModuleLoader__.load = first
1851
+ }
1789
1852
  process.stdout.write('\\n@@PG@@' + JSON.stringify(records))
1790
1853
  process.exit(0)
1791
1854
  `
@@ -1847,6 +1910,17 @@ export async function checkClientRuntime(dir, opts = {}) {
1847
1910
  if (r.applyError) {
1848
1911
  out.push(F('client-apply-throws', 'error', `apply() in ${target} throws: ${String(r.applyError).slice(0, 160)}`, 'Registration happens inside apply(); a throw there leaves the panel unregistered while the host half keeps working.', 'Wrap the failing registration, or fix the cause.'))
1849
1912
  }
1913
+ if (r.doubleLoad) {
1914
+ out.push(
1915
+ F(
1916
+ 'client-double-load',
1917
+ 'warn',
1918
+ `${target} registers a factory again when evaluated a second time (${String(r.doubleLoad).slice(0, 120)})`,
1919
+ 'A client bundle can reach the page twice: its own patch inserts it, and any sidebar-style plugin that injects dsh-client-modules inserts it again. The loader keys factories by id and throws on the second registration, which aborts web boot — the whole panel disappears, not just this one.',
1920
+ 'Guard the load call with a window-level flag: if (window.__myPluginLoaded) return; window.__myPluginLoaded = true.',
1921
+ ),
1922
+ )
1923
+ }
1850
1924
  if (r.hasApply && !r.applyError && r.registrations.length === 0) {
1851
1925
  out.push(
1852
1926
  F(
@@ -2234,7 +2308,7 @@ const RULE_ORDER = [
2234
2308
  'missing-field', 'not-esm', 'files-omits-lib', 'files-omits-patch', 'no-exports', 'client-without-export', 'client-without-platform',
2235
2309
  'entry-path-missing', 'bundle-patch-not-declared', 'no-publish-config-access', 'private-field-set', 'publish-config-registry',
2236
2310
  'client-file-missing', 'client-no-module-loader', 'client-parse-failed', 'client-load-not-called',
2237
- 'client-factory-throws', 'client-no-apply', 'client-apply-throws', 'client-no-registration',
2311
+ 'client-factory-throws', 'client-no-apply', 'client-apply-throws', 'client-no-registration', 'client-double-load',
2238
2312
  'client-inject-missing-slots', 'client-inject-missing-sidebarRightTabs', 'client-unavailable',
2239
2313
  'no-patch-file', 'no-insert-row', 'insert-missing-name', 'insert-name-mismatch',
2240
2314
  'patch-name-unresolvable', 'no-name-export', 'no-apply-export', 'no-inject', 'require-in-esm',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bushizhanghan/dsh-publish-gate",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Pre-publish gate for DSH plugins: manifest, dependencies, artifact, runtime, browser half, traceability and market readiness — plus an input fuzzer that drives every declared parameter with the wrong type. 发布前的七层门禁 + 入参模糊测试。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",