dsh-continual-evolve 0.15.5 → 0.15.7

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 CHANGED
@@ -7,7 +7,7 @@ English | [中文](README.zh.md)
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-258%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-319%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-96%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-90%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
@@ -21,10 +21,11 @@ Reusable experience gathered in one session (user preferences, pitfalls, project
21
21
  ## How it works
22
22
 
23
23
  1. **Session-start injection** — the plugin registers a single system-prompt section: the workspace's memory index as a store query (hooks only, whole-line truncated over budget, ordered feedback > user > reference), the central-library path, and a three-line guide. The section is computed once per session and reused byte-for-byte, keeping the system prompt stable and the prompt cache warm; an empty store costs zero tokens.
24
- 2. **Four tools, one writer** — `memory_write` (create/update/delete) is the explicit path for "remember …" / "forget …": every proposal passes the code gates (id shape, type enum, the feedback Why/How contract, secret screening carried over from v1, 64KB body cap) inside one transaction. `memory_read` fetches bodies by id and searches by keyword — whitespace-separated terms are AND-combined, with literal substring matching for short terms and whenever the trigram index misses; the injected index carries hooks only. `skill_write` (create/update/delete in the project, plus an explicit move-to-global) sediments reusable procedures through the same discipline (kebab ids, secret screening, 64KB cap, no duplicate ids or bodies across scopes); every applied skill is materialized by the plugin as `<workspace>/.agents/skills/<id>/SKILL.md` — the database is the source of truth, the files are a read-only projection patrol restores on drift. `skill_read` fetches skill bodies by id and searches project plus global skills (the project shadows a same-named global). On hosts without the tools service the plugin degrades to "tell the user" phrasing and stays functional.
25
- 3. **Proposal-based extraction** — the only background automation, audited in the `extraction_log`: every `agent/turn-stopping` schedules that session's boundary, with state kept **per session** (a run in flight keeps only its newest boundary — coalescing, not queueing; `compaction/start` and session close drain it) and **no idle timer**. A run reads the session increment since its cursor, feeds FTS-similar existing memories to one LLM call, and the model's structured proposal lands through the same gates; failures and rejections never advance the cursor. Internal agents, empty increments, turns without real user prose, and turns that already carried an explicit `memory_write` are mechanically skipped — and every skip is ledgered, consecutive quiet turns of one session as a single row carrying how many wake-ups it stands for.
26
- 4. **Rollback is a ledger, not a mechanism** — every mutation lands an `extraction_log` row with full before/after snapshots; undo means rewriting the `before` values. Skill mutations land the same way in `skill_log`. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted; a quarantined skill's materialized file is removed so it stops loading). The read-only card surfaces quarantine anomalies.
27
- 5. **Read-only card** — mounted on the plugin's page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card and aligned with ZCode's Settings → Memory viewer: a dropdown scope selector over the store's workspace partitions (opening on the host's current workspace), a search box, per-entry relative updated times, click-to-preview bodies (5 MiB cap), patrol warnings, and a skills section listing the workspace's project skills plus visible global skills (global rows badged, project shadowing global) with the same preview. Localized through the host's `locale/*.json` metadata channel; host theme tokens, the `Button` primitive, and zh/en copy that tracks the DSH language. The card is a request-time projection of the database and has no edit path.
24
+ 2. **Four tools, one writer** — `memory_write` (create/update/delete) is the explicit path for "remember …" / "forget …": every proposal passes the code gates (id shape, type enum, the feedback Why/How contract, secret screening carried over from v1, 64KB body cap, optional `corrects` links to the same-workspace active memory being superseded) inside one transaction. `memory_read` fetches bodies by id and searches by keyword — whitespace-separated terms are AND-combined, with literal substring matching for short terms and whenever the trigram index misses; the injected index carries hooks only. Each memory body served by id increments that memory's view counter (views + last viewed, the same presence heartbeat as skills). `skill_write` (create/update/delete in the project, plus an explicit move-to-global) sediments reusable procedures through the same discipline (kebab ids, secret screening, 64KB cap, no duplicate ids or bodies across scopes); every applied skill is materialized by the plugin as `<workspace>/.agents/skills/<id>/SKILL.md` — the database is the source of truth, the files are a read-only projection patrol restores on drift. `skill_read` fetches skill bodies by id and searches project plus global skills (the project shadows a same-named global); each body served by id increments that skill's view counter (views + last viewed), which the card shows as a presence heartbeat — viewed, not applied, so long-unviewed rows are removal candidates while pruning itself stays human. On hosts without the tools service the plugin degrades to "tell the user" phrasing and stays functional.
25
+ 3. **Proposal-based extraction** — the trajectory-to-memory automation, audited in the `extraction_log`: every `agent/turn-stopping` schedules that session's boundary, with state kept **per session** (a run in flight keeps only its newest boundary — coalescing, not queueing; `compaction/start` and session close drain it) and **no idle timer**. A run reads the session increment since its cursor, feeds FTS-similar existing memories to one LLM call, and the model's structured proposal lands through the same gates; failures and rejections never advance the cursor. Internal agents, empty increments, turns without real user prose, and turns that already carried an explicit `memory_write` are mechanically skipped — and every skip is ledgered, consecutive quiet turns of one session as a single row carrying how many wake-ups it stands for.
26
+ 4. **Autonomous content-quality loop** — the second background automation, independent from extraction by construction (own prompts, own daily budget, own `review_log` ledger, no cursor; only the store's write gates are shared): a cheap gate model call decides whether a memory/skill entry deserves a close look, and only approvals run the evolve pass that judges content quality (memories on accuracy/usability/reusability/duplication/freshness, skills on trigger precision/executability/reusability/duplication) and proposes the rewrite. New writes are reviewed as they land, the existing stock drains through a bounded backlog queue, and untouched entries requeue after 30 days. Two hard guarantees bound the autonomy: delete never lands automatically (rewrite or quarantine only — quarantine hides, never deletes) and oversized rewrites degrade to quarantine. A daily cap (default 100 items, new > corrects-linked > stale > spot-check priority, the tail drops instead of rolling over) bounds the token spend, and every terminal path lands one `item-review` verdict row plus one daily rollup row (budget vs reviewed vs backlog vs dropped, tokens split by cheap/evolve purpose) for later replay. Ships in trial mode (`review.dryRun`, default on): verdicts ledgered, nothing applied.
27
+ 5. **Rollback is a ledger, not a mechanism** — every mutation lands an `extraction_log` row with full before/after snapshots; undo means rewriting the `before` values. Skill mutations land the same way in `skill_log`, review verdicts in `review_log`. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted; a quarantined skill's materialized file is removed so it stops loading). The read-only card surfaces quarantine anomalies.
28
+ 6. **Read-only card** — mounted on the plugin's page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card and aligned with ZCode's Settings → Memory viewer: a dropdown scope selector over the store's workspace partitions (opening on the host's current workspace), a search box, per-entry relative updated times, click-to-preview bodies (5 MiB cap), patrol warnings, and a skills section listing the workspace's project skills plus visible global skills (global rows badged, project shadowing global) with the same preview, each row showing its view count. Localized through the host's `locale/*.json` metadata channel; host theme tokens, the `Button` primitive, and zh/en copy that tracks the DSH language. The card is a request-time projection of the database and has no edit path.
28
29
 
