@bushizhanghan/dsh-publish-gate 0.10.0 → 0.12.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.12.0
4
+
5
+ - `pack-ships-junk` (warn): the tarball carries `.bak-*`, `.orig`, `.swp`, `.log`, `~` or `.DS_Store` debris.
6
+ Naming a directory in `files` takes everything inside it, so a `lib/client.js.bak-20261026` left behind
7
+ during a fix ships to every consumer and stays on npm permanently. Both weapp-loop and page-monitor had
8
+ exactly that file during this session; both were deleted by hand before this rule existed.
9
+ ## 0.11.0
10
+
11
+ - `tool-no-description` (error), `tool-description-thin` (warn), `tool-no-parameters` (error): the host
12
+ renders name + description + parameters into the prompt and nothing else, so those three fields are the
13
+ tool's entire specification. A tool with no description is a name in a list; a tool that reads args with no
14
+ schema never receives them. All nine of my plugins were clean, so this guards the shape rather than a
15
+ specific crash — the first two fixtures that had `description: 'd'` did fail, which is the point.
3
16
  ## 0.10.0
4
17
 
5
18
  - `client-double-load` (warn): the client probe now evaluates the bundle a second time against the same
package/README.md CHANGED
@@ -87,6 +87,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
87
87
  | `pack-oversize` | 解包 >5 MB 是 error、>1 MB 是 warn,并点名最大的三个文件。测试 fixture 是常见元凶——我的一个插件曾把 20 MB 的算例目录发上去,装了它的人每次都要下 20 MB |
88
88
  | `pack-ships-secret` | tarball 里有 `.env` / `.npmrc` / `*.pem` / `*.key` / `id_rsa`。没写 `files` 字段时 npm 会把 `.gitignore` 没排除的一切都打包,而包一发布就是公开的、npm 不会帮你撤回 |
89
89
  | `pack-unexpected-dir` | `fixtures` / `test` / `tests` / `src` 被打进包里 |
90
+ | `pack-ships-junk` | 包里带上了 `.bak-20261026` 这类备份或编辑器残留。`files: ["lib"]` 会把 `lib/` 里所有东西都打进去——包括你自己源码的上一版,而且发上去就永久公开 |
90
91
  | `pack-script-build` | 有 `prepack` / `prepare`:dry run 用了 `--ignore-scripts`,这次清单**不代表**真正发布时的产物 |
91
92
  | `pack-unavailable` | npm 跑不起来(warn)——产物层没跑,别把它当干净 |
92
93
 
@@ -104,6 +105,9 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
104
105
  | `tool-present-call-throws` | `presentCall` 抛——调用记录渲染时炸 |
105
106
  | `tool-bad-card-kind` | `card: 'generic'` 的 `kind` 不在宿主认识的那一组里(`other` / `execute` / `search` / `read` / `edit` / `delete` / `fetch`)。注册不报错,卡片渲染坏掉——我五个插件一起踩过 |
106
107
  | `tool-bad-parameter-type` | `parameters` 里某个 `type` 写错(`"strin"`)。宿主把 `parameters` 渲染进 prompt、但**从不校验**调用值,所以这个 typo 只是悄悄把类型提示弄丢 |
108
+ | `tool-no-description` | 工具没有 description。模型是靠 name + description + parameters 挑工具的,没有描述就只剩名字——工具发出去了,但基本选不中 |
109
+ | `tool-description-thin` | description 少于 30 字符,或者就是把工具名重复一遍。等于没写 |
110
+ | `tool-no-parameters` | execute 读 `args.x`,但整个 `parameters` 都没有。模型拿不到参数清单,不知道该传什么 |
107
111
  | `tool-undeclared-param` | `execute` 读了 `args.x`,但 `parameters.properties` 里没有 `x`。模型只能传 schema 声明过的参数,所以这个值在真实调用里永远是 `undefined`——代码在,永远不跑,也不报错。`args.x`、`args['x']`、`const { x } = args` 三种写法都认 |
108
112
  | `tool-output-invalid` | 工具返回的东西**自己的 output schema 不收**。宿主每个结果都拿 schema 校验,不匹配就抛 `ToolOutputError`——工具从第一次调用起就不可用,而报错只给一个 JSON 路径,不给是哪行代码返回的。只有点名 `call` 真的调了才检查。这个插件自己踩过:schema 收紧成 `additionalProperties: false`,但有一条错误分支还在返回 `{ ok, error }` |
