From 3ce1f05be660649cd2b1ccc89c65c280fe4939f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=BA=A202?= <596540@ky-tech.com.cn> Date: Thu, 17 Sep 2026 16:05:55 +0800 Subject: [PATCH 1/2] feat(help): compact overview by default, structured catalog behind --full --- README.md | 2 +- cli.mjs | 7 +- lib/args.mjs | 2 +- lib/human.mjs | 8 +- .../brief.md | 95 +++++++++++++++++++ shadow-docs/knowledge/cli-output-contract.md | 3 +- shadow-docs/menu.md | 2 +- test/cli.test.mjs | 15 ++- 8 files changed, 118 insertions(+), 16 deletions(-) create mode 100644 shadow-docs/changes/20260917-feature-help-compact-noise/brief.md diff --git a/README.md b/README.md index 1c50807..9bae7b8 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ stdout 的 JSON 契约之外,CLI 在 stderr 渲染一层人类提示:进场 - 语言解析:`--lang zh|en` > `SHADOW_DEV_LANG` > 系统 locale 自动探测 > 默认 `zh`。非法取值报 `INVALID_LANG`(退出码 2)。 - 关闭提示:`SHADOW_DEV_QUIET=1`(或 `true`)时 stderr 零输出,适合日志管道。 - 语言只影响 stderr 文案;错误 code、JSON 结构、`nextStep` 模板均不本地化。 -- `shadow-dev help` 输出全部命令的结构化目录(`data.help` 保留原字符串 + `data.commands` 明细);`shadow-dev help <命令>` 查看单命令的参数、必填项与示例。 +- `shadow-dev help` 概览默认只回最小面:`data.help`(命令一览字符串,约 350 字节);agent 需要结构化明细(usage/参数/必填/示例/nextStep)时用 `shadow-dev help --full`。`shadow-dev help <命令>` 查看单组详情,恒定结构化(组面小)。stderr 中文命令表不受 `--full` 影响。 ## 平台兼容 diff --git a/cli.mjs b/cli.mjs index 4bd2c4c..3a1c9cd 100755 --- a/cli.mjs +++ b/cli.mjs @@ -53,9 +53,10 @@ async function planDomain(c, mod, r, o) { return e } -function helpEnvelope(p) { +// 概览默认最小面(data.help 恒字符串);结构化目录经 --full opt-in;组详情恒定返回该组 commands +function helpEnvelope(p, o) { const g = p[1] - if (!g) return { ok: true, command: 'help', data: { help: HELP, commands: COMMANDS } } + if (!g) return { ok: true, command: 'help', data: o.full ? { help: HELP, commands: COMMANDS } : { help: HELP } } const commands = Object.fromEntries(Object.entries(COMMANDS).filter(([k]) => k === g || k.startsWith(g + '.'))) if (!Object.keys(commands).length) throw err('UNKNOWN_COMMAND', `unsupported command: help ${g}`) return { ok: true, command: `help.${g}`, data: { help: Object.values(commands).map(e => e.usage).join('\n'), commands } } @@ -88,7 +89,7 @@ try { const t0 = Date.now() let v if (helpMode) { - v = helpEnvelope(p) + v = helpEnvelope(p, o) human.printHelp(L, v) } else { human.enter(L, p) diff --git a/lib/args.mjs b/lib/args.mjs index c6b8edc..01ba91b 100644 --- a/lib/args.mjs +++ b/lib/args.mjs @@ -6,7 +6,7 @@ export function args(a) { const p = [], o = {} for (let i = 0; i < a.length; i++) { if (!a[i].startsWith('--')) p.push(a[i]) - else { const k = a[i].slice(2); if (['confirm', 'json'].includes(k)) o[k] = true; else o[k] = a[++i] } + else { const k = a[i].slice(2); if (['confirm', 'json', 'full'].includes(k)) o[k] = true; else o[k] = a[++i] } } return { p, o } } diff --git a/lib/human.mjs b/lib/human.mjs index 83bf8c1..3f72b32 100644 --- a/lib/human.mjs +++ b/lib/human.mjs @@ -20,17 +20,17 @@ export function error(L, e, p) { } export function printHelp(L, v) { - const commands = v.data.commands if (v.command === 'help') { w(ui(L, 'helpHead')) w(ui(L, 'globals')) - for (const key of Object.keys(commands)) { - const e = commands[key] + // 人用表与 stdout 是否带 commands 无关,恒从命令目录直读 + for (const key of Object.keys(COMMANDS)) { + const e = COMMANDS[key] w(` ${e.usage.padEnd(26)} ${e.summary[L] ?? e.summary.zh}`) } return } - for (const e of Object.values(commands)) { + for (const e of Object.values(v.data.commands)) { w(` ${e.usage} — ${e.summary[L] ?? e.summary.zh}`) for (const a of e.args) w(` ${a.flag}${a.required ? ' *' : ' '} ${a.desc[L] ?? a.desc.zh}`) w(ui(L, 'example', { example: e.example })) diff --git a/shadow-docs/changes/20260917-feature-help-compact-noise/brief.md b/shadow-docs/changes/20260917-feature-help-compact-noise/brief.md new file mode 100644 index 0000000..6cfd76b --- /dev/null +++ b/shadow-docs/changes/20260917-feature-help-compact-noise/brief.md @@ -0,0 +1,95 @@ +--- +{ + "schema": "shadow-dev/v1", + "name": "20260917-feature-help-compact-noise", + "type": "feature", + "scope": "cli.mjs,lib", + "status": "branched", + "baseBranch": "main", + "branch": "feature/20260917-feature-help-compact-noise", + "files": [ + "README.md", + "cli.mjs", + "lib/args.mjs", + "lib/human.mjs", + "shadow-docs/knowledge/cli-output-contract.md", + "shadow-docs/menu.md", + "test/cli.test.mjs" + ], + "github": { + "repository": "stack-wuh/shadow-dev-cli", + "issue": 7, + "issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/7", + "pullRequest": null, + "pullRequestUrl": null + }, + "review": { + "conclusion": "pending", + "verifiedCommit": null, + "verifiedAt": null + }, + "workflow": { + "operation": null, + "checkpoint": "issue:7", + "planHash": "5ccc421869991a94b4be9eea88ad988aaa3270a2b6473920d9c506885a32ce28", + "updatedAt": null, + "lastError": null, + "issuePlan": { + "title": "help 输出减噪:默认摘要,--full 展开结构化目录", + "body": "交互终端实测反馈:help 默认全量 JSON 噪音过大。概览默认只留 data.help 字符串(恒形状),结构化明细经 --full opt-in;组详情不变。更新 cli-output-contract 卡片。", + "labels": [ + "feature" + ] + } + } +} +--- + +# help 输出减噪:默认摘要,--full 展开结构化目录 + +## 动机 + +#2 给人用层交付后,交互终端实测反馈暴露新问题:`shadow-dev --help` 默认把 27 条命令的全量 JSON(双语 desc、示例、next 模板)一次性砸进 stdout,人看到红框噪音,机器其实只需要按需取用。默认面应该匹配默认受众:人跑 `--help` 要的是表(已在 stderr),agent 要目录时才付全量。 + +## 引用规范 + +- shadow-docs/knowledge/cli-output-contract.md + - 当前结论: stdout 恒为单行 JSON 契约,与语言/环境无关;人用层只走 stderr;code 不本地化。 + - 适用 scope: cli.mjs, lib/args.mjs, lib/human.mjs — 本变更只收缩 stdout 默认面,不触碰双通道分层;卡片需随 ship 更新(知识动作=更新)。 +- norms/code-style.md + - 当前结论: 渐进式治理;公共能力从稳定公开入口导出。 + - 适用 scope: lib/args.mjs、cli.mjs + +## 决策 + +- **选型:** `--full` 视图开关——`help` 概览默认 `data: { help: HELP }`(恒字符串形态,零结构噪音);`shadow-dev help --full` 才附带 `data.commands` 全目录。`help <命令>` 组详情保持 `{help, commands}` 不变(组面本就小且面向查询)。stderr 人用表不受影响。 +- **对比方案:** ① 回到裸字符串 `data: HELP`——丢 `data.help` 稳定形态,#2 刚立的字段又变卦,否;② TTY 下省略 stdout JSON——环境依赖输出,方案 B 死灰,违反 cli-output-contract,否;③ 永远全量不动——无视实测反馈,否。 +- **理由:** 默认即契约的"最小充分面":概览 JSON 一屏可读(<200 字节),agent 需要目录时显式 `--full` 付费。`data.help` 类型跨版本稳定,#2 的向后兼容承诺不回收,只是把 `commands` 降级为 opt-in。 + +## 任务 + +### Phase 1 — 契约测试先行(TDD) + +- [x] 改造存量 help 测试:默认概览 `data.commands` 不存在、`--full` 才有全目录、`data.help` 恒为字符串;组详情结构不变 —— `test/cli.test.mjs` +- [x] 存量 47 项中受影响断言同步适配,其余保持绿色 —— `test/cli.test.mjs` + +### Phase 2 — 实现 + +- [x] `args()` 布尔参数表加入 `full` —— `lib/args.mjs` +- [x] `helpEnvelope`:概览默认 `{help}`,`o.full` 时加 `commands`;组详情路径不变;`human.printHelp` 概览改从 COMMANDS 直读渲染 —— `cli.mjs` `lib/human.mjs` + +### Phase 3 — 文档与知识 + +- [x] README help 段落更新(默认摘要 / --full 展开 / 组详情) —— `README.md` +- [x] 更新卡片 `cli-output-contract.md`(help 输出面规则 + source 追加本 brief)与 menu 关键词 —— `shadow-docs/knowledge/cli-output-contract.md` `shadow-docs/menu.md` + +## 结果 + +- 实际耗时: 约 15 分钟 +- 验证: `node --test` 47/47 通过(help 契约改造后全量回归);实测默认概览 stdout 348 字节(改造前 17,300,降噪 98%),`--full` 全目录 17,300 不变;stderr 中文表不受影响;修复编辑过程遗留的 human.mjs 双循环头语法错误(`node --check` 当场拦截)。 + +## 知识评估 + +- **预期影响:** 更新 +- **候选卡片:** shadow-docs/knowledge/cli-output-contract.md +- **理由:** 输出面规则新增"概览默认最小面、结构化明细 opt-in(--full)"条款,是该卡片的直接扩展;同卡合并,不新增。 diff --git a/shadow-docs/knowledge/cli-output-contract.md b/shadow-docs/knowledge/cli-output-contract.md index f3ae95a..c47d2fe 100644 --- a/shadow-docs/knowledge/cli-output-contract.md +++ b/shadow-docs/knowledge/cli-output-contract.md @@ -6,6 +6,7 @@ scope: [cli.mjs, lib/output.mjs, lib/human.mjs, lib/i18n.mjs, lib/commands.mjs] status: active source: - changes/20260917-feature-human-cli-ux/brief.md + - changes/20260917-feature-help-compact-noise/brief.md verified: 2026-09-17 --- @@ -20,7 +21,7 @@ verified: 2026-09-17 - 任何新增输出必须二选一:进 stdout JSON 契约(视为公开 API,需测试钉住),或进 stderr 人用层;**禁止**向 stdout 写非 JSON 内容。 - 错误 code 与 `data.nextStep` 模板永不本地化;语言链固定为 `--lang` > `SHADOW_DEV_LANG` > locale 探测 > 默认 zh,且只影响 stderr 文案。 - `nextStep` 为 additive 字段,写入发生在 planHash 持久化与计算之后,不得参与 hash 输入。 -- help 的 `data.help` 保留原字符串字段向后兼容,结构化明细放 `data.commands`。 +- help 的 `data.help` 恒为字符串(概览默认唯一字段,最小面 <1KB);结构化目录 `data.commands` 经 `--full` opt-in;`help <命令>` 组详情恒定返回该组 `commands`。 - 语言不变性由契约测试保护(同命令 zh/en stdout 逐字节一致),触碰输出面的变更必须保持其绿色。 ## 适用边界 diff --git a/shadow-docs/menu.md b/shadow-docs/menu.md index 4b1780d..3c747c3 100644 --- a/shadow-docs/menu.md +++ b/shadow-docs/menu.md @@ -7,5 +7,5 @@ | 技术域 | 关键词 | 应查阅 | |--------|--------|--------| | brief 读写 | brief frontmatter 行尾 CRLF autocrlf BRIEF_FRONTMATTER_REQUIRED planHash | knowledge/brief-frontmatter-crlf.md | -| CLI 输出面 | stdout stderr JSON 契约 语言 i18n 本地化 nextStep help 提示 QUIET | knowledge/cli-output-contract.md | +| CLI 输出面 | stdout stderr JSON 契约 语言 i18n 本地化 nextStep help 提示 QUIET --full 概览 摘要 减噪 | knowledge/cli-output-contract.md | | plan/execute 凭证 | planHash hash 漂移 norm changedFiles 脏工作区 porcelain trim 凭证链 | knowledge/plan-credential-chain.md | diff --git a/test/cli.test.mjs b/test/cli.test.mjs index e782128..34597b4 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -232,14 +232,19 @@ test('mutating results carry a stable untranslated nextStep in JSON', () => { assert.match(en.stderr, /next/i) }) -test('help is structured: legacy string kept, per-command detail, language-invariant stdout', () => { +test('help defaults to a compact summary; --full adds the structured catalog', () => { const overview = run(['help', '--lang', 'zh']) const data = JSON.parse(overview.stdout).data + assert.equal(typeof data.help, 'string') assert.match(data.help, /repo inspect/) - assert.equal(data.commands['branch.execute'].usage, 'branch execute') - assert.deepEqual(Object.keys(data.commands['change.create'].summary).sort(), ['en', 'zh']) - const detail = run(['help', 'branch']) - assert.equal(JSON.parse(detail.stdout).command, 'help.branch') + assert.equal(data.commands, undefined, 'default overview must not embed the full catalog') + assert.ok(overview.stdout.length < 1024, 'compact overview stdout stays small') + const full = JSON.parse(run(['help', '--full', '--lang', 'zh']).stdout).data + assert.equal(full.commands['branch.execute'].usage, 'branch execute') + assert.deepEqual(Object.keys(full.commands['change.create'].summary).sort(), ['en', 'zh']) + const detail = JSON.parse(run(['help', 'branch']).stdout) + assert.equal(detail.command, 'help.branch') + assert.ok(detail.data.commands['branch.plan']) assert.equal(overview.stdout, run(['help', '--lang', 'en']).stdout) }) From 2977a41269395754c5617c46650ee47721859f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E7=BA=A202?= <596540@ky-tech.com.cn> Date: Thu, 17 Sep 2026 16:15:07 +0800 Subject: [PATCH 2/2] chore(shadow-docs): review passed + PR #8 state checkpoint --- .../brief.md | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/shadow-docs/changes/20260917-feature-help-compact-noise/brief.md b/shadow-docs/changes/20260917-feature-help-compact-noise/brief.md index 6cfd76b..2ee1fc6 100644 --- a/shadow-docs/changes/20260917-feature-help-compact-noise/brief.md +++ b/shadow-docs/changes/20260917-feature-help-compact-noise/brief.md @@ -4,7 +4,7 @@ "name": "20260917-feature-help-compact-noise", "type": "feature", "scope": "cli.mjs,lib", - "status": "branched", + "status": "published", "baseBranch": "main", "branch": "feature/20260917-feature-help-compact-noise", "files": [ @@ -20,18 +20,18 @@ "repository": "stack-wuh/shadow-dev-cli", "issue": 7, "issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/7", - "pullRequest": null, - "pullRequestUrl": null + "pullRequest": 8, + "pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/8" }, "review": { - "conclusion": "pending", - "verifiedCommit": null, - "verifiedAt": null + "conclusion": "passed", + "verifiedCommit": "3ce1f05be660649cd2b1ccc89c65c280fe4939f2", + "verifiedAt": "2026-09-17T08:06:11.158Z" }, "workflow": { "operation": null, - "checkpoint": "issue:7", - "planHash": "5ccc421869991a94b4be9eea88ad988aaa3270a2b6473920d9c506885a32ce28", + "checkpoint": "pr:8", + "planHash": "018c3ab86a8c050e54123235ca55b59fa7f45c79a058fa6e8fc992a786e48f07", "updatedAt": null, "lastError": null, "issuePlan": { @@ -41,6 +41,11 @@ "feature" ] } + }, + "knowledge": { + "action": "更新", + "target": "shadow-docs/knowledge/cli-output-contract.md", + "reason": "输出面规则扩展:概览默认最小面(data.help 字符串恒形状),结构化 commands 经 --full opt-in,组详情恒定结构化" } } ---