29
30
  Memory types follow the ZCode taxonomy: `user` (profile and environment facts), `feedback` (corrected or confirmed practices — bodies must carry **Why:** and **How to apply:** lines), `reference` (resource pointers). The extraction boundary is person- and environment-scoped knowledge only: decisions and trade-offs are repo-scoped by nature and belong to the repo's ADR route, never to memory.
30
31
 
@@ -55,6 +56,10 @@ No commands. On the first session after restart the index is injected automatica
55
56
  | `memoryIndex.order` | `400` | Section order (upstream named slots start at `PLAN_POLICY=500`) |
56
57
  | `memoryIndex.maxChars` | `6000` | Hard character budget for the injected index |
57
58
  | `memoryIndex.extraction` | `true` | The proposal-based extraction run (turn-level, per-session slots, no idle timer) |
59
+ | `review.enabled` | `true` | The autonomous content-quality loop (own prompts/budget/ledger, trial dry-run) |
60
+ | `review.dryRun` | `true` | Ledger verdicts without landing them (flip off after the trial distribution reads clean) |
61
+ | `review.dailyBudget` | `100` | Reviewed items per UTC day (new > corrects-linked > stale > spot-check, tail drops) |
62
+ | `review.batchSize` | `10` | Max items per drain run |
58
63
  | `memoryCard.enabled` | `true` | Read-only memory card in the official plugin manager (degrades to no card when the host lacks `webServer`/`plugins.bundle.config`) |
59
64
 
60
65
  Profile patch example:
@@ -65,13 +70,15 @@ Profile patch example:
65
70
  memoryIndex:
66
71
  maxChars: 8000
67
72
  extraction: true
73
+ review:
74
+ dryRun: false
68
75
  ```
69
76
 
70
77
  ## Development
71
78
 
72
79
  ```bash
73
80
  pnpm install && pnpm build # deps + clean build -> lib/
74
- pnpm test # vitest (258 tests)
81
+ pnpm test # vitest (319 tests)
75
82
  pnpm test:coverage # v8 coverage, CI-enforced thresholds
76
83
  pnpm lint # oxlint src test client
77
84
  pnpm check:pack # published-artifact consistency (also runs on prepack)
@@ -81,9 +88,13 @@ Layout:
81
88
 
