Skip to content

Releases: Spc-jgs/obsidian-kb-skill

v1.36.0 — 检索结果的可信度判定、空壳捕获的审计、安装脚本数据化

Choose a tag to compare

@Spc-jgs Spc-jgs released this 24 Aug 03:05
4e370f6

Added

  • search-vault says whether it answered the question. Asked something the Vault does not cover, it returned hits with a score, a heading and a snippet — the shape of a search that succeeded — so a caller could not tell "here is the answer" from "here is the lexically nearest noise". An Agent either cites a Python note for a Feign question, or, worse, concludes the topic is already covered and never captures it. Every response now carries confidence, keyed on IDF-weighted coverage of the typed query: how much of the question's information the winner actually matched. IDF rather than a stop-word list, because a list needs a countable source and question frames like 有什么 have none; typed words only, because letting the ranker's own query expansion certify the ranker's results is circular. Ranking is unchanged.

    The level has two values, not three, because the measurement supports one cut and not two. On the reference Vault, 22 no-answer queries score 0.09–0.54 and 16 correctly-answered ones score 0.32–0.64 — overlapping ranges. 0.30 demotes none of the 16 while catching 20 of the 22; 0.60 would catch all 22 and demote 12 of the 16. So none is a finding and evidence is merely the absence of one: it does not claim the answer is right, and two of eighteen measured questions carry evidence with a wrong top result.

  • The audit reports a web-clip that captured nothing (web-clip-captured-nothing, defect). Two notes on the reference Vault are placeholders left by a blocked fetch, and the audit's only word on them was web-clip-missing-author at hygiene — while retrieval treated them as ordinary captures: hit, cited, and counted as evidence the topic was already covered. empty-template-note could not reach them, because it fires on content_chars == 0 and placeholder prose is still characters; one of the two has no heading at all, failing its other precondition too.

    The criterion is a floor on body content, scoped to web-clip and skipping notes that carry the Vault's draft tag. Ranked by that count, the Vault's 55 web-clips put both shells first and second (100 and 220 characters), the two self-declared drafts next (329, 383), and the smallest real capture at 799. WEB_CLIP_MIN_CONTENT_CHARS = 400 sits just below the geometric midpoint of 220 and 799, leaning toward missing a shell rather than accusing a real note. The type scoping is load-bearing, not cautious: 92 notes of all types fall under 800 characters and 87 of them are legitimate — 45 daily reports, 23 folder indexes, 9 weekly reports. A genuinely short source is the known false positive, named in docs/superpowers/specs/2026-08-24-shell-capture-detection-design.md rather than denied.

Fixed

  • install.ps1 could delete the contents of a symlink target instead of unlinking it. Uninstalling the base Skill under QoderWork and under Codex used Remove-Item -Recurse -Force, while the other seven paths used Remove-OwnedPath. On a directory symlink — which is what a Skill manager creates — the recursive form follows the link and deletes what is on the other side. The parallel hand-copy in install.sh was wrong in a different place: QoderWork's base tested -d where the other eight tested -d || -L, so an installation whose Skill directory was a symlink was skipped entirely by uninstall.

    Both are consequences of the same shape rather than two independent slips: each script carried 16 hand-copied path literals across its install, uninstall and host-validation branches, with no shared source of truth between the two languages. Both now expand one table, so neither inconsistency can be expressed again.

  • A # comment inside a fenced code block is no longer read as a heading. search_vault had no notion of a fence and scanned for ^#[ \t]+ line by line, which polluted three things at once. Two notes on the reference Vault took their title — scored at 6x body — from a line inside a ```bash block, so they lost that weight on their own subject and gained it on a shell comment. Headings the author never wrote scored at 2x. And the passage split, which is the unit ranking works on, was cut short at phantom boundaries: 22 of 199 notes carried 255 such false headings, one of them turning 100 sections into 52. Short passages are barely penalised by BM25, so notes scored high on subjects they only mention in passing.

    The fix reads the fence notion that already exists — link_graph.blank_code_examples, already used by explore-neighborhood and relatedness — rather than writing a fourth scanner. It blanks code line by line while preserving numbering, so one index addresses both the blanked copy and the body: split on the blanked copy, read content from the original. Discarding code outright would trade this defect for a worse one; npx playwright install chromium still has to be findable, and both halves are asserted. On the 20 annotated queries MRR moved 0.885 → 0.900.

Changed

  • Both installers' Skill and host layouts are data. One table per script — SKILL_ROWS/$SkillRows and HOST_ROWS/$HostRows — expanded by install, uninstall and validation alike. Deliberately two tables rather than one product, because hosts are not uniform: Cursor takes retrieval only, and Claude Code additionally migrates a legacy marker block. Steps that are not Skill payload stay in the case/switch, so a host appears there only when it needs an extra action. The guards changed with the shape: "everything installed can be uninstalled" used to be a count of literal occurrences and is now true by construction, leaving one assertion that no branch grows a path outside the table.

  • BM25's parameters are named BM25_K1 and BM25_B at module level instead of being literals inside _bm25_score. They are the textbook defaults, which is exactly why they need a guard — a round number reads as unexamined — and a sweep is now a one-line change rather than an edit to a function body.

Decided and recorded, with the losing side kept

