Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 影响。

## 平台兼容

Expand Down
7 changes: 4 additions & 3 deletions cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
8 changes: 4 additions & 4 deletions lib/human.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
Expand Down
100 changes: 100 additions & 0 deletions shadow-docs/changes/20260917-feature-help-compact-noise/brief.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
{
"schema": "shadow-dev/v1",
"name": "20260917-feature-help-compact-noise",
"type": "feature",
"scope": "cli.mjs,lib",
"status": "published",
"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": 8,
"pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/8"
},
"review": {
"conclusion": "passed",
"verifiedCommit": "3ce1f05be660649cd2b1ccc89c65c280fe4939f2",
"verifiedAt": "2026-09-17T08:06:11.158Z"
},
"workflow": {
"operation": null,
"checkpoint": "pr:8",
"planHash": "018c3ab86a8c050e54123235ca55b59fa7f45c79a058fa6e8fc992a786e48f07",
"updatedAt": null,
"lastError": null,
"issuePlan": {
"title": "help 输出减噪:默认摘要,--full 展开结构化目录",
"body": "交互终端实测反馈:help 默认全量 JSON 噪音过大。概览默认只留 data.help 字符串(恒形状),结构化明细经 --full opt-in;组详情不变。更新 cli-output-contract 卡片。",
"labels": [
"feature"
]
}
},
"knowledge": {
"action": "更新",
"target": "shadow-docs/knowledge/cli-output-contract.md",
"reason": "输出面规则扩展:概览默认最小面(data.help 字符串恒形状),结构化 commands 经 --full opt-in,组详情恒定结构化"
}
}
---

# 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)"条款,是该卡片的直接扩展;同卡合并,不新增。
3 changes: 2 additions & 1 deletion shadow-docs/knowledge/cli-output-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand All @@ -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 逐字节一致),触碰输出面的变更必须保持其绿色。

## 适用边界
Expand Down
2 changes: 1 addition & 1 deletion shadow-docs/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
15 changes: 10 additions & 5 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand Down
Loading