82
89
  ```
83
90
  ├── src/
84
- │ ├── index.ts # registration: section + tools + extraction + card wiring
91
+ │ ├── index.ts # registration: section + tools + extraction + review + card wiring
85
92
  │ ├── store.ts # central SQLite store: DDL/WAL/FTS, reads, ledger, patrol, cursors
86
93
  │ ├── store-apply.ts # the sole write gate: validate all, apply atomically, ledger
94
+ │ ├── token-usage.ts # single accounting entry: normalize provider usage, pure sums
95
+ │ ├── review.ts # autonomous quality loop: cheap gate → evolve, budget, scheduler
96
+ │ ├── review-prompt.ts # review prompts + verdict parsing (gate + memory/skill rubrics)
97
+ │ ├── store-apply.ts # the sole write gate: validate all, apply atomically, ledger
87
98
  │ ├── memory-rules.ts # mechanical gates: enums, sizes, id shape, secret screening
88
99
  │ ├── memory-section.ts # injection: store query / truncate / session freeze
89
100
  │ ├── memory-guide.ts # the main-session guide (three lines)
@@ -100,7 +111,7 @@ Layout:
100
111
  ├── client/
101
112
  │ └── client.js # hand-written client bundle: read-only card in the plugin manager
102
113
  ├── locale/ # host package metadata: localized display name + description (zh/en)
103
- ├── test/ # vitest suite (20 files)
114
+ ├── test/ # vitest suite (25 files)
104
115
  ├── lib/ # build output (tsc)
105
116
  ├── docs/ # design.md (design) · FAQ.md (real-world pitfalls)
106
117
  └── .agents/ # AI collaboration layer (AGENTS.md, skills, ADR notes)
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-258%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-319%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-96%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-90%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
@@ -21,10 +21,11 @@ Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项
21
21
  ## 工作原理
22
22
 
23
23
  1. **开局注入** —— 插件只注册一个 system-prompt section:注入本工作区的记忆索引(查库结果,只含钩子,超预算按整行截断,feedback > user > reference 排序)、中央库路径、三行指南。该 section 每会话只算一次并逐字节复用,system prompt 稳定、prompt cache 持续命中;空 store 零 token。
24
- 2. **四个工具,一个写者** —— `memory_write`(create/update/delete)承接显式的记住/忘掉:每条提案过代码门禁(id 规范、type 枚举、feedback 的 Why/How 契约、v1 移植的 secret 筛查、64KB 正文上限)后在一个事务内落盘。`memory_read` 按 id 取正文、按关键词检索——空格分隔的关键词取 AND,短词与 trigram 未命中时回退字面子串匹配;注入索引只有钩子。`skill_write`(项目内 create/update/delete + 显式搬到全局)沉淀可复用的操作规程,走同一套纪律(短横线 id、secret 筛查、64KB 上限、跨范围禁重名重文);每条落地的技能由插件代码物化为 `<workspace>/.agents/skills/<id>/SKILL.md`——库是唯一真相,文件是只读投影,漂移由巡检按库还原。`skill_read` 按 id 取技能正文、检索项目与全局技能(同名时项目遮蔽全局)。宿主没有工具服务时降级为告诉用户措辞,注入不受影响。
25
- 3. **提案制提取** —— 插件唯一的后台自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账,同一会话连续的空转轮次合成一行并累计唤醒次数。
26
- 4. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before 重写。技能变更同样落 `skill_log`。每次应用后随跑一次巡检:孤儿 FTS 行清除、含密钥条目隔离(隐藏,不物理删除;被隔离技能的物化文件一并撤下,使其不再被加载)。只读卡片显形巡检异常。
27
- 5. **只读卡片** —— 挂在官方插件管理的本 bundle 页面(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡,内容对齐 ZCode 设置的记忆选项卡——下拉作用域选择器列出库里的工作区分区(默认选中宿主当前工作区)、搜索框、每条记忆的相对更新时间、点击行内展开正文预览(5 MiB 上限)、巡检异常警告,以及技能区(本工作区技能 + 可见的全局技能,全局行打标记,同名时项目遮蔽全局),预览同记忆。显示名与描述走宿主 `locale/*.json` 通道双语;宿主主题 token、`Button` 原语、中英文案跟随 DSH 语言。卡片是数据库的请求时投影,没有编辑路径。
24
+ 2. **四个工具,一个写者** —— `memory_write`(create/update/delete)承接显式的记住/忘掉:每条提案过代码门禁(id 规范、type 枚举、feedback 的 Why/How 契约、v1 移植的 secret 筛查、64KB 正文上限、可选 `corrects` 指向被替代的同工作区 active 记忆)后在一个事务内落盘。`memory_read` 按 id 取正文、按关键词检索——空格分隔的关键词取 AND,短词与 trigram 未命中时回退字面子串匹配;注入索引只有钩子。按 id 取到的每份记忆正文计一次查阅(与技能同纪律的存在心跳)。`skill_write`(项目内 create/update/delete + 显式搬到全局)沉淀可复用的操作规程,走同一套纪律(短横线 id、secret 筛查、64KB 上限、跨范围禁重名重文);每条落地的技能由插件代码物化为 `<workspace>/.agents/skills/<id>/SKILL.md`——库是唯一真相,文件是只读投影,漂移由巡检按库还原。`skill_read` 按 id 取技能正文、检索项目与全局技能(同名时项目遮蔽全局);按 id 取到的每份正文计一次查阅(查阅次数 + 上次查阅,卡片展示)。这是存在心跳——只证明被翻过,不证明有用:长期零查阅是删除候选,剪枝拍板仍需人工。宿主没有工具服务时降级为告诉用户措辞,注入不受影响。
25
+ 3. **提案制提取** —— 轨迹到记忆的自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账,同一会话连续的空转轮次合成一行并累计唤醒次数。
26
+ 4. **内容质量自进化环** —— 第二条后台自动化,与提取在结构上独立(自有提示词、自有日预算、自有 `review_log` 账本、无游标;只共享库的写门):便宜门模型先判某条记忆/技能值不值得细看,批了才跑贵改写,按内容质量打分(记忆五维:准确/可用/可复用/不重复/不过时;技能四维:触发精度/可执行完备/可复用/不重复)并给出改写提案。新写即审、存量走有界积压队列排空、长期未动 30 天回炉。两条硬保证框住自动化:删除永不自动落盘(只有改写与隔离——隔离隐藏不删),超幅改写自动转隔离。日上限默认 100 条(新写 > 被纠正牵连 > 陈旧 > 抽查,尾部丢弃不滚雪球)框住 token 开销;每条终态落一行 `item-review` verdict 行、每天合一行日 rollup(预算 vs 实审 vs 积压 vs 丢弃,token 按便宜/贵分账),方便以后复盘。默认试运行(`review.dryRun` 开):只记 verdict,不落盘。
27
+ 5. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before 重写。技能变更同样落 `skill_log`,评审 verdict 落 `review_log`。每次应用后随跑一次巡检:孤儿 FTS 行清除、含密钥条目隔离(隐藏,不物理删除;被隔离技能的物化文件一并撤下,使其不再被加载)。只读卡片显形巡检异常。
28
+ 6. **只读卡片** —— 挂在官方插件管理的本 bundle 页面(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡,内容对齐 ZCode 设置的记忆选项卡——下拉作用域选择器列出库里的工作区分区(默认选中宿主当前工作区)、搜索框、每条记忆的相对更新时间、点击行内展开正文预览(5 MiB 上限)、巡检异常警告,以及技能区(本工作区技能 + 可见的全局技能,全局行打标记,同名时项目遮蔽全局),预览同记忆,每行显示查阅次数。显示名与描述走宿主 `locale/*.json` 通道双语;宿主主题 token、`Button` 原语、中英文案跟随 DSH 语言。卡片是数据库的请求时投影,没有编辑路径。
28
29
 
29
30
  记忆类型沿 ZCode 分类法:`user`(画像与环境事实)、`feedback`(被纠正/确认的做法——正文必带 **Why:** 与 **How to apply:**)、`reference`(资源指针)。提取边界只收"跟人与环境走的知识":决策与取舍天然属于仓库,走仓库的 ADR 路线,不进记忆库。
30
31
 
@@ -55,6 +56,10 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
55
56
  | `memoryIndex.order` | `400` | section 顺序(上游命名槽位自 `PLAN_POLICY=500` 起) |
56
57
  | `memoryIndex.maxChars` | `6000` | 注入索引的硬字符预算 |
57
58
  | `memoryIndex.extraction` | `true` | 提案制提取运行(轮级、按会话分槽、无空闲计时器) |
59
+ | `review.enabled` | `true` | 内容质量自进化环(自有提示词/预算/账本,默认试运行) |
60
+ | `review.dryRun` | `true` | 只记 verdict 不落盘(试运行分布看顺后再关) |
61
+ | `review.dailyBudget` | `100` | 每天评审上限(新写 > 被纠正牵连 > 陈旧 > 抽查,尾部丢弃) |
62
+ | `review.batchSize` | `10` | 每次 drain 最多审几条 |
58
63
  | `memoryCard.enabled` | `true` | 官方插件管理内的只读记忆卡片(宿主缺 `webServer`/`plugins.bundle.config` 时自动降级为无卡片) |
59
64
 
60
65
  profile patch 示例:
@@ -65,13 +70,15 @@ profile patch 示例:
65
70
  memoryIndex:
66
71
  maxChars: 8000
67
72
  extraction: true
73
+ review:
74
+ dryRun: false
68
75
  ```
69
76
 
70
77
  ## 开发
71
78
 
72
79
  ```bash
73
80
  pnpm install && pnpm build # 依赖 + 干净构建 -> lib/
74
- pnpm test # vitest(258 例)
81
+ pnpm test # vitest(319 例)
75
82
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
76
83
  pnpm lint # oxlint src test client
77
84
  pnpm check:pack # 发布产物一致性(prepack 也会跑)
@@ -81,9 +88,13 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
81
88
 
82
89
  ```
83
90
  ├── src/
84
- │ ├── index.ts # 注册:section + 工具 + 提取 + 卡片接线
91
+ │ ├── index.ts # 注册:section + 工具 + 提取 + 评审 + 卡片接线
85
92
  │ ├── store.ts # 中央 SQLite 库:DDL/WAL/FTS、读面、账本、巡检、游标
86
93
  │ ├── store-apply.ts # 唯一写门:全量校验、事务原子应用、账本
94
+ │ ├── token-usage.ts # 统一记账口:归一各家用量、纯累加
95
+ │ ├── review.ts # 内容质量自进化:便宜门 → 贵改写、预算、调度
96
+ │ ├── review-prompt.ts # 评审提示词 + verdict 解析(门 + 记忆/技能 rubric)
97
+ │ ├── store-apply.ts # 唯一写门:全量校验、事务原子应用、账本
87
98
  │ ├── memory-rules.ts # 机械门禁:枚举、尺寸、id 规范、secret 筛查
88
99
  │ ├── memory-section.ts # 注入:查库 / 截断 / 会话冻结
89
100
  │ ├── memory-guide.ts # 主会话指南(三行)
@@ -100,7 +111,7 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
100
111
  ├── client/
101
112
  │ └── client.js # 手写 client bundle:官方插件管理内只读卡片
102
113
  ├── locale/ # 宿主包元数据:显示名 + 描述双语(zh/en)
103
- ├── test/ # vitest 测试(20 个文件)
114
+ ├── test/ # vitest 测试(25 个文件)
104
115
  ├── lib/ # 构建产物(tsc)
105
116
  ├── docs/ # design.md(设计)· FAQ.md(踩坑)
106
117
  └── .agents/ # AI 协作层(AGENTS.md、技能、ADR 笔记)
package/client/client.js CHANGED
@@ -102,6 +102,7 @@ window.__ModuleLoader__.load({
102
102
  skillsEmpty: "该工作区还没有技能——明确告诉模型沉淀为技能即可,重启后生效。",
103
103
  skillScopeGlobal: "全局",
104
104
  skillPreviewDeleted: "该技能已被删除或搬家,请刷新列表。",
105
+ skillViewed: "查看 {count} 次",
105
106
  scopeLabel: "选择工作区",
106
107
  searchPlaceholder: "搜索记忆…",
107
108
  searchEmpty: "没有匹配的记忆文件。",
@@ -134,6 +135,7 @@ window.__ModuleLoader__.load({
134
135
  skillsEmpty: "No skills in this workspace yet — ask the model to save one as a skill, then restart.",
135
136
  skillScopeGlobal: "global",
136
137
  skillPreviewDeleted: "This skill was deleted or moved — refresh the list.",
138
+ skillViewed: "viewed {count} times",
137
139
  scopeLabel: "Choose workspace",
138
140
  searchPlaceholder: "Search memory files…",
139
141
  searchEmpty: "No matching memory files.",
@@ -457,6 +459,12 @@ window.__ModuleLoader__.load({
457
459
  if (time !== "") {
458
460
  rowChildren.push(createElement("span", { key: "time", className: "dce-file-time" }, time));
459
461
  }
462
+ // Skill rows carry V2 view counters; memory rows have no such
463
+ // field and skip this span. Zero means never opened — the count
464
+ // is a presence heartbeat (viewed, not applied), not a value score.
465
+ if (typeof file.useCount === "number" && file.useCount > 0) {
466
+ rowChildren.push(createElement("span", { key: "usage", className: "dce-file-time" }, t("skillViewed", { count: file.useCount })));
467
+ }
460
468
  var children = [
461
469
  createElement(
462
470
  "button",
@@ -173,6 +173,9 @@ export function mountCardRoutes(webServer, store, options = {}) {
173
173
  }
174
174
  return;
175
175
  }
176
+ // Human previews are display-only: people never audit skills by
177
+ // reading them, so previews leave no count. Only model-side
178
+ // consultation (skill_read by id) feeds the heartbeat.
176
179
  sendJson(response, 200, {
177
180
  id: content.id,
178
181
  scope: content.scope,
package/lib/index.d.ts CHANGED
@@ -79,6 +79,23 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
79
79
  */
