@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 +13 -0
- package/README.md +5 -1
- package/lib/engine.js +69 -8
- package/package.json +1 -1
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 #
|
|
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
|
|
1205
|
-
|
|
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)]
|
|
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
|
-
|
|
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.
|
|
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",
|