Four rulings, each with the measurement that produced it and what would reopen it.

  • Lowering BM25's length penalty is not the fix for "the fuller note ranks lower". Sweeping b from 0.00 to 1.00, b=0.25 looks best in aggregate (17/18 versus 16/18, MRR 0.972 versus 0.944) — but only 4 of the 18 queries move at all, so three notes decide the whole result, which is the overfitting the issue itself warned about. The direction is wrong where it matters: of four same-source "excerpt versus full" pairs, b=0.25 fixes one and pushes another's full note out of the Top-5, and the average size of a no-answer query's top result grows from 5916 to 10667 bytes. 2026-08-21-rejected-hypotheses.md §6.

  • The adversarial corpus keeps its shape, and no second everyday corpus is built. Both the issue and an earlier fix measured file bytes; _bm25_score normalises by average_scoring_length, a different unit, and on that unit the corpus sits 2.18x above the reference Vault with nothing at all in the 1–5x band where that Vault keeps 39.7% of its notes. Reshaping was measured, not argued: it leaves the failure the issue wanted exposed exactly where it already is (rank 2) while moving three must_see ranks the wrong way and erasing what the dilution family exists to show. The divergence is now pinned at 2.0–2.4 by assertion so that either narrowing or widening it forces the ruling to be re-read. 2026-08-23-adversarial-corpus-shape-decision.md.

  • A helper cannot be told that a capture's fetch failed, so preventing a shell note at write time is closed. Nothing in the package performs network I/O — the fetch belongs entirely to the Agent, and every fact about it a helper receives is a fact the Agent chose to type. The one name that describes the fetch outcome, retrieval_status, is deliberately never persisted: it belongs to the in-run self-check whose opening line is "do not persist it as telemetry". Requiring the Agent to declare it does not rescue the route, because the only witness to a failed fetch is the actor that failed it. 2026-08-21-rejected-hypotheses.md §7, guarded by an assertion that the package still cannot reach the network.

  • A correction to this file's own v1.35.0 entry, and to the record it summarised. That entry says the two shell notes are "prose an Agent composed while breaking that rule". They are not: Terminal Failure Means Zero Writes, and the whole of core/references/web-capture.md, was created on 2026-07-31, and the notes were written on 2026-07-22 and 2026-07-23 — eight and nine days earlier. That day's write path accepted them, because its metadata predicate asked only whether a field was a non-empty string and unknown is non-empty; today's create-note --apply refuses both. A third note, recorded as an author's deliberately brief draft, turns out to say in its own body that its fetch was blocked by Cloudflare — and that misreading was the sentence that killed the structural predicate. Rescored within web-clips and excluding self-declared drafts, that predicate finds the shell with no false positives, so #167 stayed open carrying the measurement instead of being closed by it.

v1.35.0 — 写入前拦截、审计降噪、捕获复访、三个否定结论

Choose a tag to compare

@Spc-jgs Spc-jgs released this 22 Aug 12:47
982eb45

本版有一个会让你撞到的行为变更、一次审计降噪(参考 Vault 上 finding 从 113 降到 92,去掉的全是假阳性)、一个回答「捕获之后有没有被再打开」的新 helper,以及三个被数据否掉、连同落败一起记下来的假设。

还有一个自己引入又自己修掉的回归——它值得单独说,因为它暴露的东西比它本身更重要。