80
80
  extraction: z<boolean, boolean, "plain">;
81
81
  }>>, "plain">;
82
+ /**
83
+ * The autonomous content-quality loop (own prompts, own daily budget,
84
+ * own ledger — shares only the store's write gates with extraction).
85
+ * `dryRun` ledgers verdicts without landing them (trial mode, default
86
+ * on); `dailyBudget` caps reviewed items per UTC day (default 100).
87
+ */
88
+ review: z<Schemastery.ObjectS<NoInfer<{
89
+ enabled: z<boolean, boolean, "plain">;
90
+ dryRun: z<boolean, boolean, "plain">;
91
+ dailyBudget: z<number, number, "plain">;
92
+ batchSize: z<number, number, "plain">;
93
+ }>>, Schemastery.ObjectT<NoInfer<{
94
+ enabled: z<boolean, boolean, "plain">;
95
+ dryRun: z<boolean, boolean, "plain">;
96
+ dailyBudget: z<number, number, "plain">;
97
+ batchSize: z<number, number, "plain">;
98
+ }>>, "plain">;
82
99
  /**
83
100
  * The read-only memory card in the official plugin manager
84
101
  * (`plugins.bundle.config`). Defaults to on; hosts without the
@@ -121,6 +138,23 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
121
138
  */
122
139
  extraction: z<boolean, boolean, "plain">;
