@chengzhao-dev/dsh-cpp-workflow 0.6.1 → 0.6.2

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/README.en.md CHANGED
@@ -19,13 +19,13 @@ The exception material follows the C++ Core Guidelines' Error handling section a
19
19
  Pinned npm install:
20
20
 
21
21
  ```bash
22
- dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.1
22
+ dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.2
23
23
  ```
24
24
 
25
25
  The matching Git tag:
26
26
 
27
27
  ```bash
28
- dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.1
28
+ dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.2
29
29
  ```
30
30
 
31
31
  The desktop app owns the `desktop` profile. If the command-line `add` is rejected, add the plugin in the app settings and use the same pinned npm version. A Git tag install uses the prebuilt `lib/` committed in the repository and runs no build on the target machine; the package has no install script that needs approval.
package/README.md CHANGED
@@ -19,13 +19,13 @@
19
19
  npm 安装钉死版本:
20
20
 
21
21
  ```bash
22
- dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.1
22
+ dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.2
23
23
  ```
24
24
 
25
25
  同版本的 Git 标签:
26
26
 
27
27
  ```bash
28
- dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.1
28
+ dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.2
29
29
  ```
30
30
 
31
31
  桌面应用独占 `desktop` profile。命令行 `add` 被拒绝时,在应用设置里添加插件,地址填同一个 npm 版本。Git 标签安装使用仓库内已构建的 `lib/`,不在目标机器上重新构建;包内也不含需要授权执行的安装脚本。
package/lib/client.js CHANGED
@@ -58,7 +58,9 @@ const TAG_LABEL = {
58
58
  retval: '@retval',
59
59
  };
60
60
  /** 预览里的示例摘要:用户没填 @brief 时用它,避免预览出现空标签。 */
61
- const SAMPLE_BRIEF = '定义井字棋棋盘、格子坐标与格子类型。';
61
+ const SAMPLE_BRIEF = '<一句话说明本文件>';
62
+ /** 预览里的示例文件名:只是占位符,不对应任何工程的真实文件。 */
63
+ const SAMPLE_FILE = '<文件名>';
62
64
  /** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
63
65
  const DEFAULT_CONFIG = {
64
66
  brief: { enabled: true, value: '' },
@@ -105,7 +107,7 @@ function normalizeConfig(raw) {
105
107
  */
