@aiwayds/dsh-topics-memory 0.20.0 → 0.22.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/README.md +6 -1
- package/README.zh.md +6 -1
- package/lib/commands.js +27 -11
- package/lib/config.d.ts +17 -1
- package/lib/config.js +9 -1
- package/lib/ilog.d.ts +129 -0
- package/lib/ilog.js +236 -0
- package/lib/index.d.ts +2 -0
- package/lib/retrieval.d.ts +13 -0
- package/lib/retrieval.js +10 -0
- package/lib/service.d.ts +43 -2
- package/lib/service.js +121 -36
- package/lib/store.d.ts +6 -12
- package/lib/store.js +11 -2
- package/lib/tools.js +6 -2
- package/package.json +25 -25
- package/src/commands.ts +35 -16
- package/src/config.ts +13 -1
- package/src/ilog.ts +289 -0
- package/src/retrieval.ts +22 -0
- package/src/service.ts +130 -43
- package/src/store.ts +14 -8
- package/src/tools.ts +6 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
|
|
4
4
|
|
|
5
5
|
A dsh plugin: maintains "working topic memory" as an [OKF (Open Knowledge Format v0.2)](https://github.com/GoogleCloudPlatform/open-knowledge-format) knowledge bundle, persisted in a local git repository (optionally synced to a private GitHub repo), with conclusions traceable through git history, sessions automatically observed and distilled into knowledge, and relevant topics injected to the model before every turn.
|
|
6
6
|
|
|
7
|
-
> **Requires dsh >= 0.
|
|
7
|
+
> **Requires dsh >= 0.2.0-rc.2** — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the `latest`/`next` dist-tags at runtime). **The alpha line is no longer supported.**
|
|
8
8
|
|
|
9
9
|
## Demo
|
|
10
10
|
|
|
@@ -201,6 +201,7 @@ First-time setup belongs to `/topics onboard`; day-to-day tuning is `/topics set
|
|
|
201
201
|
| `consolidateCadence` | `daily` | Consolidation-lane cadence: `daily`/`3d`/`7d`/`off`. At session start the plugin checks the last consolidation time (`meta/consolidate-state.json`) and, when due, runs the LLM gardener in the background (reusing the distill model route): local lexical clustering only feeds near-look-alike candidate clusters; four op kinds merge/promote/deprecate/refresh, out-of-scope ops are dropped; a failed call never advances the stamp, so the next start retries |
|
|
202
202
|
| `deprecatedTtlDays` | `15` | Deprecated topics older than N days are dropped at session start (local rule, no model; each drop is its own git commit — history stays recoverable); `0` disables the sweep |
|
|
203
203
|
| `usageBoost` | `0.15` | Usage boost (ADR 0015): topics injected/opened in the last 30 days score higher at retrieval (gate-scoped, capped at 0.2, never granted to zero-lexical candidates, structural gate not waived); `0` disables |
|
|
204
|
+
| `zeroOpenDecay` | `false` | Zero-open decay: halve the retrieval score of zero-open high-frequency slugs before the threshold cut (see the section below) |
|
|
204
205
|
| `pushDebounceSeconds` | `45` | GitHub-mode debounced push interval |
|
|
205
206
|
| `jevEnabled` | `false` | System One decision layer master switch (experimental): `false` = zero behavior change — see the Jev decision layer section above |
|
|
206
207
|
| `jevBackend` | `zen` | Decision endpoint: `zen` (free) / `native` / `openrouter` |
|
|
@@ -210,6 +211,10 @@ First-time setup belongs to `/topics onboard`; day-to-day tuning is `/topics set
|
|
|
210
211
|
| `jevLayaFallback` | `false` | Local laya pace-maker: fire a parallel laya call on every jev request; its answer takes over (degraded, relative-ranking only) when the primary fails |
|
|
211
212
|
| `jevLayaUrl` | `http://127.0.0.1:8000/v1/systemone` | laya-serve endpoint for the pace-maker |
|
|
212
213
|
|
|
214
|
+
### Zero-open decay (default off)
|
|
215
|
+
|
|
216
|
+
`zeroOpenDecay: true` halves the hit score of slugs the stats panel lists as 零打开高频 — truly rendered ≥5 injections in the rolling 30 days with zero `topic_open` events ever. The decay applies at score finalization, before the match threshold, so a decayed candidate that drops below it lands in nearMisses carrying the `zero-open-decay` reason — the injection log shows why it stopped ranking. Motivation: 24 high-frequency topics held the injection leaderboard with zero opens while score and opens proved only weakly correlated (the 0.3 threshold stays untouched). Default `false` means zero behavior change; unreadable injection/open logs skip the decay (fail-open). Toggle live with `/topics set zero-open-decay on|off`.
|
|
217
|
+
|
|
213
218
|
## Acknowledgements
|
|
214
219
|
|
|
215
220
|
This project's shape is directly inspired and supported by:
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
一个 dsh 插件:把「工作 topic 记忆」维护成 [OKF 标准(Open Knowledge Format v0.2)](https://github.com/GoogleCloudPlatform/open-knowledge-format)的知识 bundle,持久化在本地 git 仓库(可选同步到 GitHub 私有仓库),利用 git 历史提供结论可追溯性,自动观察会话沉淀知识,并在每轮对话前向模型注入相关 Topic。
|
|
6
6
|
|
|
7
|
-
> **要求 dsh >= 0.
|
|
7
|
+
> **要求 dsh >= 0.2.0-rc.2** — 本插件只跟随 dsh RC/stable 线(CI 与发版在运行时解析 latest/next 中更新的 dist-tag)。**不再支持 alpha 线。**
|
|
8
8
|
|
|
9
9
|
## 演示
|
|
10
10
|
|
|
@@ -187,6 +187,7 @@ dsh plugin --profile <name> remove @aiwayds/dsh-topics-memory
|
|
|
187
187
|
| `consolidateCadence` | `daily` | 整理 lane 节拍:`daily`/`3d`/`7d`/`off`。会话启动时检查上次整理时间(`meta/consolidate-state.json`),到期即在后台自动跑 LLM 园丁(复用蒸馏模型路由):本地词面聚类只送「长得像」的候选簇,四类动作 merge/promote/deprecate/refresh,越权 op 一律丢弃;模型调用失败不推进节拍,下次启动重试 |
|
|
188
188
|
| `deprecatedTtlDays` | `15` | deprecated 条目超过 N 天在会话启动时自动删除(本地规则不依赖模型,逐条 git commit 可回溯找回);`0` 关闭清扫 |
|
|
189
189
|
| `usageBoost` | `0.15` | 使用加成(ADR 0015):近 30 天被注入命中/点开过的 Topic 检索加分(计入门槛分、帽 0.2、零词面相关不加、结构门不豁免);`0` 关闭 |
|
|
190
|
+
| `zeroOpenDecay` | `false` | 零打开衰减:零打开高频 slug 的命中分在阈值裁剪前减半(见下方小节) |
|
|
190
191
|
| `pushDebounceSeconds` | `45` | GitHub 模式去抖推送间隔 |
|
|
191
192
|
| `jevEnabled` | `false` | System One 决策层总开关(实验):`false` = 零行为变化——见上方「Jev 决策层」一节 |
|
|
192
193
|
| `jevBackend` | `zen` | 决策端点:`zen`(免费)/ `native` / `openrouter` |
|
|
@@ -197,6 +198,10 @@ dsh plugin --profile <name> remove @aiwayds/dsh-topics-memory
|
|
|
197
198
|
| `jevLayaUrl` | `http://127.0.0.1:8000/v1/systemone` | 陪跑用的 laya-serve 端点 |
|
|
198
199
|
| `jevDebug` | 关 | 诊断日志开关(走宿主 logger,默认静默) |
|
|
199
200
|
|
|
201
|
+
### 零打开衰减(默认关)
|
|
202
|
+
|
|
203
|
+
`zeroOpenDecay: true` 时,命中分在 matchThreshold 裁剪前乘 0.5:滚动 30 天内被真正注入 ≥5 次、且 opens.jsonl 全期 0 次打开的 slug(即 `/topics stats` 的「零打开高频」清单——stats 面板与打分衰减共用同一条共享规则)不再霸榜;跌破阈值的候选自然落入 nearMisses 并带 `zero-open-decay` 标签,注入日志可见原因。动机:24 个「注入 ≥5 次、0 打开」的高频话题长期霸榜,而 score 与打开仅弱相关(阈值 0.3 不动)。默认 `false` 零行为变化;injections/opens 读取失败跳过衰减(fail-open);`/topics set zero-open-decay on|off` 运行时切换。
|
|
204
|
+
|
|
200
205
|
### laya 本地陪跑(实验,默认关)
|
|
201
206
|
|
|
202
207
|
`jevLayaFallback: true` 且本地跑着 [laya](https://github.com/NandhaKishorM/laya) serve 时,每次 jev 调用并发一路本地 laya 请求:主路答案驱动决策,laya 答案并排落账——一组永久运行的 laya vs jev 同题对照。laya 是**纯遥测**(真实负载实测批内排序与主路一致率 0/14、绝对分不可用),主路失败照旧回退旧路径;laya 没起 = 毫秒级连接拒绝。
|
package/lib/commands.js
CHANGED
|
@@ -12,7 +12,7 @@ import { serializeTopicDoc, slugify, firstParagraph } from "./okf.js";
|
|
|
12
12
|
import { buildGraph, renderGraphHtml } from "./viz.js";
|
|
13
13
|
import { CONFIG_KEYS, displayKey, parseConfigValue } from "./config.js";
|
|
14
14
|
import { reloadSecrets } from "./jev/gate.js";
|
|
15
|
-
import { aggregateStats } from "./ilog.js";
|
|
15
|
+
import { aggregateStats, aggregateOpenPanel, ZERO_OPEN_MIN_INJECTIONS } from "./ilog.js";
|
|
16
16
|
import { askDistillSingle, createOnboardHandler, llmDirectoryNotice, makePanel, parseDistillInput, pickLlmDirectory, } from "./onboard.js";
|
|
17
17
|
export const HELP = [
|
|
18
18
|
'dsh-topics-memory — OKF topic 记忆(本地 bundle,git 可追溯,可选 GitHub 同步)',
|
|
@@ -20,7 +20,7 @@ export const HELP = [
|
|
|
20
20
|
' /topics status bundle 健康:topic 数、观察积压、冲突、同步状态',
|
|
21
21
|
' /topics distill 手动触发一次蒸馏(观察池 → Topic,输出 marked/created/updated/gc 摘要)',
|
|
22
22
|
' /topics consolidate 手动触发一次整理(合并重复/晋升 stable/废弃过时/刷新元数据,无视 cadence 立即跑)',
|
|
23
|
-
' /topics stats 注入统计:hit rate、
|
|
23
|
+
' /topics stats 注入统计:hit rate、open rate、注入 vs 打开、near-miss 分布与调参建议',
|
|
24
24
|
' /topics list 列出全部 Topic',
|
|
25
25
|
' /topics show <slug> 查看一个 Topic 全文(含反向引用)',
|
|
26
26
|
' /topics history <slug> 一个 Topic 的结论变更史(git log)',
|
|
@@ -277,16 +277,14 @@ async function renderStats(service) {
|
|
|
277
277
|
}
|
|
278
278
|
lines.push(`| 慢道参与轮 | ${slowRounds}(快 ${records.length - slowRounds} / 慢或混合 ${slowRounds}${medianLag}) |`);
|
|
279
279
|
}
|
|
280
|
-
//
|
|
281
|
-
//
|
|
280
|
+
// Injection-vs-open metrics (v5): the denominator counts only pointers
|
|
281
|
+
// that truly rendered (deduped/echoed/dropped all excluded, slow pointers
|
|
282
|
+
// included) and the numerator only counts DISTINCT slugs from that
|
|
283
|
+
// rendered set opened inside the injection window — search opens and
|
|
284
|
+
// never-rendered slugs cannot inflate the rate.
|
|
282
285
|
const opens = await service.store.readOpenRecords();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
r.hits.filter((h) => !(r.deduped ?? []).includes(h.slug) && !(r.echoed ?? []).includes(h.slug)).length +
|
|
286
|
-
(r.slow?.length ?? 0), 0);
|
|
287
|
-
const rate = entries === 0 ? 0 : Math.min(1, opens.length / entries);
|
|
288
|
-
lines.push(`| 指针打开率 | ${(rate * 100).toFixed(1)}%(${opens.length} 次 topic_open / ${entries} 条注入指针) |`);
|
|
289
|
-
}
|
|
286
|
+
const panel = aggregateOpenPanel(records, opens);
|
|
287
|
+
lines.push(`| open rate | ${(panel.openRate * 100).toFixed(1)}%(${panel.opensInWindow} 个被打开指针 / ${panel.pointerEntries} 条注入指针) |`);
|
|
290
288
|
const echoedCount = records.reduce((acc, r) => acc + (r.echoed?.length ?? 0), 0);
|
|
291
289
|
if (echoedCount > 0) {
|
|
292
290
|
lines.push(`| 回声抑制 | ${echoedCount} 次(本会话蒸馏出的 topic 不回注) |`);
|
|
@@ -296,6 +294,24 @@ async function renderStats(service) {
|
|
|
296
294
|
for (const t of stats.topTopics.slice(0, 5))
|
|
297
295
|
lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`);
|
|
298
296
|
}
|
|
297
|
+
// ---- 注入 vs 打开 panel ----
|
|
298
|
+
// Per-topic cut: what the header's windowed rate cannot show — which
|
|
299
|
+
// topics the model actually bothers to open (wide-scope slug attribution).
|
|
300
|
+
if (panel.topics.length > 0) {
|
|
301
|
+
lines.push('', '注入 vs 打开(Top-10,注入=真正渲染的指针;打开按 slug 归因,全窗口):', '', '| Topic | 注入 | 打开 | 打开率 |', '| --- | --- | --- | --- |', ...panel.topics.map((t) => `| \`${cell(t.slug)}\` | ${t.injections} | ${t.opens} | ${(t.openRate * 100).toFixed(1)}% |`));
|
|
302
|
+
}
|
|
303
|
+
if (panel.zeroOpenTotal > 0) {
|
|
304
|
+
const shown = panel.zeroOpen.map((z) => `\`${cell(z.slug)}\``).join('、');
|
|
305
|
+
const more = panel.zeroOpenTotal > panel.zeroOpen.length ? `(共 ${panel.zeroOpenTotal} 个)` : '';
|
|
306
|
+
lines.push('', `零打开高频(注入 ≥${ZERO_OPEN_MIN_INJECTIONS}、打开 0):${shown}${more}`);
|
|
307
|
+
}
|
|
308
|
+
lines.push('', panel.hasSourceData ? `打开来源:pointer ${panel.pointerOpens} / search ${panel.searchOpens}` : '打开来源:暂无来源数据(需新版本 open 记录)');
|
|
309
|
+
if (panel.pointerEntries > 0) {
|
|
310
|
+
lines.push('', '| 分桶 | 指针 | 打开率 |', '| --- | --- | --- |');
|
|
311
|
+
for (const b of panel.bands) {
|
|
312
|
+
lines.push(`| ${b.label} | ${b.pointers} | ${b.pointers === 0 ? '—' : `${(b.openRate * 100).toFixed(1)}%`} |`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
299
315
|
if (stats.nearMissHistogram.length > 0) {
|
|
300
316
|
lines.push('', `Near-miss 分布(低于阈值 ${service.cfg.matchThreshold.toFixed(2)} 或被结构门挡下,对数刻度):`, '', '```', ...renderNearMissSparkline(stats.nearMissHistogram), '```');
|
|
301
317
|
const hint = tuningHint(stats, service.cfg.matchThreshold);
|
package/lib/config.d.ts
CHANGED
|
@@ -70,6 +70,12 @@ export declare const TopicsConfig: z<Schemastery.ObjectS<NoInfer<{
|
|
|
70
70
|
* granted to zero-lexical candidates; the structural gate still applies.
|
|
71
71
|
* 0 = off. */
|
|
72
72
|
usageBoost: z<number, number, "volatile-defined">;
|
|
73
|
+
/** Zero-open decay (v5, default off): halve the retrieval score of slugs
|
|
74
|
+
* injected ≥5 times in the rolling 30 days with zero opens ever — the
|
|
75
|
+
* 零打开高频 leaderboard stops out-ranking topics the model actually
|
|
76
|
+
* reads. false = zero behavior change; the reason tag `zero-open-decay`
|
|
77
|
+
* rides the hit/near-miss reasons when on. */
|
|
78
|
+
zeroOpenDecay: z<boolean, boolean, "volatile-defined">;
|
|
73
79
|
/** Debounced push delay in GitHub mode. */
|
|
74
80
|
pushDebounceSeconds: z<number, number, "volatile-defined">;
|
|
75
81
|
/** System One decision-model integration (design 2026-09-25), default-off
|
|
@@ -162,6 +168,12 @@ export declare const TopicsConfig: z<Schemastery.ObjectS<NoInfer<{
|
|
|
162
168
|
* granted to zero-lexical candidates; the structural gate still applies.
|
|
163
169
|
* 0 = off. */
|
|
164
170
|
usageBoost: z<number, number, "volatile-defined">;
|
|
171
|
+
/** Zero-open decay (v5, default off): halve the retrieval score of slugs
|
|
172
|
+
* injected ≥5 times in the rolling 30 days with zero opens ever — the
|
|
173
|
+
* 零打开高频 leaderboard stops out-ranking topics the model actually
|
|
174
|
+
* reads. false = zero behavior change; the reason tag `zero-open-decay`
|
|
175
|
+
* rides the hit/near-miss reasons when on. */
|
|
176
|
+
zeroOpenDecay: z<boolean, boolean, "volatile-defined">;
|
|
165
177
|
/** Debounced push delay in GitHub mode. */
|
|
166
178
|
pushDebounceSeconds: z<number, number, "volatile-defined">;
|
|
167
179
|
/** System One decision-model integration (design 2026-09-25), default-off
|
|
@@ -216,6 +228,10 @@ export type TopicsConfigValue = {
|
|
|
216
228
|
consolidateCadence: string;
|
|
217
229
|
deprecatedTtlDays: number;
|
|
218
230
|
usageBoost: number;
|
|
231
|
+
/** Zero-open decay (v5). Optional in this TS view only (same sentinel
|
|
232
|
+
* pattern as the jev keys): bare-harness DEFAULTS predates it —
|
|
233
|
+
* consumers must treat undefined as the documented default (false). */
|
|
234
|
+
zeroOpenDecay?: boolean;
|
|
219
235
|
pushDebounceSeconds: number;
|
|
220
236
|
/** System One integration keys (design 2026-09-25 §4). Optional in this TS
|
|
221
237
|
* view only: index.ts's bare-harness DEFAULTS literal predates them and
|
|
@@ -229,7 +245,7 @@ export type TopicsConfigValue = {
|
|
|
229
245
|
jevLayaFallback?: boolean;
|
|
230
246
|
jevLayaUrl?: string;
|
|
231
247
|
};
|
|
232
|
-
export declare const CONFIG_KEYS: readonly ["repo", "autoInject", "injectDedup", "suppressEcho", "topK", "perTopicBudget", "totalBudget", "matchThreshold", "tagBoost", "injectMode", "qualityLane", "graphDepth", "recencyWindowDays", "autoObserve", "includeSubagents", "observationMaxChars", "distillEveryTurns", "distillOnSessionEnd", "distillProvider", "distillModel", "distillBatchSize", "distillMaxModelCalls", "consolidateCadence", "deprecatedTtlDays", "usageBoost", "pushDebounceSeconds", "jevEnabled", "jevBackend", "jevModel", "jevTimeoutMs", "jevSecretFile", "jevLayaFallback", "jevLayaUrl"];
|
|
248
|
+
export declare const CONFIG_KEYS: readonly ["repo", "autoInject", "injectDedup", "suppressEcho", "topK", "perTopicBudget", "totalBudget", "matchThreshold", "tagBoost", "injectMode", "qualityLane", "graphDepth", "recencyWindowDays", "autoObserve", "includeSubagents", "observationMaxChars", "distillEveryTurns", "distillOnSessionEnd", "distillProvider", "distillModel", "distillBatchSize", "distillMaxModelCalls", "consolidateCadence", "deprecatedTtlDays", "usageBoost", "zeroOpenDecay", "pushDebounceSeconds", "jevEnabled", "jevBackend", "jevModel", "jevTimeoutMs", "jevSecretFile", "jevLayaFallback", "jevLayaUrl"];
|
|
233
249
|
export type ConfigKey = (typeof CONFIG_KEYS)[number];
|
|
234
250
|
/** CamelCase → dash-display (topK → top-k) for command output. */
|
|
235
251
|
export declare function displayKey(key: string): string;
|
package/lib/config.js
CHANGED
|
@@ -70,6 +70,12 @@ export const TopicsConfig = z.object({
|
|
|
70
70
|
* granted to zero-lexical candidates; the structural gate still applies.
|
|
71
71
|
* 0 = off. */
|
|
72
72
|
usageBoost: z.number().default(0.15).volatile(),
|
|
73
|
+
/** Zero-open decay (v5, default off): halve the retrieval score of slugs
|
|
74
|
+
* injected ≥5 times in the rolling 30 days with zero opens ever — the
|
|
75
|
+
* 零打开高频 leaderboard stops out-ranking topics the model actually
|
|
76
|
+
* reads. false = zero behavior change; the reason tag `zero-open-decay`
|
|
77
|
+
* rides the hit/near-miss reasons when on. */
|
|
78
|
+
zeroOpenDecay: z.boolean().default(false).volatile(),
|
|
73
79
|
/** Debounced push delay in GitHub mode. */
|
|
74
80
|
pushDebounceSeconds: z.number().default(45).volatile(),
|
|
75
81
|
/** System One decision-model integration (design 2026-09-25), default-off
|
|
@@ -124,6 +130,7 @@ export const CONFIG_KEYS = [
|
|
|
124
130
|
'consolidateCadence',
|
|
125
131
|
'deprecatedTtlDays',
|
|
126
132
|
'usageBoost',
|
|
133
|
+
'zeroOpenDecay',
|
|
127
134
|
'pushDebounceSeconds',
|
|
128
135
|
'jevEnabled',
|
|
129
136
|
'jevBackend',
|
|
@@ -152,7 +159,8 @@ export function parseConfigValue(key, raw) {
|
|
|
152
159
|
case 'autoObserve':
|
|
153
160
|
case 'distillOnSessionEnd':
|
|
154
161
|
case 'includeSubagents':
|
|
155
|
-
case 'jevEnabled':
|
|
162
|
+
case 'jevEnabled':
|
|
163
|
+
case 'zeroOpenDecay': {
|
|
156
164
|
if (raw === 'on' || raw === 'true')
|
|
157
165
|
return true;
|
|
158
166
|
if (raw === 'off' || raw === 'false')
|
package/lib/ilog.d.ts
CHANGED
|
@@ -70,6 +70,23 @@ export interface InjectionRecord {
|
|
|
70
70
|
/** Slow-lane pointers this round, in delivery order. */
|
|
71
71
|
slow?: SlowItem[];
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* One topic_open event (pointer-open log, v4 §4.3). Everything beyond
|
|
75
|
+
* `slug`/`at` is optional and late-added: older lines carry only the
|
|
76
|
+
* original pair, and every reader must tolerate the rest being absent.
|
|
77
|
+
*/
|
|
78
|
+
export interface OpenRecord {
|
|
79
|
+
slug: string;
|
|
80
|
+
at: string;
|
|
81
|
+
/** Session that issued the topic_open, when the host surface exposed one. */
|
|
82
|
+
sessionId?: string;
|
|
83
|
+
/** Attribution: opened off an injected pointer, or off a topic_search hit. */
|
|
84
|
+
source?: 'pointer' | 'search';
|
|
85
|
+
/** Retrieval score of the matched injection hit (source='pointer' only). */
|
|
86
|
+
score?: number;
|
|
87
|
+
/** open time − the matched injection record's at, in ms (source='pointer' only). */
|
|
88
|
+
sinceInjectionMs?: number;
|
|
89
|
+
}
|
|
73
90
|
export interface AggregateStats {
|
|
74
91
|
rounds: number;
|
|
75
92
|
injectedRounds: number;
|
|
@@ -107,3 +124,115 @@ export declare function aggregateUsage(injections: readonly InjectionRecord[], o
|
|
|
107
124
|
slug: string;
|
|
108
125
|
at: string;
|
|
109
126
|
}[], windowDays: number, now: number): Map<string, UsageSignal>;
|
|
127
|
+
/** How many tail injection records the cheap open attribution scans. */
|
|
128
|
+
export declare const OPEN_ATTRIBUTION_TAIL = 50;
|
|
129
|
+
/** An injected pointer stays attributable for 48h (a work session's span). */
|
|
130
|
+
export declare const OPEN_ATTRIBUTION_WINDOW_MS: number;
|
|
131
|
+
/**
|
|
132
|
+
* Cheap source attribution for one open event: scan the injection tail
|
|
133
|
+
* (newest first) for a same-session record within the attribution window
|
|
134
|
+
* where the slug was truly delivered to the model's eyes. That is hits
|
|
135
|
+
* minus echoed/dropped (echo suppression and budget drops never rendered),
|
|
136
|
+
* PLUS slow-lane deliveries, PLUS deduped hits — dedup only blocks
|
|
137
|
+
* re-packing THIS round; the pointer rendered earlier in the same session
|
|
138
|
+
* and the model saw it (deliberately wider than renderedPointers: the panel
|
|
139
|
+
* denominator asks "did it render this round", attribution asks "did the
|
|
140
|
+
* model ever see it"). A fast-hit match carries that hit's score; a
|
|
141
|
+
* slow-only match carries none (slow pointers have no lexical score).
|
|
142
|
+
* Anything else (no session id, other session, expired, never rendered)
|
|
143
|
+
* is `search`.
|
|
144
|
+
*/
|
|
145
|
+
export declare function attributeOpen(injections: readonly InjectionRecord[], slug: string, sessionId: string | undefined, nowMs: number): {
|
|
146
|
+
source: 'pointer' | 'search';
|
|
147
|
+
score?: number;
|
|
148
|
+
sinceInjectionMs?: number;
|
|
149
|
+
};
|
|
150
|
+
/** Score bands for the open-rate-by-score cut of the stats panel. */
|
|
151
|
+
export declare const SCORE_BANDS: readonly {
|
|
152
|
+
label: string;
|
|
153
|
+
lo: number;
|
|
154
|
+
hi: number;
|
|
155
|
+
}[];
|
|
156
|
+
/** Zero-open listing threshold: injections at or above this with zero opens. */
|
|
157
|
+
export declare const ZERO_OPEN_MIN_INJECTIONS = 5;
|
|
158
|
+
/** Rolling window of the zero-open rule: only rendered injections inside it count. */
|
|
159
|
+
export declare const ZERO_OPEN_WINDOW_DAYS = 30;
|
|
160
|
+
/** Injection-tail rows the decay read scans — a bound comfortably covering
|
|
161
|
+
* the 30-day window at observed round volumes. */
|
|
162
|
+
export declare const ZERO_OPEN_TAIL = 500;
|
|
163
|
+
/**
|
|
164
|
+
* Slugs that truly rendered in one round, in delivery order: raw hits minus
|
|
165
|
+
* the round's exclusions (deduped/echoed/dropped were never assembled), plus
|
|
166
|
+
* delivered slow pointers; a slug the fast lane already delivered counts
|
|
167
|
+
* once even when the slow lane also reports it delivered ("not packed
|
|
168
|
+
* twice"). Scores ride along from the fast hit only — slow pointers carry
|
|
169
|
+
* no lexical score. Shared by the stats panel and the zero-open rule.
|
|
170
|
+
*/
|
|
171
|
+
export declare function renderedPointers(record: InjectionRecord): {
|
|
172
|
+
slug: string;
|
|
173
|
+
score?: number;
|
|
174
|
+
}[];
|
|
175
|
+
/**
|
|
176
|
+
* Zero-open slug set — the single shared 零打开高频 rule (v5): slugs whose
|
|
177
|
+
* truly-rendered pointer count within the rolling ZERO_OPEN_WINDOW_DAYS
|
|
178
|
+
* reaches ZERO_OPEN_MIN_INJECTIONS while the WHOLE open log has never
|
|
179
|
+
* recorded an open (wide scope — an open long after the last injection
|
|
180
|
+
* still disqualifies). Returns slug → windowed rendered-pointer count for
|
|
181
|
+
* the qualifying slugs; `/topics stats` (the panel's zero-open list) and
|
|
182
|
+
* the retrieval decay (`zeroOpenDecay`) both consume this one
|
|
183
|
+
* implementation. Records with unparsable `at` cannot prove window
|
|
184
|
+
* membership and are skipped (same tolerance as aggregateUsage).
|
|
185
|
+
*/
|
|
186
|
+
export declare function aggregateZeroOpen(injections: readonly InjectionRecord[], opens: readonly {
|
|
187
|
+
slug: string;
|
|
188
|
+
}[], nowMs: number): Map<string, number>;
|
|
189
|
+
export interface OpenTopicRow {
|
|
190
|
+
slug: string;
|
|
191
|
+
/** Truly rendered pointers for this slug (deduped/echoed/dropped excluded). */
|
|
192
|
+
injections: number;
|
|
193
|
+
/** Opens attributed by slug over the whole open log (wide scope, by design). */
|
|
194
|
+
opens: number;
|
|
195
|
+
openRate: number;
|
|
196
|
+
}
|
|
197
|
+
export interface OpenBandRow {
|
|
198
|
+
label: string;
|
|
199
|
+
/** Rendered fast pointers whose hit score falls in this band. */
|
|
200
|
+
pointers: number;
|
|
201
|
+
/** Pointer-sourced opens whose attributed score falls in this band. */
|
|
202
|
+
opens: number;
|
|
203
|
+
openRate: number;
|
|
204
|
+
}
|
|
205
|
+
export interface OpenPanelStats {
|
|
206
|
+
/** Rendered pointer entries across the window: hits − deduped − echoed − dropped, plus slow pointers (same-slug fast+slow pairs counted once). */
|
|
207
|
+
pointerEntries: number;
|
|
208
|
+
/** DISTINCT slugs from the window's rendered set with an in-window open
|
|
209
|
+
* (search-sourced and never-rendered slugs excluded; one per slug). */
|
|
210
|
+
opensInWindow: number;
|
|
211
|
+
openRate: number;
|
|
212
|
+
topics: OpenTopicRow[];
|
|
213
|
+
zeroOpen: {
|
|
214
|
+
slug: string;
|
|
215
|
+
injections: number;
|
|
216
|
+
}[];
|
|
217
|
+
zeroOpenTotal: number;
|
|
218
|
+
pointerOpens: number;
|
|
219
|
+
searchOpens: number;
|
|
220
|
+
/** False when no open record carries a source field (pre-enrichment data). */
|
|
221
|
+
hasSourceData: boolean;
|
|
222
|
+
bands: OpenBandRow[];
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Aggregate the injection-vs-open panel from the two JSONL windows. The
|
|
226
|
+
* per-topic/band denominators count only pointers that truly rendered
|
|
227
|
+
* (deduped/echoed/dropped excluded — same exclusion family as
|
|
228
|
+
* aggregateUsage); the header numerator counts only DISTINCT slugs from
|
|
229
|
+
* that rendered set with an open inside the injection window — a search
|
|
230
|
+
* open or an open of a never-rendered slug cannot enter, and repeat opens
|
|
231
|
+
* of the same slug count once, so the rate can no longer be pinned at
|
|
232
|
+
* 100% by repeated or off-scope opens. Opens per slug stay wide-scope
|
|
233
|
+
* (the model may open a topic long after its last injection). The
|
|
234
|
+
* zero-open list shares the decay's rule: rendered injections counted
|
|
235
|
+
* within the rolling 30-day window (anchored at the newest round), opens
|
|
236
|
+
* wide-scope.
|
|
237
|
+
*/
|
|
238
|
+
export declare function aggregateOpenPanel(injections: readonly InjectionRecord[], opens: readonly OpenRecord[]): OpenPanelStats;
|
package/lib/ilog.js
CHANGED
|
@@ -117,3 +117,239 @@ export function aggregateUsage(injections, opens, windowDays, now) {
|
|
|
117
117
|
}
|
|
118
118
|
return out;
|
|
119
119
|
}
|
|
120
|
+
// ---------------------------------------------------------------------
|
|
121
|
+
// Injection-vs-open metrics (v5) — open enrichment attribution + the
|
|
122
|
+
// /topics stats panel aggregate. All pure: file I/O stays in store/service.
|
|
123
|
+
// ---------------------------------------------------------------------
|
|
124
|
+
/** How many tail injection records the cheap open attribution scans. */
|
|
125
|
+
export const OPEN_ATTRIBUTION_TAIL = 50;
|
|
126
|
+
/** An injected pointer stays attributable for 48h (a work session's span). */
|
|
127
|
+
export const OPEN_ATTRIBUTION_WINDOW_MS = 48 * 3_600_000;
|
|
128
|
+
/**
|
|
129
|
+
* Cheap source attribution for one open event: scan the injection tail
|
|
130
|
+
* (newest first) for a same-session record within the attribution window
|
|
131
|
+
* where the slug was truly delivered to the model's eyes. That is hits
|
|
132
|
+
* minus echoed/dropped (echo suppression and budget drops never rendered),
|
|
133
|
+
* PLUS slow-lane deliveries, PLUS deduped hits — dedup only blocks
|
|
134
|
+
* re-packing THIS round; the pointer rendered earlier in the same session
|
|
135
|
+
* and the model saw it (deliberately wider than renderedPointers: the panel
|
|
136
|
+
* denominator asks "did it render this round", attribution asks "did the
|
|
137
|
+
* model ever see it"). A fast-hit match carries that hit's score; a
|
|
138
|
+
* slow-only match carries none (slow pointers have no lexical score).
|
|
139
|
+
* Anything else (no session id, other session, expired, never rendered)
|
|
140
|
+
* is `search`.
|
|
141
|
+
*/
|
|
142
|
+
export function attributeOpen(injections, slug, sessionId, nowMs) {
|
|
143
|
+
if (sessionId === undefined)
|
|
144
|
+
return { source: 'search' };
|
|
145
|
+
for (let i = injections.length - 1; i >= 0; i -= 1) {
|
|
146
|
+
const record = injections[i];
|
|
147
|
+
if (record.sessionId !== sessionId)
|
|
148
|
+
continue;
|
|
149
|
+
const atMs = Date.parse(record.at);
|
|
150
|
+
if (!Number.isFinite(atMs) || nowMs - atMs > OPEN_ATTRIBUTION_WINDOW_MS)
|
|
151
|
+
continue;
|
|
152
|
+
const sinceInjectionMs = Math.max(0, nowMs - atMs);
|
|
153
|
+
const suppressed = new Set([...(record.echoed ?? []), ...(record.dropped ?? []).map((d) => d.slug)]);
|
|
154
|
+
const hit = record.hits.find((h) => h.slug === slug && !suppressed.has(h.slug));
|
|
155
|
+
if (hit !== undefined)
|
|
156
|
+
return { source: 'pointer', score: hit.score, sinceInjectionMs };
|
|
157
|
+
if ((record.slow ?? []).some((s) => s.slug === slug))
|
|
158
|
+
return { source: 'pointer', sinceInjectionMs };
|
|
159
|
+
}
|
|
160
|
+
return { source: 'search' };
|
|
161
|
+
}
|
|
162
|
+
/** Score bands for the open-rate-by-score cut of the stats panel. */
|
|
163
|
+
export const SCORE_BANDS = [
|
|
164
|
+
{ label: '[0.3,0.5)', lo: 0.3, hi: 0.5 },
|
|
165
|
+
{ label: '[0.5,1)', lo: 0.5, hi: 1 },
|
|
166
|
+
{ label: '[1,2)', lo: 1, hi: 2 },
|
|
167
|
+
{ label: '[2,5)', lo: 2, hi: 5 },
|
|
168
|
+
{ label: '[5,∞)', lo: 5, hi: Number.POSITIVE_INFINITY },
|
|
169
|
+
];
|
|
170
|
+
/** Zero-open listing threshold: injections at or above this with zero opens. */
|
|
171
|
+
export const ZERO_OPEN_MIN_INJECTIONS = 5;
|
|
172
|
+
/** Rolling window of the zero-open rule: only rendered injections inside it count. */
|
|
173
|
+
export const ZERO_OPEN_WINDOW_DAYS = 30;
|
|
174
|
+
/** Injection-tail rows the decay read scans — a bound comfortably covering
|
|
175
|
+
* the 30-day window at observed round volumes. */
|
|
176
|
+
export const ZERO_OPEN_TAIL = 500;
|
|
177
|
+
/**
|
|
178
|
+
* Slugs that truly rendered in one round, in delivery order: raw hits minus
|
|
179
|
+
* the round's exclusions (deduped/echoed/dropped were never assembled), plus
|
|
180
|
+
* delivered slow pointers; a slug the fast lane already delivered counts
|
|
181
|
+
* once even when the slow lane also reports it delivered ("not packed
|
|
182
|
+
* twice"). Scores ride along from the fast hit only — slow pointers carry
|
|
183
|
+
* no lexical score. Shared by the stats panel and the zero-open rule.
|
|
184
|
+
*/
|
|
185
|
+
export function renderedPointers(record) {
|
|
186
|
+
const excluded = new Set([
|
|
187
|
+
...(record.deduped ?? []),
|
|
188
|
+
...(record.echoed ?? []),
|
|
189
|
+
...(record.dropped ?? []).map((d) => d.slug),
|
|
190
|
+
]);
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
const out = [];
|
|
193
|
+
for (const h of record.hits) {
|
|
194
|
+
if (excluded.has(h.slug) || seen.has(h.slug))
|
|
195
|
+
continue;
|
|
196
|
+
seen.add(h.slug);
|
|
197
|
+
out.push({ slug: h.slug, score: h.score });
|
|
198
|
+
}
|
|
199
|
+
for (const s of record.slow ?? []) {
|
|
200
|
+
if (seen.has(s.slug))
|
|
201
|
+
continue;
|
|
202
|
+
seen.add(s.slug);
|
|
203
|
+
out.push({ slug: s.slug });
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Zero-open slug set — the single shared 零打开高频 rule (v5): slugs whose
|
|
209
|
+
* truly-rendered pointer count within the rolling ZERO_OPEN_WINDOW_DAYS
|
|
210
|
+
* reaches ZERO_OPEN_MIN_INJECTIONS while the WHOLE open log has never
|
|
211
|
+
* recorded an open (wide scope — an open long after the last injection
|
|
212
|
+
* still disqualifies). Returns slug → windowed rendered-pointer count for
|
|
213
|
+
* the qualifying slugs; `/topics stats` (the panel's zero-open list) and
|
|
214
|
+
* the retrieval decay (`zeroOpenDecay`) both consume this one
|
|
215
|
+
* implementation. Records with unparsable `at` cannot prove window
|
|
216
|
+
* membership and are skipped (same tolerance as aggregateUsage).
|
|
217
|
+
*/
|
|
218
|
+
export function aggregateZeroOpen(injections, opens, nowMs) {
|
|
219
|
+
const cutoff = nowMs - ZERO_OPEN_WINDOW_DAYS * 86_400_000;
|
|
220
|
+
const counts = new Map();
|
|
221
|
+
for (const record of injections) {
|
|
222
|
+
const at = Date.parse(record.at);
|
|
223
|
+
if (!Number.isFinite(at) || at < cutoff)
|
|
224
|
+
continue;
|
|
225
|
+
for (const p of renderedPointers(record)) {
|
|
226
|
+
counts.set(p.slug, (counts.get(p.slug) ?? 0) + 1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const opened = new Set(opens.map((o) => o.slug));
|
|
230
|
+
const out = new Map();
|
|
231
|
+
for (const [slug, n] of counts) {
|
|
232
|
+
if (n >= ZERO_OPEN_MIN_INJECTIONS && !opened.has(slug))
|
|
233
|
+
out.set(slug, n);
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Aggregate the injection-vs-open panel from the two JSONL windows. The
|
|
239
|
+
* per-topic/band denominators count only pointers that truly rendered
|
|
240
|
+
* (deduped/echoed/dropped excluded — same exclusion family as
|
|
241
|
+
* aggregateUsage); the header numerator counts only DISTINCT slugs from
|
|
242
|
+
* that rendered set with an open inside the injection window — a search
|
|
243
|
+
* open or an open of a never-rendered slug cannot enter, and repeat opens
|
|
244
|
+
* of the same slug count once, so the rate can no longer be pinned at
|
|
245
|
+
* 100% by repeated or off-scope opens. Opens per slug stay wide-scope
|
|
246
|
+
* (the model may open a topic long after its last injection). The
|
|
247
|
+
* zero-open list shares the decay's rule: rendered injections counted
|
|
248
|
+
* within the rolling 30-day window (anchored at the newest round), opens
|
|
249
|
+
* wide-scope.
|
|
250
|
+
*/
|
|
251
|
+
export function aggregateOpenPanel(injections, opens) {
|
|
252
|
+
// Numerator window: the earliest round represented in the denominator;
|
|
253
|
+
// the zero-open anchor is the newest round — the panel describes the log
|
|
254
|
+
// window it was fed, so a stale log still shows the topics that
|
|
255
|
+
// dominated it. The rendered-slug set below shares that window: only
|
|
256
|
+
// opens of slugs this window rendered can enter the numerator.
|
|
257
|
+
let windowStart = Number.POSITIVE_INFINITY;
|
|
258
|
+
let windowEnd = Number.NEGATIVE_INFINITY;
|
|
259
|
+
for (const r of injections) {
|
|
260
|
+
const atMs = Date.parse(r.at);
|
|
261
|
+
if (!Number.isFinite(atMs))
|
|
262
|
+
continue;
|
|
263
|
+
if (atMs < windowStart)
|
|
264
|
+
windowStart = atMs;
|
|
265
|
+
if (atMs > windowEnd)
|
|
266
|
+
windowEnd = atMs;
|
|
267
|
+
}
|
|
268
|
+
const topicCounts = new Map();
|
|
269
|
+
const bandPointers = new Array(SCORE_BANDS.length).fill(0);
|
|
270
|
+
let pointerEntries = 0;
|
|
271
|
+
const renderedSlugs = new Set();
|
|
272
|
+
for (const r of injections) {
|
|
273
|
+
// renderedPointers keeps the entry/per-topic counts honest: excluded
|
|
274
|
+
// slugs never counted, a fast+slow pair for the same slug counted once.
|
|
275
|
+
for (const p of renderedPointers(r)) {
|
|
276
|
+
pointerEntries += 1;
|
|
277
|
+
renderedSlugs.add(p.slug);
|
|
278
|
+
topicCounts.set(p.slug, (topicCounts.get(p.slug) ?? 0) + 1);
|
|
279
|
+
const score = p.score;
|
|
280
|
+
if (score === undefined)
|
|
281
|
+
continue;
|
|
282
|
+
const bi = SCORE_BANDS.findIndex((b) => score >= b.lo && score < b.hi);
|
|
283
|
+
if (bi >= 0)
|
|
284
|
+
bandPointers[bi] += 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const opensBySlug = new Map();
|
|
288
|
+
const bandOpens = new Array(SCORE_BANDS.length).fill(0);
|
|
289
|
+
let pointerOpens = 0;
|
|
290
|
+
let searchOpens = 0;
|
|
291
|
+
let hasSourceData = false;
|
|
292
|
+
// Numerator: DISTINCT slugs from the window's rendered set with an
|
|
293
|
+
// in-window open — search-sourced opens never count (the model found the
|
|
294
|
+
// topic by asking, not by following a rendered pointer), a never-rendered
|
|
295
|
+
// slug never counts, and repeat opens of the same slug count once.
|
|
296
|
+
const openedSlugsInWindow = new Set();
|
|
297
|
+
for (const o of opens) {
|
|
298
|
+
opensBySlug.set(o.slug, (opensBySlug.get(o.slug) ?? 0) + 1);
|
|
299
|
+
if (o.source === 'pointer' || o.source === 'search')
|
|
300
|
+
hasSourceData = true;
|
|
301
|
+
if (o.source === 'pointer')
|
|
302
|
+
pointerOpens += 1;
|
|
303
|
+
else if (o.source === 'search')
|
|
304
|
+
searchOpens += 1;
|
|
305
|
+
if (o.source === 'pointer' && typeof o.score === 'number') {
|
|
306
|
+
const score = o.score;
|
|
307
|
+
const bi = SCORE_BANDS.findIndex((b) => score >= b.lo && score < b.hi);
|
|
308
|
+
if (bi >= 0)
|
|
309
|
+
bandOpens[bi] += 1;
|
|
310
|
+
}
|
|
311
|
+
if (o.source === 'search')
|
|
312
|
+
continue;
|
|
313
|
+
const atMs = Date.parse(o.at);
|
|
314
|
+
// An open joins the windowed numerator unless it is provably older than
|
|
315
|
+
// the window's earliest round (unparsable stamps cannot prove either
|
|
316
|
+
// way) — and only when the window actually rendered its slug.
|
|
317
|
+
if (Number.isFinite(windowStart) && Number.isFinite(atMs) && atMs < windowStart)
|
|
318
|
+
continue;
|
|
319
|
+
if (!renderedSlugs.has(o.slug))
|
|
320
|
+
continue;
|
|
321
|
+
openedSlugsInWindow.add(o.slug);
|
|
322
|
+
}
|
|
323
|
+
const opensInWindow = openedSlugsInWindow.size;
|
|
324
|
+
const rate = (num, den) => (den === 0 ? 0 : Math.min(1, num / den));
|
|
325
|
+
const topics = [...topicCounts.entries()]
|
|
326
|
+
.map(([slug, injections]) => {
|
|
327
|
+
const opens = opensBySlug.get(slug) ?? 0;
|
|
328
|
+
return { slug, injections, opens, openRate: rate(opens, injections) };
|
|
329
|
+
})
|
|
330
|
+
.sort((a, b) => b.injections - a.injections || a.slug.localeCompare(b.slug));
|
|
331
|
+
// Zero-open cut: the shared 30-day rule (aggregateZeroOpen), anchored at
|
|
332
|
+
// the newest round so window semantics match the decay's rolling window
|
|
333
|
+
// without growing a time-bomb dependency on the wall clock.
|
|
334
|
+
const zeroOpenCounts = windowEnd === Number.NEGATIVE_INFINITY ? new Map() : aggregateZeroOpen(injections, opens, windowEnd);
|
|
335
|
+
const zeroOpenAll = [...zeroOpenCounts.entries()]
|
|
336
|
+
.map(([slug, injections]) => ({ slug, injections }))
|
|
337
|
+
.sort((a, b) => b.injections - a.injections || a.slug.localeCompare(b.slug));
|
|
338
|
+
return {
|
|
339
|
+
pointerEntries,
|
|
340
|
+
opensInWindow,
|
|
341
|
+
openRate: rate(opensInWindow, pointerEntries),
|
|
342
|
+
topics: topics.slice(0, 10),
|
|
343
|
+
zeroOpen: zeroOpenAll.slice(0, 10).map((t) => ({ slug: t.slug, injections: t.injections })),
|
|
344
|
+
zeroOpenTotal: zeroOpenAll.length,
|
|
345
|
+
pointerOpens,
|
|
346
|
+
searchOpens,
|
|
347
|
+
hasSourceData,
|
|
348
|
+
bands: SCORE_BANDS.map((b, i) => ({
|
|
349
|
+
label: b.label,
|
|
350
|
+
pointers: bandPointers[i],
|
|
351
|
+
opens: bandOpens[i],
|
|
352
|
+
openRate: rate(bandOpens[i], bandPointers[i]),
|
|
353
|
+
})),
|
|
354
|
+
};
|
|
355
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -112,6 +112,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
112
112
|
consolidateCadence: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
113
113
|
deprecatedTtlDays: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
114
114
|
usageBoost: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
115
|
+
zeroOpenDecay: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
115
116
|
pushDebounceSeconds: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
116
117
|
jevEnabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
117
118
|
jevBackend: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
@@ -146,6 +147,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
146
147
|
consolidateCadence: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
147
148
|
deprecatedTtlDays: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
148
149
|
usageBoost: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
150
|
+
zeroOpenDecay: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
149
151
|
pushDebounceSeconds: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
|
|
150
152
|
jevEnabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
151
153
|
jevBackend: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
package/lib/retrieval.d.ts
CHANGED
|
@@ -48,10 +48,23 @@ export interface RetrievalConfig {
|
|
|
48
48
|
hits: number;
|
|
49
49
|
opens: number;
|
|
50
50
|
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Zero-open decay (v5, default off — config `zeroOpenDecay`): halve the
|
|
53
|
+
* score of slugs on the zero-open list (≥5 rendered injections in the
|
|
54
|
+
* rolling 30 days, zero opens ever) at score-finalization time, BEFORE
|
|
55
|
+
* the threshold cut — a decayed candidate that drops below the threshold
|
|
56
|
+
* lands in nearMisses with the `zero-open-decay` reason, so the log
|
|
57
|
+
* shows why it stopped injecting. Absent/off = zero behavior change.
|
|
58
|
+
*/
|
|
59
|
+
zeroOpenDecay?: boolean;
|
|
60
|
+
/** Zero-open slugs for this round (service-built from the ilog tail). */
|
|
61
|
+
zeroOpenSlugs?: ReadonlySet<string>;
|
|
51
62
|
}
|
|
52
63
|
/** Hard cap for the usage bonus — below threshold 0.3 so usage can never
|
|
53
64
|
* carry a candidate across the gate on its own (ADR 0015). */
|
|
54
65
|
export declare const USAGE_BOOST_CAP = 0.2;
|
|
66
|
+
/** Score multiplier for zero-open slugs when zeroOpenDecay is on (v5). */
|
|
67
|
+
export declare const ZERO_OPEN_DECAY_FACTOR = 0.5;
|
|
55
68
|
export declare const DEFAULT_RETRIEVAL: RetrievalConfig;
|
|
56
69
|
/** Trigger hits outrank every other lexical field (pi-llm-wiki: highest weight). */
|
|
57
70
|
export declare const TRIGGER_WEIGHT = 5;
|
package/lib/retrieval.js
CHANGED
|
@@ -15,6 +15,8 @@ import { dependsSlugs } from "./okf.js";
|
|
|
15
15
|
/** Hard cap for the usage bonus — below threshold 0.3 so usage can never
|
|
16
16
|
* carry a candidate across the gate on its own (ADR 0015). */
|
|
17
17
|
export const USAGE_BOOST_CAP = 0.2;
|
|
18
|
+
/** Score multiplier for zero-open slugs when zeroOpenDecay is on (v5). */
|
|
19
|
+
export const ZERO_OPEN_DECAY_FACTOR = 0.5;
|
|
18
20
|
export const DEFAULT_RETRIEVAL = {
|
|
19
21
|
threshold: 0.3,
|
|
20
22
|
topK: 4,
|
|
@@ -149,6 +151,14 @@ export function scoreTopic(queryTokens, topic, cfg) {
|
|
|
149
151
|
score *= 0.3;
|
|
150
152
|
reasons.push('conflicted-demoted');
|
|
151
153
|
}
|
|
154
|
+
// Zero-open decay (v5): same demotion family as conflicts — applied to
|
|
155
|
+
// the content score BEFORE gateScore is taken, so the threshold cut and
|
|
156
|
+
// the near-miss band both see the halved score; recency still rides
|
|
157
|
+
// outside it as the ranking tiebreaker.
|
|
158
|
+
if (cfg.zeroOpenDecay === true && cfg.zeroOpenSlugs?.has(topic.slug) === true) {
|
|
159
|
+
score *= ZERO_OPEN_DECAY_FACTOR;
|
|
160
|
+
reasons.push('zero-open-decay');
|
|
161
|
+
}
|
|
152
162
|
const gateScore = score;
|
|
153
163
|
score += recencyBonus;
|
|
154
164
|
const bodySet = new Set([...description, ...conclusion]);
|