123
140
  }>>, "plain">;
141
+ /**
142
+ * The autonomous content-quality loop (own prompts, own daily budget,
143
+ * own ledger — shares only the store's write gates with extraction).
144
+ * `dryRun` ledgers verdicts without landing them (trial mode, default
145
+ * on); `dailyBudget` caps reviewed items per UTC day (default 100).
146
+ */
147
+ review: z<Schemastery.ObjectS<NoInfer<{
148
+ enabled: z<boolean, boolean, "plain">;
149
+ dryRun: z<boolean, boolean, "plain">;
150
+ dailyBudget: z<number, number, "plain">;
151
+ batchSize: z<number, number, "plain">;
152
+ }>>, Schemastery.ObjectT<NoInfer<{
153
+ enabled: z<boolean, boolean, "plain">;
154
+ dryRun: z<boolean, boolean, "plain">;
155
+ dailyBudget: z<number, number, "plain">;
156
+ batchSize: z<number, number, "plain">;
157
+ }>>, "plain">;
124
158
  /**
125
159
  * The read-only memory card in the official plugin manager
126
160
  * (`plugins.bundle.config`). Defaults to on; hosts without the
package/lib/index.js CHANGED
@@ -31,6 +31,7 @@
31
31
  import z from "@deepseek-ai/schemastery";
32
32
  import { mountCardRoutes } from "./card-routes.js";
33
33
  import { createExtractionScheduler } from "./extraction.js";
34
+ import { createReviewScheduler } from "./review.js";
34
35
  import { createFrozenMemorySection, cwdOf, DEFAULT_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memorySectionText, } from "./memory-section.js";
35
36
  import { registerMemoryTools } from "./memory-tools.js";
36
37
  import { registerSkillTools } from "./skill-tools.js";
@@ -59,6 +60,18 @@ export const Config = z.object({
59
60
  */
60
61
  extraction: z.boolean(),
61
62
  }),
63
+ /**
64
+ * The autonomous content-quality loop (own prompts, own daily budget,
65
+ * own ledger — shares only the store's write gates with extraction).
66
+ * `dryRun` ledgers verdicts without landing them (trial mode, default
67
+ * on); `dailyBudget` caps reviewed items per UTC day (default 100).
68
+ */
69
+ review: z.object({
70
+ enabled: z.boolean(),
71
+ dryRun: z.boolean(),
72
+ dailyBudget: z.natural(),
73
+ batchSize: z.natural(),
74
+ }),
62
75
  /**
63
76
  * The read-only memory card in the official plugin manager
64
77
  * (`plugins.bundle.config`). Defaults to on; hosts without the
@@ -119,6 +132,37 @@ function mountExtractionWhenAvailable(host, store, config) {
119
132
  createExtractionScheduler(events, { store, surface, llm });
120
133
  });
121
134
  }
135
+ /**
136
+ * Arm the review scheduler through a nested inject: the loop needs only the
137
+ * LLM sibling (no trajectory reader — it judges stored items, not session
138
+ * increments), so it mounts independently from extraction. Own prompts, own
139
+ * budget, own ledger; the store's write gates are the only shared surface.
140
+ */
141
+ function mountReviewWhenAvailable(host, store, config) {
142
+ const log = host.logger("continual-evolve");
143
+ if (config.review?.enabled === false) {
144
+ log.info("review disabled by config — extraction and tools unaffected");
145
+ return;
146
+ }
147
+ const nested = host;
148
+ const events = host;
149
+ if (typeof nested.inject !== "function" || typeof events.on !== "function") {
150
+ log.warn("host context exposes no inject/on — review not armed (injection and tools unaffected)");
151
+ return;
152
+ }
153
+ nested.inject(["llm"], (scoped) => {
154
+ const context = scoped;
155
+ const llm = (context.llm ?? context.get?.("llm"));
156
+ createReviewScheduler(events, { store, llm }, {
157
+ enabled: true,
158
+ dryRun: config.review?.dryRun ?? true,
159
+ dailyBudget: config.review?.dailyBudget ?? 100,
160
+ batchSize: config.review?.batchSize ?? 10,
161
+ staleDays: 30,
162
+ minIntervalMs: 5 * 60 * 1000,
163
+ });
164
+ });
165
+ }
122
166
  /**
123
167
  * Mount the card routes on the host web server through a nested inject, so
124
168
  * the plugin stays mountable on hosts without that service. Called once the
@@ -156,6 +200,7 @@ export function apply(host, config) {
156
200
  state.store = store;
157
201
  mountToolsWhenAvailable(host, store, state);
158
202
  mountExtractionWhenAvailable(host, store, config);
203
+ mountReviewWhenAvailable(host, store, config);
159
204
  if (config.memoryCard?.enabled !== false) {
160
205
  mountCardWhenAvailable(host, store);
161
206
  }
@@ -38,6 +38,13 @@ export interface MemoryProposal {
38
38
  body?: string | undefined;
39
39
  /** Trajectory provenance for extraction writes ("820-831"). */
40
40
  sourceSeqs?: string | undefined;