106
108
  function renderFileHeader(raw, fileName) {
107
109
  const config = normalizeConfig(raw);
108
- const name = text(fileName) || 'board.h';
110
+ const name = text(fileName) || SAMPLE_FILE;
109
111
  const lines = [`/// @file ${name}`];
110
112
  if (config.brief.enabled) {
111
113
  lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
@@ -121,19 +123,19 @@ function renderFileHeader(raw, fileName) {
121
123
  */
122
124
  function previewParts(raw, sampleName) {
123
125
  const config = normalizeConfig(raw);
124
- const header = renderFileHeader(config, sampleName || 'board.h');
126
+ const header = renderFileHeader(config, sampleName || SAMPLE_FILE);
125
127
  // 预览是示例:@brief 开着但用户没填句子时,用示例文件自己的摘要顶替。
126
128
  const briefIndex = header.indexOf('/// @brief');
127
129
  if (briefIndex !== -1)
128
130
  header[briefIndex] = `/// @brief ${SAMPLE_BRIEF}`;
129
131
  const contract = [];
130
132
  if (config.param)
131
- contract.push('/// @param position 目标行列坐标。');
133
+ contract.push('/// @param row 目标行号。');
132
134
  if (config.returns)
133
135
  contract.push('/// @return 放置成功时为 true。');
134
136
  if (config.throws)
135
- contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
136
- const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
137
+ contract.push('/// @throws std::out_of_range 行号越界时抛出。');
138
+ const declaration = '[[nodiscard]] bool Place(int row, int column);';
137
139
  return { header, contract, declaration };
138
140
  }
139
141
  /**
@@ -141,7 +143,7 @@ function previewParts(raw, sampleName) {
141
143
  */
142
144
  function previewText(raw, sampleName) {
143
145
  const { header, contract, declaration } = previewParts(raw, sampleName);
144
- const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
146
+ const lines = [...header, '///', '/// 放置成功时返回 true;行号越界或目标格子已占时返回 false。'];
145
147
  if (contract.length > 0)
146
148
  lines.push('///', ...contract);
147
149
  lines.push(declaration);
@@ -193,27 +195,28 @@ function snapshotText(raw) {
193
195
  // 客户端半侧:设置导航里的「C++ 工程流程」页与插件页的配置卡片共用同一个组件。
194
196
  // 标签策略在本文件上方内联(由 scripts/building-client.mjs 从 src/comment-policy.js
195
197
  // 拼进来),所以预览、默认值与服务端快照永远同源;这里只做表单与排版。
198
+ //
199
+ // 版式参照 VS Code 设置页:左侧标签与说明、右侧控件,行间细分隔线;
200
+ // 修改过默认值的行在标签前加一个圆点,底部固定一条保存栏。
196
201
  const React = require('react')
197
202
  const h = React.createElement
198
203
 
199
204
  const PACKAGE_NAME = '@chengzhao-dev/dsh-cpp-workflow'
200
- const SAMPLE_FILE = 'board.h'
201
205
 
202
206
  const STRINGS = {
203
207
  zh: {
204
208
  nav: 'C++ 工程流程',
205
209
  title: '文档注释',
206
- intro: '文件头与函数契约默认写出哪些 Doxygen 标签。配置只影响代理写注释的默认值,不改已有源码。',
210
+ intro: '设置文件头与函数声明注释里默认写出的 Doxygen 标签;改动影响之后写出的注释,已有源码保持原样。',
207
211
  header: '文件头',
208
- headerHint: '每个手写源文件开头的一段 ///;带值的标签填写后写出,@brief 留空由文件自述。',
212
+ headerHint: '每个手写源文件开头的一段 /// 注释。',
209
213
  contract: '函数契约',
210
- contractHint: '声明注释里的参数与失败方式;关闭后不再要求写出这些标签。',
214
+ contractHint: '函数声明注释里要求的参数与失败信息。',
211
215
  extra: '更多标签',
212
- extraHint: '默认关闭;需要时在单个函数上补写。',
216
+ extraHint: '需要时在单个函数上补写。',
213
217
  file: '@file',
214
- fixed: '自动',
215
- fileHint: '固定为当前文件名,不参与配置。',
216
- briefHint: '一句话说明本文件定义或实现什么;留空则由每个文件自己写。',
218
+ fileHint: '取自当前文件名。',
219
+ briefHint: '一句话说明这个文件定义或实现什么;留空则由文件自述。',
217
220
  authorHint: '作者署名;留空不写。',
218
221
  dateHint: '创建或修改日期;留空不写。',
219
222
  copyrightHint: '版权声明;留空不写。',
@@ -221,7 +224,7 @@ const STRINGS = {
221
224
  emptyValue: '留空不写',
222
225
  briefEmpty: '留空则由文件自述',
223
226
  preview: '预览',
224
- previewHint: '示例文件名 board.h;预览随开关即时更新。',
227
+ previewHint: '按当前开关生成;文件名以占位符表示。',
225
228
  save: '保存',
226
229
  reset: '重置',
227
230
  saved: '已保存,下一次读取技能时生效。',
@@ -229,24 +232,23 @@ const STRINGS = {
229
232
  unsaved: '有未保存的改动。',
230
233
  loading: '正在读取配置…',
231
234
  unreadable: '当前账户不能修改这份配置。',
232
- readOnly: '此处只显示出厂值。要修改配置,请到「插件」页打开 dsh-cpp-workflow 的配置卡片。',
235
+ readOnly: '此处显示出厂值;修改在「插件」页的配置卡片里进行。',
233
236
  openSettings: '打开插件配置',
234
- noForm: '这个入口没有可写表单,配置在插件页修改。',
237
+ modified: '与出厂值不同',
235
238
  },
236
239
  en: {
237
240
  nav: 'C++ Workflow',
238
241
  title: 'Documentation comments',
239
- intro: 'Which Doxygen tags the agent writes by default. This changes defaults only; existing sources are untouched.',
242
+ intro: 'Choose the Doxygen tags written by default in file headers and declaration comments. Changes affect new comments; existing sources stay as they are.',
240
243
  header: 'File header',
241
- headerHint: 'The /// block at the top of every hand-written source; value tags are written only when filled.',
244
+ headerHint: 'The /// block at the top of every hand-written source.',
242
245
  contract: 'Function contract',
243
- contractHint: 'Parameter and failure tags in declaration comments; turning one off stops requiring it.',
246
+ contractHint: 'Parameter and failure tags required in declaration comments.',
244
247
  extra: 'More tags',
245
- extraHint: 'Off by default; add them per function when needed.',
248
+ extraHint: 'Add them per function when needed.',
246
249
  file: '@file',
247
- fixed: 'Auto',
248
- fileHint: 'Always the current file name; not configurable.',
249
- briefHint: 'One sentence about what this file defines; leave empty and each file writes its own.',
250
+ fileHint: 'Taken from the current file name.',
251
+ briefHint: 'One sentence about what this file defines; leave empty and the file writes its own.',
250
252
  authorHint: 'Author credit; left empty, omitted.',
251
253
  dateHint: 'Creation or change date; left empty, omitted.',
252
254
  copyrightHint: 'Copyright notice; left empty, omitted.',
@@ -254,7 +256,7 @@ const STRINGS = {
254
256
  emptyValue: 'Empty means omit',
255
257
  briefEmpty: 'Empty: the file writes its own',
256
258
  preview: 'Preview',
257
- previewHint: 'Sample name board.h; the preview follows the switches.',
259
+ previewHint: 'Generated from the switches; the file name is a placeholder.',
258
260
  save: 'Save',
259
261
  reset: 'Reset',
260
262
  saved: 'Saved; the next skill read uses it.',
@@ -262,9 +264,9 @@ const STRINGS = {
262
264
  unsaved: 'Unsaved changes.',
263
265
  loading: 'Reading configuration…',
264
266
  unreadable: 'This account cannot edit the configuration.',
265
- readOnly: 'Showing factory defaults. Edit the dsh-cpp-workflow card on the Plugins page.',
267
+ readOnly: 'Factory values are shown here; edit them on the Plugins page.',
266
268
  openSettings: 'Open plugin settings',
267
- noForm: 'This entry has no writable form; edit it on the Plugins page.',
269
+ modified: 'Differs from the factory value',
268
270
  },
269
271
  }
270
272
 
@@ -280,7 +282,6 @@ function t() {
280
282
  const palette = {
281
283
  label: 'var(--dsw-alias-label-primary, inherit)',
282
284
  muted: 'var(--dsw-alias-label-secondary, #6b7280)',
283
- faint: 'var(--dsw-alias-label-tertiary, #8b93a1)',
284
285
  line: 'var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.24))',
285
286
  surface: 'var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, 0.06))',
286
287
  field: 'var(--dsw-alias-bg-layer-1, rgba(127, 127, 127, 0.04))',
@@ -288,115 +289,143 @@ const palette = {
288
289
  mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
289
290
  }
290
291
 
291
- const hintStyle = () => ({ margin: '2px 0 0', fontSize: 12, lineHeight: 1.55, color: palette.muted })
292
- const labelCellStyle = () => ({
293
- padding: '10px 0',
294
- fontFamily: palette.mono,
295
- fontSize: 12.5,
296
- lineHeight: '20px',
297
- color: palette.label,
298
- whiteSpace: 'nowrap',
299
- borderBottom: `1px solid ${palette.line}`,
300
- })
301
- const controlCellStyle = () => ({
302
- padding: '10px 0',
303
- minWidth: 0,
304
- borderBottom: `1px solid ${palette.line}`,
305
- })
306
- const gridStyle = () => ({ display: 'grid', gridTemplateColumns: '92px minmax(0, 1fr)', columnGap: 14 })
292
+ const descriptionStyle = () => ({ margin: '2px 0 0', fontSize: 12, lineHeight: 1.55, color: palette.muted })
293
+ const sectionTitleStyle = () => ({ margin: '0 0 2px', fontSize: 13, fontWeight: 600, color: palette.label })
294
+
295
+ function Section(props) {
296
+ return h(
297
+ 'section',
298
+ {
299
+ style: {
300
+ marginTop: props.first ? 16 : 22,
301
+ paddingTop: props.first ? 0 : 14,
302
+ borderTop: props.first ? 'none' : `1px solid ${palette.line}`,
303
+ },
304
+ },
305
+ h('h4', { style: sectionTitleStyle() }, props.title),
306
+ props.description ? h('p', { style: descriptionStyle() }, props.description) : null,
307
+ h('div', { style: { marginTop: 6 } }, props.children),
308
+ )
309
+ }
310
+
311
+ function Row(props) {
312
+ return h(
313
+ 'div',
314
+ {
315
+ style: {
316
+ display: 'grid',
317
+ gridTemplateColumns: 'minmax(0, 1fr) minmax(160px, 240px)',
318
+ columnGap: 24,
319
+ alignItems: 'start',
320
+ padding: '10px 0',
321
+ borderBottom: `1px solid ${palette.line}`,
322
+ },
323
+ },
324
+ h(
325
+ 'div',
326
+ { style: { minWidth: 0 } },
327
+ h(
328
+ 'div',
329
+ { style: { display: 'flex', alignItems: 'center', gap: 6, minHeight: 20 } },
330
+ props.modified
331
+ ? h('span', {
332
+ key: 'dot',
333
+ title: props.modifiedLabel,
334
+ 'aria-label': props.modifiedLabel,
335
+ style: { width: 6, height: 6, borderRadius: '50%', background: palette.brand, flex: 'none' },
336
+ })
337
+ : null,
338
+ h('span', { style: { fontSize: 13, lineHeight: '20px', color: palette.label, fontFamily: props.mono ? palette.mono : 'inherit' } }, props.label),
339
+ ),
340
+ props.description ? h('p', { style: descriptionStyle() }, props.description) : null,
341
+ ),
342
+ h(
343
+ 'div',
344
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, minHeight: 20 } },
345
+ props.control,
346
+ ),
347
+ )
348
+ }
307
349
 
308
- function Toggle(props) {
350
+ function Checkbox(props) {
309
351
  return h(
310
352
  'button',
311
353
  {
312
354
  type: 'button',
313
- role: 'switch',
355
+ role: 'checkbox',
314
356
  'aria-checked': Boolean(props.checked),
315
357
  'aria-label': props.label,
316
358
  disabled: Boolean(props.disabled),
317
359
  onClick: () => props.onChange(!props.checked),
318
360
  style: {
319
361
  flex: 'none',
320
- width: 34,
321
- height: 20,
362
+ width: 16,
363
+ height: 16,
322
364
  padding: 0,
323
- border: 0,
324
- borderRadius: 999,
325
- position: 'relative',
365
+ borderRadius: 4,
366
+ border: `1px solid ${props.checked ? palette.brand : palette.line}`,
367
+ background: props.checked ? palette.brand : 'transparent',
368
+ display: 'inline-flex',
369
+ alignItems: 'center',
370
+ justifyContent: 'center',
326
371
  cursor: props.disabled ? 'default' : 'pointer',
327
- opacity: props.disabled ? 0.5 : 1,
328
- background: props.checked ? palette.brand : 'var(--dsw-alias-bg-layer-4, rgba(127, 127, 127, 0.35))',
329
- transition: 'background-color 0.15s ease',
372
+ opacity: props.disabled ? 0.55 : 1,
373
+ transition: 'background-color 0.12s ease, border-color 0.12s ease',
330
374
  },
331
375
  },
332
- h('span', {
333
- 'aria-hidden': 'true',
334
- style: {
335
- position: 'absolute',
336
- top: 2,
337
- left: props.checked ? 16 : 2,
338
- width: 16,
339
- height: 16,
340
- borderRadius: '50%',
341
- background: '#fff',
342
- boxShadow: '0 1px 2px rgba(0, 0, 0, 0.25)',
343
- transition: 'left 0.15s ease',
344
- },
345
- }),
346
- )
347
- }
348
-
349
- function TagRow(props) {
350
- const s = props.strings
351
- return [
352
- h('div', { key: `${props.tag}-label`, style: labelCellStyle() }, props.label),
353
376
  h(
354
- 'div',
355
- { key: `${props.tag}-control`, style: controlCellStyle() },
356
- h(
357
- 'div',
358
- { style: { display: 'flex', alignItems: 'center', gap: 10, minHeight: 22, flexWrap: 'wrap' } },
359
- props.control,
360
- ),
361
- props.hint ? h('p', { style: hintStyle() }, props.hint) : null,
377
+ 'svg',
378
+ { width: 11, height: 11, viewBox: '0 0 12 12', 'aria-hidden': 'true' },
379
+ h('path', {
380
+ d: 'M2.6 6.3 5 8.7l4.4-5.2',
381
+ fill: 'none',
382
+ stroke: '#fff',
383
+ strokeWidth: 1.7,
384
+ strokeLinecap: 'round',
385
+ strokeLinejoin: 'round',
386
+ opacity: props.checked ? 1 : 0,
387
+ }),
362
388
  ),
363
- ]
389
+ )
364
390
  }
365
391
 
366
392
  function ValueRow(props) {
367
393
  const s = props.strings
368
- return h(TagRow, {
369
- tag: props.tag,
394
+ const entry = props.entry
395
+ return h(Row, {
370
396
  label: TAG_LABEL[props.tag],
371
- strings: s,
372
- hint: props.hint,
397
+ mono: true,
398
+ description: props.hint,
399
+ modified: props.modified,
400
+ modifiedLabel: s.modified,
373
401
  control: [
374
- h(Toggle, {
375
- key: 'toggle',
376
- label: props.label || TAG_LABEL[props.tag],
377
- checked: props.entry.enabled,
402
+ h(Checkbox, {
403
+ key: 'check',
404
+ label: TAG_LABEL[props.tag],
405
+ checked: entry.enabled,
378
406
  disabled: props.disabled,
379
- onChange: (enabled) => !props.disabled && props.onChange({ ...props.entry, enabled }),
407
+ onChange: (enabled) => props.onChange({ ...entry, enabled }),
380
408
  }),
381
409
  h('input', {
382
410
  key: 'input',
383
411
  type: 'text',
384
- value: props.entry.value,
412
+ value: entry.value,
385
413
  placeholder: props.tag === 'brief' ? s.briefEmpty : s.emptyValue,
386
- disabled: props.disabled || !props.entry.enabled,
414
+ disabled: props.disabled || !entry.enabled,
387
415
  'aria-label': TAG_LABEL[props.tag],
388
- onChange: (event) => props.onChange({ ...props.entry, value: event.target.value }),
416
+ onChange: (event) => props.onChange({ ...entry, value: event.target.value }),
389
417
  style: {
390
- flex: '1 1 180px',
418
+ width: '100%',
391
419
  minWidth: 0,
392
- maxWidth: 320,
393
420
  fontSize: 13,
394
- padding: '4px 8px',
395
- borderRadius: 6,
421
+ lineHeight: '18px',
422
+ padding: '3px 7px',
423
+ borderRadius: 4,
396
424
  border: `1px solid ${palette.line}`,
397
- background: props.entry.enabled && !props.disabled ? palette.field : 'transparent',
398
- color: props.entry.enabled ? 'inherit' : palette.muted,
425
+ background: entry.enabled && !props.disabled ? palette.field : 'transparent',
426
+ color: entry.enabled ? 'inherit' : palette.muted,
399
427
  fontFamily: palette.mono,
428
+ boxSizing: 'border-box',
400
429
  },
401
430
  }),
402
431
  ],
@@ -404,16 +433,17 @@ function ValueRow(props) {
404
433
  }
405
434
 
406
435
  function FlagRow(props) {
407
- return h(TagRow, {
408
- tag: props.tag,
436
+ return h(Row, {
409
437
  label: TAG_LABEL[props.tag],
410
- strings: props.strings,
411
- hint: props.hint,
412
- control: h(Toggle, {
438
+ mono: true,
439
+ description: props.hint,
440
+ modified: props.modified,
441
+ modifiedLabel: props.strings.modified,
442
+ control: h(Checkbox, {
413
443
  label: TAG_LABEL[props.tag],
414
444
  checked: Boolean(props.checked),
415
445
  disabled: props.disabled,
416
- onChange: (value) => !props.disabled && props.onChange(value),
446
+ onChange: (value) => props.onChange(value),
417
447
  }),
418
448
  })
419
449
  }
@@ -422,16 +452,16 @@ function Preview(props) {
422
452
  const s = props.strings
423
453
  return h(
424
454
  'section',
425
- { 'aria-label': s.preview, style: { marginTop: 20 } },
426
- h('h4', { style: { margin: '0 0 4px', fontSize: 13, fontWeight: 600, color: palette.label } }, s.preview),
427
- h('p', { style: hintStyle() }, s.previewHint),
455
+ { 'aria-label': s.preview, style: { marginTop: 22, paddingTop: 14, borderTop: `1px solid ${palette.line}` } },
456
+ h('h4', { style: sectionTitleStyle() }, s.preview),
457
+ h('p', { style: descriptionStyle() }, s.previewHint),
428
458
  h(
429
459
  'pre',
430
460
  {
431
461
  style: {
432
462
  margin: '8px 0 0',
433
463
  padding: '10px 12px',
434
- borderRadius: 8,
464
+ borderRadius: 6,
435
465
  background: palette.surface,
436
466
  border: `1px solid ${palette.line}`,
437
467
  overflowX: 'auto',
@@ -442,7 +472,7 @@ function Preview(props) {
442
472
  color: palette.label,
443
473
  },
444
474
  },
445
- previewText(props.config, SAMPLE_FILE),
475
+ previewText(props.config),
446
476
  ),
447
477
  )
448
478
  }
@@ -457,6 +487,10 @@ function diffOperations(draft, base) {
457
487
  return operations
458
488
  }
459
489
 
490
+ function isModified(key, draft) {
491
+ return JSON.stringify(draft[key]) !== JSON.stringify(DEFAULT_CONFIG[key])
492
+ }
493
+
460
494
  function ConfigPage(props) {
461
495
  const s = t()
462
496
  const form = props.form
@@ -511,133 +545,97 @@ function ConfigPage(props) {
511
545
  }
512
546
 
513
547
  const editable = Boolean(form)
514
- const editableReady = editable && ready
515
- const header = [
516
- { tag: 'brief', hint: s.briefHint },
517
- { tag: 'author', hint: s.authorHint },
518
- { tag: 'date', hint: s.dateHint },
519
- { tag: 'copyright', hint: s.copyrightHint },
520
- { tag: 'version', hint: s.versionHint },
521
- ]
522
- const contractHints = { param: TAG_TEXT.param, returns: TAG_TEXT.returns, throws: TAG_TEXT.throws }
523
- const extraHints = { note: TAG_TEXT.note, see: TAG_TEXT.see, pre: TAG_TEXT.pre, post: TAG_TEXT.post, tparam: TAG_TEXT.tparam, retval: TAG_TEXT.retval }
548
+ const headerHints = {
549
+ brief: s.briefHint,
550
+ author: s.authorHint,
551
+ date: s.dateHint,
552
+ copyright: s.copyrightHint,
553
+ version: s.versionHint,
554
+ }
524
555
 
525
556
  return h(
526
557
  'div',
527
- { style: { display: 'flex', flexDirection: 'column', padding: '4px 0 12px', maxWidth: 720, color: palette.label } },
528
- h('h3', { style: { margin: '0 0 4px', fontSize: 15, fontWeight: 600 } }, s.title),
529
- h('p', { style: hintStyle() }, s.intro),
558
+ { style: { display: 'flex', flexDirection: 'column', padding: '2px 0 8px', maxWidth: 760, color: palette.label } },
559
+ h('h3', { style: { margin: '0 0 2px', fontSize: 15, fontWeight: 600 } }, s.title),
560
+ h('p', { style: descriptionStyle() }, s.intro),
530
561
  !editable
531
562
  ? h(
532
563
  'div',
533
- { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginTop: 8 } },
534
- h('p', { role: 'status', style: { ...hintStyle(), margin: 0 } }, s.readOnly),
564
+ {
565
+ role: 'status',
566
+ style: {
567
+ display: 'flex',
568
+ alignItems: 'center',
569
+ gap: 10,
570
+ flexWrap: 'wrap',
571
+ marginTop: 10,
572
+ padding: '8px 10px',
573
+ borderRadius: 6,
574
+ background: palette.surface,
575
+ border: `1px solid ${palette.line}`,
576
+ },
577
+ },
578
+ h('p', { style: { ...descriptionStyle(), margin: 0, flex: '1 1 200px' } }, s.readOnly),
535
579
  props.openPluginPage
536
580
  ? h('button', { type: 'button', onClick: props.openPluginPage, style: secondaryButton(true) }, s.openSettings)
537
581
  : null,
538
582
  )
539
583
  : null,
540
584
  editable && !ready && status !== 'missing'
541
- ? h('p', { role: 'status', style: hintStyle() }, status === 'loading' ? s.loading : s.unreadable)
585
+ ? h('p', { role: 'status', style: descriptionStyle() }, status === 'loading' ? s.loading : s.unreadable)
542
586
  : null,
543
587
  h(
544
- 'section',
545
- { style: { marginTop: 18 } },
546
- h('h4', { style: { margin: '0 0 4px', fontSize: 13, fontWeight: 600 } }, s.header),
547
- h('p', { style: hintStyle() }, s.headerHint),
548
- h(
549
- 'div',
550
- { role: 'group', 'aria-label': s.header, style: { ...gridStyle(), marginTop: 8 } },
551
- h(
552
- 'div',
553
- { key: 'file-label', style: labelCellStyle() },
554
- s.file,
555
- ),
556
- h(
557
- 'div',
558
- { key: 'file-control', style: controlCellStyle() },
559
- h(
560
- 'div',
561
- { style: { display: 'flex', alignItems: 'center', gap: 10, minHeight: 22 } },
562
- h(
563
- 'span',
564
- {
565
- style: {
566
- fontSize: 12,
567
- lineHeight: '20px',
568
- padding: '0 8px',
569
- borderRadius: 999,
570
- border: `1px solid ${palette.line}`,
571
- color: palette.muted,
572
- },
573
- },
574
- s.fixed,
575
- ),
576
- h('code', { style: { fontSize: 12.5, fontFamily: palette.mono, color: palette.muted } }, SAMPLE_FILE),
577
- ),
578
- h('p', { style: hintStyle() }, s.fileHint),
579
- ),
580
- ...header.flatMap(({ tag, hint }) =>
581
- h(ValueRow, {
582
- key: tag,
583
- tag,
584
- hint,
585
- strings: s,
586
- disabled: !editable,
587
- entry: draft[tag],
588
- onChange: (entry) => update({ [tag]: entry }),
589
- }),
590
- ),
588
+ Section,
589
+ { title: s.header, description: s.headerHint, first: true },
590
+ h(Row, {
591
+ label: s.file,
592
+ mono: true,
593
+ description: s.fileHint,
594
+ control: null,
595
+ }),
596
+ ...HEADER_TAGS.map((tag) =>
597
+ h(ValueRow, {
598
+ key: tag,
599
+ tag,
600
+ hint: headerHints[tag],
601
+ strings: s,
602
+ disabled: !editable,
603
+ modified: editable && isModified(tag, draft),
604
+ entry: draft[tag],
605
+ onChange: (entry) => update({ [tag]: entry }),
606
+ }),
591
607
  ),
592
608
  ),
593
609
  h(
594
- 'section',
595
- { style: { marginTop: 20 } },
596
- h('h4', { style: { margin: '0 0 4px', fontSize: 13, fontWeight: 600 } }, s.contract),
597
- h('p', { style: hintStyle() }, s.contractHint),
598
- h(
599
- 'div',
600
- { role: 'group', 'aria-label': s.contract, style: { ...gridStyle(), marginTop: 8 } },
601
- ...CONTRACT_TAGS.flatMap((tag) =>
602
- h(FlagRow, {
603
- key: tag,
604
- tag,
605
- hint: contractHints[tag],
606
- strings: s,
607
- disabled: !editable,
608
- checked: draft[tag],
609
- onChange: (value) => update({ [tag]: value }),
610
- }),
611
- ),
610
+ Section,
611
+ { title: s.contract, description: s.contractHint },
612
+ ...CONTRACT_TAGS.map((tag) =>
613
+ h(FlagRow, {
614
+ key: tag,
615
+ tag,
616
+ hint: TAG_TEXT[tag],
617
+ strings: s,
618
+ disabled: !editable,
619
+ modified: editable && isModified(tag, draft),
620
+ checked: draft[tag],
621
+ onChange: (value) => update({ [tag]: value }),
622
+ }),
612
623
  ),
613
624
  ),
614
625
  h(
615
- 'section',
616
- { style: { marginTop: 20, paddingTop: 12, borderTop: `1px solid ${palette.line}` } },
617
- h(
618
- 'details',
619
- {},
620
- h(
621
- 'summary',
622
- { style: { cursor: 'pointer', fontSize: 13, fontWeight: 600, listStylePosition: 'inside' } },
623
- s.extra,
624
- ),
625
- h('p', { style: hintStyle() }, s.extraHint),
626
- h(
627
- 'div',
628
- { role: 'group', 'aria-label': s.extra, style: { ...gridStyle(), marginTop: 8 } },
629
- ...EXTRA_TAGS.flatMap((tag) =>
630
- h(FlagRow, {
631
- key: tag,
632
- tag,
633
- hint: extraHints[tag],
634
- strings: s,
635
- disabled: !editable,
636
- checked: draft[tag],
637
- onChange: (value) => update({ [tag]: value }),
638
- }),
639
- ),
640
- ),
626
+ Section,
627
+ { title: s.extra, description: s.extraHint },
628
+ ...EXTRA_TAGS.map((tag) =>
629
+ h(FlagRow, {
630
+ key: tag,
631
+ tag,
632
+ hint: TAG_TEXT[tag],
633
+ strings: s,
634
+ disabled: !editable,
635
+ modified: editable && isModified(tag, draft),
636
+ checked: draft[tag],
637
+ onChange: (value) => update({ [tag]: value }),
638
+ }),
641
639
  ),
642
640
  ),
643
641
  h(Preview, { config: draft, strings: s }),
@@ -649,7 +647,7 @@ function ConfigPage(props) {
649
647
  display: 'flex',
650
648
  alignItems: 'center',
651
649
  gap: 10,
652
- marginTop: 18,
650
+ marginTop: 20,
653
651
  paddingTop: 12,
654
652
  borderTop: `1px solid ${palette.line}`,
655
653
  flexWrap: 'wrap',
@@ -660,8 +658,8 @@ function ConfigPage(props) {
660
658
  {
661
659
  type: 'button',
662
660
  onClick: save,
663
- disabled: busy || !editableReady || !dirty,
664
- style: primaryButton(dirty && editableReady && !busy),
661
+ disabled: busy || !ready || !dirty,
662
+ style: primaryButton(dirty && ready && !busy),
665
663
  },
666
664
  s.save,
667
665
  ),
@@ -675,7 +673,7 @@ function ConfigPage(props) {
675
673
  },
676
674
  s.reset,
677
675
  ),
678
- h('p', { role: 'status', style: { ...hintStyle(), margin: 0, flex: '1 1 220px' } }, message || (dirty ? s.unsaved : '')),
676
+ h('p', { role: 'status', style: { ...descriptionStyle(), margin: 0, flex: '1 1 200px' } }, message || (dirty ? s.unsaved : '')),
679
677
  )
680
678
  : null,
681
679
  )
@@ -685,8 +683,8 @@ function primaryButton(enabled) {
685
683
  return {
686
684
  fontSize: 13,
687
685
  lineHeight: '20px',
688
- padding: '5px 14px',
689
- borderRadius: 6,
686
+ padding: '4px 12px',
687
+ borderRadius: 4,
690
688
  border: '1px solid transparent',
691
689
  background: enabled ? palette.brand : 'var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, 0.2))',
692
690
  color: enabled ? '#fff' : palette.muted,
@@ -698,8 +696,8 @@ function secondaryButton(enabled) {
698
696
  return {
699
697
  fontSize: 13,
700
698
  lineHeight: '20px',
701
- padding: '5px 14px',
702
- borderRadius: 6,
699
+ padding: '4px 12px',
700
+ borderRadius: 4,
703
701
  border: `1px solid ${palette.line}`,
704
702
  background: 'transparent',
705
703
  color: enabled ? palette.label : palette.muted,
@@ -51,7 +51,9 @@ export const TAG_LABEL = {
51
51
  retval: '@retval',
52
52
  };
53
53
  /** 预览里的示例摘要:用户没填 @brief 时用它,避免预览出现空标签。 */
54
- export const SAMPLE_BRIEF = '定义井字棋棋盘、格子坐标与格子类型。';
54
+ export const SAMPLE_BRIEF = '<一句话说明本文件>';
55
+ /** 预览里的示例文件名:只是占位符,不对应任何工程的真实文件。 */
56
+ export const SAMPLE_FILE = '<文件名>';
55
57
  /** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
56
58
  export const DEFAULT_CONFIG = {
57
59
  brief: { enabled: true, value: '' },
@@ -98,7 +100,7 @@ export function normalizeConfig(raw) {
98
100
  */
99
101
  export function renderFileHeader(raw, fileName) {
100
102
  const config = normalizeConfig(raw);
101
- const name = text(fileName) || 'board.h';
103
+ const name = text(fileName) || SAMPLE_FILE;
102
104
  const lines = [`/// @file ${name}`];
103
105
  if (config.brief.enabled) {
104
106
  lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
@@ -114,19 +116,19 @@ export function renderFileHeader(raw, fileName) {
114
116
  */
115
117
  export function previewParts(raw, sampleName) {
116
118
  const config = normalizeConfig(raw);
117
- const header = renderFileHeader(config, sampleName || 'board.h');
119
+ const header = renderFileHeader(config, sampleName || SAMPLE_FILE);
118
120
  // 预览是示例:@brief 开着但用户没填句子时,用示例文件自己的摘要顶替。
119
121
  const briefIndex = header.indexOf('/// @brief');
120
122
  if (briefIndex !== -1)
121
123
  header[briefIndex] = `/// @brief ${SAMPLE_BRIEF}`;
122
124
  const contract = [];
123
125
  if (config.param)
124
- contract.push('/// @param position 目标行列坐标。');
126
+ contract.push('/// @param row 目标行号。');
125
127
  if (config.returns)
126
128
  contract.push('/// @return 放置成功时为 true。');
127
129
  if (config.throws)
128
- contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
129
- const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
130
+ contract.push('/// @throws std::out_of_range 行号越界时抛出。');
131
+ const declaration = '[[nodiscard]] bool Place(int row, int column);';
130
132
  return { header, contract, declaration };
131
133
  }
132
134
  /**
@@ -134,7 +136,7 @@ export function previewParts(raw, sampleName) {
134
136
  */
135
137
  export function previewText(raw, sampleName) {
136
138
  const { header, contract, declaration } = previewParts(raw, sampleName);
137
- const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
139
+ const lines = [...header, '///', '/// 放置成功时返回 true;行号越界或目标格子已占时返回 false。'];
138
140
  if (contract.length > 0)
139
141
  lines.push('///', ...contract);
140
142
  lines.push(declaration);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chengzhao-dev/dsh-cpp-workflow",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "写 C++ 源码、配 CMake、用 Clang 构建;文档注释统一为 /// Doxygen,文件头标签可在设置里改。 · Write C++ sources, configure CMake, and build with Clang; /// Doxygen comments, with file-header tags you can change in Settings.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## 适用场景
11
11
 
12
- 管 `environment.json`、`running-build.sh`、`.clang-format`、`.clang-tidy`、`.clangd`、`.vscode/`、构建命令、编译数据库、CTest 和运行。`CMakeLists.txt` 与 `cmake/` 下的 target、属性、标准和模块由 `configuring-cmake` 负责;本技能只读它们来配置和检查,不在这一步改 target 图。函数体、类型、命名、异常和源码注释只开 `developing-cpp`。
12
+ 本技能处理环境声明、构建入口与工具配置:`environment.json`、`running-build.sh`、`.clang-format`、`.clang-tidy`、`.clangd`、`.vscode/`、构建命令、编译数据库、CTest 和运行。`CMakeLists.txt` 与 `cmake/` 下的 target、属性、标准和模块由 `configuring-cmake` 负责;本技能只读它们来配置和检查,不在这一步改 target 图。函数体、类型、命名、异常和源码注释只开 `developing-cpp`。
13
13
 
14
14
  一次改动同时碰到源码和构建文件时,先打开 `developing-cpp` 定行为、边界与实现;需要新增或移动源文件、改库与程序的边界、补测试 target 时,转到 `configuring-cmake` 把文件写进 CMake;最后回到本技能做定工具配置与通过检查。只改函数体、命名、异常或注释且入口命令可运行时,定实现后只开本技能的「通过检查」。
15
15
 
@@ -26,7 +26,7 @@ metadata:
26
26
  | 增量修正 | 用户点了一条工具配置、一条诊断或一个入口脚本错误 | 先判断属于定构建还是定工具配置,只改那一处,不动无关 target 和配置 |
27
27
  | 整段重构 | 新工程、新阶段、换环境组合,或第一次接构建 | 同样三步;先确认 target 与标准已由 configuring-cmake 写好,再改工具文件,最后跑完整检查 |
28
28
 
29
- 共同不变量:不发明项目没有声明的环境轴、命令和 target;只改格式或配置时不改可观察行为;不覆盖已有源码、`CMakeLists.txt` 和 `.clang-*`;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步。拿不准先用增量修正;发现环境轴、编译器、构建目录或 target 契约需要改,退回整段,并转 `configuring-cmake`。
29
+ 两种走法共用这些约束:不发明项目没有声明的环境轴、命令和 target;只改格式或配置时不改可观察行为;不覆盖已有源码、`CMakeLists.txt` 和 `.clang-*`;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步。拿不准先用增量修正;发现环境轴、编译器、构建目录或 target 契约需要改,退回整段,并转 `configuring-cmake`。
30
30
 
31
31
  ## 步骤
32
32
 
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## 适用场景
11
11
 
12
- 管 `CMakeLists.txt`、`cmake/` 下的模块、`target_*` 命令、目标属性、include 与链接边界、输出目录、共享库运行时路径、`FetchContent`、preset 与安装导出。函数体、类型、命名、异常和源码注释只开 `developing-cpp`;配置完成后要跑构建、格式化、静态检查或入口脚本时开 `building-cpp`。
12
+ 本技能处理 CMake 文件与 target 契约:`CMakeLists.txt`、`cmake/` 模块、`target_*` 命令、目标属性、include 与链接边界、输出目录、共享库运行时路径、`FetchContent`、preset 与安装导出。函数体、类型、命名、异常和源码注释只开 `developing-cpp`;配置完成后要跑构建、格式化、静态检查或入口脚本时开 `building-cpp`。
13
13
 
14
14
  一次改动同时碰到源码和 CMake 时先开 `developing-cpp` 定行为与边界,再用本技能把源文件、库和程序写进 target,最后到 `building-cpp` 跑检查。只改一个 target 的 include、链接或标准时走增量修正;新工程、新组件、换语言标准或抽公共函数时走整段重构。
15
15
 
@@ -22,7 +22,7 @@ metadata:
22
22
  | 增量修正 | 一个 target 的 include、链接、标准、输出目录或一条属性不对 | 先读现有 `CMakeLists.txt`,只改这一处,不动无关 target 和全局设置 |
23
23
  | 整段重构 | 新工程、新组件、抽公共函数、换语言标准或整理目录 | 同样三步;先定 target 图与标准,再定模块与函数,最后跑完整检查 |
24
24
 
25
- 共同不变量:不发明项目没有声明的 target、标准、编译器或目录;不覆盖已有 `CMakeLists.txt`;不用全局 `include_directories()`、`add_definitions()`、`CMAKE_CXX_FLAGS` 给所有 target 加料;不写 `file(GLOB ...)`;不把 `main()` 编进库;库与程序不下载依赖,只有测试需要时调用 `LinkSettings_FetchGoogleTest`。后一步发现前一步没做完,退回那一步。
25
+ 两种走法共用这些约束:不发明项目没有声明的 target、标准、编译器或目录;不覆盖已有 `CMakeLists.txt`;不用全局 `include_directories()`、`add_definitions()`、`CMAKE_CXX_FLAGS` 给所有 target 加料;不写 `file(GLOB ...)`;不把 `main()` 编进库;库与程序不下载依赖,只有测试需要时调用 `LinkSettings_FetchGoogleTest`。后一步发现前一步没做完,退回那一步。
26
26
 
27
27
  ## 步骤
28
28
 
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## 适用场景
11
11
 
12
- 管 `.h` / `.hpp` / `.cpp` / `.cc` 里换一台机器也不会改的内容:可观察行为、库与程序的边界、声明、命名、异常和源码注释。文件槽位与守卫以 `references/file-template.md` 为准;排版、命名、头文件与包含顺序以 `references/google-cpp-style.md` 的 Google 基线为准。异常条文采用 C++ Core Guidelines 的 Error handling,作为开发阶段的源码指导;它不进入插件运行时。`CMakeLists.txt` 与 `cmake/` 开 `configuring-cmake`;`running-build.sh`、`.clang-*`、`.vscode/` 和 `environment.json` 开 `building-cpp`。只改函数体、命名、异常或注释且入口命令可运行时,定实现后只开 `building-cpp` 的「通过检查」;新增或移动源文件、改库与程序的边界、补测试 target 时,声明和实现可以先写,但完成前必须开 `configuring-cmake` 把文件写进 CMake,再开 `building-cpp` 定工具配置并通过检查。`.qmd`、仓库 README、Python 和非 C++ 文档不打开本技能。
12
+ 本技能处理换一台机器也不会改的源码内容:可观察行为、库与程序的边界、声明、命名、异常和源码注释。文件槽位与守卫以 `references/file-template.md` 为准;排版、命名、头文件与包含顺序以 `references/google-cpp-style.md` 的 Google 基线为准。异常条文取自 C++ Core Guidelines 的 Error handling,只约束开发期的源码,不进入插件运行时。`CMakeLists.txt` 与 `cmake/` 开 `configuring-cmake`;`running-build.sh`、`.clang-*`、`.vscode/` 和 `environment.json` 开 `building-cpp`。只改函数体、命名、异常或注释且入口命令可运行时,定实现后只开 `building-cpp` 的「通过检查」;新增或移动源文件、改库与程序的边界、补测试 target 时,声明和实现可以先写,但完成前必须开 `configuring-cmake` 把文件写进 CMake,再开 `building-cpp` 定工具配置并通过检查。`.qmd`、仓库 README、Python 和非 C++ 文档不打开本技能。
13
13
 
14
14
  存在项目技能 `cpp-workflow-project` 时先读它的 `SKILL.md`:工程名、库与程序的边界、阶段表、术语增补、入口命令。没有项目技能时门禁不会放出本技能,不要猜项目约定。
15
15
 
@@ -22,7 +22,7 @@ metadata:
22
22
  | 增量修正 | 用户点了一个函数、一条诊断或一个文件 | 沿本技能步骤走,只改判为不合格的那一处,其余保持原样 |
23
23
  | 整段重构 | 新工程、新阶段、用户没点名范围,或改动跨过库与程序的边界 | 同样四步;第 1、2 步先定删留,不动函数体;第 3、4 步才改代码 |
24
24
 
25
- 共同不变量:不发明阶段表和用户请求里没有的行为、类型和命令;只改注释或格式时不改可观察行为;不覆盖已有源码;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步,不在当前步补结构。拿不准先用增量修正;做到一半发现边界被打破,退回整段重构,从被打破的那一步重走。
25
+ 两种走法共用这些约束:不发明阶段表和用户请求里没有的行为、类型和命令;只改注释或格式时不改可观察行为;不覆盖已有源码;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步,不在当前步补结构。拿不准先用增量修正;做到一半发现边界被打破,退回整段重构,从被打破的那一步重走。
26
26
 
27
27
  ## 步骤
28
28
 
@@ -129,20 +129,19 @@ GameResult result_ = GameResult::kInProgress;
129
129
  ```
130
130
 
131
131
  - C++ 源文件用以上格式;CMake 用 `#`,YAML/JSON 用 `#`。
132
- - 超过约 200 行的文件用章节横幅分组,组名是读者要跳转的锚点;一个文件不超过 8 个横幅。
132
+ - 超过约 200 行的文件用空行分成几节,每节的说明写在一行 `///` 上,不画装饰线。
133
133
  - 文件头 3–7 行;`@file` 与 `@brief` 各占一行,后面只写本文件的范围与组成,不写实现步骤、作者和变更史。
134
134
 
135
- ## 排版:符号、宽度、横幅与标签
135
+ ## 排版:符号、宽度与标签
136
136
 
137
- - 行宽跟项目 `.clang-format` 的 `ColumnLimit` 走(Google 基线是 80 列,中文按两列计);横幅与列宽同宽,不另设数字。
138
- - 横幅宽度等于 `ColumnLimit`:`//===` 起头、`===//` 收尾,中间补 `-`;章节横幅同一格式。
137
+ - 行宽跟项目 `.clang-format` 的 `ColumnLimit` 走(Google 基线是 80 列,中文按两列计)。
139
138
  - 行尾注释与代码之间空两格;`///<` 只接在枚举项和 `struct` 短字段声明行尾。
140
139
  - `@param`、`@return`、`@throws` 标签保持英文;`@param` 后先写参数名,再写一句。
141
140
  - `TODO:` 后先写定位(阶段名或 issue 号),再写一个短横线,再写动作;不写 `TODO(某人)`,不写没有动作的 TODO。
142
141
  - `.clang-format`、`.clang-tidy`、`.clangd`、CMake 的配置项旁边写「为什么这样设」,不写「这一项是什么」;配置注释贴在被解释的键上方。
143
142
  - 禁止:大段表格、装饰图案、表情;外部资料路径、跨仓库引用、团队流程;注释掉的代码;注释元叙述(「其余内容见某处」「两处不重复」)。
144
- - `///`、`@param`、横幅不进入 `.qmd`、YAML、CSS。
145
- - C++ 源文件的说明性注释一律 `///`,实现区不与 `//` 混用;`//` 只留给文件头横幅边框与被引用的配置行。
143
+ - `///` 与 `@param` 不进入 `.qmd`、YAML、CSS。
144
+ - C++ 源文件的说明性注释一律 `///`,实现区不与 `//` 混用;`//` 只留给 `// NOLINTNEXTLINE(...)` 这类工具指令。
146
145
 
147
146
  ## 润色:删复述,统一句式
148
147
 
@@ -98,5 +98,5 @@ Google 原文不使用 C++ 异常;本工作流只在开发阶段的源码指
98
98
  ## 注释
99
99
 
100
100
  - 注释写意图、契约、取舍,不复述代码。
101
- - 文件头、章节横幅、声明注释、实现注释、配置注释的格式见 `references/comments.md`。
101
+ - 文件头、声明注释、实现注释与配置注释的格式见 `references/comments.md`。
102
102
  - TODO 统一写 `// TODO: <阶段或 issue> - <要做什么>`;不要写 `@todo`、`FIXME` 或只有「参考」没有动作的占位。
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // 只读注释审计:按 references/comments.md 的硬规则检查 C/C++ 源文件。
3
3
  //
4
- // 检查项:文件头横幅、横幅宽度、注释行宽度、Doxygen 标签、注释与声明的贴合、
4
+ // 检查项:Doxygen 文件头、注释行宽度、标签、注释与声明的贴合、
5
5
  // 类型注释存在性、成员功能清单、设计论证(方案对比)、否定式边界、领域尺寸、谓词首句、@param 完整性、枚举成员注释一致性、数据成员主注释位置。
6
6
  // 不重写任何文件。
7
7
  //
8
8
  // 用法:
9
- // node checking-comments.mjs [--root DIR] [--width N] [--banner-width N] [--no-header] [--json] [--quiet]
9
+ // node checking-comments.mjs [--root DIR] [--width N] [--no-header] [--json] [--quiet]
10
10
  //
11
11
  // 退出码:0 无问题,1 发现问题,2 用法或读取错误。
12
12
  import { readdirSync, readFileSync, statSync } from 'node:fs'
@@ -28,7 +28,6 @@ const SKIP_DIR = new Set([
28
28
  const STATIC_FORBIDDEN_TAGS = ['@returns', '@details', '@since', '@todo']
29
29
  const PRIVACY_TAGS = ['@author', '@date', '@copyright', '@version']
30
30
  const FORBIDDEN_TAGS = [...STATIC_FORBIDDEN_TAGS, ...PRIVACY_TAGS]
31
- const BANNER = /^\/\/=+-+=+\/\/$/
32
31
  const DOXY_FILE = /^\/\/\/\s*@file\s+\S/
33
32
  const DOXY_BRIEF = /^\/\/\/\s*@brief\s+\S/
34
33
  const MEMBER_LIST = /提供[^。]{0,40}、[^。]{0,40}(的功能|的能力|的接口)|功能[::]/
@@ -42,12 +41,12 @@ const DECL_KEYWORDS = /^(class|struct|union|enum|namespace|using|typedef|templat
42
41
 
43
42
  function usage(message) {
44
43
  if (message) process.stderr.write(`错误:${message}\n`)
45
- process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--banner-width N] [--no-header] [--no-brief] [--allow-tag TAG] [--json] [--quiet]\n')
44
+ process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--no-header] [--no-brief] [--allow-tag TAG] [--json] [--quiet]\n')
46
45
  return EXIT.usage
47
46
  }
48
47
 
49
48
  function parseArgs(argv) {
50
- const options = { root: undefined, width: 80, bannerWidth: 80, widthExplicit: false, bannerWidthExplicit: false, requireHeader: true, requireBrief: true, allowTags: new Set(), json: false, quiet: false }
49
+ const options = { root: undefined, width: 80, widthExplicit: false, requireHeader: true, requireBrief: true, allowTags: new Set(), json: false, quiet: false }
51
50
  for (let index = 0; index < argv.length; index += 1) {
52
51
  const arg = argv[index]
53
52
  if (arg === '--root') {
@@ -57,10 +56,6 @@ function parseArgs(argv) {
57
56
  options.width = Number(argv[index + 1])
58
57
  options.widthExplicit = true
59
58
  index += 1
60
- } else if (arg === '--banner-width') {
61
- options.bannerWidth = Number(argv[index + 1])
62
- options.bannerWidthExplicit = true
63
- index += 1
64
59
  } else if (arg === '--no-header') {
65
60
  options.requireHeader = false
66
61
  } else if (arg === '--no-brief') {
@@ -80,10 +75,8 @@ function parseArgs(argv) {
80
75
  throw new Error(`未知参数 ${arg}`)
81
76
  }
82
77
  }
83
- for (const key of ['width', 'bannerWidth']) {
84
- if (!Number.isInteger(options[key]) || options[key] < 40) {
85
- throw new Error(`--${key} 需要不小于 40 的整数`)
86
- }
78
+ if (!Number.isInteger(options.width) || options.width < 40) {
79
+ throw new Error('--width 需要不小于 40 的整数')
87
80
  }
88
81
  return options
89
82
  }
@@ -208,7 +201,6 @@ function classMemberLines(lines) {
208
201
  /** 检查一段文本,返回问题列表;每个问题带 1 起始的行号。 */
209
202
  export function checkText(text, options = {}) {
210
203
  const width = options.width ?? 80
211
- const bannerWidth = options.bannerWidth ?? 80
212
204
  const requireHeader = options.requireHeader ?? true
213
205
  const lines = text.split(/\r?\n/)
214
206
  const issues = []
@@ -223,13 +215,12 @@ export function checkText(text, options = {}) {
223
215
  doxyHeaderEnd = firstContent
224
216
  while (doxyHeaderEnd + 1 < lines.length && lines[doxyHeaderEnd + 1].trim().startsWith('///')) doxyHeaderEnd += 1
225
217
  }
226
- if (requireHeader && firstContent !== -1 && !BANNER.test(firstTrimmed) && !hasDoxyHeader) {
218
+ if (requireHeader && firstContent !== -1 && !hasDoxyHeader) {
227
219
  add(firstContent + 1, 'header-missing', '文件开头缺少文件头:写 /// @file <文件名>,下一行写 /// @brief')
228
220
  }
229
- if (firstContent !== -1 && (BANNER.test(firstTrimmed) || hasDoxyHeader)) {
221
+ if (firstContent !== -1 && hasDoxyHeader) {
230
222
  const own = options.fileName ? options.fileName.split(/[\\/]/).pop() : ''
231
- const bannerHeader = BANNER.test(firstTrimmed)
232
- if (hasDoxyHeader && own) {
223
+ if (own) {
233
224
  const declared = /@file\s+(\S+)/.exec(lines[firstContent].trim())
234
225
  if (declared && declared[1] !== own) {
235
226
  add(firstContent + 1, 'header-file-name', `文件头写的是 ${declared[1]},本文件是 ${own}:@file 必须等于文件名`)
@@ -240,7 +231,7 @@ export function checkText(text, options = {}) {
240
231
  }
241
232
  for (let index = firstContent + 1; index < lines.length; index += 1) {
242
233
  const trimmed = lines[index].trim()
243
- if (bannerHeader ? BANNER.test(trimmed) || !trimmed.startsWith('//') : !trimmed.startsWith('///')) break
234
+ if (!trimmed.startsWith('///')) break
244
235
  const names = trimmed.match(/\b[A-Za-z0-9_-]+\.(?:h|hpp|cpp|cc)\b/g) ?? []
245
236
  const fileMatches = trimmed.match(/@file\s+(\S+)/)
246
237
  const namesToCheck = fileMatches ? names.filter((name) => name !== fileMatches[1]) : names
@@ -254,11 +245,6 @@ export function checkText(text, options = {}) {
254
245
  for (let index = 0; index < lines.length; index += 1) {
255
246
  const line = lines[index]
256
247
  const trimmed = line.trim()
257
- if (BANNER.test(trimmed)) {
258
- const actual = displayWidth(trimmed)
259
- if (actual !== bannerWidth) add(index + 1, 'banner-width', `横幅宽度 ${actual} 列,应为 ${bannerWidth} 列`)
260
- continue
261
- }
262
248
  if (trimmed.startsWith('///') || (trimmed.startsWith('//') && !trimmed.startsWith('//!'))) {
263
249
  if (displayWidth(trimmed) > width) add(index + 1, 'comment-width', `注释行 ${displayWidth(trimmed)} 列,超过 ${width} 列`)
264
250
  for (const tag of FORBIDDEN_TAGS) {
@@ -453,7 +439,6 @@ function main(argv) {
453
439
  const columnLimit = readColumnLimit(root)
454
440
  if (columnLimit !== undefined) {
455
441
  if (!options.widthExplicit) options.width = columnLimit
456
- if (!options.bannerWidthExplicit) options.bannerWidth = columnLimit
457
442
  }
458
443
  let files
459
444
  try {
@@ -24,7 +24,7 @@
24
24
 
25
25
  **异常声明**:构造函数、`Get` 类查询、解析函数最容易抛;在声明注释里写「当……时抛出 `std::out_of_range`」这样的具体类型,不要只写「可能抛异常」,也不要给每个参数错误都新建类型。类型选择见 `references/exceptions.md`。
26
26
 
27
- **注释**:声明注释用 `///` 写中文散文,只写调用方需要知道的;实现细节留给 `.cpp`。`@param`、`@return`、`@throws` 要么全写要么全不写,标识符和标签保持英文。文件头默认写 `@file` 与 `@brief` 两行,`@file` 必须与文件名相同;作者、日期、版权、版本默认不写。标签开关在插件设置页,技能正文末尾附带当前配置的快照。写完运行 `node "<skill-dir>/scripts/checking-comments.mjs" --root <项目根>`;关闭 `@brief` 时加 `--no-brief`,配置里打开了隐私标签并填了值时加 `--allow-tag <标签>`。横幅、标签、参数完整性、成员清单与设计论证由脚本兜底。文件头、章节横幅和 TODO 的格式见 `references/comments.md`。
27
+ **注释**:声明注释用 `///` 写中文散文,只写调用方需要知道的;实现细节留给 `.cpp`。`@param`、`@return`、`@throws` 要么全写要么全不写,标识符和标签保持英文。文件头默认写 `@file` 与 `@brief` 两行,`@file` 必须与文件名相同;作者、日期、版权、版本默认不写。标签开关在插件设置页,技能正文末尾附带当前配置的快照。写完运行 `node "<skill-dir>/scripts/checking-comments.mjs" --root <项目根>`;关闭 `@brief` 时加 `--no-brief`,配置里打开了隐私标签并填了值时加 `--allow-tag <标签>`。文件头、标签、参数完整性、成员清单与设计论证由脚本兜底。文件头与 TODO 的格式见 `references/comments.md`。
28
28
 
29
29
  ## 正反例指针
30
30
 
@@ -9,7 +9,7 @@
9
9
  - 函数保持短小;一个函数只做一件事。超过约 40 行时先找可以命名的中间步骤,不要用注释把长函数切成几段。
10
10
  - 按「准备 → 操作 → 收尾」排列语句,用空行分组;组与组之间的空行表达阶段,不用大段叙述。
11
11
  - `.cpp` 的第一个 include 是自己的配对头,然后直接包含本文件用到的标准库头;不要依赖公共头替本文件转发声明。包含顺序见 `references/google-cpp-style.md`;文件槽位见 `references/file-template.md`。
12
- - `.cpp` / `.cc` 里,重要函数按空行分阶段,每个阶段上方写一句 `//`:写边界、失败后果或为什么停在这里,不把下一行的条件翻译一遍。`IsWin` 这种单行函数不配注释;只写删掉会让下一次修改得到错误结果、而代码本身看不出的内容,变更史留给提交说明。
12
+ - `.cpp` / `.cc` 里,重要函数按空行分阶段,每个阶段上方写一句 `///`:写边界、失败后果或为什么停在这里,不把下一行的条件翻译一遍。`IsWin` 这种单行函数不配注释;只写删掉会让下一次修改得到错误结果、而代码本身看不出的内容,变更史留给提交说明。
13
13
  - 错误处理:失败是调用方正常结果时返回结果类型、`bool` 或 `std::optional`;函数无法完成自己的任务时抛异常,不拿异常做正常分支。Debug 与 Release 用同一套返回值与抛出条件,只有诊断详略不同。两种模式都要守的 invariant 用 `Check`(项目保留时),只在开发期做的自检用 `DebugCheck`;没有这两个宏就分别用 `assert` 与 `std::terminate`。捕获时按 `const` 引用,只处理能处理的类型,其余继续抛。消息按 `Type::Function: 条件 (值); 限度` 组织,见 `references/exceptions.md` 的「消息」节。
14
14
  - 声明 `noexcept` 前核对函数体:出现 `at()` 或其他可能抛出的调用就去掉。越界用 `at()` 抛 `std::out_of_range`,这个函数就不能标 `noexcept`。
15
15
  - 组合输出用 `std::format`,再写入 `std::cout` / `std::cerr`;不要在库实现里直接打印。C++23 且库支持时程序边界可用 `std::print`,C++20 及更早没有。