109
113
  | `tool-input-throws` | 把每个声明过的参数**塞一个错类型的值**去调,工具直接抛 `TypeError` 而不是回答。宿主从不拿 `parameters` 校验入参,所以这个值真的会到你的代码里——调用方拿到的是 `The "path" argument must be of type string`,插件日志里没有一行说是哪个工具。这条规则是在自己的工作区里跑出 **12 个真实崩溃** 之后才加的(`session`/`profile`/`casePath`/`workspace` 各一个入口),现在九个插件全绿。默认关:它会把每个工具都调一遍,用 `skip` 点名排除那些会开程序、写文件的 |
@@ -217,7 +221,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
217
221
  ## 开发
218
222
 
219
223
  ```sh
220
- node --test # 99 个引擎单测
224
+ node --test # 104 个引擎单测
221
225
  node test/integration.test.mjs # 27 条集成断言(9 个真实插件作 fixture)
222
226
  ```
223
227
 
package/lib/engine.js CHANGED
@@ -1145,6 +1145,22 @@ export async function checkPack(dir, opts = {}) {
1145
1145
  if (has(d)) out.push(F('pack-unexpected-dir', 'warn', `the tarball includes ${d}/`, 'A development directory is being published: it bloats the install and ships code nobody runs.', `Add "!${d}" to files if it is not meant to ship.`))
1146
1146
  }
1147
1147
 