41
+ /**
42
+ * Optional id of the same-workspace memory this proposal corrects or
43
+ * supersedes. The gate requires the target to exist and be active and
44
+ * forbids self-links; the link persists on the row for future
45
+ * correction-chasing. Append/replace only — no unlink path.
46
+ */
47
+ corrects?: string | undefined;
41
48
  }
42
49
  /** The merged field values a create/update would write (delete checks none). */
43
50
  export interface ProposedFields {
@@ -74,6 +74,10 @@ export interface SkillFileInfo {
74
74
  status: string;
75
75
  /** Update time in epoch milliseconds (the card's "updated" line). */
76
76
  updatedAt: number;
77
+ /** Bodies served by id (V2 usage loop); 0 when never consumed. */
78
+ useCount: number;
79
+ /** Last consumption in epoch milliseconds, `null` when never consumed. */
80
+ lastUsedAt: number | null;
77
81
  }
78
82
  /** The full read-only skill snapshot the card renders for one workspace root. */
79
83
  export interface SkillSnapshot {
@@ -9,6 +9,7 @@
9
9
  import { existsSync } from "node:fs";
10
10
  import { basename } from "node:path";
11
11
  import { MEMORY_FILE_PREVIEW_LIMIT } from "./store.js";
12
+ import { skillUsagePartition } from "./skill-rules.js";
12
13
  export { MEMORY_FILE_PREVIEW_LIMIT };
13
14
  /**
14
15
  * The read-only snapshot for one workspace partition. A workspace without
@@ -168,7 +169,19 @@ function scopedSkills(store, root) {
168
169
  }
169
170
  return merged;
170
171
  }
171
- function skillInfoOf(entry) {
172
+ function skillInfoOf(store, entry) {
173
+ let useCount = 0;
174
+ let lastUsedAt = null;
175
+ try {
176
+ const use = store.getSkillUse(skillUsagePartition(entry.scope, entry.record.workspaceId), entry.record.id, entry.scope);
177
+ if (use !== undefined) {
178
+ useCount = use.useCount;
179
+ lastUsedAt = use.lastUsedAt === null ? null : Date.parse(use.lastUsedAt) || 0;
180
+ }
181
+ }
182
+ catch {
183
+ // Usage is decoration: the snapshot degrades to zeros, never throws.
184
+ }
172
185
  return {
173
186
  id: entry.record.id,
174
187
  title: entry.record.title,
@@ -176,6 +189,8 @@ function skillInfoOf(entry) {
176
189
  scope: entry.scope,
177
190
  status: entry.record.status,
178
191
  updatedAt: Date.parse(entry.record.updatedAt) || 0,
192
+ useCount,
193
+ lastUsedAt,
179
194
  };
180
195
  }
181
196
  /**
@@ -200,7 +215,7 @@ export function skillSnapshot(store, root) {
200
215
  exists: true,
201
216
  readError: null,
202
217
  fileCount: entries.filter((entry) => entry.record.status === "active").length,
203
- files: entries.filter((entry) => entry.record.status === "active").map(skillInfoOf),
218
+ files: entries.filter((entry) => entry.record.status === "active").map((entry) => skillInfoOf(store, entry)),
204
219
  quarantined: entries
205
220
  .filter((entry) => entry.record.status === "quarantined")
206
221
  .map((entry) => ({ id: entry.record.id, scope: entry.scope, reason: quarantineReasonOf(store, entry.record) })),
@@ -106,7 +106,14 @@ export function registerMemoryTools(tools, store) {
106
106
  const limit = clampLimit(args.limit);
107
107
  if (typeof args.id === "string" && args.id.length > 0) {
108
108
  const record = store.get(cwd, args.id);
109
- return record === undefined ? `没有找到 id 为 "${args.id}" 的记忆。` : formatRecord(record);
109
+ if (record === undefined) {
110
+ return `没有找到 id 为 "${args.id}" 的记忆。`;
111
+ }
112
+ // Presence heartbeat (mirrors skill_read): a body served
113
+ // by id means the model opened it for the task at hand.
114
+ // Search/list hits are noise and stay uncounted.
115
+ store.recordMemoryUse(cwd, record.id);
116
+ return formatRecord(record);
110
117
  }
111
118
  const query = typeof args.query === "string" ? args.query : "";
112
119
  const records = query.length > 0 ? store.search(cwd, query, limit) : store.list(cwd).slice(0, limit);
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The content-quality review prompts (the model side of the autonomous
3
+ * evolution loop): a cheap gate that decides whether an item deserves a
4
+ * close look, and the evolve pass that judges content quality dimension by
5
+ * dimension and proposes the rewrite. Code owns every consequence — the
6
+ * model only returns verdict JSON, never touches the store.
7
+ *
8
+ * Memories are judged on five dimensions (accurate, usable, reusable,
9
+ * non-duplicate, fresh); skills on four (description trigger precision,
10
+ * body executability, reusability, non-duplicate). The prompts are
11
+ * deliberately behavior-blind: there are no task datasets here (unlike
12
+ * Hermes GEPA), only the item text plus its provenance.
13
+ */
14
+ export declare const REVIEW_CHEAP_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u95E8\u3002\u53EA\u5224\u8FD9\u6761\u8BB0\u5FC6/\u6280\u80FD\u503C\u4E0D\u503C\u5F97\u7EC6\u770B\uFF1A\u6709\u660E\u786E\u4E8B\u5B9E\u3001\u505A\u6CD5\u3001\u504F\u597D\u6216\u53EF\u590D\u7528\u89C4\u7A0B\u5C31\u6279\uFF1B\u7A7A\u8BDD\u3001\u7EAF\u8FC7\u7A0B\u8BB0\u5F55\u3001\u4E00\u6B21\u6027\u7684\u77AC\u65F6\u8F93\u51FA\u3001\u5F53\u8F6E\u5DF2\u53EF\u63A8\u5BFC\u7684\u4E00\u5F8B\u62D2\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"worthReview\": true|false, \"reason\": \"\u4E00\u53E5\u8BDD\"}\u3002";
15
+ /** Memory rubric: accurate, usable, reusable, non-duplicate, fresh. */
16
+ export declare const REVIEW_MEMORY_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u3002\u6309\u4E94\u7EF4\u5224\u8FD9\u6761\u8BB0\u5FC6\uFF1A\u2460\u51C6\u786E\uFF08\u4E8B\u5B9E\u53EF\u4FE1\u3001\u65E0\u5E7B\u89C9\uFF09\uFF1B\u2461\u53EF\u7528\uFF08Why/How \u5177\u4F53\u3001\u53EF\u6267\u884C\uFF0C\u53CD\u9988\u7C7B\u5FC5\u987B\u5E26 Why \u4E0E How to apply\uFF09\uFF1B\u2462\u53EF\u590D\u7528\uFF08\u672A\u6765\u4F1A\u8BDD\u7528\u5F97\u4E0A\uFF09\uFF1B\u2463\u4E0D\u91CD\u590D\uFF08\u4E0E\u5019\u9009\u65E7\u6761\u6BD4\uFF0C\u6709\u91CD\u590D\u5C31\u6307\u51FA\u6765\uFF09\uFF1B\u2464\u4E0D\u8FC7\u65F6\uFF08\u672A\u88AB\u7EA0\u6B63\u94FE\u53D6\u4EE3\uFF09\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"\u4E00\u53E5\u8BDD\", \"title\"?: \"\u6539\u5199\u540E\u6807\u9898\", \"description\"?: \"\u6539\u5199\u540E\u94A9\u5B50\", \"body\"?: \"\u6539\u5199\u540E\u6B63\u6587\u5168\u6587\", \"corrects\"?: \"\u88AB\u53D6\u4EE3\u7684\u540C\u533A\u6761\u76EEid\"}\u3002verdict \u8BF4\u660E\uFF1Akeep=\u4E0D\u52A8\uFF1Brewrite=\u7ED9\u51FA\u6539\u5199\u540E\u5168\u6587\u4E09\u4EF6\uFF1Bquarantine=\u6D89\u5BC6/\u6709\u5BB3/\u5B8C\u5168\u8FC7\u65F6\u3002\u7981\u5220\u9664\uFF0C\u7981\u7F16\u9020\u5F15\u7528\u3002";
17
+ /** Skill rubric: trigger precision, executability, reusability, non-duplicate. */
18
+ export declare const REVIEW_SKILL_SYSTEM_PROMPT = "\u4F60\u662F\u6280\u80FD\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u3002\u6309\u56DB\u7EF4\u5224\u8FD9\u6761\u6280\u80FD\uFF1A\u2460\u89E6\u53D1\u7CBE\u5EA6\uFF08description \u662F\u5426\u8BA9\u4EBA/\u6A21\u578B\u5728\u8BE5\u7528\u65F6\u60F3\u8D77\u3001\u4E0D\u8BE5\u7528\u65F6\u4E0D\u6253\u6270\uFF09\uFF1B\u2461\u6B63\u6587\u53EF\u6267\u884C\u5B8C\u5907\uFF08\u6B65\u9AA4\u5B8C\u6574\u3001\u8F93\u5165\u8F93\u51FA\u660E\u786E\uFF09\uFF1B\u2462\u53EF\u590D\u7528\uFF08\u8DE8\u4EFB\u52A1\u53EF\u7528\uFF09\uFF1B\u2463\u4E0D\u91CD\u590D\uFF08\u4E0E\u5019\u9009\u65E2\u6709\u6280\u80FD\u6BD4\uFF09\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"\u4E00\u53E5\u8BDD\", \"title\"?: \"\u6539\u5199\u540E\u6807\u9898\", \"description\"?: \"\u6539\u5199\u540E\u94A9\u5B50\", \"body\"?: \"\u6539\u5199\u540E\u6B63\u6587\u5168\u6587\"}\u3002verdict \u8BF4\u660E\uFF1Akeep=\u4E0D\u52A8\uFF1Brewrite=\u7ED9\u51FA\u6539\u5199\u540E\u5168\u6587\u4E09\u4EF6\uFF1Bquarantine=\u6D89\u5BC6/\u6709\u5BB3/\u5B8C\u5168\u8FC7\u65F6\u3002\u7981\u5220\u9664\u3002";
19
+ export interface GateVerdict {
20
+ worthReview: boolean;
21
+ reason: string;
22
+ }
23
+ export interface EvolveVerdict {
24
+ verdict: "keep" | "rewrite" | "quarantine";
25
+ reason: string;
26
+ title?: string | undefined;
27
+ description?: string | undefined;
28
+ body?: string | undefined;
29
+ corrects?: string | undefined;
30
+ }
31
+ /** The cheap-gate prompt for one item (id + hook + body, truncated by caller). */
32
+ export declare function gatePrompt(entry: {
33
+ kind: string;
34
+ id: string;
35
+ title: string;
36
+ description: string;
37
+ body: string;
38
+ }): string;
39
+ /** The evolve prompt for one item plus its similar neighbors for dup-checking. */
40
+ export declare function evolvePrompt(entry: {
41
+ id: string;
42
+ title: string;
43
+ description: string;
44
+ body: string;
45
+ extra?: string | undefined;
46
+ }, neighbors: readonly {
47
+ id: string;
48
+ title: string;
49
+ description: string;
50
+ }[]): string;
51
+ /** Parse the cheap-gate answer; `null` when the model did not return verdict JSON. */
52
+ export declare function parseGateAnswer(text: string): GateVerdict | null;
53
+ /** Parse the evolve answer; `null` when the model did not return verdict JSON. */
54
+ export declare function parseEvolveAnswer(text: string): EvolveVerdict | null;
55
+ //# sourceMappingURL=review-prompt.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The content-quality review prompts (the model side of the autonomous
3
+ * evolution loop): a cheap gate that decides whether an item deserves a
4
+ * close look, and the evolve pass that judges content quality dimension by
5
+ * dimension and proposes the rewrite. Code owns every consequence — the
6
+ * model only returns verdict JSON, never touches the store.
7
+ *
8
+ * Memories are judged on five dimensions (accurate, usable, reusable,
9
+ * non-duplicate, fresh); skills on four (description trigger precision,
10
+ * body executability, reusability, non-duplicate). The prompts are
11
+ * deliberately behavior-blind: there are no task datasets here (unlike
12
+ * Hermes GEPA), only the item text plus its provenance.
13
+ */
14
+ export const REVIEW_CHEAP_SYSTEM_PROMPT = "你是记忆库的内容评审门。只判这条记忆/技能值不值得细看:有明确事实、做法、偏好或可复用规程就批;空话、纯过程记录、一次性的瞬时输出、当轮已可推导的一律拒。只输出 JSON:{\"worthReview\": true|false, \"reason\": \"一句话\"}。";
15
+ /** Memory rubric: accurate, usable, reusable, non-duplicate, fresh. */
16
+ export const REVIEW_MEMORY_SYSTEM_PROMPT = "你是记忆库的内容评审。按五维判这条记忆:①准确(事实可信、无幻觉);②可用(Why/How 具体、可执行,反馈类必须带 Why 与 How to apply);③可复用(未来会话用得上);④不重复(与候选旧条比,有重复就指出来);⑤不过时(未被纠正链取代)。只输出 JSON:{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"一句话\", \"title\"?: \"改写后标题\", \"description\"?: \"改写后钩子\", \"body\"?: \"改写后正文全文\", \"corrects\"?: \"被取代的同区条目id\"}。verdict 说明:keep=不动;rewrite=给出改写后全文三件;quarantine=涉密/有害/完全过时。禁删除,禁编造引用。";
17
+ /** Skill rubric: trigger precision, executability, reusability, non-duplicate. */
18
+ export const REVIEW_SKILL_SYSTEM_PROMPT = "你是技能库的内容评审。按四维判这条技能:①触发精度(description 是否让人/模型在该用时想起、不该用时不打扰);②正文可执行完备(步骤完整、输入输出明确);③可复用(跨任务可用);④不重复(与候选既有技能比)。只输出 JSON:{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"一句话\", \"title\"?: \"改写后标题\", \"description\"?: \"改写后钩子\", \"body\"?: \"改写后正文全文\"}。verdict 说明:keep=不动;rewrite=给出改写后全文三件;quarantine=涉密/有害/完全过时。禁删除。";
19
+ /** The cheap-gate prompt for one item (id + hook + body, truncated by caller). */
20
+ export function gatePrompt(entry) {
21
+ return [
22
+ `kind=${entry.kind} id=${entry.id}`,
23
+ `title=${entry.title}`,
24
+ `钩子=${entry.description}`,
25
+ "全文:",
26
+ entry.body,
27
+ ].join("\n");
28
+ }
29
+ /** The evolve prompt for one item plus its similar neighbors for dup-checking. */
30
+ export function evolvePrompt(entry, neighbors) {
31
+ const lines = [
32
+ `id=${entry.id}`,
33
+ `title=${entry.title}`,
34
+ `钩子=${entry.description}`,
35
+ "全文:",
36
+ entry.body,
37
+ ];
38
+ if (entry.extra !== undefined && entry.extra.length > 0) {
39
+ lines.push("", entry.extra);
40
+ }
41
+ if (neighbors.length > 0) {
42
+ lines.push("", "同类候选(判重复用):");
43
+ for (const neighbor of neighbors) {
44
+ lines.push(`- id=${neighbor.id} title=${neighbor.title} 钩子=${neighbor.description}`);
45
+ }
46
+ }
47
+ return lines.join("\n");
48
+ }
49
+ /** Parse the cheap-gate answer; `null` when the model did not return verdict JSON. */
50
+ export function parseGateAnswer(text) {
51
+ const parsed = parseJsonObject(text);
52
+ if (parsed === null || typeof parsed.worthReview !== "boolean") {
53
+ return null;
54
+ }
55
+ return {
56
+ worthReview: parsed.worthReview,
57
+ reason: typeof parsed.reason === "string" && parsed.reason.length > 0 ? parsed.reason.slice(0, 300) : "无理由",
58
+ };
59
+ }
60
+ /** Parse the evolve answer; `null` when the model did not return verdict JSON. */
61
+ export function parseEvolveAnswer(text) {
62
+ const parsed = parseJsonObject(text);
63
+ if (parsed === null || (parsed.verdict !== "keep" && parsed.verdict !== "rewrite" && parsed.verdict !== "quarantine")) {
64
+ return null;
65
+ }
66
+ const verdict = {
67
+ verdict: parsed.verdict,
68
+ reason: typeof parsed.reason === "string" && parsed.reason.length > 0 ? parsed.reason.slice(0, 300) : "无理由",
69
+ };
70
+ if (typeof parsed.title === "string" && parsed.title.length > 0) {
71
+ verdict.title = parsed.title.slice(0, 500);
72
+ }
73
+ if (typeof parsed.description === "string" && parsed.description.length > 0) {
74
+ verdict.description = parsed.description.slice(0, 1000);
75
+ }
76
+ if (typeof parsed.body === "string" && parsed.body.length > 0) {
77
+ verdict.body = parsed.body.slice(0, 65536);
78
+ }
79
+ if (typeof parsed.corrects === "string" && parsed.corrects.length > 0) {
80
+ verdict.corrects = parsed.corrects.slice(0, 200);
81
+ }
82
+ return verdict;
83
+ }
84
+ /** Extract the first JSON object from model output (fenced or bare). */
85
+ function parseJsonObject(text) {
86
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
87
+ const candidate = (fenced?.[1] ?? text).trim();
88
+ const start = candidate.indexOf("{");
89
+ const end = candidate.lastIndexOf("}");
90
+ if (start === -1 || end <= start) {
91
+ return null;
92
+ }
93
+ try {
94
+ const parsed = JSON.parse(candidate.slice(start, end + 1));
95
+ /* v8 ignore next 3 — the slice always starts with `{`, so a
96
+ successful parse is an object by construction; the guard stays. */
97
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
98
+ ? parsed
99
+ : null;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ //# sourceMappingURL=review-prompt.js.map