写入前拦截:审计知道有缺陷,文件却已经落盘(#156

create-note --apply 现在会拒绝仍带模板脚手架的正文,退出码 2,不落盘

此前的形状是:写后审计在同一次调用里就知道这篇笔记有缺陷,但文件已经落盘、索引已经更新、退出码是 0,--json 顶层没有任何字段报告判定结果。唯一的信号埋在嵌套的 audit.ok: false 里。任何按退出码判断成败的调用方都被告知写入成功。

参考 Vault 上 9 篇笔记因此带着写给 Agent 的指令存活下来——<!-- 用 2–4 句话区分原文观点与自己的推论 --> 是给写笔记的人看的,现在是用户笔记的一部分。

拒绝集不是全部 defect。二十个 defect 码里多数描述的是 Vault 而非这篇笔记,broken-wikilink 更是被同期裁定为标准用法——链接一篇还没写的笔记正是 Obsidian 的图谱提示概念值得单开一篇的方式。按 defect 拦截会让「新建笔记时链接一个未来概念」变成不可能,所以有一条断言点名 broken-wikilink 不在拒绝集里。

--no-audit 跳过写后报告,不跳过这道拦截。让它绕过就等于把它变成「写一篇已知有缺陷的笔记」的开关。

那个回归,以及它暴露的东西(#163#179

上面那条拦截的判据是裸正则 \{\{([^}]+)\}\}没有任何代码块豁免。于是:

```html
<span>Message: {{ msg }}</span>

一篇讲 Vue 的笔记根本写不进去。Jinja2、Handlebars、Liquid、GitHub Actions、Obsidian Templater 同理。

最难堪的是:**我在同一个 PR 里为 `broken-wikilink` 写过一模一样的论证,却没把它应用到自己收进拒绝集的那一码上。** 三条新断言全绿——因为它们测的是我想到的场景。

修复不损失任何检测:参考 Vault 上被报的 8 篇**全部是 `{{date}}` 且全部在代码块外**,忽略围栏与行内代码之后仍是 8 篇。

判据收进 `note_catalog`,因为要共享的不只是正则,还包括**「忽略代码」这个决定**——`audit-vault` 与 `process-inbox` 必须一致,否则一篇笔记能通过归档却过不了审计。而 `template_contract` 继续用裸正则:它读的是**模板文件**,那里的 `{{date}}` 正是要找的东西。

顺带:为 `note_catalog` 复制围栏遮罩时,闭合正则的转义写坏了,**围栏永远不闭合**,代码块之后的占位符会被整段吞掉。新加的「两份实现必须一致」那条断言**写完五分钟就红了**。

## 审计降噪:113 → 92,去掉的全是假阳性

- **YAML 裸日期不再被判占位符。** YAML 把 `published: 2026-08-13` 解析成 `datetime.date`,只有带引号的才是 `str`。而判据拒绝一切非 `str`,于是它实际在评判**作者有没有给日期加引号**——两篇把 `published` 与 `author` 都填对了的 web-clip 被报成缺失。**写得最规范的反而被罚。**
- **`disconnected-note` 不再命中 web-clip。** 23 条全部 ≤47 天、中位 27 天,这个状态会自己消失;而 `suggest-directed-links` 在这 23 篇上**候选数为 0**,所以也无从建议。23 篇里 20 篇已被 `review-captures` 以更强的问题覆盖,一条断言把豁免绑在那份覆盖上:把 `web-clip` 从 `CAPTURE_TYPES` 拿掉,测试就红——否则豁免的理由会静默失效。
- **日报这类编号序列不再被当成近重复。**

## review-captures:捕获之后呢(#160)

其它每一项测量问的都是捕获是否**忠实**,没有一项问它是否被用过。

参考 Vault 上 94 次捕获里 **55 次再没被打开过**,而复访率按类型相差三倍:`learning-note` 0.75、`insight-note` 0.31、`web-clip` 0.275。

证据来源是 git 历史(Vault 在仓库里时)或文件 mtime,**报告会说明用的是哪一个**——两者精确度不同,不该被读成同一回事。

同期修掉一个它自己的缺陷:`.obsidian-kb-backups/` 下的副本被算成了捕获。副本不会被任何人再打开,把它算进来只会污染这个指标。

## 三个被否掉的假设,连同落败一起记下

否定结论在树里不留痕迹:没有测试守着它,没有代码提到它。下一个看到同一条 finding 的人会从头再推一遍,**而且可能不在证据停下的地方停下**。三条都在 `docs/superpowers/specs/2026-08-21-rejected-hypotheses.md`,各自带着试过的判据、杀死它的数据、以及什么会重开它。

- **断链无法区分「概念占位」与「被删笔记」。** 判据是「概念占位会被多篇引用」——实测每一个概念占位都**只被引用一次**,和一次删除完全一样。更根本的是:单次快照里两者留下的痕迹相同,**Obsidian 自己也不区分**。
- **web-clip 里的 `unknown` 不是「正确捕获被误报」。** 两种写法**在时间上零重叠**:`unknown`/`未知` 止于 2026-07-26,`原文未标明`/`原文未署名` 起于 07-27,而分界线正是那次把 `unknown` 列为占位符、并在指令里指定替代写法的提交落地那天。规则生效后使用被弃形式的 web-clip 是 **0 篇**——裁定生效了,豁免它等于撤销它。
- **空壳笔记的结构判据不成立。** 按「空章节占比」排序,六篇日报和一篇有意留简的草稿排在真正的空壳前面。而那些措辞也不是仓内常量:`web-capture.md` 明令禁止保存占位物,代码里根本不存在这条写入路径——那两篇是 Agent 违反规则时自己编的散文。

## 评估基础设施

- **硬门禁不再中止整批。** 硬失败意味着「这次运行不算数」,而退出码已经承载了这个语义;中止额外丢掉的正是这次运行存在的目的——一次基线因此停在 36 次中的 15 次。`--stop-on-hard-failure` 保留旧行为,`stopped_after_case` 记录截断,好让部分均值不被读成整体。
- **收据门禁不再按 Agent 选了哪种内容来源判分。** 它额外要求 `--from-preflight`,而 `create-note` 同样接受 `--stdin` 与 `--content-file`,于是一次完全正确的 verified 捕获被判成硬失败。绑定关系由 helper 自己的输出证明,去掉那个要求什么都没削弱。
- **对抗集语料的平均长度对齐真实 Vault。** 两篇 76KB 笔记独占语料 **97.9%** 的字节,把均值顶到 9170 对真实的 4247——BM25 的长度惩罚因此从不触及任何日常长度的笔记,**一整类排序失败在这个集合里根本不可观测**。现在是 38KB,落在参考 Vault 第 2/3 大笔记的量级,而 `adv-dilution-06` 把那个失败冻结下来:同一来源的 2.4KB 完整捕获,在关键词密集查询上排在 0.5KB 摘要之后。

---

发版清单外还撞到三个硬编码版本号的测试文件(`test_build.py` 的 `test_project_version_reads_pyproject`、`test_doctor.py` 两处、`test_installers.py` 一处)。全套测试会抓到它们,但只会在 CHANGELOG 已经写完之后。

v1.34.0 — 有向 relatedness、结论可复算、评估集自检

Choose a tag to compare

@Spc-jgs Spc-jgs released this 17 Aug 03:08
8373913

本版一个新 helper,两条关于怎么得出结论的仓库约束,一次裁定,以及一轮把评估集自己送上审判席的返工 —— 三个「测量的东西不是它声称在测的东西」被抓出来并修掉。

有向 relatedness:笔记声明自己依赖谁(#75)

suggest-directed-links 回答的是跟随一条链接时读者真正的问题:这些链接里哪些是这篇笔记倚仗的,倚仗它做什么。

判断刻意不是相似度,而 #75 冻结的标签就是理由。它 16 条硬负例每一条都与来源共享一个词、此外毫无关系:Release Quality GateAirport Departure Gates,Source Archive FormatMuseum Archive Visit,Read-only RetrievalReading List。任何基于词重叠的排序器都会给它们高分 —— 这个集合的存在就是为了惩罚 search-vault 用的那种方法

16 条正例的共同点是来源在自己的正文里说出了它拿目标做什么:它引用委托给导入遵循表示为其倍数

所以候选需要同一句话里同时有显式引用和依赖短语。裸链接不是依赖:一篇 See also 列了五条链接的笔记,声明了五条链接、零条依赖,links_without_a_dependency 数着它们,好让「没找到」读起来不同于「没链接」。没有分数、没有阈值、没有置信度 —— 一个候选要么有声明的依赖,要么不是候选。

阈值在写任何代码之前就登记在 issue 上,附一条可证伪的预测:负例会得而不是「低于某阈值」,而如果需要调一个数字,就说明判据本身错了。预测成立:16/16 正例带证据找到,0/16 负例被提出,负例侧根本没看见任何引用。

参考 Vault 上:195 篇笔记、262 条链接行、5 个候选、256 条链接被正确拒绝。五个都是真的 —— 一篇把另一篇引作选型依据,以及 Python 系列的前置链:迭代器依赖生成器依赖切片迭代。262 里出 5 条是发现,不是缺陷:放宽判据把这个数字做大,会把 16 条硬负例全部放回来。

两条词表项来自那个 Vault 里观察到的形式,记录计数与位置,从不猜同义词:前置知识 ×3、前序知识 ×2,把候选从 1 个带到 5 个。同一遍量到并刻意不收的:详见 ×4、参考 ×2 —— 那是指针,不是依赖。

故意砸守卫砸出了两件测试没在说的事。 整个删掉依赖要求,16 条硬负例照样全被拒 —— 它们守的是「不许从共享的词推断」,对「有依赖的链接 vs 没依赖的链接」什么都没证明。而「同句」规则根本没人守:放宽到整篇,什么都没红。两件现在都有测试,也都写进评估报告,而不是留给读者从 16/16 里过度解读。

复述下一步行动时必须报出它所在的标题(#109)

每次都报,不只是在条目看起来可疑时 —— 因为「看起来没问题」正是读者需要能核对的那个判断。

参考 Vault 上,一份可复用检查表和真正的 P0 计划是同一个 ## 下一步行动 之下的兄弟小节。没有任何结构能分开它们,分开它们的是作者给它起的名字。

本项目已经三次拒绝让 helper 读标题的语义 —— #86 设计续航包时、#115 自由结构笔记几乎无输出时、#109 从相反方向。三次裁决躺在三条 issue 评论里,第四次 issue 得把三条都找出来才知道这是一条已定的边界而不是疏漏。现在它是 docs/superpowers/specs/2026-08-17-heading-semantics-boundary-decision.md,含被拒的替代方案和什么会重开它。

计数行为不变,包括那篇的 open_tasks_in_next_actions 仍然是 15。那个数字对它测量的东西是对的,它为什么会误导,记录在案而不是修掉。

AGENTS.md 多了两条约束:结论怎么得出的(#124)

已有的那条管「两处必须一致」。新的两条管另一类:

关于总体的结论 —— 多少个、哪几个、多大比例 —— 必须给出一条输出可复算的命令。

一道新断言必须至少见过一次红。

两条都点名本项目已经产生的实例,而不是写成泛泛的劝诫:

  • #93 正文说加严可达性判据会让六个 helper 不可达,紧接其后的分解按它自己的判据推出的是一个,今天实测输出是两个 —— 它列了 13 个里的 11 个。同一个习惯让 #133 的引文指向了错误的文件,从而提出了错误的修法。
  • 清单第 17 行断言为真,覆盖面小于它的名字;#118 为上报信号写的断言是空的、绿着、放过了错误输出;故意删掉 #75 两条守卫本该守的判据,它们断言的东西一样没变。这些在一次全绿的测试里都看不见。

段落明写这两条不做成 CI 检查,以免日后有人补一道正则匹配 commit 文本的假守卫。

三个「测量的东西不是它声称在测的东西」

深度选择:量完发现没东西可修(#74)

#74 的第一条验收标准要求用 v1.30 那个模型重跑 12 次。本项目不再运行那个产品,所以这条按原文不可满足,它的 8/12 也不再是可比基线。绝对门槛换成同一 Agent 的前后对比,summary.json 记录 agent / agent_version / comparable_with_fixture_baseline,免得一个产品的数字被读成另一个的。

量出来的答案是没东西可修。 现状指令下深度选择 12/12 全对,由写出来的 15 篇笔记逐篇读 capture_depth 确认。对比 v1.30 的 8/12,以及在那两个曾经误升级的用例上 6/6 对 2/6 —— 按旧比率,连中 6 次的概率 (1/3)^6 ≈ 0.0014

跑之前登记的预测就是:12/12 意味着候选措辞没有可移动的信号,不该合。它没合。

这不证明指令无歧义:诊断出的那个歧义真实存在于 web-capture.md:26,那里命名 verified 触发条件的词,在请求描述来源内容时同样出现。一个 Agent 没被它绊倒。

事实分有一部分在测输出语言(#147)

同一用例的两篇笔记都用中文完整记录了全部五条事实 —— 其中一篇 只读 ×9、检索 ×7、写入 ×12、用户意图 ×3、预检 ×7 —— 一篇 5/5,一篇 1/5。差别全在有没有顺手各回显一次英文原词。两篇保留的知识一样。

一条事实现在可以有多个可接受形式。每个形式都是某次真实运行写出来的,连同计数记在 fact_form_provenance 里,按 #75 给词表定的规矩:收录观察到的,不收录听起来像的。一条断言守着,猜来的翻译加不进去。

考「必须读图」的用例,一张图不看也能满分(#146)

那个用例五条 required fact 全部出现在它自己的来源文本里,而来源明写着文本不指定图里的东西。一个不会在它存在意义那件事上失败的评估资产,就是一道出生即绿的守卫 —— #117 的形状。

补了两条只有图里有的事实。但颜色判据分不出「读了图」和「猜了个像样的颜色」,所以真正的判据放在事件流:新硬失败 material-not-inspected 查有没有读取类工具调用指向那个资产。目录列表会打印图片名字而没人看过它,所以笔记和文件名都不是证据。手上的运行六次全都打开了它。

顺带,评估的硬失败契约现在列的是门禁真能抛出的码。它记了 14 个里的 8 个,还记了两个判分器从不抛出的:一个由别的名字实现,另一个 —— invented-source-access —— 从来没有任何检查,所以 prompt 禁止抓取来源 URL 这件事从来不是一道门禁。这一对没删,移进 hard_failures_not_implemented 写明各自下落。

判分器的两个中文假阳性

一篇按 required fact 记下来源自己说的「不支持 Python 3.10」的笔记 —— 那是被禁断言的反面 —— 被判成断言了它,因为否定只认 /没有 加四个写入动词之一。而「,」不算子句边界,于是「原文把 2.4.1 和 Python 3.12 绑定,并单独排除 3.10」把两条各说各话的陈述凑成了一条断言。英文「,」仍然刻意不算边界。

早先那版基线两个都没撞上。那是措辞碰巧,不是判分器可靠。

升级

git pull && ./install.sh

已经用 Skill 管理器的机器改用:

git pull && ./install.sh --runtime-only

v1.33.0 — 章节级排序、知识邻域、检索视图、续航包修正

Choose a tag to compare

@Spc-jgs Spc-jgs released this 17 Aug 01:30
abdcc46

本版把检索从「找到哪篇笔记」推进到「找到笔记里的哪一段」,并把几处会静默漂移的重复删掉而不是再加一道断言。

排序单位从整篇变成章节(#118

search-vault 此前按整篇算 BM25,选完再挑摘要——排上来的理由和读者被送到的位置可以是同一篇笔记的不同部分。现在一篇笔记按它自己的最佳章节竞争,摘要就从那一节出。

旧单位的代价是量出来的,不是假设的:同一段证据在 0.3 KB 笔记里得 11.9 分,在 75 KB 笔记里得 2.2 分,两篇长笔记都进不了 Top-5,而五篇只是顺带提到这些词的笔记进了。有章节的那篇现在排到第 2;它那篇内容与体积几乎相同、只是没有章节的孪生笔记仍然缺席——这是测量在起作用,不是数字在动:章节只有存在才能竞争。

  • 标题、别名、标签、小标题、链接描述整篇,进入每一节的分数;正文被切开,取最好的一节
  • 没有小标题的笔记只有一节等于全文,所以短笔记的分数和以前逐位相同
  • 一节的长度不低于本篇的典型章节长度。BM25 奖励短文档,这对短笔记是对的,对长笔记里的一小片是错的——够到它仍然要打开那篇长笔记。#118 在写代码前就列了这个风险,第一版实现照样犯:一段只有 jitter 上限。 的残节以 0.580 压过真正解释答案的 0.504
  • 延迟 P95 144 ms vs 138 ms(1.04x,#118 给的预算是 2x)。做到这一点靠的是不把正文切两次分词,而是把各节的计数相加——这是精确而非近似,因为换行符既不是拉丁字母也不是汉字,任何一个词元都跨不过行

还有一处:用户输入的词若只出现在本节之外,不再报进 body 信号。这条同样事先列为风险、同样实现错了,是靠打印输出发现的,不是靠为它写的断言——那条断言的第一版是空的。

知识邻域(#121

explore-neighborhood 回答找到一篇好笔记之后的问题:这个 Vault 说什么和它连着。

每条边都是声明,没有一条是推断。不打分、不提议链接、不把链接读成「支持」或「是证据」——发现新候选是 #75 的事,证据谱系是 #85 的事,在这里做任何一件都会让一个猜测读起来像用户写下的东西。同主题、同目录、同日期不算连接。 节点顺序是稳定路径序,reference 直说它不是排名。

  • 歧义名列出 candidates 但一个都不用;解析不了的链接返回而不丢弃——一篇有三条失效链接的笔记不该看起来像连接更少的笔记
  • 代码围栏里的链接是被引用的语法,Vault 自己的笔记天天引用它
  • 目录索引与原文归档默认排除并计入 excluded:索引链接目录里的每一篇,跟着它走返回的是目录不是邻域。--include-structural 可以跟

一跳。参考 Vault 上:一篇项目复盘返回一个邻居,同时经由正文链接和 related 两条路到达,去重后两个来源都标出;而 AI Bug 工作流那篇诚实地返回,因为它没有 wikilink、related 为空,并且它自己正文里就说了是刻意不做链接的。

把问题写下来再运行(#122

run-retrieval-view 运行 Vault 已经写下来的搜索。用户反复问的问题并不无限——「上周的学习笔记」「当前项目风险」——而每次 Agent 重新把它翻译成参数时可能翻译得不一样,同一个问题于是悄悄得到不同答案。视图就是那次翻译,做一次,存在 .obsidian-kb/retrieval-views.json

只有结构化字段:没有命令、没有管道、没有模板、没有环境变量插值,也没有 search-vault 本来没有的字段。视图只能收窄,走的是直接调用同样的校验参数。未知的键是拒绝而不是忽略——静默丢掉一个就等于运行了一次这个文件没有描述的搜索。

--as-of 是必填的,helper 从不读时钟。含义来自「现在」的时间窗每次运行给不同答案,那正好是这东西要防的;「上周」是 Agent 转成日期的短语,不是配置能持有的值。

返回结果时一并返回解析出的 plan,并且有测试把那个 plan 重新过一遍 search-vault 做比对——一个描述不了实际调用的 plan 比没有 plan 更糟,它是一个穿着可核对外衣的错误答案。

指向已不存在目录的视图返回 invalid-view-scope,而不是回落到全库:静默变宽的视图会继续工作、返回比以往更多的东西、并且什么都不说。

续航包的三处修正

  • 不再把项目自己的目录索引当作材料(#133)。 _instance_sources 的 docstring 承诺排除索引文件,实际只排除了 README.md/AGENTS.md/CLAUDE.md。参考 Vault 上这不是一个项目的不幸:四篇项目笔记里有实例目录的三篇,每一篇都恰好返回一个来源,且三次都是该项目自己的目录索引fields: []。三篇现在都返回 sources: []——对一个只装着一篇笔记和一份清单的目录,这才是诚实的答案。判据是笔记声明的类型,并且对每条路径生效,不只是目录扫描。
  • 「章节缺失」不再和「章节没被认出来」混为一谈(#115)。 这两种读法把用户送向相反方向:一个去找,一个去写一份已经存在的东西。现在返回 headings,分成 matchedunmatched。顺带修掉一个真实缺陷:词表只认识 project overview,所以每一篇按本项目自己的英文模板写的笔记都把 goal 报成缺失
  • 补上 #86 点名却从未交付的两类目录外来源(#110)。 project frontmatter 与项目笔记自己的 related。两条路都比位置弱并且说出来:origin 报最强的那条,origins 列全部。歧义只报不解——related 名字撞上两篇返回 ambiguous-related-link 并且两篇都不用,选一篇会把别的项目的材料归进这个包,读起来像本项目自己的历史而读者无从分辨。顺带记一笔:参考 Vault 上 project: 根本没人用(三处,全空,两处是模板),所以这条路唯一的证据是它的测试。这句写在这里,而不是留给谁去发现。

复苏队列说出它的数字从哪来(#109

open_tasks 数全文每一个未勾选的框,而那个数排的序——于是一篇装着可复用检查表的笔记压过了真有活干的项目。参考 Vault 上一篇项目笔记末尾有十五条给别的项目落地用的检查项,而它自己的计划是一个连一个 checkbox 都没有的 P0/P1/P2 编号列表;它排成了全库最忙的项目,并把一句检查表问句报成了下一步。

现在按 open_tasks_in_next_actions 排序,两个数都报,外加 open_tasks_scopenull 不是零:零是这篇在那一节里什么都没放,null 是它根本没有那一节。

next_action_heading 是新的,也是这件事不再机械的地方。那篇笔记里检查表是嵌在下一步行动章节里的——### 可复用的项目落地检查表### P0:下一次迭代前完成 并列在 ## 下一步行动 之下。没有任何结构能分开它们,分开它们的是作者给它起的名字,而那是一个关于内容的判断,这个 helper 不做。所以它报出标题,由读者判断。

归档拒绝没写完的草稿(#116

新拒绝码 draft-incomplete。参考 Vault 上一篇标了 incomplete、四个章节全写着「待后续详细阅读后补充完整」的 web-clip 被提议迁进 20-Learning。两阶段闸门拦住了它——一个人读了计划说不——但那是注意力,不是守卫,而批准一份长计划的用户读的是列表不是每一行。

两个信号,都是笔记关于它自己的陈述而不是对内容的判断:草稿标签,和正文里没被替换的 {{placeholder}}。正文里用散文写「待后续补充」不匹配——为含义读散文正是归档不做的事。

更少的重复,而不是更多的断言

四条边界这一版是被删掉的,不是被守起来的:

未替换占位符正则 audit-vaulttemplate-contract 各自一份且已经分叉,归档正要加第三份 → 合并到 note_catalog,按对象同一性断言
「下一步行动」词表 两个检索 helper 各持一套,上一版只扩了其中一侧 → 共用 PROJECT_NOTE_NEXT_ACTION_HEADINGS
{"folder-index", "moc"} 两份拷贝,检索正要成为第三份 → 一个 INDEX_TYPES
wikilink 解析 audit_vault 提到共享的 link_graphaudit_vault 少了 146 行

两处能互相 import 的时候,删掉边界胜过在它上面加断言。

零结果搜索现在说出四种情况里的哪一种(#120),文本与 JSON 从同一张 ZERO_RESULT_REASONS 表生成——这也是一条被删掉的关系。要防的读法是把 no-token-overlap 报成「你的 Vault 里没有这方面的东西」:那是一个关于词的事实,不是关于知识的事实。

--updated-* 让搜索能问笔记什么时候变过,而不只是什么时候写的(#119)。参考 Vault 上一篇 2026-06-09 的项目笔记 updated2026-08-12--after 2026-08-01 返回五篇并静默漏掉那个月真正变过的那篇。200 篇里 177 篇根本没有 updated,单独计入 missing-updated——「没人记录它什么时候变的」是 Vault 的事实,「它在你的窗口外变的」是过滤器在工作。review-projects 故意用另一套口径,这条差异登记为第 28 行,是登记表第一条记录两处必须保持不同的关系。

装机:和 Skill 管理器共存(#113 / #114

  • --runtime-only / -RuntimeOnly 装 Skill 管理器不提供的一切,不装它提供的。安装器有六件事要做,只有一件(把 Skill 文件写进五个平台目录)和管理器重叠——「用管理器就行」装出来的环境第一次调 helper 就 ModuleNotFoundError
  • 安装器不再毁掉不是它建的 Skill 链接。实测过:安装器打印 Installation complete 五个平台全打勾,同时 skillctl doctorOK 变成 FAILED 带八个 runtime link drift两边都不看对方,所以没有任何东西报告出问题。

评估集自己先被修了(#136

对抗集里每一篇长笔记都是靠追加无结构填充生成的,因为作者就是那么想象长笔记的。事后在参考 Vault 上量:19 篇 ≥10 KB 的笔记里,最少的有 12 个小标题,中位数 30,没有一篇只有一个。那个集合复现的是这个 Vault 根本不表现的机制。

后果不是理论上的:一个章节级排序的候选实现在全部 22 个用例上与 master 逐字节相同——只有一个小标题的笔记恰好只有一节,任何按小标题的切分在它上面都是空操作。#117 自己立的标准是「不会失败的评估集就是一道出生即绿的守卫」;这个集合能失败,但在 #118 正要问它的那个问题上不能。

升级

git pull && ./install.sh

已经用 Skill 管理器的机器改用:

git pull && ./install.sh --runtime-only

v1.32.0 — 入口接通、实体目录、项目续航包、一致性登记表

Choose a tag to compare

@Spc-jgs Spc-jgs released this 12 Aug 10:39
6bb2ceb

本版把「已交付但不可达」这条线走完,并把发现它的方法固化下来。

两个 helper 接通(#90 / #96

process-inboxaudit-vault 此前已实现、有测试、注册在 [project.scripts]、随安装分发到每台机器、写进对外文档——而 core/ 从未引用,因此没有 Agent 能选中它们。现在「整理一下 Inbox」「体检一下 Vault」都能触发。

audit-vault 的归属按可测成本裁定:放进只读包会让其 Python 体积从 98 KB 涨到 217 KB(+121%),而新增的九个模块几乎全是写侧契约。

40-Projects 成为实体目录(#95

一个项目一个子目录,项目笔记与其异构产出同处。拥挤度契约现在声明了自己只管主题目录——它从未声明过,而读作普适规则正是问题成因:那套规则会禁止项目目录,因为项目目录按定义从一篇笔记开始。

  • 新增审计 duplicate-project-note:一个目录多篇项目笔记会让复苏雷达把一个项目数成多个
  • 新增拒绝码 entity-instance-unknown:归档项目笔记时不落根级、也不从正文关键词猜实例目录

项目续航包(#86

新增只读 helper resume-project,回答「怎么接着干」:项目的目标、阻塞、决定、下一步,加上其 digest 持有的边界约束与证据产物,每条带路径与行号

归属由目录布局确定,不依赖 frontmatter 字段是否被正确维护。缺失的章节报 missing-section 而非从散文拼凑;项目笔记与来源都回答的字段列入 contested 并双方都返回——新不等于权威,续航包不替读者裁定。

失败开始说话(#92 / #93 / #103

  • 归档 apply 阶段的拒绝原因进入结构化输出,不再只有 applied: false。留下改动的那条失败(副本已写入、源删不掉、回滚也失败)有独立的 partial-apply
  • 可达性守卫要求指令演示如何运行,而非提到名字——一句「不要用某 helper」曾能让它转绿
  • 用错 Skill 的 runner 会指出能力在对侧,而不是只回 invalid choice

一致性登记表(#106

同一种缺陷形状在一天内出现七次:某件事写在两处,没有东西检查一致性,失败是静默的。16 条边界登记在案,含一条明确 guard: none#91 的二十处安装脚本硬编码——它长期无人守,正是因为从未有人给它命名)。

AGENTS.md 现在要求:让两处必须一致的改动,断言与登记行必须在同一次改动里补上。

升级

git pull && ./install.sh

本版包含 #84 的复苏队列修复,装旧版的 Vault 需要重装才能生效。

v1.31.0 — 跨语言检索、复苏雷达、词表收敛、判分器加固

Choose a tag to compare

@Spc-jgs Spc-jgs released this 11 Aug 04:06
15d1cdd

跨语言查询扩展(#73

中文提问现在能命中英文笔记。v1.30 的语义改写只命中 3/8,而其中 5 条失败返回的是零结果不是错结果 —— 语料是英文、查询是中文,分词器产出拉丁词元和 CJK bigram,两套字母表永远碰不到,BM25 没有可排的东西。

一份 83 个概念的双语词表匹配原始查询,把另一种语言的说法按 0.45 权重一起检索。仍是词法检索:不算向量、不跑模型、不建索引,mode 保持 lexical

Semantic(门禁 ≥5/8) 3/8 · MRR 0.3125 8/8 · MRR 0.9375
Semantic holdout 4/8 · MRR 0.5 8/8 · MRR 1.0
Exact / Alias / Filtered 全 1.0 全 1.0
No-answer 误报 0 0
P95 延迟 7.58 ms 7.20 ms

扩展是假设不是证据,所以全程上报:expansion 块、每条结果的 expansion signal、--no-expand 复现扩展前行为。中文「代理」同时是 agent 和 proxy,两种读法都展开都报告。

Vault 可用 .obsidian-kb/retrieval-lexicon.json 补自己的词汇。文件坏了用 invalid-lexicon 拒绝,不悄悄降级。

0.45 这个权重没有被评测证明 —— 权重从 0.25 扫到 1.0 召回完全不动,16 篇的合成语料区分不出来。报告里写明了。

项目复苏雷达

新的只读入口 review-projects:主题搜索要求用户先想起某个项目,复苏队列不要求。只扫 project-note,受阻 / 缺活动日期 / 超过失温阈值的进队列,给出活动日期、失温天数、可见未完成 checkbox 数、已有的第一步和机器稳定的入选原因。不写状态、不写 review 标记、不编综合价值分数。

完成态在中英文下都识别(completed / 已完成 / 已归档 / 已取消),认不出来的状态一律当作进行中。

聚类词表收敛(#68 #69

「哪些词不算主题」原来在代码里有好几份互相不一致的答案:

  • subject_clusters 改读 Vault 自己的 Templates/,和 tag_vocabulary 同源。旧的硬编码表把 java 当通用词(真实主题),又留着模板早已改名的 person
  • 补齐「一篇东西」这类通用标题词:文章 笔记 记录 等 12 个。
  • 来源站点名不硬编码 —— 对做剪藏的 Vault 是噪声,对写那个平台的人是正当主题。改由 Vault 用 .obsidian-kb/vault-vocabulary.json 自己声明。

Web Capture 判分器加固(#76

硬门禁原来在给措辞打分,不是给断言打分,三种无效回答能拿到 hard_failures: []

  • 完成状态靠散文正则猜 → 改成要求 OUTCOME: / BLOCKER: 结构块,解析不出来本身就是硬失败;
  • 停止理由「关键词出现过」就算数 → blocker 必须同时点名材料并断言它拿不到;点名后又说它无关是新的 dismissed-required-material
  • 禁用事实按原短语匹配(禁 CVSS 9.8,"9.8 on the CVSS scale" 零命中)→ 改成词集,全部词落在同一子句且未否定才算。

新增 --rescore-messages 离线重判。判分器 docstring 现在写明:机械规则证明不了笔记为真

升级

git pull && ./install.sh

不带 --force 不会碰你 Vault 里的 Templates/

v1.30.0 — 语义质量门禁

Choose a tag to compare

@Spc-jgs Spc-jgs released this 09 Aug 12:46
0d9843c

v1.30.0 是 evaluation-first 版本:新增可复现的 Web Capture、检索与有向链接质量门禁,不改变现有词法检索和链接算法。

  • Web Capture:12 个场景各运行 3 次,最终 36/36 重评分通过,0 个硬失败。
  • Retrieval:40 条查询基线;exact/alias/filtered 保持满分,semantic Recall@5 为 0.375,no-answer FP 为 0。
  • Directed Links:新增 16 个正向标签与 16 个同主题硬负例,不引入自动链接。
  • Release 验证:888 tests passed;Python 3.11、Python 3.14 与 Windows installer CI 全部通过。

后续语义候选必须把 8 个语义查询中的有效命中从 3 个提升到至少 5 个,同时保持稳定组、零误报、只读契约和 2 倍 P95 延迟预算。

v1.29.2 — 原文归档发布闭环

Choose a tag to compare

@Spc-jgs Spc-jgs released this 09 Aug 09:15
cd5ad5d

补齐 v1.29.0 已发布的原文归档能力在所有交付表面的入口和验证。

  • wheel 新增 obsidian-archive-source
  • installed doctor 覆盖 archive_source
  • Skill runner 与 wheel 增加仓库外黑盒验证
  • 中英文功能地图和完整 CLI 清单补齐入口
  • 不改变归档目录、哈希、双向链接或 --replace 契约

验证:868 tests;Python 3.11、Python 3.14、Windows CI 全绿。

v1.29.1 — 聚类词质量

Choose a tag to compare

@Spc-jgs Spc-jgs released this 07 Aug 06:58
6cf2f1b

一项修复:拥挤目录的聚类词不再把名额花在描述目录本身的词上。

Changed

  • 聚类词质量(#55)。 一个词要成为拆分候选,拆出去的和留下的都得能独立成夹 —— 原来只检查了前者。现在余数也必须过 CLUSTER_MIN_NOTES

    用余数而不是百分比是刻意的:不需要引入第二个魔法数字,直接复用已在用的阈值,而且随目录大小自动缩放 —— 7 篇里覆盖 6 篇和 200 篇里覆盖 172 篇都是 86%,但完全不是同一个决定。

    另外两条:词等于目录名的,不看比例直接剔除 —— 20-Learning/AI-Agent/ai-agent/ 是给目录改名,不是拆分,而余数规则漏得掉它(那个词覆盖 25/34,留下 9 篇,够建目录)。标题里若是某个已计数标签的连字符组成部分,也不单独上榜 —— aiagent 就是 ai-agent 数了两遍,而原来的守卫只挡「和整个标签同名」的情况。

    参考 Vault 上 20-Learning/AI-Agent 有 11 个达标词、只报 6 个,而那 6 个里有 4 个是噪音:目录名本身、目录名的两半、以及「文章」。两个真候选 llm-engineeringvibe-coding 被截断在名单之外。现在四个真候选标签全部进榜。

    10-Work/日报 现在什么都不报,这是诚实的答案 —— 四个各覆盖 30/30 的词描述的是这个目录,不是它里面的子主题;而 spring-boot(5/13)这样正当的大集群照常上报。剔除而不是排到最后:名额本身就是稀缺资源,被挤掉的正是真候选。

v1.29.0 — 原文存档、检索元数据过滤与连通性信号

Choose a tag to compare

@Spc-jgs Spc-jgs released this 07 Aug 06:14
015fbb7

1.28.0 之后累积的四项新增和两项修复。

新增里最大的一件是原文存档:剪藏的原文不再堆在笔记里,单独存放、双向挂钩、检索默认不碰。另一件是审计终于能分辨「没人读的笔记」和「找不到的笔记」

Added

  • 原文单独存档。 archive-source 把原文写进 95-Sources/<YYYY-MM>/,按源字节(不含 frontmatter)记 SHA-256,笔记和存档双向挂钩 —— 笔记多一个 source_archive 字段和一行可点击的入口,存档的 frontmatter 反向指回笔记。检索默认跳过 95-Sources/,用户问「原文到底怎么说的」时用 --scope 才进去;审计不拿笔记契约约束存档 —— 存档是证据,它的标题、标签和占位符属于原作者。

    Skill 此前对这件事没有任何概念,于是用户要一篇文章的原文时,Agent 自己编了个标题、把 35 KB 原文追加在 7.6 KB 的沉淀后面,占了文件的 82%。那篇笔记四分之一的检索引用因此落在作者的行文里而不是读者自己的知识上,BM25 的长度归一化又让沉淀部分掉了 20–30% 的分。归档之后,同样十二个查询没有一个再引用原文,笔记自己的章节排名持平或更好。

    逐字保真是被强制的而不是被期望的:存档按字节读写,所以换行符原样保留;哈希不含 frontmatter;非 UTF-8 的原文直接以 undecodable-source-content 拒绝而不是替你转码 —— 猜出来的编码不算证据。存档文件名在整棵树内唯一(不是只在当月目录内),因为笔记按裸 stem 链接而 Obsidian 是全库解析的。--replace追加第二份而不是替换:source_archive 一份时是字符串、多份时变列表,旧存档不会和它支撑的笔记失联。

  • 检索能按元数据过滤。 search-vault 新增 --type--tag(都可重复,flag 内 OR、flag 间 AND)和闭区间的 --after / --before,每条结果都带上 typedate

    词法排序回答不了「什么时候」和「哪一类」,而 CJK 分词会把 7月 切成互不相干的 token —— 参考 Vault 上「7 月的日报」把一篇 6 月写的笔记排在了 7 月日报前面,「最近的周报」把一份设计文档排在了周报前面。真正麻烦的不是返回空,是返回了错的东西却没有任何信号说明它错了。

    过滤是排序之前的硬约束,所以 score 的含义没有变。相对时间留给调用方:helper 只收 ISO 日历日,其他一律以 invalid-date 拒绝 —— 与其把一套双语日期文法加上「一周从哪天开始」的政策塞进一个靠确定性立身的 helper,不如让它就只做确定的事。笔记自己的 date 现在和过滤参数一样按真实日历日解析,不再是「长得像 ISO」就算数,所以第 13 个月不会被当成日期来排序。--tag 走审计同一套归一化,--tag springboot 能找到 spring-boot。新增拒绝码:invalid-dateinvalid-date-rangeinvalid-typeinvalid-tag

  • 过滤后的检索会说明自己过滤掉了什么。 响应新增 filters,含 appliedcandidatesmatched 和按维度拆分的 excluded,「压根没有 date 字段」和「date 落在范围外」分开计。没有它,过窄的过滤和空 Vault 长得一模一样,「这个条件没匹配到」会被当成「你没有关于这个主题的笔记」报给用户。

  • 审计能分辨「没人读的笔记」和「找不到的笔记」。 orphan-note 测的是可达性,它在索引齐全的 Vault 上接近于 0 是正确的:Folder Index 插件按目录内容生成列表,目录有索引就确实让目录内每篇笔记都翻得到。但这和知识有没有连起来是两回事。

    disconnected-note 报的是零入链零出链 —— 只报交集,因为任何单边都太吵且含义模糊,一篇被三处引用的概念笔记本来就不该有出链。周期性日志直接豁免:参考 Vault 上完全没有链接的 57 篇里,daily-reportweekly-report 占 36 篇,报它们会把真正值得看的 21 篇埋掉 —— 那 21 篇里有 14 篇是剪进来之后再没接上任何东西的剪藏。

    可达性是前置条件而不是假设:索引覆盖不到的笔记报 orphan-note,两个 finding 不会对同一篇笔记给出两套说法。链到 95-Sources/ 也不算数 —— 存档是笔记自己捕获的证据,归档一份原文不能把这条 finding 悄悄消掉。严重度是 informational,reference 里写明不要为了消掉它去编一条链接 —— 不相干的链接比没有更糟。

Fixed

  • 没有候选时 Agent 会编一个 wikilink。 deep-capture 契约要求 web-clip 必须带 ## 关联笔记 标题,而 note-creation.md 却说链接可以「skip them」—— 必填的段落没法 skip;同一句话还承诺 helper 会「列出目标目录的文件名」,等于把一份原始目录清单当作候选集递过去。结果是四篇毫不相干的笔记(Fluss 存储、SQL 优化器、RAG 流式、Zig Coding Agent)全都链到同一个文件,理由仅仅是它在 20-Learning/Backend/ 里排序第一 —— 而 suggest-links 对它们一个都没推荐过,其中一篇还明确返回了「没有建议」。

    现在:零候选是答案,不是待填的空缺;没有可信关联时用一行明确说明写满该段落;并且把「邻近」点名为不该链接的理由 —— 同目录、同类型、同大方向,每一条单独都足以否决。

  • 检索把脚手架当知识排。 EXEMPT_NAMES 一直声明 README.mdAGENTS.mdCLAUDE.md 是治理文件而不是笔记,但只有写入 Skill 知道这件事 —— 而 Vault 的 README 又长又提到每一个主题,是天然的词法磁铁。参考 Vault 上十二个真实问题、60 个 top-5 名额里有 11 个(18%)被非知识文件占据,光 README.md 就在其中一半里出现;问「洞察」时前三名是 README.mdAGENTS.mdINDEX.md,而 30-Insights 里 13 篇笔记全被挤出去。这个判断现在只有一份定义、两个 Skill 共享,实测噪音降到 4 个名额(7%),剩下的是 INDEX.md —— 它是导航性知识,本来就该在。被排除的文件计入 scanned.excluded,绝不报成 issues:脚手架不是格式有问题的笔记。