Skip to content

Commit dc345d5

Browse files
authored
Merge pull request #8 from stack-wuh/feature/20260917-feature-help-compact-noise
help 输出减噪:默认摘要,--full 展开结构化目录
2 parents 472e9ca + 2977a41 commit dc345d5

8 files changed

Lines changed: 123 additions & 16 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ stdout 的 JSON 契约之外,CLI 在 stderr 渲染一层人类提示:进场
3333
- 语言解析:`--lang zh|en` > `SHADOW_DEV_LANG` > 系统 locale 自动探测 > 默认 `zh`。非法取值报 `INVALID_LANG`(退出码 2)。
3434
- 关闭提示:`SHADOW_DEV_QUIET=1`(或 `true`)时 stderr 零输出,适合日志管道。
3535
- 语言只影响 stderr 文案;错误 code、JSON 结构、`nextStep` 模板均不本地化。
36-
- `shadow-dev help` 输出全部命令的结构化目录(`data.help` 保留原字符串 + `data.commands` 明细);`shadow-dev help <命令>` 查看单命令的参数、必填项与示例。
36+
- `shadow-dev help` 概览默认只回最小面:`data.help`(命令一览字符串,约 350 字节);agent 需要结构化明细(usage/参数/必填/示例/nextStep)时用 `shadow-dev help --full`。`shadow-dev help <命令>` 查看单组详情,恒定结构化(组面小)。stderr 中文命令表不受 `--full` 影响。
3737

3838
## 平台兼容
3939

‎cli.mjs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ async function planDomain(c, mod, r, o) {
5353
return e
5454
}
5555