1148
+ // Editor and backup debris rides along whenever "files" names a directory:
1149
+ // `files: ["lib"]` ships lib/client.js.bak-20261026 without asking. npm keeps
1150
+ // it forever, and it is the previous version of your code, in public.
1151
+ const junk = files.map((f) => f.path).filter((p) => /\.(?:bak|orig|rej|swp|tmp|log)$|\.bak[-.]|~$|(?:^|\/)\.DS_Store$|(?:^|\/)node_modules\//i.test(p))
1152
+ if (junk.length > 0) {
1153
+ out.push(
1154
+ F(
1155
+ 'pack-ships-junk',
1156
+ 'warn',
1157
+ `the tarball contains ${junk.length} backup or editor file(s)`,
1158
+ `${junk.slice(0, 3).join(', ')} — naming a directory in "files" takes everything in it, so a .bak copy of your own source ships to every consumer and stays on npm forever.`,
1159
+ 'Delete the backups, or add "!lib/**/*.bak*" to files.',
1160
+ ),
1161
+ )
1162
+ }
1163
+
1148
1164
  const scripts = json?.scripts || {}
1149
1165
  const buildHook = scripts.prepack ? 'prepack' : scripts.prepare ? 'prepare' : null
1150
1166
  if (buildHook) {
@@ -1201,11 +1217,8 @@ const walkParams = (schema, path) => {
1201
1217
  const ARG_DOT = /\\bargs\\s*\\??\\.\\s*([A-Za-z_$][\\w$]*)/g
1202
1218
  const ARG_INDEX = /\\bargs\\s*\\[\\s*['"]([^'"]+)['"]\\s*\\]/g
1203
1219
  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 []
1220
+ const argReads = (def) => {
1221
+ if (typeof def?.execute !== 'function') return []
1209
1222
  let src = ''
1210
1223
  try { src = def.execute.toString() } catch { return [] }
1211
1224
  const reads = []
@@ -1217,7 +1230,13 @@ const undeclaredArgs = (def) => {
1217
1230
  if (/^[A-Za-z_$][\\w$]*$/.test(key)) reads.push(key)
1218
1231
  }
1219
1232
  }
1220
- return [...new Set(reads)].filter((r) => !declared.includes(r))
1233
+ return [...new Set(reads)]
1234
+ }
1235
+ const undeclaredArgs = (def) => {
1236
+ const props = def?.parameters?.properties
1237
+ if (!props || typeof props !== 'object') return []
1238
+ const declared = Object.keys(props)
1239
+ return argReads(def).filter((r) => !declared.includes(r))
1221
1240
  }
1222
1241
  const noopService = () => new Proxy(function () {}, {
1223
1242
  get: (target, prop) => {
@@ -1249,6 +1268,8 @@ ctx.tools = {
1249
1268
  hasOutputSchema: Boolean(def && def.output && def.output.schema),
1250
1269
  schema: (def && def.output && def.output.schema) || null,
1251
1270
  parameters: (def && def.parameters) || null,
1271
+ description: typeof def?.description === 'string' ? def.description : '',
1272
+ argReads: argReads(def),
1252
1273
  cardKind: card,
1253
1274
  undeclaredParams: undeclaredArgs(def),
1254
1275
  run: typeof def?.execute === 'function' ? def.execute : null,
@@ -1477,7 +1498,47 @@ export async function checkToolRegistrations(dir, opts = {}) {
1477
1498
  else seen.add(t.name)
1478
1499
 
1479
1500
  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 }."))
1501
+
1502
+ // The description is the only thing that tells the model what the tool is
1503
+ // for. Without one the tool is a name in a list, and a name is not a
1504
+ // specification: it gets called for the wrong thing or not at all.
1505
+ const desc = typeof t.description === 'string' ? t.description.trim() : ''
1506
+ if (!desc) {
1507
+ out.push(
1508
+ F(
1509
+ 'tool-no-description',
1510
+ 'error',
1511
+ `tool "${label}" has no description`,
1512
+ "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.",
1513
+ 'Write one sentence: what it reads, and what it returns.',
1514
+ ),
1515
+ )
1516
+ } else if (desc.length < 30 || desc.toLowerCase() === String(label).toLowerCase()) {
1517
+ out.push(
1518
+ F(
1519
+ 'tool-description-thin',
1520
+ 'warn',
1521
+ `tool "${label}" has a ${desc.length}-character description`,
1522
+ 'A description that only repeats the name adds nothing the model did not already have, so it picks between tools on vibes.',
1523
+ 'Say what it takes and what it gives back, in one sentence.',
1524
+ ),
1525
+ )
1526
+ }
1527
+
1528
+ // A tool that reads args but declares no parameters schema: the model is
1529
+ // given no argument list at all, so every field it must send is invisible.
1530
+ if (!t.parameters && Array.isArray(t.argReads) && t.argReads.length > 0) {
1531
+ out.push(
1532
+ F(
1533
+ 'tool-no-parameters',
1534
+ 'error',
1535
+ `tool "${label}" reads args.${t.argReads[0]} but declares no parameters schema`,
1536
+ 'The host renders parameters into the prompt. With none, the model is never told which arguments exist, so it cannot send them.',
1537
+ 'Declare parameters: { type: "object", properties: { … } }.',
1538
+ ),
1539
+ )
1540
+ }
1541
+ 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
1542
  if (t.cardKind && typeof t.cardKind === 'object') {
1482
1543
  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
1544
  } else if (typeof t.cardKind === 'string' && !CARD_KINDS.has(t.cardKind)) {
@@ -2275,7 +2336,7 @@ const RULE_ORDER = [
2275
2336
  'lib-debugger', 'lib-console-log', 'lib-hardcoded-path', 'lib-url-pathname', 'node-too-old', 'stale-build', 'hand-edited-build',
2276
2337
  'undeclared-import', 'dep-in-dev-only', 'unused-dependency',
2277
2338
  'pack-omits-lib', 'pack-omits-patch', 'pack-omits-client', 'pack-oversize', 'pack-omits-readme', 'pack-omits-license', 'pack-ships-secret',
2278
- 'pack-unexpected-dir', 'pack-script-build', 'pack-unavailable',
2339
+ 'pack-unexpected-dir', 'pack-ships-junk', 'pack-script-build', 'pack-unavailable',
2279
2340
  'no-license', 'license-mismatch', 'version-taken', 'version-behind-latest', 'npm-not-logged-in', 'npm-scope-mismatch', 'npm-whoami-unavailable', 'registry-unreachable', 'publish-not-served',
2280
2341
  'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
2281
2342
  'market-no-name', 'market-name-shape', 'market-no-category', 'market-bad-category', 'market-no-desc', 'market-no-en', 'market-no-zh', 'unscoped-name', 'no-git', 'no-readme', 'readme-no-install', 'readme-no-profile', 'readme-wrong-package',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bushizhanghan/dsh-publish-gate",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",