@dsh-workbench/module-tools 0.2.9 → 0.2.10
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/package.json +2 -2
- package/src/checks/contracts.mjs +132 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"esbuild": "^0.28.2",
|
|
24
|
-
"@dsh-workbench/shared": "0.2.
|
|
24
|
+
"@dsh-workbench/shared": "0.2.10"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"selfcheck": "node scripts/selfcheck.mjs"
|
package/src/checks/contracts.mjs
CHANGED
|
@@ -381,9 +381,100 @@ function probeCallOf(tool) {
|
|
|
381
381
|
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
382
382
|
const args = {};
|
|
383
383
|
for (const key of required) args[key] = placeholderOf(properties[key]);
|
|
384
|
+
if (Object.prototype.hasOwnProperty.call(properties, 'confirm')) args.confirm = true;
|
|
384
385
|
return { arguments: args, shaped: true };
|
|
385
386
|
}
|
|
386
387
|
|
|
388
|
+
// 审批头行(dsh 那张卡的 `.headline`)的预算。
|
|
389
|
+
//
|
|
390
|
+
// 为什么要有这条:`.headline` 是**纯文本的一格**,没有 `white-space` —— 换行会被塌成空格,
|
|
391
|
+
// 长句就不是"多几行"而是"一大坨"。三段都会落进那一格(外壳按 `明细 —— 问句 —— 附注` 拼):
|
|
392
|
+
// · `approvalDetail` 的 zh/en = 明细(模块写的"这次要动什么",本该是一句短标题)
|
|
393
|
+
// · `question` 的 zh/en = 问句(外壳拼在明细后面的那句"允许吗")
|
|
394
|
+
// · `fallback` 的 zh/en = 兜底整句(明细缺失时顶上来)
|
|
395
|
+
// 长内容(文件路径 / diff / 列表 / 正文 / 多字段)该走模块的 `approval.render`:
|
|
396
|
+
// 那是卡上**另一格**(`conversation.approval.detail`),有排版、能换行、能画 diff。
|
|
397
|
+
// 实测(2026-10-16 一条真实审批卡):明细里塞了一整条 commit message、问句 77 字、附注 75 字,
|
|
398
|
+
// 屏幕上 5 行 300 字。
|
|
399
|
+
export const HEAD_BUDGETS = { detail: 20, question: 40, fallback: 40 };
|
|
400
|
+
|
|
401
|
+
// 英文那一侧只用来抓"真正的一坨":按中文的 3 倍算。长度主要看中文那侧(用户读的是它),
|
|
402
|
+
// 实测同一句的英文是中文的 2.2–2.8 倍(machine 直译的风格),按 2 倍算会把正常英文问句全判红。
|
|
403
|
+
const EN_RATIO = 3;
|
|
404
|
+
|
|
405
|
+
// 长度预算是**两轮**的:
|
|
406
|
+
// · 第 1 轮(占位短值,见 probeCallOf):紧预算 —— 头行本来就该是一句短句;
|
|
407
|
+
// · 第 2 轮(长值,见 budgetProbeOf):宽 3 倍 —— 专抓"把可变长字段塞进头行"。
|
|
408
|
+
// 宽 3 倍是故意的:对象名、文件名这类标识符本来就有长有短,不该因为"名字长"判红;
|
|
409
|
+
// 而一条 commit message、一段正文(60 字)塞进标题,3 倍也照样兜不住 —— 那才是要抓的。
|
|
410
|
+
const LONG_SCALE = 3;
|
|
411
|
+
|
|
412
|
+
// 只按必填项造的探针(`probeCallOf`)给的是**占位短值**(`probe` / `1` / `true`)——
|
|
413
|
+
// 于是"把可变长字段塞进头行"这件事它是**瞎的**:实测那条真实审批卡的明细是
|
|
414
|
+
// `改对象文件:对象「diep-dify」的「tencent-test-values.yaml」 仓库:helm 提交信息:<一整条 commit message>`(157 字),
|
|
415
|
+
// 而探针上只有 20 字(`probe`)⇒ 判据照绿。所以第 2 轮把**自由文本字段**换成真长度(60 字):
|
|
416
|
+
// 名字里带 message / note / reason / body / text / content / comment / title / description / summary
|
|
417
|
+
// 的字段(这些就是"人可以写一段话"的地方),其余 string(标识符)给 12 字的正常长度。
|
|
418
|
+
const FREE_TEXT = /message|note|reason|body|text|content|comment|title|description|summary/i;
|
|
419
|
+
const LONG_TEXT = '这里是一段真实长度的正文用来量头行预算不要把这种内容放进标题里'.repeat(2);
|
|
420
|
+
const LONG_ID = '真实长度的名字十二个字';
|
|
421
|
+
|
|
422
|
+
function budgetProbeOf(tool) {
|
|
423
|
+
const schema = tool?.parameters;
|
|
424
|
+
if (typeof schema !== 'object' || schema === null) return undefined;
|
|
425
|
+
const properties = typeof schema.properties === 'object' && schema.properties !== null ? schema.properties : {};
|
|
426
|
+
const args = {};
|
|
427
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
428
|
+
if (key === 'confirm') {
|
|
429
|
+
args.confirm = true;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (Array.isArray(prop?.enum) && prop.enum.length > 0) args[key] = prop.enum[0];
|
|
433
|
+
else if (prop?.type === 'string') args[key] = FREE_TEXT.test(key) ? LONG_TEXT : LONG_ID;
|
|
434
|
+
else if (prop?.type === 'number' || prop?.type === 'integer') args[key] = 1;
|
|
435
|
+
else if (prop?.type === 'boolean') args[key] = true;
|
|
436
|
+
else if (prop?.type === 'array') args[key] = [];
|
|
437
|
+
else if (prop?.type === 'object') args[key] = {};
|
|
438
|
+
}
|
|
439
|
+
return args;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function headlineProblemsOf(name, toolName, verdict, facts, when = '', scale = 1) {
|
|
443
|
+
const problems = [];
|
|
444
|
+
const blocked = facts !== undefined && 'blocked' in facts;
|
|
445
|
+
const hasDetail = facts !== undefined && !blocked;
|
|
446
|
+
const segments = [
|
|
447
|
+
['明细(approvalDetail)', 'detail', hasDetail ? text(facts.zh) : '', hasDetail ? text(facts.en) : ''],
|
|
448
|
+
['问句(question)', 'question', text(verdict?.question?.zh), text(verdict?.question?.en)],
|
|
449
|
+
['兜底句(fallback)', 'fallback', text(verdict?.fallback?.zh), text(verdict?.fallback?.en)],
|
|
450
|
+
];
|
|
451
|
+
for (const [label, key, zh, en] of segments) {
|
|
452
|
+
if (zh === '' && en === '') continue;
|
|
453
|
+
if (/\n|\r/.test(zh) || /\n|\r/.test(en)) {
|
|
454
|
+
problems.push(
|
|
455
|
+
`${name}: 写工具 ${toolName} 的${label}${when}里有换行 —— 审批头行是纯文本、没有 white-space,` +
|
|
456
|
+
`换行会被塌成空格,屏幕上就是一坨。压成一句,长内容放 approval.render(卡上那一格详情)`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const cap = HEAD_BUDGETS[key] * scale;
|
|
460
|
+
const enCap = Math.round(cap * EN_RATIO);
|
|
461
|
+
if (zh.length > cap) {
|
|
462
|
+
problems.push(
|
|
463
|
+
`${name}: 写工具 ${toolName} 的${label}${when}太长(${zh.length} 字 > 预算 ${cap} 字):「${zh}」—— ` +
|
|
464
|
+
`头行三段各有预算(明细 ≤${HEAD_BUDGETS.detail} / 问句 ≤${HEAD_BUDGETS.question} / ` +
|
|
465
|
+
`兜底句 ≤${HEAD_BUDGETS.fallback} 字),其余(列表 / 正文 / 多字段 / 可变长内容)放进 approval.render 那一格`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (en.length > enCap) {
|
|
469
|
+
problems.push(
|
|
470
|
+
`${name}: 写工具 ${toolName} 的${label}${when}(英文)太长(${en.length} 字符 > 预算 ${enCap}):「${en}」—— ` +
|
|
471
|
+
`英文侧按中文的 ${EN_RATIO} 倍算,超了同样挪进 approval.render`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return problems;
|
|
476
|
+
}
|
|
477
|
+
|
|
387
478
|
function installed(specifier, from) {
|
|
388
479
|
try {
|
|
389
480
|
createRequire(join(from, 'package.json')).resolve(`${packageOf(specifier)}/package.json`);
|
|
@@ -429,6 +520,7 @@ export async function run() {
|
|
|
429
520
|
// 判据"看到了但下不了结论"的那些,写进 notes(红绿两边都印)——
|
|
430
521
|
// 与其给一个撑不住的 verdict,不如把作者该自己确认的那一件事说清楚(ADR-0038 §6.15)。
|
|
431
522
|
const probeNotes = [];
|
|
523
|
+
const headlineNotes = [];
|
|
432
524
|
let declared = 0;
|
|
433
525
|
let modules = 0;
|
|
434
526
|
|
|
@@ -894,6 +986,44 @@ export async function run() {
|
|
|
894
986
|
('blocked' in facts
|
|
895
987
|
? text(facts.blocked) !== ''
|
|
896
988
|
: text(facts.zh) !== '' && text(facts.en) !== '');
|
|
989
|
+
problems.push(...headlineProblemsOf(name, tool.name, verdict, facts));
|
|
990
|
+
// 第二轮:**长值**探(把声明里的 string 都填成 40 字正文、可选字段也填上),
|
|
991
|
+
// 专抓"标题里带了可变长字段"—— 第一轮用的是占位短值,对这件事是瞎的(见 budgetProbeOf)。
|
|
992
|
+
const longArgs = budgetProbeOf(registered.get(tool.name));
|
|
993
|
+
if (longArgs !== undefined) {
|
|
994
|
+
const longCall = { ...call, arguments: longArgs };
|
|
995
|
+
let longVerdict;
|
|
996
|
+
try {
|
|
997
|
+
longVerdict = mounted.needsApproval(longCall);
|
|
998
|
+
} catch {
|
|
999
|
+
longVerdict = undefined;
|
|
1000
|
+
}
|
|
1001
|
+
if (longVerdict?.kind === 'ask') {
|
|
1002
|
+
let longFacts;
|
|
1003
|
+
try {
|
|
1004
|
+
longFacts = await mounted.approvalDetail?.(longCall);
|
|
1005
|
+
} catch {
|
|
1006
|
+
longFacts = undefined;
|
|
1007
|
+
}
|
|
1008
|
+
problems.push(
|
|
1009
|
+
...headlineProblemsOf(
|
|
1010
|
+
name,
|
|
1011
|
+
tool.name,
|
|
1012
|
+
longVerdict,
|
|
1013
|
+
longFacts,
|
|
1014
|
+
'(把自由文本字段填上真实长度之后)',
|
|
1015
|
+
LONG_SCALE,
|
|
1016
|
+
),
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (facts === undefined || 'blocked' in facts) {
|
|
1021
|
+
headlineNotes.push(
|
|
1022
|
+
`${name}: 写工具 ${tool.name} 的 approvalDetail 在合成调用上没有明细(要真的计划/预览过才有)—— ` +
|
|
1023
|
+
`头行预算只验到问句与兜底句那一截;明细那一句作者自己按「一句话、≤${HEAD_BUDGETS.detail} 字」看一眼:` +
|
|
1024
|
+
`超了就把长内容放进 approval.render 那一格`,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
897
1027
|
if (!described) {
|
|
898
1028
|
problems.push(
|
|
899
1029
|
`${name}: 写工具 ${tool.name} 的 needsApproval 对合成调用回了 ask(要点头),` +
|
|
@@ -942,7 +1072,7 @@ export async function run() {
|
|
|
942
1072
|
}
|
|
943
1073
|
|
|
944
1074
|
if (problems.length > 0) {
|
|
945
|
-
const notes = [`${declared} 个带 dsh 声明的包,其中 ${modules} 个模块`, ...probeNotes];
|
|
1075
|
+
const notes = [`${declared} 个带 dsh 声明的包,其中 ${modules} 个模块`, ...probeNotes, ...headlineNotes];
|
|
946
1076
|
// 落不到图里最常见的成因不是"声明写错",而是"这个包不在任何 workspace 里"(裸目录里 init
|
|
947
1077
|
// 出来的骨架)。不点出来的话,屏幕上的话会把责任推给那两条声明,而真正该做的是换到工坊里跑。
|
|
948
1078
|
const bare = [...unresolvedIn].filter(barePackage);
|
|
@@ -964,6 +1094,7 @@ export async function run() {
|
|
|
964
1094
|
'(test 不在这儿跑:状态级的东西在模块自己的 vitest 里 —— 四个脚本自己跑一遍)',
|
|
965
1095
|
`单元:${unitList}`,
|
|
966
1096
|
...probeNotes,
|
|
1097
|
+
...headlineNotes,
|
|
967
1098
|
],
|
|
968
1099
|
problems: [],
|
|
969
1100
|
};
|