56-
function helpEnvelope(p) {
56+
// 概览默认最小面(data.help 恒字符串);结构化目录经 --full opt-in;组详情恒定返回该组 commands
57+
function helpEnvelope(p, o) {
5758
const g = p[1]
58-
if (!g) return { ok: true, command: 'help', data: { help: HELP, commands: COMMANDS } }
59+
if (!g) return { ok: true, command: 'help', data: o.full ? { help: HELP, commands: COMMANDS } : { help: HELP } }
5960
const commands = Object.fromEntries(Object.entries(COMMANDS).filter(([k]) => k === g || k.startsWith(g + '.')))
6061
if (!Object.keys(commands).length) throw err('UNKNOWN_COMMAND', `unsupported command: help ${g}`)
6162
return { ok: true, command: `help.${g}`, data: { help: Object.values(commands).map(e => e.usage).join('\n'), commands } }
@@ -88,7 +89,7 @@ try {
8889
const t0 = Date.now()
8990
let v
9091
if (helpMode) {
91-
v = helpEnvelope(p)
92+
v = helpEnvelope(p, o)
9293
human.printHelp(L, v)
9394
} else {
9495
human.enter(L, p)

‎lib/args.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function args(a) {
66
const p = [], o = {}
77
for (let i = 0; i < a.length; i++) {
88
if (!a[i].startsWith('--')) p.push(a[i])
9-
else { const k = a[i].slice(2); if (['confirm', 'json'].includes(k)) o[k] = true; else o[k] = a[++i] }
9+
else { const k = a[i].slice(2); if (['confirm', 'json', 'full'].includes(k)) o[k] = true; else o[k] = a[++i] }
1010
}
1111
return { p, o }
1212
}

‎lib/human.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ export function error(L, e, p) {
2020
}
2121

2222
export function printHelp(L, v) {
23-
const commands = v.data.commands
2423
if (v.command === 'help') {
2524
w(ui(L, 'helpHead'))
2625
w(ui(L, 'globals'))
27-
for (const key of Object.keys(commands)) {
28-
const e = commands[key]
26+
// 人用表与 stdout 是否带 commands 无关,恒从命令目录直读
27+
for (const key of Object.keys(COMMANDS)) {
28+
const e = COMMANDS[key]
2929
w(` ${e.usage.padEnd(26)} ${e.summary[L] ?? e.summary.zh}`)
3030
}
3131
return
3232
}
33-
for (const e of Object.values(commands)) {
33+
for (const e of Object.values(v.data.commands)) {
3434
w(` ${e.usage} — ${e.summary[L] ?? e.summary.zh}`)
3535
for (const a of e.args) w(` ${a.flag}${a.required ? ' *' : ' '} ${a.desc[L] ?? a.desc.zh}`)
3636
w(ui(L, 'example', { example: e.example }))
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
{
3+
"schema": "shadow-dev/v1",
4+
"name": "20260917-feature-help-compact-noise",
5+
"type": "feature",
6+
"scope": "cli.mjs,lib",
7+
"status": "published",
8+
"baseBranch": "main",
9+
"branch": "feature/20260917-feature-help-compact-noise",
10+
"files": [
11+
"README.md",
12+
"cli.mjs",
13+
"lib/args.mjs",
14+
"lib/human.mjs",
15+
"shadow-docs/knowledge/cli-output-contract.md",
16+
"shadow-docs/menu.md",
17+
"test/cli.test.mjs"
18+
],
19+
"github": {
20+
"repository": "stack-wuh/shadow-dev-cli",
21+
"issue": 7,
22+
"issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/7",
23+
"pullRequest": 8,
24+
"pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/8"
25+
},
26+
"review": {
27+
"conclusion": "passed",
28+
"verifiedCommit": "3ce1f05be660649cd2b1ccc89c65c280fe4939f2",
29+
"verifiedAt": "2026-09-17T08:06:11.158Z"
30+
},
31+
"workflow": {
32+
"operation": null,
33+
"checkpoint": "pr:8",
34+
"planHash": "018c3ab86a8c050e54123235ca55b59fa7f45c79a058fa6e8fc992a786e48f07",
35+
"updatedAt": null,
36+
"lastError": null,
37+
"issuePlan": {
38+
"title": "help 输出减噪:默认摘要,--full 展开结构化目录",
39+
"body": "交互终端实测反馈:help 默认全量 JSON 噪音过大。概览默认只留 data.help 字符串(恒形状),结构化明细经 --full opt-in;组详情不变。更新 cli-output-contract 卡片。",
40+
"labels": [
41+
"feature"
42+
]
43+
}
44+
},
45+
"knowledge": {
46+
"action": "更新",
47+
"target": "shadow-docs/knowledge/cli-output-contract.md",
48+
"reason": "输出面规则扩展:概览默认最小面(data.help 字符串恒形状),结构化 commands 经 --full opt-in,组详情恒定结构化"
49+
}
50+
}
51+
---
52+
53+
# help 输出减噪:默认摘要,--full 展开结构化目录
54+
55+
## 动机
56+
57+
#2 给人用层交付后,交互终端实测反馈暴露新问题:`shadow-dev --help` 默认把 27 条命令的全量 JSON(双语 desc、示例、next 模板)一次性砸进 stdout,人看到红框噪音,机器其实只需要按需取用。默认面应该匹配默认受众:人跑 `--help` 要的是表(已在 stderr),agent 要目录时才付全量。
58+
59+
## 引用规范
60+
61+
- shadow-docs/knowledge/cli-output-contract.md
62+
- 当前结论: stdout 恒为单行 JSON 契约,与语言/环境无关;人用层只走 stderr;code 不本地化。
63+
- 适用 scope: cli.mjs, lib/args.mjs, lib/human.mjs — 本变更只收缩 stdout 默认面,不触碰双通道分层;卡片需随 ship 更新(知识动作=更新)。
64+
- norms/code-style.md
65+
- 当前结论: 渐进式治理;公共能力从稳定公开入口导出。
66+
- 适用 scope: lib/args.mjs、cli.mjs
67+
68+
## 决策
69+
70+
- **选型:** `--full` 视图开关——`help` 概览默认 `data: { help: HELP }`(恒字符串形态,零结构噪音);`shadow-dev help --full` 才附带 `data.commands` 全目录。`help <命令>` 组详情保持 `{help, commands}` 不变(组面本就小且面向查询)。stderr 人用表不受影响。
71+
- **对比方案:** ① 回到裸字符串 `data: HELP`——丢 `data.help` 稳定形态,#2 刚立的字段又变卦,否;② TTY 下省略 stdout JSON——环境依赖输出,方案 B 死灰,违反 cli-output-contract,否;③ 永远全量不动——无视实测反馈,否。
72+
- **理由:** 默认即契约的"最小充分面":概览 JSON 一屏可读(<200 字节),agent 需要目录时显式 `--full` 付费。`data.help` 类型跨版本稳定,#2 的向后兼容承诺不回收,只是把 `commands` 降级为 opt-in。
73+
74+
## 任务
75+
76+
### Phase 1 — 契约测试先行(TDD)
77+
78+
- [x] 改造存量 help 测试:默认概览 `data.commands` 不存在、`--full` 才有全目录、`data.help` 恒为字符串;组详情结构不变 —— `test/cli.test.mjs`
79+
- [x] 存量 47 项中受影响断言同步适配,其余保持绿色 —— `test/cli.test.mjs`
80+
81+
### Phase 2 — 实现
82+
83+
- [x] `args()` 布尔参数表加入 `full` —— `lib/args.mjs`
84+
- [x] `helpEnvelope`:概览默认 `{help}`,`o.full` 时加 `commands`;组详情路径不变;`human.printHelp` 概览改从 COMMANDS 直读渲染 —— `cli.mjs` `lib/human.mjs`
85+
86+
### Phase 3 — 文档与知识
87+
88+
- [x] README help 段落更新(默认摘要 / --full 展开 / 组详情) —— `README.md`
89+
- [x] 更新卡片 `cli-output-contract.md`(help 输出面规则 + source 追加本 brief)与 menu 关键词 —— `shadow-docs/knowledge/cli-output-contract.md` `shadow-docs/menu.md`
90+
91+
## 结果
92+
93+
- 实际耗时: 约 15 分钟
94+
- 验证: `node --test` 47/47 通过(help 契约改造后全量回归);实测默认概览 stdout 348 字节(改造前 17,300,降噪 98%),`--full` 全目录 17,300 不变;stderr 中文表不受影响;修复编辑过程遗留的 human.mjs 双循环头语法错误(`node --check` 当场拦截)。
95+
96+
## 知识评估
97+
98+
- **预期影响:** 更新
99+
- **候选卡片:** shadow-docs/knowledge/cli-output-contract.md
100+
- **理由:** 输出面规则新增"概览默认最小面、结构化明细 opt-in(--full)"条款,是该卡片的直接扩展;同卡合并,不新增。

‎shadow-docs/knowledge/cli-output-contract.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ scope: [cli.mjs, lib/output.mjs, lib/human.mjs, lib/i18n.mjs, lib/commands.mjs]
66
status: active
77
source:
88
- changes/20260917-feature-human-cli-ux/brief.md
9+
- changes/20260917-feature-help-compact-noise/brief.md
910
verified: 2026-09-17
1011
---
1112

@@ -20,7 +21,7 @@ verified: 2026-09-17
2021
- 任何新增输出必须二选一:进 stdout JSON 契约(视为公开 API,需测试钉住),或进 stderr 人用层;**禁止**向 stdout 写非 JSON 内容。
2122
- 错误 code 与 `data.nextStep` 模板永不本地化;语言链固定为 `--lang` > `SHADOW_DEV_LANG` > locale 探测 > 默认 zh,且只影响 stderr 文案。
2223
- `nextStep` 为 additive 字段,写入发生在 planHash 持久化与计算之后,不得参与 hash 输入。
23-
- help 的 `data.help` 保留原字符串字段向后兼容,结构化明细放 `data.commands`。
24+
- help 的 `data.help` 恒为字符串(概览默认唯一字段,最小面 <1KB);结构化目录 `data.commands` 经 `--full` opt-in;`help <命令>` 组详情恒定返回该组 `commands`。
2425
- 语言不变性由契约测试保护(同命令 zh/en stdout 逐字节一致),触碰输出面的变更必须保持其绿色。
2526

2627
## 适用边界

‎shadow-docs/menu.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
| 技术域 | 关键词 | 应查阅 |
88
|--------|--------|--------|
99
| brief 读写 | brief frontmatter 行尾 CRLF autocrlf BRIEF_FRONTMATTER_REQUIRED planHash | knowledge/brief-frontmatter-crlf.md |
10-
| CLI 输出面 | stdout stderr JSON 契约 语言 i18n 本地化 nextStep help 提示 QUIET | knowledge/cli-output-contract.md |
10+
| CLI 输出面 | stdout stderr JSON 契约 语言 i18n 本地化 nextStep help 提示 QUIET --full 概览 摘要 减噪 | knowledge/cli-output-contract.md |
1111
| plan/execute 凭证 | planHash hash 漂移 norm changedFiles 脏工作区 porcelain trim 凭证链 | knowledge/plan-credential-chain.md |

‎test/cli.test.mjs‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,19 @@ test('mutating results carry a stable untranslated nextStep in JSON', () => {
232232
assert.match(en.stderr, /next/i)
233233
})
234234

235-
test('help is structured: legacy string kept, per-command detail, language-invariant stdout', () => {
235+
test('help defaults to a compact summary; --full adds the structured catalog', () => {
236236
const overview = run(['help', '--lang', 'zh'])
237237
const data = JSON.parse(overview.stdout).data
238+
assert.equal(typeof data.help, 'string')
238239
assert.match(data.help, /repo inspect/)
239-
assert.equal(data.commands['branch.execute'].usage, 'branch execute')
240-
assert.deepEqual(Object.keys(data.commands['change.create'].summary).sort(), ['en', 'zh'])
241-
const detail = run(['help', 'branch'])
242-
assert.equal(JSON.parse(detail.stdout).command, 'help.branch')
240+
assert.equal(data.commands, undefined, 'default overview must not embed the full catalog')
241+
assert.ok(overview.stdout.length < 1024, 'compact overview stdout stays small')
242+
const full = JSON.parse(run(['help', '--full', '--lang', 'zh']).stdout).data
243+
assert.equal(full.commands['branch.execute'].usage, 'branch execute')
244+
assert.deepEqual(Object.keys(full.commands['change.create'].summary).sort(), ['en', 'zh'])
245+
const detail = JSON.parse(run(['help', 'branch']).stdout)
246+
assert.equal(detail.command, 'help.branch')
247+
assert.ok(detail.data.commands['branch.plan'])
243248
assert.equal(overview.stdout, run(['help', '--lang', 'en']).stdout)
244249
})
245250

0 commit comments

Comments
 (0)