@aiwayds/dsh-topics-memory 0.15.0 → 0.16.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.d.ts +10 -0
- package/lib/commands.js +54 -3
- package/package.json +1 -1
- package/src/commands.ts +59 -2
package/lib/commands.d.ts
CHANGED
|
@@ -26,3 +26,13 @@ export type ManualConsolidate = (invocation: CommandInvocation) => Promise<Conso
|
|
|
26
26
|
export declare const HELP: string;
|
|
27
27
|
export declare function buildTopicsCommand(service: TopicsService, mutate: MutateFn, resolveAsk?: AskServiceResolver, resolveLlm?: LlmDirectoryResolver, distillNow?: ManualDistill, consolidateNow?: ManualConsolidate): CommandDefinition;
|
|
28
28
|
export declare function tuningHint(stats: ReturnType<typeof aggregateStats>, threshold: number): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Render the near-miss histogram as a two-line horizontal sparkline: tick
|
|
31
|
+
* labels over a log-scaled (count+1) strip, so a live session's 200:1 count
|
|
32
|
+
* range stays legible. Zero-count buckets inside the observed range draw as
|
|
33
|
+
* '·' to keep the score axis honest. Returns [] for empty or unparsable input.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderNearMissSparkline(buckets: {
|
|
36
|
+
bucket: string;
|
|
37
|
+
count: number;
|
|
38
|
+
}[]): string[];
|
package/lib/commands.js
CHANGED
|
@@ -267,9 +267,7 @@ async function renderStats(service) {
|
|
|
267
267
|
lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`);
|
|
268
268
|
}
|
|
269
269
|
if (stats.nearMissHistogram.length > 0) {
|
|
270
|
-
lines.push('',
|
|
271
|
-
for (const b of stats.nearMissHistogram)
|
|
272
|
-
lines.push(`| ${cell(b.bucket)} | ${b.count} |`);
|
|
270
|
+
lines.push('', `Near-miss 分布(低于阈值 ${service.cfg.matchThreshold.toFixed(2)} 或被结构门挡下,对数刻度):`, '', '```', ...renderNearMissSparkline(stats.nearMissHistogram), '```');
|
|
273
271
|
const hint = tuningHint(stats, service.cfg.matchThreshold);
|
|
274
272
|
if (hint !== undefined)
|
|
275
273
|
lines.push('', `💡 ${hint}`);
|
|
@@ -286,6 +284,59 @@ export function tuningHint(stats, threshold) {
|
|
|
286
284
|
}
|
|
287
285
|
return undefined;
|
|
288
286
|
}
|
|
287
|
+
const SPARK_LEVELS = '▁▂▃▄▅▆▇█';
|
|
288
|
+
const SPARK_EMPTY = '·';
|
|
289
|
+
/** Columns per histogram bucket; 2 keeps a 25-bucket range at 50 cols so the
|
|
290
|
+
* TUI renders both lines without wrapping. */
|
|
291
|
+
const SPARK_COLS_PER_BUCKET = 2;
|
|
292
|
+
/** Axis tick every 0.30 score units. */
|
|
293
|
+
const SPARK_TICK_STEP = 0.3;
|
|
294
|
+
/** Bucket width of the histogram built by nmBucket() in ilog.ts. */
|
|
295
|
+
const SPARK_BUCKET_WIDTH = 0.05;
|
|
296
|
+
/**
|
|
297
|
+
* Render the near-miss histogram as a two-line horizontal sparkline: tick
|
|
298
|
+
* labels over a log-scaled (count+1) strip, so a live session's 200:1 count
|
|
299
|
+
* range stays legible. Zero-count buckets inside the observed range draw as
|
|
300
|
+
* '·' to keep the score axis honest. Returns [] for empty or unparsable input.
|
|
301
|
+
*/
|
|
302
|
+
export function renderNearMissSparkline(buckets) {
|
|
303
|
+
const starts = buckets.map((b) => Number(b.bucket.split('–')[0]));
|
|
304
|
+
if (buckets.length === 0 || starts.some((s) => Number.isNaN(s)))
|
|
305
|
+
return [];
|
|
306
|
+
const first = Math.min(...starts);
|
|
307
|
+
const last = Math.max(...starts);
|
|
308
|
+
const n = Math.round((last - first) / SPARK_BUCKET_WIDTH) + 1;
|
|
309
|
+
const counts = new Array(n).fill(0);
|
|
310
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
311
|
+
counts[Math.round((starts[i] - first) / SPARK_BUCKET_WIDTH)] += buckets[i].count;
|
|
312
|
+
}
|
|
313
|
+
const max = Math.max(...counts);
|
|
314
|
+
const denom = Math.log10(max + 1);
|
|
315
|
+
const strip = counts
|
|
316
|
+
.map((c) => (c === 0 || denom === 0 ? SPARK_EMPTY : SPARK_LEVELS[Math.round((Math.log10(c + 1) / denom) * (SPARK_LEVELS.length - 1))]))
|
|
317
|
+
.map((ch) => ch.repeat(SPARK_COLS_PER_BUCKET))
|
|
318
|
+
.join('');
|
|
319
|
+
const width = n * SPARK_COLS_PER_BUCKET;
|
|
320
|
+
const line = new Array(width).fill(' ');
|
|
321
|
+
const used = new Array(width).fill(false);
|
|
322
|
+
const put = (at, label) => {
|
|
323
|
+
if (at < 0 || at + label.length > width)
|
|
324
|
+
return;
|
|
325
|
+
if (used.slice(at, at + label.length).some(Boolean))
|
|
326
|
+
return;
|
|
327
|
+
for (let k = 0; k < label.length; k++) {
|
|
328
|
+
line[at + k] = label[k];
|
|
329
|
+
used[at + k] = true;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
for (let i = 0;; i += Math.round(SPARK_TICK_STEP / SPARK_BUCKET_WIDTH)) {
|
|
333
|
+
if (i > 0 && i >= n - 2)
|
|
334
|
+
break;
|
|
335
|
+
put(i * SPARK_COLS_PER_BUCKET, (first + i * SPARK_BUCKET_WIDTH).toFixed(2));
|
|
336
|
+
}
|
|
337
|
+
put(width - 4, (first + n * SPARK_BUCKET_WIDTH).toFixed(2));
|
|
338
|
+
return [line.join('').replace(/ +$/, ''), strip];
|
|
339
|
+
}
|
|
289
340
|
/** /topics list caps the table at this many newest topics. */
|
|
290
341
|
const LIST_LIMIT = 100;
|
|
291
342
|
async function renderList(service) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-topics-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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
|
@@ -306,8 +306,14 @@ async function renderStats(service: TopicsService): Promise<string> {
|
|
|
306
306
|
for (const t of stats.topTopics.slice(0, 5)) lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`)
|
|
307
307
|
}
|
|
308
308
|
if (stats.nearMissHistogram.length > 0) {
|
|
309
|
-
lines.push(
|
|
310
|
-
|
|
309
|
+
lines.push(
|
|
310
|
+
'',
|
|
311
|
+
`Near-miss 分布(低于阈值 ${service.cfg.matchThreshold.toFixed(2)} 或被结构门挡下,对数刻度):`,
|
|
312
|
+
'',
|
|
313
|
+
'```',
|
|
314
|
+
...renderNearMissSparkline(stats.nearMissHistogram),
|
|
315
|
+
'```',
|
|
316
|
+
)
|
|
311
317
|
const hint = tuningHint(stats, service.cfg.matchThreshold)
|
|
312
318
|
if (hint !== undefined) lines.push('', `💡 ${hint}`)
|
|
313
319
|
}
|
|
@@ -325,6 +331,57 @@ export function tuningHint(stats: ReturnType<typeof aggregateStats>, threshold:
|
|
|
325
331
|
return undefined
|
|
326
332
|
}
|
|
327
333
|
|
|
334
|
+
const SPARK_LEVELS = '▁▂▃▄▅▆▇█'
|
|
335
|
+
const SPARK_EMPTY = '·'
|
|
336
|
+
/** Columns per histogram bucket; 2 keeps a 25-bucket range at 50 cols so the
|
|
337
|
+
* TUI renders both lines without wrapping. */
|
|
338
|
+
const SPARK_COLS_PER_BUCKET = 2
|
|
339
|
+
/** Axis tick every 0.30 score units. */
|
|
340
|
+
const SPARK_TICK_STEP = 0.3
|
|
341
|
+
/** Bucket width of the histogram built by nmBucket() in ilog.ts. */
|
|
342
|
+
const SPARK_BUCKET_WIDTH = 0.05
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Render the near-miss histogram as a two-line horizontal sparkline: tick
|
|
346
|
+
* labels over a log-scaled (count+1) strip, so a live session's 200:1 count
|
|
347
|
+
* range stays legible. Zero-count buckets inside the observed range draw as
|
|
348
|
+
* '·' to keep the score axis honest. Returns [] for empty or unparsable input.
|
|
349
|
+
*/
|
|
350
|
+
export function renderNearMissSparkline(buckets: { bucket: string; count: number }[]): string[] {
|
|
351
|
+
const starts = buckets.map((b) => Number(b.bucket.split('–')[0]))
|
|
352
|
+
if (buckets.length === 0 || starts.some((s) => Number.isNaN(s))) return []
|
|
353
|
+
const first = Math.min(...starts)
|
|
354
|
+
const last = Math.max(...starts)
|
|
355
|
+
const n = Math.round((last - first) / SPARK_BUCKET_WIDTH) + 1
|
|
356
|
+
const counts = new Array<number>(n).fill(0)
|
|
357
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
358
|
+
counts[Math.round((starts[i] - first) / SPARK_BUCKET_WIDTH)] += buckets[i].count
|
|
359
|
+
}
|
|
360
|
+
const max = Math.max(...counts)
|
|
361
|
+
const denom = Math.log10(max + 1)
|
|
362
|
+
const strip = counts
|
|
363
|
+
.map((c) => (c === 0 || denom === 0 ? SPARK_EMPTY : SPARK_LEVELS[Math.round((Math.log10(c + 1) / denom) * (SPARK_LEVELS.length - 1))]))
|
|
364
|
+
.map((ch) => ch.repeat(SPARK_COLS_PER_BUCKET))
|
|
365
|
+
.join('')
|
|
366
|
+
const width = n * SPARK_COLS_PER_BUCKET
|
|
367
|
+
const line = new Array<string>(width).fill(' ')
|
|
368
|
+
const used = new Array<boolean>(width).fill(false)
|
|
369
|
+
const put = (at: number, label: string) => {
|
|
370
|
+
if (at < 0 || at + label.length > width) return
|
|
371
|
+
if (used.slice(at, at + label.length).some(Boolean)) return
|
|
372
|
+
for (let k = 0; k < label.length; k++) {
|
|
373
|
+
line[at + k] = label[k]
|
|
374
|
+
used[at + k] = true
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (let i = 0; ; i += Math.round(SPARK_TICK_STEP / SPARK_BUCKET_WIDTH)) {
|
|
378
|
+
if (i > 0 && i >= n - 2) break
|
|
379
|
+
put(i * SPARK_COLS_PER_BUCKET, (first + i * SPARK_BUCKET_WIDTH).toFixed(2))
|
|
380
|
+
}
|
|
381
|
+
put(width - 4, (first + n * SPARK_BUCKET_WIDTH).toFixed(2))
|
|
382
|
+
return [line.join('').replace(/ +$/, ''), strip]
|
|
383
|
+
}
|
|
384
|
+
|
|
328
385
|
/** /topics list caps the table at this many newest topics. */
|
|
329
386
|
const LIST_LIMIT = 100
|
|
330
387
|
|