@aiwayds/dsh-topics-memory 0.11.0 → 0.12.0

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/lib/commands.js CHANGED
@@ -86,18 +86,17 @@ function fail(text) {
86
86
  async function renderStatus(service) {
87
87
  const s = await service.store.status();
88
88
  const cfg = service.cfg;
89
- const lines = [
90
- `dsh-topics-memory @ ${s.root}`,
91
- ` 模式:${service.githubMode ? `github(${cfg.repo})` : 'local-only'}`,
92
- ` Topics:${s.topicCount}(draft ${s.byStatus.draft} / stable ${s.byStatus.stable} / deprecated ${s.byStatus.deprecated})`,
93
- ` 观察积压:${s.observationsPending} 未蒸馏 / ${s.observationsTotal} 总量`,
94
- ` 冲突:${s.conflicts.length === 0 ? '无' : s.conflicts.join('、')}`,
95
- ` 损坏文件:${s.broken.length === 0 ? '无' : s.broken.join('、')}`,
96
- ` git:${s.git ? `是(HEAD ${s.head?.slice(0, 10) ?? '??'})` : '否'}`,
97
- ` 注入:${cfg.autoInject ? `${cfg.injectMode === 'digest' ? 'digest' : 'pointer'} 模式(topK ${cfg.topK},预算 ${cfg.injectMode === 'digest' ? cfg.totalBudget : Math.min(cfg.totalBudget, 600)} tok,阈值 ${cfg.matchThreshold})` : '关'}`,
98
- ` 去重:${cfg.injectDedup ? '开(同会话已注入的 Topic 不重注)' : '关'}`,
99
- ` 慢道:${cfg.qualityLane === 'off' ? '关' : cfg.qualityLane === 'always' ? `每轮(${cfg.distillProvider}/${cfg.distillModel})` : `采样 1/3(${cfg.distillProvider}/${cfg.distillModel})`}`,
100
- ` 蒸馏:${cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'}`,
89
+ const rows = [
90
+ ['模式', service.githubMode ? `github(${cell(cfg.repo)})` : 'local-only'],
91
+ ['Topics', `${s.topicCount}(draft ${s.byStatus.draft} / stable ${s.byStatus.stable} / deprecated ${s.byStatus.deprecated})`],
92
+ ['观察积压', `${s.observationsPending} 未蒸馏 / ${s.observationsTotal} 总量`],
93
+ ['冲突', s.conflicts.length === 0 ? '无' : s.conflicts.map((c) => cell(c)).join('、')],
94
+ ['损坏文件', s.broken.length === 0 ? '无' : s.broken.map((b) => cell(b)).join('、')],
95
+ ['git', s.git ? `是(HEAD ${s.head?.slice(0, 10) ?? '??'})` : '否'],
96
+ ['注入', cfg.autoInject ? `${cfg.injectMode === 'digest' ? 'digest' : 'pointer'} 模式(topK ${cfg.topK},预算 ${cfg.injectMode === 'digest' ? cfg.totalBudget : Math.min(cfg.totalBudget, 600)} tok,阈值 ${cfg.matchThreshold})` : '关'],
97
+ ['注入去重', cfg.injectDedup ? '开(同会话已注入的 Topic 不重注)' : '关'],
98
+ ['慢道', cfg.qualityLane === 'off' ? '关' : cfg.qualityLane === 'always' ? `每轮(${cfg.distillProvider}/${cfg.distillModel})` : `采样 1/3(${cfg.distillProvider}/${cfg.distillModel})`],
99
+ ['蒸馏', cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'],
101
100
  ];
102
101
  // Last lane outcome (distill-state summary) — what "checkable via
103
102
  // /topics status" promises; absent until the first run of this bundle.
@@ -107,14 +106,20 @@ async function renderStatus(service) {
107
106
  ? `标记 ${String(lastRun.marked ?? 0)} 条观察`
108
107
  : `失败(${String(lastRun.reason ?? 'unknown')})`;
109
108
  const gc = typeof lastRun.gcDropped === 'number' && lastRun.gcDropped > 0 ? `,GC 回收 ${lastRun.gcDropped}` : '';
110
- lines.push(` 最近蒸馏:${outcome}${gc} @ ${String(lastRun.at ?? '').slice(0, 19).replace('T', ' ')}`);
109
+ rows.push(['最近蒸馏', `${outcome}${gc} @ ${cell(String(lastRun.at ?? '').slice(0, 19).replace('T', ' '))}`]);
111
110
  }
112
111
  if (service.sync !== undefined) {
113
- lines.push(` 上次推送:${service.sync.lastPushAt ?? '从未'}`);
112
+ rows.push(['上次推送', cell(String(service.sync.lastPushAt ?? '从未'))]);
114
113
  if (service.sync.lastError !== '')
115
- lines.push(` 同步错误:${service.sync.lastError.split('\n').at(-1)}`);
114
+ rows.push(['同步错误', cell(service.sync.lastError.split('\n').at(-1) ?? '')]);
116
115
  }
117
- return lines.join('\n');
116
+ return [
117
+ `dsh-topics-memory @ ${s.root}`,
118
+ '',
119
+ '| 字段 | 值 |',
120
+ '| --- | --- |',
121
+ ...rows.map(([k, v]) => `| ${k} | ${v} |`),
122
+ ].join('\n');
118
123
  }
119
124
  /**
120
125
  * `/topics distill` — one manual distill run over the current observation pool.
@@ -151,9 +156,13 @@ async function renderStats(service) {
151
156
  return '还没有注入记录 —— 用起来之后这里会有 hit rate / top-N / near-miss 分布。';
152
157
  const lines = [
153
158
  `注入统计(最近 ${records.length} 轮):`,
154
- ` hit rate:${(stats.hitRate * 100).toFixed(1)}%(${stats.injectedRounds}/${stats.rounds} 轮注入)`,
155
- ` 零命中轮:${stats.zeroHitRounds};平均命中 ${stats.avgHitsPerRound} 条/轮`,
156
- ` 平均预算占用:${stats.avgBudgetUtilization} tok`,
159
+ '',
160
+ '| 指标 | 值 |',
161
+ '| --- | --- |',
162
+ `| hit rate | ${(stats.hitRate * 100).toFixed(1)}%(${stats.injectedRounds}/${stats.rounds} 轮注入) |`,
163
+ `| 零命中轮 | ${stats.zeroHitRounds} |`,
164
+ `| 平均命中 | ${stats.avgHitsPerRound} 条/轮 |`,
165
+ `| 平均预算占用 | ${stats.avgBudgetUtilization} tok |`,
157
166
  ];
158
167
  // v4 lane split — how much of the injection traffic each lane carries.
159
168
  const slowRounds = records.filter((r) => r.lane === 'slow' || r.lane === 'mixed').length;
@@ -166,7 +175,7 @@ async function renderStats(service) {
166
175
  .sort((a, b) => a - b);
167
176
  medianLag = `,赶上中位时延 ${(lags[Math.floor(lags.length / 2)] / 1000).toFixed(1)}s`;
168
177
  }
169
- lines.push(` 慢道参与轮:${slowRounds}(快 ${(records.length - slowRounds)} / 慢或混合 ${slowRounds}${medianLag})`);
178
+ lines.push(`| 慢道参与轮 | ${slowRounds}(快 ${records.length - slowRounds} / 慢或混合 ${slowRounds}${medianLag}) |`);
170
179
  }
171
180
  // Pointer open rate (v4 §4.3): topic_open calls vs pointer entries injected.
172
181
  // Echoed hits ride the retrieval but never became pointers — excluded here.
@@ -176,24 +185,24 @@ async function renderStats(service) {
176
185
  r.hits.filter((h) => !(r.deduped ?? []).includes(h.slug) && !(r.echoed ?? []).includes(h.slug)).length +
177
186
  (r.slow?.length ?? 0), 0);
178
187
  const rate = entries === 0 ? 0 : Math.min(1, opens.length / entries);
179
- lines.push(` 指针打开率:${(rate * 100).toFixed(1)}%(${opens.length} 次 topic_open / ${entries} 条注入指针)`);
188
+ lines.push(`| 指针打开率 | ${(rate * 100).toFixed(1)}%(${opens.length} 次 topic_open / ${entries} 条注入指针) |`);
180
189
  }
181
190
  const echoedCount = records.reduce((acc, r) => acc + (r.echoed?.length ?? 0), 0);
182
191
  if (echoedCount > 0) {
183
- lines.push(` 回声抑制:${echoedCount} 次(本会话蒸馏出的 topic 不回注)`);
192
+ lines.push(`| 回声抑制 | ${echoedCount} 次(本会话蒸馏出的 topic 不回注) |`);
184
193
  }
185
194
  if (stats.topTopics.length > 0) {
186
- lines.push(' Top-N 被注入 Topic:');
195
+ lines.push('', 'Top-N 被注入 Topic:', '', '| Slug | 注入次数 |', '| --- | --- |');
187
196
  for (const t of stats.topTopics.slice(0, 5))
188
- lines.push(` ${t.slug} ×${t.count}`);
197
+ lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`);
189
198
  }
190
199
  if (stats.nearMissHistogram.length > 0) {
191
- lines.push(' Near-miss 分布(低于阈值或被结构门挡下):');
200
+ lines.push('', 'Near-miss 分布(低于阈值或被结构门挡下):', '', '| 分数段 | 次数 |', '| --- | --- |');
192
201
  for (const b of stats.nearMissHistogram)
193
- lines.push(` ${b.bucket}: ${b.count}`);
202
+ lines.push(`| ${cell(b.bucket)} | ${b.count} |`);
194
203
  const hint = tuningHint(stats, service.cfg.matchThreshold);
195
204
  if (hint !== undefined)
196
- lines.push(` 💡 ${hint}`);
205
+ lines.push('', `💡 ${hint}`);
197
206
  }
198
207
  return lines.join('\n');
199
208
  }
@@ -207,20 +216,31 @@ export function tuningHint(stats, threshold) {
207
216
  }
208
217
  return undefined;
209
218
  }
219
+ /** /topics list caps the table at this many newest topics. */
220
+ const LIST_LIMIT = 100;
210
221
  async function renderList(service) {
211
222
  const metas = await service.store.listTopics();
212
223
  if (metas.length === 0)
213
224
  return 'Bundle 里还没有 Topic —— 在会话里让我记点什么,或 /topics set 配置好蒸馏。';
214
- metas.sort((a, b) => a.slug.localeCompare(b.slug));
225
+ // Newest first, capped: with a large bundle the table is for scanning
226
+ // recent work, not an exhaustive roster — /topics show <slug> reaches any
227
+ // topic the cap hides. (store.ts re-stamps generated.at on every topic
228
+ // write, so it IS the updated-at time; the slug tiebreak keeps same-stamp
229
+ // rows stable.)
230
+ metas.sort((a, b) => b.generatedAt.localeCompare(a.generatedAt) || a.slug.localeCompare(b.slug));
231
+ const shown = metas.slice(0, LIST_LIMIT);
215
232
  const lines = [
216
- `共 ${metas.length} 个 Topic:`,
233
+ `共 ${metas.length} 个 Topic(按最近更新排序):`,
217
234
  '',
218
- '| Slug | 标题 | 状态 | 标签 | 更新 |',
219
- '| --- | --- | --- | --- | --- |',
235
+ '| # | Slug | 标题 | 状态 | 标签 | 更新 |',
236
+ '| --- | --- | --- | --- | --- | --- |',
220
237
  ];
221
- for (const m of metas) {
238
+ shown.forEach((m, i) => {
222
239
  const tags = m.tags.length > 0 ? m.tags.map((t) => `#${cell(t)}`).join(' ') : '—';
223
- lines.push(`| \`${cell(m.slug)}\` | ${cell(m.title)} | ${m.status} | ${tags} | ${cell(m.generatedAt.slice(0, 10))} |`);
240
+ lines.push(`| ${i + 1} | \`${cell(m.slug)}\` | ${cell(m.title)} | ${m.status} | ${tags} | ${cell(m.generatedAt.slice(0, 10))} |`);
241
+ });
242
+ if (metas.length > shown.length) {
243
+ lines.push('', `… 其余 ${metas.length - shown.length} 个更早的 Topic 未显示(/topics show <slug> 直达)。`);
224
244
  }
225
245
  return lines.join('\n');
226
246
  }
@@ -254,11 +274,15 @@ async function renderHistory(service, slug) {
254
274
  const { entries } = await service.history(slug, 30);
255
275
  if (entries.length === 0)
256
276
  return fail(`Topic “${slug}” 没有历史(不存在或 bundle 不是 git 仓库)`);
257
- const lines = [`${slug} 的变更史(${entries.length} 条):`];
277
+ const lines = [
278
+ `${slug} 的变更史(${entries.length} 条):`,
279
+ '',
280
+ '| Hash | 时间 | 变更 | 当时的结论 |',
281
+ '| --- | --- | --- | --- |',
282
+ ];
258
283
  for (const e of entries) {
259
- lines.push(` ${e.hash} ${e.date.slice(0, 19).replace('T', ' ')} ${e.message}`);
260
- if (e.conclusion !== undefined && e.conclusion !== '')
261
- lines.push(` └ 结论当时:${e.conclusion}`);
284
+ const conclusion = e.conclusion !== undefined && e.conclusion !== '' ? cell(e.conclusion) : '—';
285
+ lines.push(`| \`${cell(e.hash)}\` | ${cell(e.date.slice(0, 19).replace('T', ' '))} | ${cell(e.message)} | ${conclusion} |`);
262
286
  }
263
287
  return ok(lines.join('\n'));
264
288
  }
@@ -316,9 +340,14 @@ async function doSync(service, direction) {
316
340
  return fail('用法:/topics sync [pull|push]');
317
341
  }
318
342
  function renderConfig(cfg) {
319
- const lines = ['当前配置:'];
343
+ const lines = [
344
+ '当前配置:',
345
+ '',
346
+ '| 配置项 | 值 |',
347
+ '| --- | --- |',
348
+ ];
320
349
  for (const key of CONFIG_KEYS) {
321
- lines.push(` ${displayKey(key)} = ${String(cfg[key])}`);
350
+ lines.push(`| ${displayKey(key)} | ${cell(String(cfg[key]))} |`);
322
351
  }
323
352
  return lines.join('\n');
324
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-topics-memory",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "dsh-plugin: quality-first topic memory for dsh — a topic keeps the starting question, conclusion, impact and dependencies, not the process; local-first git-traced OKF bundle, budgeted LLM-free injection",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/src/commands.ts CHANGED
@@ -114,18 +114,17 @@ function fail(text: string): CommandResult {
114
114
  async function renderStatus(service: TopicsService): Promise<string> {
115
115
  const s = await service.store.status()
116
116
  const cfg = service.cfg
117
- const lines = [
118
- `dsh-topics-memory @ ${s.root}`,
119
- ` 模式:${service.githubMode ? `github(${cfg.repo})` : 'local-only'}`,
120
- ` Topics:${s.topicCount}(draft ${s.byStatus.draft} / stable ${s.byStatus.stable} / deprecated ${s.byStatus.deprecated})`,
121
- ` 观察积压:${s.observationsPending} 未蒸馏 / ${s.observationsTotal} 总量`,
122
- ` 冲突:${s.conflicts.length === 0 ? '无' : s.conflicts.join('、')}`,
123
- ` 损坏文件:${s.broken.length === 0 ? '无' : s.broken.join('、')}`,
124
- ` git:${s.git ? `是(HEAD ${s.head?.slice(0, 10) ?? '??'})` : '否'}`,
125
- ` 注入:${cfg.autoInject ? `${cfg.injectMode === 'digest' ? 'digest' : 'pointer'} 模式(topK ${cfg.topK},预算 ${cfg.injectMode === 'digest' ? cfg.totalBudget : Math.min(cfg.totalBudget, 600)} tok,阈值 ${cfg.matchThreshold})` : '关'}`,
126
- ` 去重:${cfg.injectDedup ? '开(同会话已注入的 Topic 不重注)' : '关'}`,
127
- ` 慢道:${cfg.qualityLane === 'off' ? '关' : cfg.qualityLane === 'always' ? `每轮(${cfg.distillProvider}/${cfg.distillModel})` : `采样 1/3(${cfg.distillProvider}/${cfg.distillModel})`}`,
128
- ` 蒸馏:${cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'}`,
117
+ const rows: [string, string][] = [
118
+ ['模式', service.githubMode ? `github(${cell(cfg.repo)})` : 'local-only'],
119
+ ['Topics', `${s.topicCount}(draft ${s.byStatus.draft} / stable ${s.byStatus.stable} / deprecated ${s.byStatus.deprecated})`],
120
+ ['观察积压', `${s.observationsPending} 未蒸馏 / ${s.observationsTotal} 总量`],
121
+ ['冲突', s.conflicts.length === 0 ? '无' : s.conflicts.map((c) => cell(c)).join('、')],
122
+ ['损坏文件', s.broken.length === 0 ? '无' : s.broken.map((b) => cell(b)).join('、')],
123
+ ['git', s.git ? `是(HEAD ${s.head?.slice(0, 10) ?? '??'})` : '否'],
124
+ ['注入', cfg.autoInject ? `${cfg.injectMode === 'digest' ? 'digest' : 'pointer'} 模式(topK ${cfg.topK},预算 ${cfg.injectMode === 'digest' ? cfg.totalBudget : Math.min(cfg.totalBudget, 600)} tok,阈值 ${cfg.matchThreshold})` : '关'],
125
+ ['注入去重', cfg.injectDedup ? '开(同会话已注入的 Topic 不重注)' : '关'],
126
+ ['慢道', cfg.qualityLane === 'off' ? '关' : cfg.qualityLane === 'always' ? `每轮(${cfg.distillProvider}/${cfg.distillModel})` : `采样 1/3(${cfg.distillProvider}/${cfg.distillModel})`],
127
+ ['蒸馏', cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'],
129
128
  ]
130
129
  // Last lane outcome (distill-state summary) — what "checkable via
131
130
  // /topics status" promises; absent until the first run of this bundle.
@@ -136,13 +135,19 @@ async function renderStatus(service: TopicsService): Promise<string> {
136
135
  ? `标记 ${String(lastRun.marked ?? 0)} 条观察`
137
136
  : `失败(${String(lastRun.reason ?? 'unknown')})`
138
137
  const gc = typeof lastRun.gcDropped === 'number' && lastRun.gcDropped > 0 ? `,GC 回收 ${lastRun.gcDropped}` : ''
139
- lines.push(` 最近蒸馏:${outcome}${gc} @ ${String(lastRun.at ?? '').slice(0, 19).replace('T', ' ')}`)
138
+ rows.push(['最近蒸馏', `${outcome}${gc} @ ${cell(String(lastRun.at ?? '').slice(0, 19).replace('T', ' '))}`])
140
139
  }
141
140
  if (service.sync !== undefined) {
142
- lines.push(` 上次推送:${service.sync.lastPushAt ?? '从未'}`)
143
- if (service.sync.lastError !== '') lines.push(` 同步错误:${service.sync.lastError.split('\n').at(-1)}`)
141
+ rows.push(['上次推送', cell(String(service.sync.lastPushAt ?? '从未'))])
142
+ if (service.sync.lastError !== '') rows.push(['同步错误', cell(service.sync.lastError.split('\n').at(-1) ?? '')])
144
143
  }
145
- return lines.join('\n')
144
+ return [
145
+ `dsh-topics-memory @ ${s.root}`,
146
+ '',
147
+ '| 字段 | 值 |',
148
+ '| --- | --- |',
149
+ ...rows.map(([k, v]) => `| ${k} | ${v} |`),
150
+ ].join('\n')
146
151
  }
147
152
 
148
153
  /**
@@ -178,9 +183,13 @@ async function renderStats(service: TopicsService): Promise<string> {
178
183
  if (records.length === 0) return '还没有注入记录 —— 用起来之后这里会有 hit rate / top-N / near-miss 分布。'
179
184
  const lines = [
180
185
  `注入统计(最近 ${records.length} 轮):`,
181
- ` hit rate:${(stats.hitRate * 100).toFixed(1)}%(${stats.injectedRounds}/${stats.rounds} 轮注入)`,
182
- ` 零命中轮:${stats.zeroHitRounds};平均命中 ${stats.avgHitsPerRound} 条/轮`,
183
- ` 平均预算占用:${stats.avgBudgetUtilization} tok`,
186
+ '',
187
+ '| 指标 | 值 |',
188
+ '| --- | --- |',
189
+ `| hit rate | ${(stats.hitRate * 100).toFixed(1)}%(${stats.injectedRounds}/${stats.rounds} 轮注入) |`,
190
+ `| 零命中轮 | ${stats.zeroHitRounds} |`,
191
+ `| 平均命中 | ${stats.avgHitsPerRound} 条/轮 |`,
192
+ `| 平均预算占用 | ${stats.avgBudgetUtilization} tok |`,
184
193
  ]
185
194
  // v4 lane split — how much of the injection traffic each lane carries.
186
195
  const slowRounds = records.filter((r) => r.lane === 'slow' || r.lane === 'mixed').length
@@ -193,7 +202,7 @@ async function renderStats(service: TopicsService): Promise<string> {
193
202
  .sort((a, b) => a - b)
194
203
  medianLag = `,赶上中位时延 ${(lags[Math.floor(lags.length / 2)] / 1000).toFixed(1)}s`
195
204
  }
196
- lines.push(` 慢道参与轮:${slowRounds}(快 ${(records.length - slowRounds)} / 慢或混合 ${slowRounds}${medianLag})`)
205
+ lines.push(`| 慢道参与轮 | ${slowRounds}(快 ${records.length - slowRounds} / 慢或混合 ${slowRounds}${medianLag}) |`)
197
206
  }
198
207
  // Pointer open rate (v4 §4.3): topic_open calls vs pointer entries injected.
199
208
  // Echoed hits ride the retrieval but never became pointers — excluded here.
@@ -207,21 +216,21 @@ async function renderStats(service: TopicsService): Promise<string> {
207
216
  0,
208
217
  )
209
218
  const rate = entries === 0 ? 0 : Math.min(1, opens.length / entries)
210
- lines.push(` 指针打开率:${(rate * 100).toFixed(1)}%(${opens.length} 次 topic_open / ${entries} 条注入指针)`)
219
+ lines.push(`| 指针打开率 | ${(rate * 100).toFixed(1)}%(${opens.length} 次 topic_open / ${entries} 条注入指针) |`)
211
220
  }
212
221
  const echoedCount = records.reduce((acc, r) => acc + (r.echoed?.length ?? 0), 0)
213
222
  if (echoedCount > 0) {
214
- lines.push(` 回声抑制:${echoedCount} 次(本会话蒸馏出的 topic 不回注)`)
223
+ lines.push(`| 回声抑制 | ${echoedCount} 次(本会话蒸馏出的 topic 不回注) |`)
215
224
  }
216
225
  if (stats.topTopics.length > 0) {
217
- lines.push(' Top-N 被注入 Topic:')
218
- for (const t of stats.topTopics.slice(0, 5)) lines.push(` ${t.slug} ×${t.count}`)
226
+ lines.push('', 'Top-N 被注入 Topic:', '', '| Slug | 注入次数 |', '| --- | --- |')
227
+ for (const t of stats.topTopics.slice(0, 5)) lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`)
219
228
  }
220
229
  if (stats.nearMissHistogram.length > 0) {
221
- lines.push(' Near-miss 分布(低于阈值或被结构门挡下):')
222
- for (const b of stats.nearMissHistogram) lines.push(` ${b.bucket}: ${b.count}`)
230
+ lines.push('', 'Near-miss 分布(低于阈值或被结构门挡下):', '', '| 分数段 | 次数 |', '| --- | --- |')
231
+ for (const b of stats.nearMissHistogram) lines.push(`| ${cell(b.bucket)} | ${b.count} |`)
223
232
  const hint = tuningHint(stats, service.cfg.matchThreshold)
224
- if (hint !== undefined) lines.push(` 💡 ${hint}`)
233
+ if (hint !== undefined) lines.push('', `💡 ${hint}`)
225
234
  }
226
235
  return lines.join('\n')
227
236
  }
@@ -237,19 +246,31 @@ export function tuningHint(stats: ReturnType<typeof aggregateStats>, threshold:
237
246
  return undefined
238
247
  }
239
248
 
249
+ /** /topics list caps the table at this many newest topics. */
250
+ const LIST_LIMIT = 100
251
+
240
252
  async function renderList(service: TopicsService): Promise<string> {
241
253
  const metas = await service.store.listTopics()
242
254
  if (metas.length === 0) return 'Bundle 里还没有 Topic —— 在会话里让我记点什么,或 /topics set 配置好蒸馏。'
243
- metas.sort((a, b) => a.slug.localeCompare(b.slug))
255
+ // Newest first, capped: with a large bundle the table is for scanning
256
+ // recent work, not an exhaustive roster — /topics show <slug> reaches any
257
+ // topic the cap hides. (store.ts re-stamps generated.at on every topic
258
+ // write, so it IS the updated-at time; the slug tiebreak keeps same-stamp
259
+ // rows stable.)
260
+ metas.sort((a, b) => b.generatedAt.localeCompare(a.generatedAt) || a.slug.localeCompare(b.slug))
261
+ const shown = metas.slice(0, LIST_LIMIT)
244
262
  const lines = [
245
- `共 ${metas.length} 个 Topic:`,
263
+ `共 ${metas.length} 个 Topic(按最近更新排序):`,
246
264
  '',
247
- '| Slug | 标题 | 状态 | 标签 | 更新 |',
248
- '| --- | --- | --- | --- | --- |',
265
+ '| # | Slug | 标题 | 状态 | 标签 | 更新 |',
266
+ '| --- | --- | --- | --- | --- | --- |',
249
267
  ]
250
- for (const m of metas) {
268
+ shown.forEach((m, i) => {
251
269
  const tags = m.tags.length > 0 ? m.tags.map((t) => `#${cell(t)}`).join(' ') : '—'
252
- lines.push(`| \`${cell(m.slug)}\` | ${cell(m.title)} | ${m.status} | ${tags} | ${cell(m.generatedAt.slice(0, 10))} |`)
270
+ lines.push(`| ${i + 1} | \`${cell(m.slug)}\` | ${cell(m.title)} | ${m.status} | ${tags} | ${cell(m.generatedAt.slice(0, 10))} |`)
271
+ })
272
+ if (metas.length > shown.length) {
273
+ lines.push('', `… 其余 ${metas.length - shown.length} 个更早的 Topic 未显示(/topics show <slug> 直达)。`)
253
274
  }
254
275
  return lines.join('\n')
255
276
  }
@@ -281,10 +302,15 @@ async function renderHistory(service: TopicsService, slug: string | undefined):
281
302
  if (slug === undefined || slug === '') return fail('用法:/topics history <slug>')
282
303
  const { entries } = await service.history(slug, 30)
283
304
  if (entries.length === 0) return fail(`Topic “${slug}” 没有历史(不存在或 bundle 不是 git 仓库)`)
284
- const lines = [`${slug} 的变更史(${entries.length} 条):`]
305
+ const lines = [
306
+ `${slug} 的变更史(${entries.length} 条):`,
307
+ '',
308
+ '| Hash | 时间 | 变更 | 当时的结论 |',
309
+ '| --- | --- | --- | --- |',
310
+ ]
285
311
  for (const e of entries) {
286
- lines.push(` ${e.hash} ${e.date.slice(0, 19).replace('T', ' ')} ${e.message}`)
287
- if (e.conclusion !== undefined && e.conclusion !== '') lines.push(` └ 结论当时:${e.conclusion}`)
312
+ const conclusion = e.conclusion !== undefined && e.conclusion !== '' ? cell(e.conclusion) : '—'
313
+ lines.push(`| \`${cell(e.hash)}\` | ${cell(e.date.slice(0, 19).replace('T', ' '))} | ${cell(e.message)} | ${conclusion} |`)
288
314
  }
289
315
  return ok(lines.join('\n'))
290
316
  }
@@ -341,9 +367,14 @@ async function doSync(service: TopicsService, direction: string | undefined): Pr
341
367
  }
342
368
 
343
369
  function renderConfig(cfg: TopicsConfigValue): string {
344
- const lines = ['当前配置:']
370
+ const lines = [
371
+ '当前配置:',
372
+ '',
373
+ '| 配置项 | 值 |',
374
+ '| --- | --- |',
375
+ ]
345
376
  for (const key of CONFIG_KEYS) {
346
- lines.push(` ${displayKey(key)} = ${String(cfg[key])}`)
377
+ lines.push(`| ${displayKey(key)} | ${cell(String(cfg[key]))} |`)
347
378
  }
348
379
  return lines.join('\n')
349
380
  }