@oomkapwn/enquire-mcp 3.11.0-rc.9 → 3.11.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/CHANGELOG.md +196 -0
- package/README.ar.md +3 -3
- package/README.es.md +3 -3
- package/README.fr.md +6 -6
- package/README.hi.md +3 -3
- package/README.ja.md +5 -5
- package/README.md +6 -6
- package/README.pt.md +6 -6
- package/README.ru.md +6 -6
- package/README.zh.md +3 -3
- package/dist/bases.d.ts.map +1 -1
- package/dist/bases.js +19 -7
- package/dist/bases.js.map +1 -1
- package/dist/dql.d.ts.map +1 -1
- package/dist/dql.js +5 -2
- package/dist/dql.js.map +1 -1
- package/dist/embed-db.d.ts.map +1 -1
- package/dist/embed-db.js +2 -1
- package/dist/embed-db.js.map +1 -1
- package/dist/embed-pipeline.d.ts.map +1 -1
- package/dist/embed-pipeline.js +6 -1
- package/dist/embed-pipeline.js.map +1 -1
- package/dist/embeddings.d.ts +6 -0
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +5 -2
- package/dist/embeddings.js.map +1 -1
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +13 -0
- package/dist/feedback.js.map +1 -1
- package/dist/fts5.d.ts.map +1 -1
- package/dist/fts5.js +18 -12
- package/dist/fts5.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/name-fold.d.ts +39 -0
- package/dist/name-fold.d.ts.map +1 -1
- package/dist/name-fold.js +52 -0
- package/dist/name-fold.js.map +1 -1
- package/dist/parser.d.ts +44 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +111 -18
- package/dist/parser.js.map +1 -1
- package/dist/periodic.d.ts.map +1 -1
- package/dist/periodic.js +2 -1
- package/dist/periodic.js.map +1 -1
- package/dist/tool-registry.d.ts.map +1 -1
- package/dist/tool-registry.js +102 -13
- package/dist/tool-registry.js.map +1 -1
- package/dist/tools/meta.d.ts +4 -0
- package/dist/tools/meta.d.ts.map +1 -1
- package/dist/tools/meta.js +59 -17
- package/dist/tools/meta.js.map +1 -1
- package/dist/tools/read.d.ts +14 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +55 -14
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/search.d.ts +30 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +82 -11
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +95 -37
- package/dist/tools/write.js.map +1 -1
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +37 -4
- package/dist/vault.js.map +1 -1
- package/dist/wildcard-match.d.ts +41 -0
- package/dist/wildcard-match.d.ts.map +1 -1
- package/dist/wildcard-match.js +105 -0
- package/dist/wildcard-match.js.map +1 -1
- package/docs/COMPARISON.md +1 -1
- package/docs/api.md +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,202 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [3.11.0] — 2026-06-27
|
|
6
|
+
|
|
7
|
+
> **TL;DR:** **v3.11.0 STABLE — promoted `@rc` → `@latest` after 25 RCs (maintainer call on the ≥2-auditor gate, strongly met).** The v3.11.0 line delivers the **46th tool** `obsidian_mark_useful` (closed-loop retrieval feedback — opt-in `--feedback-weight`, default 0 = provable no-op; per-vault sidecar with relative paths + counts only, no content/query, prune-erased), **README in 9 languages** (en/zh/es/hi/ar/ru/pt/fr/ja), and **all 3 open dependabot majors closed** (`@types/node` 26, `actions/checkout` 7, `js-yaml` 4→5). The bulk of the line is a **multi-round external-audit hardening campaign** (rc.8→rc.25): a fresh from-scratch pre-promotion audit (proto-pollution in the feedback tool), then **five independent external-auditor passes** — the rc.9 NFC-tag class, the rc.13 dual-auditor catch of a symlink-escape I had introduced in rc.12, the rc.14 CodeQL polynomial-ReDoS class the project had been wrongly dismissing as "false positive" at 6+ sites (→ linear strips), the rc.17 self-refutation that found a HIGH wikilink/embed quadratic-ReDoS my own 5-agent re-challenge surfaced, and the **3-auditor rc.21 round** (Cursor+Composer, Goose+GLM-5.2, Minimax+Minimax-m3) that found the **line-terminator class** — `split("\n")`/`match(/\n/g)` splitting/counting only LF, defeating code-fence detection on CR/U+2028/U+2029 notes (write-path data-corruption MED) — closed at the root in rc.23/rc.24/rc.25 with `splitLines`/`countLineBreaks` + an inventory invariant. **Methodology wins this line:** the obligation-gated external-audit brief (committed under `docs/audits/`) made all 3 rc.21 auditors produce verifiable, cross-comparable reports — even the one that rubber-stamped the prior round did real work; the mandatory pre-promotion re-sweep caught a sibling of the audit-response class *after* 3 external audits. **The ≥2-independent-auditor promotion gate (v3.6.1 rule) is strongly met** — five+ external auditors with distinct methodologies across the line, the strongest two finding non-overlapping bugs. **46 tools · 19 prompts · 1427 tests · 0 CodeQL alerts · 0 open dependabot.** No API breaks (additive minor). CI publishes `@latest` + signed provenance (SLSA L2) + OIDC MCP-registry sync (stable-only). **Per-RC detail: rc.1→rc.25 entries below.**
|
|
8
|
+
|
|
9
|
+
## [3.11.0-rc.25] — 2026-06-27
|
|
10
|
+
|
|
11
|
+
> **TL;DR:** **Pre-promotion re-sweep found the line-terminator class's COUNTING sibling — LOW, closed before `@latest`.** Before flipping `@rc → @latest`, the mandatory post-merge re-sweep checked the rc.23/rc.24 audit-response code the 3 external auditors never graded (they graded rc.21) — and the rc.24 brief's own "does `splitLines` reach EVERY line-oriented sink?" obligation surfaced a sibling: **line-NUMBER math via `(text.match(/\n/g) ?? []).length` counts only LF**, so on a bare-CR / U+2028 / U+2029 note the reported `chat_thread_append` `line_start`/`line_end` and FTS breadcrumb line numbers drifted — the **counting form** of the rc.23 `split("\n")` class. rc.23 routed the *splitting* form through `splitLines` but my inventory invariant only patrolled `.split("\n")`, not `.match(/\n/g)` — the textbook scope-too-narrow defense. Fixed: a `countLineBreaks(text)` helper (`splitLines(text).length - 1`) routed through all 5 sites (`read.ts` chatThreadAppend ×2, `fts5.ts` breadcrumb ×3), and the **inventory invariant extended to flag `.match(/\n/g)` too** (+ a NEGATIVE control for the counting form) — so the next CR/LS/PS-blind line op of EITHER form fails CI. LOW (observability — wrong line numbers on rare CR/LS/PS notes, no corruption/loss); byte-identical for LF-only notes. **1427 tests unchanged** (the 2 inventory `it()`s extended in place). **Lesson (the one this session keeps teaching): the mandatory pre-promotion re-sweep earns its keep EVERY time — even right after 3 external audits + a class fix, the audit-response code (which the auditors never saw) contained a sibling of the very class it closed; an inventory invariant is only as complete as the SHAPES it enumerates (`.split` vs `.match`), so when you write one, enumerate every form of the operation, not just the one the bug appeared in.**
|
|
12
|
+
|
|
13
|
+
## [3.11.0-rc.24] — 2026-06-27
|
|
14
|
+
|
|
15
|
+
> **TL;DR:** **External rc.21 audit tail — 3 defense-in-depth LOW caps + a CI timing-flake widen (CLOSES the 3-auditor rc.21 cascade).** After rc.23 closed the line-terminator MED+LOW class, this RC clears the remaining confirmed-LOW findings, all re-verified against the code. **(1) LOW — `filter_frontmatter` value uncapped (Cursor LOW-2).** `obsidian_search`'s `filter_frontmatter` accepted arbitrary-length `z.string()` values; it runs only on the fused candidate pool (O(fanOutK), bounded — NOT the vault-wide DoS the rc.21 `frontmatter_search` cap closed), so this is defense-in-depth: `.max(MAX_FRONTMATTER_VALUE_LEN)` on both string arms of the value union. **(2) LOW — `frontmatter_set` value uncapped (Cursor LOW-3).** The `set` record's values were `z.unknown()` (write-gated single-note YAML materialization); added a `.refine()` bounding each stringified value to `MAX_FRONTMATTER_VALUE_LEN` (`null`/`undefined` delete-sentinel exempt). Both caps are pinned by the **`parser-input-cap-invariant` inventory** (extended with the two value-dimension entries + a generalized detector that already accepts the `.refine()` form). **(3) LOW — `feedback.ts:open` missing file-size guard (Goose FIND-2).** `open()` did `readFile`+`JSON.parse` with no `stat.size` check (inconsistent with `vault.ts:loadDiskCache`); a corrupt/hostile multi-GB sidecar would be parsed into memory. Added a `MAX_FEEDBACK_FILE_BYTES` (64 MiB) guard that fail-softs to an empty store before the read — operator-controlled (cache dir, not bearer-reachable), so defense-in-depth. **(4) INFO — CI timing-flake (Cursor INFO-1).** The `<500ms` "must not hang" ceilings in `wildcard-match.test.ts` (×3) + `security.test.ts` (×1) flaked under default-parallel `test:coverage` CPU contention (Cursor measured 528–1084ms isolated-pass) — the rc.20/rc.22 timing-flake class in a different file. Widened to **`<3000ms`** (immune to the observed load spike; a genuine ReDoS regression at these inputs is seconds-to-minutes, so the guard still fires). **+2 tests (1425 → 1427)** — `feedback.ts` file-size guard POSITIVE + NEGATIVE (a sparse over-cap file fail-softs to empty). **This CLOSES the 3-auditor external rc.21 cascade** (rc.23 line-terminator MED+LOW · rc.24 the 3 caps + timing). **All 3 auditors followed the new obligation-gated brief and produced verifiable, cross-comparable reports** — the redesign worked. **Lesson: the rc.21 frontmatter_search cap was an instance fix; its VALUE-dimension siblings (filter_frontmatter, frontmatter_set) lived one schema field over — the inventory invariant generalized to the `.refine()` form is what now patrols the whole class; and a `<500ms` "must not hang" ceiling is the same flake-blocks-release surface as rc.20/22 — widen to a ReDoS-catching-but-load-immune budget, never a tight one.**
|
|
16
|
+
|
|
17
|
+
## [3.11.0-rc.23] — 2026-06-27
|
|
18
|
+
|
|
19
|
+
> **TL;DR:** **External rc.21 audit response (3 independent auditors) — the LINE-TERMINATOR class: a write-path code-fence DATA-CORRUPTION MEDIUM + the read-path observability LOW, closed at the root with a shared `splitLines` helper + an inventory invariant.** Three external auditors (Cursor+Composer, Goose+GLM-5.2, Minimax+Minimax-m3) all graded rc.21 against the new obligation-gated brief and all independently surfaced the same root: **`text.split("\n")` splits ONLY on LF**, so a note saved with bare CR (classic-Mac) or with U+2028/U+2029 as the line SEPARATOR collapses to one "line". rc.17/rc.19 had papered the CRLF *symptom* by stripping a trailing `\r`/LS/PS from an LF-split line (`stripTrailingLineEnds`) — but the split itself never isolated the lines, so the strip and the split disagreed about what a line is. **Confirmed + fixed:** **(1) MEDIUM (data corruption, `--enable-write`-gated; Goose FIND-3) — `rewriteOutsideCodeFences` / `replaceStringOutsideCodeFences` code-fence blindness.** Both detect fenced code blocks per `split("\n")` line; on a CR/LS/PS note the whole body is ONE line, the fence regex `/^\s*(```|~~~)/` never fires, and a wikilink INSIDE a code fence is rewritten on `rename_note`/`replace_in_notes` — **empirically reproduced** (count=2, expected 1) and **fix-verified** (count=1 for every separator). **(2) LOW (observability; Cursor LOW-1 / Goose FIND-1 / Minimax F-1) — read-path line merging** in `extractHeadings`, `getOpenQuestions`, snippet line-numbers (`sliceSnippet`), FTS breadcrumbs, `collectTags`, `bodyStartLine` — a CR/LS-separated note's headings/questions/line-numbers were merged or mis-counted. **The fix (root, not instance):** a shared `splitLines()` in `wildcard-match.ts` that splits on the SAME set `stripTrailingLineEnds` strips — `\n` / `\r\n` / `\r` / U+2028 / U+2029 — routed through ALL 10 note-content split sites (write ×2, read ×5, meta, search, fts5, bases, parser). For an LF-only note it is **byte-identical to `split("\n")`** (no regression on the common path). **NEL (U+0085) / VT (U+000B) / FF (U+000C) are deliberately EXCLUDED** — neither `stripTrailingLineEnds` nor CommonMark/Obsidian treat them as line breaks, so splitting on them would diverge from how the note renders (correcting Goose's repro, which included them). **Structural defense:** `tests/line-terminator.test.ts` — a `splitLines` unit (incl. the NEL/VT/FF NEGATIVE control), behavioral code-fence + read-path tests with NEGATIVE controls reproducing the pre-rc.23 corruption, and an **inventory invariant** that fails CI on any raw `.split("\n")` in `src/` outside the allowlist (the helper + `eval.ts`'s internal JSONL) — so the next note-content split must use `splitLines`. **1416 → 1425 tests** (+9). **All 3 auditors followed the new obligation-gated brief** (filled the gating sink-trace table, re-ran the mandatory hostile-FS/offline probes through `dist/`, ran the named 3-point complexity curves) — the brief redesign worked: even Minimax (rc.17's rubber-stamper) did real work this round. **Lesson: rc.17/rc.19 fixed the CRLF *trailing-strip* but not the *split-set* — a textbook scope-too-narrow defense; the brief's named shape "CRLF + LS + PS as both line content AND line separator" is exactly what made all three auditors find the root. The durable close is one helper splitting on the project's own terminator set + an inventory invariant, not N per-site edits.** _(Remaining confirmed-LOW tail — `filter_frontmatter`/`frontmatter_set` value caps, `feedback.ts:open` file-size guard, the wildcard-match/security `<500ms` timing-flake ceilings — batched into rc.24.)_
|
|
20
|
+
|
|
21
|
+
## [3.11.0-rc.22] — 2026-06-26
|
|
22
|
+
|
|
23
|
+
> **TL;DR:** **The rc.20 timing-RATIO hardening itself flaked — re-fixed durably to an absolute floor on the OLD (slow) time + ships the comparison-ready external-audit brief for the v3.11.0 → `@latest` promotion gate.** **(1) TEST-INFRA [release-blocker, recursion of rc.20].** rc.20 converted the 6 wall-clock timing tests' NEGATIVE controls to `quad / linear > 8` ("ratio, robust to runner speed"). But on a docs-only PR's `coverage` job the wikilink dense-`[[a]]` NEGATIVE flaked: `quad/linear = 7.50 < 8`. Root cause: the **`linear` denominator is a sub-millisecond op whose wall-clock is noise-dominated on a contended CI runner** (read ~43 ms there vs ~2.5 ms locally), compressing the ratio below the threshold — the rc.20 fix moved the flake from the POSITIVE budget to the ratio's noisy denominator. Re-fixed durably: the 3 NEGATIVE controls (`wikilink-scan` ×2 + `write` ×1) now assert an **absolute FLOOR on the OLD catastrophic time** (`expect(quad).toBeGreaterThan(50)`) and drop the `linear` baseline entirely. Calibrated empirically: the old functions are ~325 ms (dense `\n`-scan), ~430 ms (lazy-regex unclosed run), ~830 ms (O(n²) slice+concat) on a dev laptop; **a CI runner is only SLOWER, so a 50 ms floor can only fail if the runner ran the quadratic 6–16× FASTER than a laptop (impossible)** — and load pushes the quad's time UP, never below the floor. The POSITIVE budgets (rc.20's generous `< 2000 ms` ceilings) were already robust and are unchanged. **(2) DOCS — comprehensive comparison-ready external-audit brief** (`docs/audits/AUDIT-REQUEST-FULL-FROM-SCRATCH-v3.11.0-rc.21-2026-06-26.md`, targeting the `v3.11.0-rc.21` tag) for the promotion gate. Redesigned from the rc.17 brief after a 3-report Workflow analysis of what 4 LLM auditors did well/badly: **obligation-GATING** tables (the per-always-on-tool sink-trace + the prior-finding re-probe cap the score at 2/5 if unfilled — so a "ship/clean" verdict is expensive to claim), **named worst-case shapes** (dense-CLOSED `[[a]]` run, not an unclosed one — the wrong probe 3 auditors used to re-bless the wikilink quadratic), a **wrong-probe 3-point complexity-curve self-check**, **evidence-or-downgrade** (no self-reported effort scalars; empirical requires a pasted harness + ≥3-point timing + stderr-clean run), a **severity rubric anchored to the real rc.18→20 outcomes** + reachability-proof, an **anti-anchoring novel-class tier**, and a fix to the rc.17 brief's own `40 src/*.ts` count drift (it's 33 top-level / 40 recursive). **No `src/` behavior change** (only the `VERSION` constant); **1416 tests unchanged** (3 NEGATIVE-control assertions rewritten in place). **Lesson: a RATIO timing test is only robust if BOTH terms are above the measurement-noise floor — dividing by a sub-ms baseline reintroduces the very flake the ratio was meant to kill; the durable form for a "the old code is slow" NEGATIVE control is an absolute floor on the OLD (genuinely-slow) time, which load can only push further from failing.**
|
|
24
|
+
|
|
25
|
+
## [3.11.0-rc.21] — 2026-06-26
|
|
26
|
+
|
|
27
|
+
> **TL;DR:** **Post-rc.20 re-sweep — 3 confirmed SIBLINGS of the rc.18 fix classes (the session's signature "instance fixed, adjacent sibling missed" pattern), each 3/3 adversarial-skeptic-confirmed.** A focused 4-lens behavioral re-sweep of the shipped rc.18→rc.20 commit (`41dc699`) hunting recursions of the four fix classes found them in adjacent code the rc.18 sweep didn't reach. **Fixed:** **(1) MED — `obsidian_frontmatter_search` value predicates uncapped (input-cap class, rc.18 sibling).** rc.13 capped the `key` arg but left `equals`/`contains` as `z.unknown()` — and the handler `JSON.stringify`s the predicate and string-compares it against EVERY note's frontmatter across the whole vault, so a multi-MB value is a bearer-reachable O(notes × valueLen) CPU/event-loop amplifier (measured ~3.9 s for a 4 MB `contains` over a 2k-note vault). This is the exact class rc.18 closed for `obsidian_full_text_search`, in the VALUE dimension the inventory missed. Fixed at two layers: a `.refine()` length bound (`MAX_FRONTMATTER_VALUE_LEN` = 8 KiB) rejects an over-cap predicate at the schema boundary, AND the per-note `JSON.stringify(arg)` is hoisted OUT of the loop (it is loop-invariant). The `parser-input-cap-invariant` inventory + its detector were extended to recognize the `.refine()` cap form (the value is arbitrary JSON, so `.max()` doesn't apply) and now pin both `equals`/`contains`. **(2) LOW ×2 — fold-offset in the read/snippet path (`replaceLineOnce` class, rc.18 sibling).** `semanticSearch` (bearer-reachable via the always-on `obsidian_search` TF-IDF arm) and `searchText` (`--diagnostic-search-tools`-gated) both computed a snippet offset with `text.toLowerCase().indexOf(term)` and then sliced the ORIGINAL `text` at that offset — but `toLowerCase()` is NOT length-preserving (`İ` U+0130 → `i̇`, 1 unit → 2; final-sigma; …), so a length-expanding fold char before the match drifts the offset, mis-centring the snippet window and miscounting the line number. Both now route through a shared `foldWithMap` / `foldedIndexOf` (search.ts) that returns offsets into the ORIGINAL string; a `tests/fold-offset.test.ts` unit + behavioral test + an anti-pattern inventory guard (no `toLowerCase().indexOf` offset feeds `sliceSnippet`) pins both sites. **Re-sweep verdict:** the CRLF-drop class was independently confirmed CLEAN (no uncovered sibling — the rc.17/rc.19 inventory guard is complete); `filter_frontmatter`'s uncapped value was checked and is NOT the same DoS (it runs only on the bounded fused candidate pool, O(limit × len), not a whole-vault scan). **1410 → 1416 tests** (+6 fold-offset; the cap is structurally pinned via the input-cap inventory, the hoist is behavior-verified by the existing `frontmatter-ops` equals/contains tests). **Lesson: the post-merge re-sweep is non-negotiable — rc.18 closed the input-cap class for `full_text_search` and the fold-offset class for `replaceLineOnce`, but each had a live sibling one module over (`frontmatter_search` value predicates; the read-path snippet offsets); the durable close for the fold-offset class is the shared offset-safe helper, and for the input-cap class the inventory invariant generalized to the `.refine()` form.**
|
|
28
|
+
|
|
29
|
+
## [3.11.0-rc.20] — 2026-06-26
|
|
30
|
+
|
|
31
|
+
> **TL;DR:** **Batch 3/3 (CLOSES the 4-way external rc.17 audit) — a release-blocking timing-test flake hardened to be CI-load-immune, plus the verified INFO/hygiene tail.** **Fixed:** **(1) TEST-INFRA [release-blocker] — wall-clock timing assertions flaked the rc.19 release.** The rc.19 publish failed at the release workflow's own `npm test`: a loaded CI runner missed a tight absolute `<150ms` budget in `wikilink-scan.test.ts` (the rc.18 dense-`[[a]]` linearity test) — the same flake-blocks-a-release class as rc.7 (`gh auth`) and rc.20-of-v3.9.0 (`npm ci`). The release recovered on a re-run (the flake is intermittent/load-dependent), but a transient must never gate a publish. Hardened all 6 wall-clock timing tests (`wikilink-scan` ×4 + `write` ×2): the big-input POSITIVE now uses a **generous absolute ceiling** (`<2000ms`; the op is ~14ms and a re-introduced O(n²) would be many seconds — so load can't trip it yet a regression still fails), and every NEGATIVE control became a **runner-speed-independent RATIO** (`old/new ≥ 8×`; both run on the same machine so absolute speed cancels — the rc.25 generative-fuzz robustness move applied to timing). **(2) INFO — lint strictness (Codex INFO).** `npm run lint` was `biome check` (exit 0 on warnings), so the "0 findings" bar was aspirational — 1 warning (`useTemplate` in a test) + 1 info (`noTemplateCurlyInString` in a deliberate source-sample string) were emitted but ignored. Cleared both (auto-fix the template-literal nit + a single-line `biome-ignore` with rationale on the intentional one) and switched the script to `biome check --error-on-warnings` so a future warning fails CI — the bar is now enforced, not just stated. **(3) INFO — 6 phantom CLI exempts pruned (Cursor LOW-3).** `scope-completeness-audit.mjs`'s `subcommandExempts` carried `--no-recurse`/`--lang`/`--max-pages`/`--scale`/`--pages`/`--out` — leftovers of a removed media subcommand that match NO current `cli.ts` flag (grep-verified absent), so they were dead cover that could silently exempt a future real flag of the same name. Removed; audit still reports "No gaps." **Re-verified + DOCUMENT-ACCEPTED (per the v3.5.14 rule, with reason):** **(a) Cursor INFO-1 `Vault.ensureExists` startup path in the throw** — NOT bearer-reachable (boot-only, before any transport binds; the path is the operator's own CLI input), so echoing it aids startup debugging and leaks nothing to a remote client; out of the rc.45/rc.49 abs-path-leak class scope (which sanitizes errors that reach bearer clients). **(b) Cursor INFO-2 nonce-`.tmp` crash-leftover** — the `wx` O_EXCL random-nonce tmp is not attacker-triggerable and a crash leftover is swept by the rc.36 `.tmp` eraser; precision-only. **(c) Cursor INFO-3 HNSW per-value validation** — self-DoS only (operator's own malformed index), already fail-soft→rebuild. **(d) Minimax LOW `isCatastrophicRegex` `.*X.*` detector gap** — neutralized at the SINK by the rc.39 worker wall-clock bound (`MAX_QUESTION_SCAN_MS`), the architectural class-ender; extending the static detector adds false-positive surface for zero residual risk (the rc.39 "bound the sink, don't chase shapes" rule). **No `src/` behavior change** (only the `VERSION` constant); **1410 tests unchanged** (6 timing assertions rewritten in place, no new `it()`). **This CLOSES the 4-way external rc.17 audit** (rc.18 wikilink-HIGH + FTS/replace MED×3 · rc.19 CRLF-MED + CI-gate LOW · rc.20 release-flake + INFO tail). **Lesson: a wall-clock timing test is a latent release-blocker — assert the algorithmic property as a same-runner RATIO (immune to load) and reserve absolute budgets for generous ceilings, never tight ones; and a strict-lint bar must be ENFORCED by the flag, not just claimed, or it silently rots.**
|
|
32
|
+
|
|
33
|
+
## [3.11.0-rc.19] — 2026-06-25
|
|
34
|
+
|
|
35
|
+
> **TL;DR:** **Batch 2/3 of the 4-way external rc.17 audit — line-terminator correctness + a CI-gate hardening, plus a re-verified rejection of the "cap all listMarkdown sites" recommendation.** **Fixed:** **(1) MED — `getOpenQuestions` CRLF-blind candidate match (Cursor MED-1, the 4th `(.+)$`-over-a-`split("\n")`-line site the rc.17 heading fix missed).** The open-questions matcher ran the default/caller pattern on raw `c.line` candidates built from `parsed.body.split("\n")`, so a CRLF (Windows) note's `Q: …` lines (trailing `\r`) never matched JS `(.+)$` (no `s`/`m` flag) → a Windows note's open questions were silently dropped from `obsidian_open_questions`. Strip the trailing line terminator when building `lineTexts` (covers BOTH the worker `matchLinesBounded` path and the inline default-pattern path). **(2) LOW — CI coverage-comment gate silent-skip (Cursor LOW-1).** The `// current X%` annotations in `check-per-file-coverage.mjs` were only validated by OIA Check 6, which runs in the artifact-less `oia` CI job (`existsSync`-gated → silent skip), so a drift could only be caught locally. Added a self-validation INSIDE `check-per-file-coverage.mjs` itself — it runs in the `coverage` CI job (which HAS `coverage-summary.json`), so a stale comment now fails CI where the data lives, with no workflow-scope change. Calibrated to a 2pp tolerance (matching the FLOORS margin — branch coverage is mildly environment-sensitive) and exempts the two integration-dep files (`embeddings.ts`/`ocr.ts`, whose branch% swings by model-download presence). _Note: Cursor's specific drift instance (embed-pipeline 85.41→86.95) did NOT reproduce in this environment (measured exactly 85.41%) — it's environment-dependent; the structural silent-skip is the durable finding, and the comment is accurate._ **Re-verified + REJECTED (with reason, per the v3.5.14 rule):** **Cursor MED-2 "10 uncapped `vault.listMarkdown()` sites"** — re-verified against the project's OWN documented CAP-vs-EXEMPT discipline (`tests/resource-bound-invariant.test.ts`, rc.36): of the 10 sites Cursor flagged, `findPath`/`getOpenQuestions` are already CAPPED, `lintWiki`/`paperAudit`/`buildTfidfIndex`/`readCanvas` are deliberately EXEMPT ("capping would silently corrupt an exhaustive/aggregation result, so a cap is the WRONG fix"), and the write-path sites (`renameNote`/`replaceInNotes` backlink rewrites) are `--enable-write`-gated exhaustive scans where truncation = silent data loss. **Capping them would introduce correctness regressions worse than the LOW DoS it prevents** — and the dangerous compounding that made Cursor rate it MED (per-note quadratic × N notes) was already removed by rc.18's wikilink O(n) fix. The resource-bound invariant already PASSES (no unclassified scanner), so the class is structurally closed. **1407 → 1410 tests** (+3). **Lesson: an auditor's "cap every whole-vault scan" looks right until you read the project's existing EXEMPT manifest — blindly capping exhaustive enumerators (backlinks, broken-link lints) silently corrupts results; per-item re-verification against the codebase's own discipline separates the real CRLF/CI findings from the over-broad cap recommendation. rc.20 closes the INFO/hygiene tail.**
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **MED — `src/tools/meta.ts` `getOpenQuestions` CRLF-blind candidate match.** `stripTrailingLineEnds` applied when building `lineTexts` so CRLF notes' open questions are matched (Cursor MED-1; the 4th line-terminator site after the rc.17 3 heading sites).
|
|
40
|
+
- **LOW — `scripts/check-per-file-coverage.mjs` coverage-comment self-validation.** Validates each `// current X%` against the summary it consumes, running in the `coverage` CI job — closes the OIA Check 6 artifact-less silent-skip (Cursor LOW-1). 2pp tolerance; integration-dep files exempt.
|
|
41
|
+
|
|
42
|
+
### Tests
|
|
43
|
+
|
|
44
|
+
- **`tests/crlf-heading.test.ts` (+3)** — a CRLF note's open question is found through the real `getOpenQuestions` (+ LF control) + an inventory guard that the `lineTexts` map routes through `stripTrailingLineEnds` (+ NEGATIVE control).
|
|
45
|
+
|
|
46
|
+
### Re-verified — rejected with reason (no code)
|
|
47
|
+
|
|
48
|
+
- **Cursor MED-2 (10 uncapped `listMarkdown` sites) → REJECTED.** Already governed by the rc.36 CAP-vs-EXEMPT `resource-bound-invariant`: the flagged sites are CAPPED, deliberately EXEMPT (capping corrupts exhaustive results), or `--enable-write`-gated exhaustive backlink scans. rc.18's wikilink O(n) fix removed the compounding that made it MED. No code change; capping would regress correctness.
|
|
49
|
+
|
|
50
|
+
### Audit response — rc.20 (INFO/hygiene tail, verified + queued)
|
|
51
|
+
|
|
52
|
+
- stale CLI exempts (Cursor LOW-3); `biome --error-on-warnings` (Codex INFO); `ensureExists` startup abs-path leak (Cursor INFO-1, not bearer-reachable); nonce-`.tmp` crash-leftover claim precision (Cursor INFO-2); HNSW per-value validation (Cursor INFO-3, self-DoS only); `isCatastrophicRegex` `.*X.*` detector gap, neutralized by the rc.39 worker sink-bound (Minimax LOW).
|
|
53
|
+
|
|
54
|
+
## [3.11.0-rc.18] — 2026-06-25
|
|
55
|
+
|
|
56
|
+
> **TL;DR:** **Batch 1/3 of the 4-way external rc.17 audit response — the 4 confirmed remote-DoS + write-path findings (1 HIGH + 3 MED) that made 3 of 4 auditors recommend "do not promote".** Four independent external auditors (Cursor+GLM-5.2, Goose+minimax-m3, MinimaxDesktop+m3, Codex+gpt-5.5-high) graded `1be598e` against the comparable-format brief; I re-verified every finding empirically (repro through compiled `dist/`). **The two strongest auditors found NON-OVERLAPPING real bugs — neither alone caught everything — a textbook validation of the ≥2-auditor gate** (Cursor's generative fuzz found the wikilink regression; Codex's registered-schema-introspection found the FTS/replace bugs). **Fixed:** **(1) HIGH — `scanWikilinkInners` quadratic regression (a bug rc.17 itself introduced).** My rc.17 wikilink rewrite replaced an O(n²)-on-unclosed-runs regex with a scanner that used the UNBOUNDED `text.indexOf("\n", innerStart)` — so on a DENSE single-line `[[a]]`-run with no newline (a real MOC/index note), the `\n` search rescanned to EOF every iteration → O(n²) again (measured 400k links = 8.2s; ~50s at the 5 MB cap, bearer-reachable via the always-on `obsidian_read_note` → `parseNote`). Fixed by scanning ONLY the bounded inner window `[innerStart, bracket)` (charCodeAt loop, byte-equivalent, O(n)) → 400k links now 14ms; the rc.17 timing test only exercised the unclosed-run shape so the regression slipped through (the recurring "the corpus can't produce the failing shape"). **(2) MED — `obsidian_full_text_search` query uncapped (Codex).** The opt-in FTS5 diagnostic tool registered `query`/`folder`/`tag`/`since` as bare `z.string()` (no `.max()`); a 4096-byte repeated-token `query` stalled SQLite FTS5 `MATCH` ~33s (bearer-reachable under `--persistent-index --diagnostic-search-tools`). The `parser-input-cap-invariant` inventory had omitted it. Capped all four + added the tool to the inventory. **(3) MED — `replace_in_notes` output-expansion DoS (Codex).** The per-line replace loop rebuilt the string with `slice + concat` per occurrence (+ a full `toLowerCase()` recompute every iteration in the case-insensitive branch) = O(n²) per line; a 5 KB single-match note + a 4096-byte replacement burned ~30s and projected to tens of MB, and `dry_run` never hit `writeNote`'s size cap. Rewrote the inner replace as a single-pass O(n) builder + added a projected-byte cap refused in BOTH apply and dry_run (n=5000 now 1ms; over-limit notes surface as an error, not a phantom success). **(4) MED — `replace_in_notes` case-insensitive Unicode corruption (Codex).** The case-insensitive branch found the match index in `line.toLowerCase()` but sliced the ORIGINAL line at that index; `String.toLowerCase()` is not length-preserving (`İ`→`i̇`, 1→2 units), so a match after an expanding char hit the wrong span (`İX`+search`x` wrote `İXY` instead of `İY`). The single-pass rewrite builds the folded line once with a per-folded-unit map back to the original `[start,end)` span → correct. **1398 → 1407 tests** (+9: wikilink dense-run regression + the unbounded-`\n` NEGATIVE control; replace Unicode-offset + differential-vs-old + linearity + projected-cap × both modes). **Verified/rejected the rest of the audits** (rc.19/rc.20 will close the LOW/INFO tail): Cursor's "62% scanner divergence" re-verified as benign adversarial-only (downgraded to INFO); Goose's "3 HIGH" collapse to the one wikilink root cause via 3 reachability paths; MinimaxDesktop's "ship it / 4.75" recommendation **rejected** — it missed every confirmed bug. **Lesson: my own rc.17 ReDoS fix shipped a fresh instance of the very class it closed (the project's signature recursion) — the durable close is a regression test that GENERATES the dense-run shape, not just the unclosed-run shape the original fix saw; and four auditors with different methodologies were needed because no single one found both the wikilink regression AND the FTS/replace class.**
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **HIGH — `src/parser.ts` `scanWikilinkInners` quadratic on a dense closed `[[a]]`-run.** Bound the newline search to the inner window `[innerStart, bracket)` instead of the unbounded `indexOf("\n", innerStart)` (rc.17 external audit — Cursor HIGH-1 + Goose FIND-1/3/4).
|
|
61
|
+
- **MED — `src/tool-registry.ts` `obsidian_full_text_search` uncapped inputs.** `.max(MAX_QUERY_LEN)` on `query`/`folder`, `.max(MAX_TAG_ARG_LEN)` on `tag`/`since`; the tool added to `tests/parser-input-cap-invariant.test.ts` (Codex RESOURCE-DOS-tool-registry-fts-query-cap).
|
|
62
|
+
- **MED — `src/tools/write.ts` `replace_in_notes` O(n²) per-line + unbounded materialization.** Single-pass O(n) `replaceLineOnce` builder; projected-byte cap refused in apply AND dry_run; dropped the unused retained `before` content (Codex RESOURCE-DOS-replace-in-notes-expansion).
|
|
63
|
+
- **MED — `src/tools/write.ts` `replace_in_notes` case-insensitive Unicode offset corruption.** The folded line is built once with a per-unit map back to original char spans; folded match indices resolve to whole original chars (Codex DATA-INTEGRITY-replace-in-notes-unicode-lower-index).
|
|
64
|
+
|
|
65
|
+
### Tests
|
|
66
|
+
|
|
67
|
+
- **`tests/wikilink-scan.test.ts` (+2)** — dense `[[a]]`×N (no newline) is O(n) (<150 ms at 2 MB) + a NEGATIVE control inlining the rc.17 unbounded-`\n` scan to prove it was quadratic.
|
|
68
|
+
- **`tests/write.test.ts` (+7)** — Unicode offset correctness (`İX`→`İY`) + a NEGATIVE control producing the old `İXY`; a case-sensitive differential vs the inlined pre-rc.18 loop; linearity POSITIVE + quadratic NEGATIVE control; projected-byte cap refusal in dry_run AND apply.
|
|
69
|
+
|
|
70
|
+
### Audit response — deferred to rc.19/rc.20 (verified, sequenced)
|
|
71
|
+
|
|
72
|
+
- **rc.19 (defense-in-depth):** CRLF-blind `getOpenQuestions` candidate match (4th line-terminator site; Cursor MED-1 + Goose/Minimax); 10 uncapped `vault.listMarkdown()` sites + a cap-coverage invariant (Cursor MED-2); CI `oia` job not running coverage → Check 6 silently skips + `embed-pipeline.ts` comment drift (Cursor LOW-1).
|
|
73
|
+
- **rc.20 (INFO/hygiene):** stale CLI exempts (Cursor LOW-3); `biome check --error-on-warnings` (Codex INFO); `ensureExists` startup abs-path leak (Cursor INFO-1); nonce-`.tmp` crash-leftover claim precision (Cursor INFO-2); HNSW per-value validation (Cursor INFO-3); `isCatastrophicRegex` `.*X.*` detector gap, bounded by the worker sink (Minimax LOW).
|
|
74
|
+
|
|
75
|
+
## [3.11.0-rc.17] — 2026-06-24
|
|
76
|
+
|
|
77
|
+
> **TL;DR:** **A "CLEAN" external rc.16 audit (0/0/0/0/0) was REFUTED by our own independent re-challenge — 2 real defects the auditor's class-driven sweep missed (1 HIGH + 1 MEDIUM), both empirically reproduced.** The project's own rule ("a single external auditor can miss the one finding that matters" — v3.6.1 ≥2-auditor gate) earned its keep again: a 3rd external auditor (goose) returned a fully clean audit + 3 optional-polish items, but a 5-agent verification Workflow (3 verify + 2 adversarial re-challenge of the freshest surfaces and the auditor's own §10 admitted blind spots) found two genuine bugs. **HIGH — wikilink/embed quadratic ReDoS** (`src/parser.ts` `WIKILINK_RE`/`EMBED_RE` + a byte-identical hand-copy at `src/tools/meta.ts:179`): the lazy `[^\]\n]+?` hunting the 2-char `]]` delimiter is O(n²) on an unclosed `[[`-run — each `[[` start re-scans to EOF. Measured 48 KB → 665 ms, 97 KB → 2.7 s, **195 KB → 10.7 s** (textbook quadratic; benign 780 KB of valid links → 17 ms), extrapolating to minutes-to-hours of single-threaded event-loop hang at the 5 MB file cap. **Reachable via the always-on, bearer-reachable `obsidian_read_note` → `parseNote` over adversarial note CONTENT** (the read threat-model treats note bodies as adversarial), and amplified through every whole-vault scanner (`communities`/`find_path`/`lint_wiki`). The rc.14 strip-idiom fix, the rc.39 worker sink-bound, and the rc.71 non-backtracking LIKE/glob matcher all closed OTHER regex sinks — this wikilink/embed sink was never enumerated (the project's signature "instance fixed, adjacent sibling missed"; and rc.67 even capped `validate_note_proposal.content` *because* it saw the `[[`-packed body but mis-attributed the cost to the suggestion-walk, never capping the worse `read_note` path). **MEDIUM — CRLF heading drop** (a regression rc.16 itself shipped): the rc.16 heading split `/^(#{1,6})\s+(.+)$/` returns `null` on a `\r`-terminated line (JS `.`/`$` without `s`/`m` won't match across a line terminator), so a CRLF/Windows note gets `headings: []` from `readNote(format:"map")`, loses section breadcrumbs in `obsidian_open_questions`, and loses heading enrichment in FTS5 (latent at `fts5.ts` since v3.5.8). The rc.16 "byte-identical" POSITIVE test corpus was LF-only and structurally could not produce the divergent CRLF shape — the recurring rc.36/rc.54 "differential corpus can't produce the failing shape" lesson. **3 optional items resolved:** #1 (7 "undocumented" CLI flags) = **FALSE POSITIVE** (all 7 ARE in api.md's Subcommands table; the auditor's regex only scanned the top-level flag table — and a code→docs guard `runCliFlagCoverageAudit` already exists since rc.4 and passes); #2 (`atomsOverlap` missing `u` flag) = **INFO, and the auditor's proposed fix is WRONG** — adding `u` would desync the detector from the `i`-only sink (`matchLinesBounded`), the exact rc.24 anti-pattern (overclaim #17); the real (bounded) gap is the ASCII-only probe alphabet, fully neutralized by the rc.39 worker wall-clock budget → deferred to rc.18 hardening; #3 (new producer-fold invariant) = **redundant file** (already gated by `name-fold-invariant`) but surfaces a real vacuous-on-deletion weakness in that guard → deferred to rc.18. **1385 → 1398 tests** (+13). **Lesson: a CLEAN external audit is precisely when to run the independent re-challenge — the auditor's class-driven ReDoS sweep and LF-only corpus were blind to the wikilink-delimiter shape and the CRLF terminator; the durable closes are a linear non-backtracking scanner (proven byte-equivalent by a differential vs the old regex) + a per-site inventory guard, not another shape-specific patch.**
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- **HIGH — wikilink/embed quadratic ReDoS — `src/parser.ts` (+ de-dup of the `src/tools/meta.ts` copy).** Replaced the lazy-quantifier regexes `/(?<!!)\[\[([^\]\n]+?)\]\]/g` and `/!\[\[([^\]\n]+?)\]\]/g` with a linear, non-backtracking `scanWikilinkInners(text, embed)` (each `]`/`[[`/`\n` visited at most once → O(n) for any input). `validateNoteProposal`'s byte-identical hand-copy now routes through the same exported scanner (closes the rc.10 `INLINE_TAG_RE` copy-class).
|
|
82
|
+
- **MEDIUM — CRLF heading drop — `src/tools/read.ts` (`extractHeadings`), `src/tools/meta.ts` (`getOpenQuestions`), `src/fts5.ts` (heading enrichment).** Strip the trailing line terminator with the new linear `stripTrailingLineEnds` (`\n`/`\r`/U+2028/U+2029) before the `/^(#{1,6})\s+(.+)$/` match, so CRLF notes keep their headings. `stripTrailingLineEnds` added to `src/wildcard-match.ts` (a `replace(/[\r…]+$/)` would itself be the polynomial-ReDoS class the static guard polices).
|
|
83
|
+
|
|
84
|
+
### Tests / structural defenses
|
|
85
|
+
|
|
86
|
+
- **`tests/wikilink-scan.test.ts`** (NEW) — DIFFERENTIAL (the scanner ≡ inlined copies of the pre-rc.17 regexes' `m[1]` sequence over a 26-case corpus incl. unclosed runs, lone brackets, newlines, empty `[[]]`, `!![[`, Unicode) + TIMING (2 MB unclosed `[[`-run < 100 ms; NEGATIVE control proving the old regex is quadratic at 40k) + a CLASS GUARD that no `src/` code reintroduces the lazy `[^\]\n]+?]]` wikilink shape (+ NEGATIVE control).
|
|
87
|
+
- **`tests/crlf-heading.test.ts`** (NEW) — `stripTrailingLineEnds` unit (+ NEGATIVE control proving the `(.+)$` match fails without it) + a behavioral test that a real CRLF note's headings come back from `readNote(map)` (was `[]`) with an LF control + an inventory guard that all 3 heading-exec sites strip line-ends (a 4th site fails CI).
|
|
88
|
+
|
|
89
|
+
### Reasoned verdicts on the auditor's optional items (no code this RC)
|
|
90
|
+
|
|
91
|
+
- **Optional #1 (7 undocumented CLI flags) → FALSE POSITIVE.** All 7 (`--embed-file`/`--embedding-model`/`--k`/`--limit`/`--per-query`/`--reranker`/`--skip-embeddings`) are documented in `docs/api.md`'s Subcommands table; the auditor's regex only scanned the top-level CLI-flags table. The `runCliFlagCoverageAudit` code→docs guard (rc.4) already covers this and passes.
|
|
92
|
+
- **Optional #2 (`atomsOverlap` `u` flag) → INFO; auditor's fix REJECTED.** Adding `u` would make the detector analyze a different semantic space than the `i`-only sink it guards (the rc.24 overclaim-#17 anti-pattern). The real ASCII-only-probe gap is bounded by the rc.39 worker sink-bound (no event-loop hang). Proper hardening (probe the atom's own char + widen the redos-fuzz generator alphabet) → **rc.18**.
|
|
93
|
+
- **Optional #3 (new `frontmatter-key-fold-invariant.test.ts`) → REDUNDANT file, real weakness.** The producer fold is already gated by `name-fold-invariant.test.ts`, but that guard is vacuous-on-deletion (absence-only) and misses the bases.ts single-key `fm.tags` shape. Strengthening it (positive presence assertion) → **rc.18**.
|
|
94
|
+
|
|
95
|
+
## [3.11.0-rc.16] — 2026-06-24
|
|
96
|
+
|
|
97
|
+
> **TL;DR:** **Pre-promotion prep-audit found (and closed) two genuine LOW siblings of already-closed classes — and caught one of my own audit subagents OVER-CLAIMING a "HIGH ReDoS" with timings that don't reproduce.** While grounding the fresh from-scratch external-audit brief (a 6-reader + critic inventory Workflow), the completeness-critic asserted a **HIGH** event-loop DoS in two heading-regex `.exec()` sites (`read.ts:239` extractHeadings, `meta.ts:1523` getOpenQuestions) using the combined `/^(#{1,6})\s+(.+?)\s*#*\s*$/` capture — claiming "1.5 s @ n=2000, 11.7 s @ n=4000". **Per the project's own rule (the rc.14 CodeQL lesson: prove it, never reason it), I empirically timed the worst-case shapes — and the claim does NOT reproduce:** the precise O(n²) theory shape (`# `+space×500k+`x`) runs in **0.4 ms**, and the worst real shape (`(# )`×250k = a 500 KB line) in **~12 ms** — mildly super-linear, bounded by `maxFileBytes`, **NOT** a multi-second DoS. So this is a **LOW** (defense-in-depth + cross-file consistency), not the HIGH the subagent claimed — an anti-overclaim record, this time against my own audit tooling. **But the underlying observation is still a real, cheap fix worth making:** `fts5.ts:796` already split this *exact* regex for CodeQL `js/polynomial-redos` (#13) back in v3.5.8, yet left the two siblings combined — the project's signature "instance fixed, sibling missed". rc.16 splits both to fts5's safe form (`^(#{1,6})\s+(.+)$` + `.trim()` + `stripTrailingHashes` + `.trim()`, byte-identical on every heading except a degenerate ATX-close-only `# ###` → empty text → skipped, matching fts5) + a static guard so no heading sink can reintroduce the combined shape. The prep-audit also surfaced a **second** genuine LOW: `obsidian_hyde_search` was the ONE always-on, bearer-reachable free-form query tool the rc.11 input-cap sweep missed — its `query` + `hypothetical_answer` (the string that gets embedded) were `z.string().min(1)` with no `.max()`. Both now capped to `MAX_QUERY_LEN` + added to the `parser-input-cap-invariant` inventory. **1381 → 1385 tests** (+4). No behavior change on valid input. **Lesson: an audit subagent is not exempt from the anti-overclaim rule — a claimed-HIGH with fabricated timings is exactly what "empirically time the worst-case shape" exists to catch; and `fts5` having split this regex while two siblings stayed combined is the canonical reason a class isn't closed until EVERY sink is enumerated.**
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
|
|
101
|
+
- **Heading-regex polynomial-ReDoS class — split the two remaining siblings (LOW, parity with `fts5.ts:796`).** `src/tools/read.ts` (`extractHeadings`, `obsidian_read_note format:map`) + `src/tools/meta.ts` (`getOpenQuestions`, `obsidian_open_questions`) replaced `/^(#{1,6})\s+(.+?)\s*#*\s*$/` with `^(#{1,6})\s+(.+)$` + `stripTrailingHashes(text.trim()).trim()` (the form `fts5.ts` has shipped since v3.5.8). Empirically the old form is only mildly super-linear (~12 ms at a 500 KB line, bounded by `maxFileBytes`) — a LOW, not the HIGH a prep-audit subagent over-claimed; fixed for defense-in-depth + to foreclose any future CodeQL flag + cross-file consistency.
|
|
102
|
+
- **`obsidian_hyde_search` input caps (LOW — uncovered sibling of the rc.11 input-cap class).** `query` + `hypothetical_answer` now carry `.max(MAX_QUERY_LEN)` (the rc.11 L1 sweep capped 5 other query tools but missed this one; `hypothetical_answer` is embedded → an uncapped multi-MB answer hit `embedder.embed()`, bounded only by the HTTP body cap).
|
|
103
|
+
|
|
104
|
+
### Tests / structural defenses
|
|
105
|
+
|
|
106
|
+
- **`tests/redos-trailing-strip.test.ts` (+4)** — a static guard asserting no heading sink (`read.ts`/`meta.ts`/`fts5.ts`) retains the combined `(.+?)\s*#*\s*$` capture (+ a NEGATIVE control proving the detector fires on the pre-rc.16 shape), a POSITIVE byte-identical-parity check of the split vs the old capture across a heading corpus, and the documented degenerate-`# ###`→empty edge.
|
|
107
|
+
- **`tests/parser-input-cap-invariant.test.ts`** — `obsidian_hyde_search.query` + `.hypothetical_answer` added to the always-on parser-input-cap inventory so a future drop of either cap fails CI.
|
|
108
|
+
|
|
109
|
+
## [3.11.0-rc.15] — 2026-06-24
|
|
110
|
+
|
|
111
|
+
> **TL;DR:** **External rc.14 re-audit response — the one new finding (AUD-NEW-1) re-verified as a FALSE POSITIVE; all 6 prior findings re-confirmed closed; 1 doc-accept, 2 reasoned rejections.** A third independent external audit (goose, read-only, on the rc.14 commit `29c912a`) empirically re-confirmed every rc.13 fix closed (AUD-01 symlink-escape, AUD-02/03/04/05) and surfaced **one** new item — **AUD-NEW-1 (LOW)**: a claim that `paperAudit` (`obsidian_paper_audit`) misses case-variant/NFD frontmatter citation keys (`ArXiv:`) because it `.toLowerCase()`s without NFC-folding, framed as a sibling of the rc.10/rc.12 H1 NFC-key class. **Re-verified EMPIRICALLY as a FALSE POSITIVE** (the project's own "prove it, never reason it" rule — the same rule the rc.14 CodeQL episode reinforced): the four target keys (`arxiv`/`doi`/`url`/`isbn`) are **pure ASCII**, so `nfcLower` (NFC + lowercase) is byte-IDENTICAL to `.toLowerCase()` for any key that could match them — an ASCII word has no distinct NFD form (`"Arxiv" === "Arxiv".normalize("NFD")` is `true`), and `.toLowerCase()` already covers every case variant (`ArXiv`/`ARXIV` → `arxiv` → matched). The auditor's own repro is also broken: its note has no `#paper` tag (→ never scanned) and even with the tag `Arxiv:` IS recognized → NOT flagged. Run through the real `paperAudit` code: all of `ArXiv:`/`Arxiv:`/`DOI:` are recognized (flagged=0). The non-bug is now **pinned as a contract test** so a future audit doesn't re-litigate it. **Accepted** the auditor's rec #2 (doc-only): the in-process `frontmatterGet`/`frontmatterSearch` trust their `key` length, while the MCP boundary caps it at `MAX_FRONTMATTER_KEY_LEN` (256, rc.13 AUD-04) — documented as a `@remarks` trust boundary. **Rejected** rec #3 (an `lstat(tmp).isSymbolicLink()` "belt" before the overwrite `rename()`): the rc.13 guard is a random-nonce tmp opened `wx`/O_EXCL — race-free by construction; an lstat-then-rename reintroduces the exact check-then-act TOCTOU rc.13 removed and adds false confidence, not security (the future-proofing is the existing AUD-01 behavioral test). **This RC also recovers the rc.14 ReDoS fix on npm** — the rc.14 tag's release was stranded by a GitHub transient (`ci.yml` never fired on the squash commit, so the release guard timed out waiting for CI), leaving `@rc` at rc.13 with the ReDoS fix merged-but-unpublished; rc.15 re-triggers a clean CI+release on a fresh squash SHA and carries the rc.14 linear-strip fix to npm. **1379 → 1381 tests** (+2). No code-behavior change (doc + test only). **Quality of this audit:** strong on re-verification (empirical probe table, all prior fixes confirmed), but its single new finding misapplied the NFC class to immutably-ASCII keys on a broken repro — a useful confirmation pass with no new actionable bug. **Lesson: re-verify EVERY auditor finding empirically AND for severity — a LOW "sibling of a known class" can be a false positive when the class's precondition (non-ASCII keys) doesn't hold; pin the rejection with a contract test, not just prose, so the next round can't re-raise it.**
|
|
112
|
+
|
|
113
|
+
### Changed
|
|
114
|
+
|
|
115
|
+
- **`src/tools/read.ts` (`frontmatterGet`, `frontmatterSearch`) — rec #2 trust-boundary `@remarks`.** Documented that these are in-process APIs that trust the `key` argument's length; the MCP boundary (`tool-registry.ts`) caps `key` at `MAX_FRONTMATTER_KEY_LEN` (256, rc.13 AUD-04) so a remote bearer client can't drive an unbounded multi-MB key through the per-note fold. Doc-only; no behavior change.
|
|
116
|
+
|
|
117
|
+
### Tests / structural defenses
|
|
118
|
+
|
|
119
|
+
- **`tests/lint.test.ts` — AUD-NEW-1 contract (+2).** A POSITIVE pinning that mixed-case/variant citation keys (`ArXiv:`/`Arxiv:`/`DOI:`) ARE recognized by `paperAudit` (flagged=0, since `.toLowerCase()` already handles ASCII case-folding and `nfcLower` would be byte-identical), and a NEGATIVE control (a `#paper` note with no citation key + no body identifier IS flagged) proving the test discriminates. Turns the reasoned false-positive rejection into a regression-guarded contract.
|
|
120
|
+
|
|
121
|
+
### Reasoned rejections (no code)
|
|
122
|
+
|
|
123
|
+
- **AUD-NEW-1 (LOW) → FALSE POSITIVE.** `.toLowerCase()` is already correct for the immutably-ASCII citation keys; NFC normalization is a no-op there; empirically every case/NFD variant is matched; the auditor's repro is broken (missing `#paper` tag). No `lookupFoldedKey` change needed (would be byte-identical churn). Pinned by the contract test above.
|
|
124
|
+
- **Rec #3 (lstat-before-rename belt) → REJECTED.** The rc.13 overwrite path is a random-nonce tmp opened `wx` (O_EXCL) → atomically refuses a pre-planted symlink; an `lstat(tmp)` before `rename()` is a classic check-then-act TOCTOU (the very pattern rc.13 removed) and gives false confidence. The correct future-proofing is the existing AUD-01 behavioral test, not a runtime belt.
|
|
125
|
+
|
|
126
|
+
## [3.11.0-rc.14] — 2026-06-24
|
|
127
|
+
|
|
128
|
+
> **TL;DR:** **CodeQL-surfaced HIGH ReDoS — a polynomial-regex class the project had been WRONGLY dismissing as "false positive" across 6+ sites.** The GitHub code-scanning page had one open `js/polynomial-redos` alert (HIGH) at `src/fts5.ts:526` — `opts.folder.replace(/\/+$/, "")` — with an in-code comment asserting it's a false positive ("the `$` anchor makes it O(n)"). **That reasoning is wrong, and empirically so:** the comment only considered all-slash input (genuinely O(n)); the polynomial trigger is `/`×n **followed by a non-slash** — the anchored `\/+$` retries from every slash position → **O(n²)**. Measured: `"/"×20k+"x"` = 173 ms, `×80k` = 2.5 s, `×160k` = **10.4 s**; the `folder` arg is **uncapped + bearer-reachable** on `serve-http`, so a 4 MB `folder` = a minutes-long event-loop hang for all clients. Worse, the **dismissed** alert list showed the team had marked the *identical* pattern at `embed-db.ts:564` (+ heading strips at `fts5.ts:794/797`) as "false positive" too — a systematically mis-triaged class. **Fix (class-wide, per the rc.39 "bound the sink, don't reason about ReDoS" rule):** a linear, non-backtracking strip in `wildcard-match.ts` (`stripTrailingSlashes`/`stripSurroundingSlashes`/`stripTrailingNewlines`/`stripTrailingHashes` over a `charCodeAt` loop, O(n) for ANY input) replaces the `replace(/<class>+$/, "")` idiom at **all 9 sites**: `fts5.ts` (folder prefix + heading `\s+$`/`#+$`), `embed-db.ts` (folder), `tools/search.ts` ×2 (folder), `tools/write.ts` ×2 (archive/replace folder), `periodic.ts` (`^\/+|\/+$`), `tools/read.ts` (`\n+$`). Adversarially re-verified: the fixed `stripTrailingSlashes` on `"/"×4 000 000 + "x"` runs in **0.60 ms** and is byte-identical to the old regex on a correctness corpus. **The open CodeQL alert auto-resolves on the next scan; the wrong "false positive" dismissals are now moot (code changed).** **1374 → 1379 tests** (+5). No API changes. **Lesson: a ReDoS "false positive" justified by *reasoning about the regex* is exactly what the project's own rule forbids — you EMPIRICALLY time the worst-case shape (`<class>×n + one non-class char`), you don't argue from the `$` anchor; and a CodeQL dismissal is a claim that must be empirically proven, not waved through. CodeQL caught what 13 RCs + 2 external audits this line had missed — the static-analysis gate earns its keep precisely on the class the human reasoning keeps mis-calling.**
|
|
129
|
+
|
|
130
|
+
### Fixed
|
|
131
|
+
|
|
132
|
+
- **CodeQL `js/polynomial-redos` #13 (HIGH) — folder/body trailing-run ReDoS class — `src/wildcard-match.ts` (new linear helpers) + `fts5.ts`, `embed-db.ts`, `tools/search.ts` ×2, `tools/write.ts` ×2, `periodic.ts`, `tools/read.ts`.** Replaced every `s.replace(/<class>+$/, "")` (polynomial on `<class>×n + non-class`) with an O(n) `charCodeAt`-loop strip. The bearer-reachable `folder` arg was the worst case (minutes-long hang at a few MB). Removed the incorrect "false positive / `$`-anchor makes it linear" code comments.
|
|
133
|
+
|
|
134
|
+
### Tests / structural defenses
|
|
135
|
+
|
|
136
|
+
- **`tests/redos-trailing-strip.test.ts`** (NEW, +5) — correctness parity (linear strip == old regex over a corpus); a timing-bound POSITIVE (4 MB worst-case shape < 100 ms); a NEGATIVE control proving the old `\/+$` regex is quadratic-slow at 20k (so the timing test discriminates); and a static guard that no folder/body sink reintroduces a `replace(/<class>+$/)` trailing-run regex (+ its own NEGATIVE control). The guard is tightened to the exact polynomial shape (single anchored `<class>+$`), excluding linear forms (`^<class>+`, `<class>+/g`, `#\d+$`).
|
|
137
|
+
|
|
138
|
+
## [3.11.0-rc.13] — 2026-06-23
|
|
139
|
+
|
|
140
|
+
> **TL;DR:** **dual external rc.12 audit response — 2 independent auditors, all 6 confirmed findings fixed (1 a security regression I introduced in rc.12).** Two independent external audits graded the rc.12 commit (`5fd3cda`): a state-driven re-verification pass (1 LOW) and a runtime-probe pass (2 MED + 3 LOW). I re-verified every finding against the code — **all 6 were real, zero hallucinations** — and the pair is a textbook case for the ≥2-auditor gate: the runtime-probe auditor found **AUD-01**, a symlink-escape regression my own rc.12 L-7 atomic-write fix introduced, which the re-verification auditor had explicitly "verified clean (a–f)" and missed. **Fixed (all 6):** **AUD-01 [MED, security] — `writeNote` overwrite followed a pre-planted `<note>.md.tmp` symlink.** The rc.12 fix wrote a deterministic `${abs}.tmp` with plain `writeFile` (which follows a symlink) and only lstat-checked the final target, not the tmp leaf — so an attacker who could drop `victim.md.tmp` as a symlink redirected the write out of the vault and replaced the note with a symlink. Now a **random nonce tmp name opened `wx` (O_EXCL)** — O_EXCL refuses to open an existing path (incl. a symlink) and the unpredictable name can't be pre-planted; adversarially re-verified (external file untouched, note stays a regular file). Also fixes the rc.12 stale-`.tmp`-blocks-overwrite footgun. **AUD-04 [MED, DoS] — `frontmatter_search`/`frontmatter_get`/`filter_frontmatter`/`frontmatter_set` keys were uncapped** → a multi-MB key × whole-vault scan × per-note `nfcLower(key)` = bearer-reachable CPU-DoS (measured ~9.5s for a 4 MB key). Added `MAX_FRONTMATTER_KEY_LEN=256` at all 4 key sites + extended `parser-input-cap-invariant`. **AUD-03 [LOW] + the embed-title sibling [LOW] — the frontmatter-key PRODUCER fold class:** rc.10/rc.12 folded the *consumer* (query) key reads but the tag/title *producers* read the key raw (`fm.tags ?? fm.tag` in parser/meta/write, `fm.tags` in bases, `frontmatter?.title` in embed-pipeline), so a case/NFC-variant `Tags:`/`Title:` was invisible to tag search / DQL / Bases / embedding context. New shared `lookupFoldedAny` helper routes all 5 producers through a folded key lookup + a narrow producer-fold structural guard. **AUD-05 [LOW] — `frontmatter_set` couldn't add a literal `__proto__` key** (`after[k]=v` hit the prototype setter, mis-reported `~__proto__`). Now null-proto maps + `Object.hasOwn` + `defineProperty` (aligns with the rc.61 serializer). **AUD-02 [LOW, CI-integrity] — the `coverage → OIA` gate was RED on a clean checkout:** the `src/embeddings.ts` per-file-coverage comment said `29.41%` but rc.12's L-2 test had raised it to `35.29%` (OIA Check 6 drift); synced. (rc.12 passed CI only because the cold-CI `oia` job skips Check 6 without a coverage artifact — the deeper CI-wiring is a maintainer-gated workflow follow-up.) **1365 → 1374 tests** (+9: AUD-01 symlink regression ×2, AUD-03 producer-fold ×3 + e2e, AUD-05 proto ×2, + producer-fold structural guard ×2). No API changes (additive caps + a new helper). **Lesson: my rc.12 L-7 write-fidelity fix shipped a fresh SECURITY regression (symlink-escape) — the exact "audit-driven fix recurs an adjacent class" pattern — and the re-verification auditor blessed it while the runtime-probe auditor (who actually pre-planted a hostile symlink) caught it. Two auditors with DIFFERENT methodologies is not ceremony; here it was the difference between a symlink escape shipping to `@latest` or not. A security fix needs an adversarial behavioral repro (which the re-verifier didn't run); the project's own self-fuzz rule now extends to "any fix touching the FS write path must be re-probed with a hostile pre-existing FS state."**
|
|
141
|
+
|
|
142
|
+
### Fixed
|
|
143
|
+
|
|
144
|
+
- **AUD-01 (MED, symlink-escape) — atomic `writeNote` nonce + `wx` — `src/vault.ts`.** Random tmp name opened exclusive-create (O_EXCL) so the overwrite can't follow a pre-planted `.tmp` symlink; `openSafe` gains an optional mode; `wx`/symlink/privacy/`overwrite=false` paths unchanged. Adversarially re-verified.
|
|
145
|
+
- **AUD-04 (MED, DoS) — `MAX_FRONTMATTER_KEY_LEN=256` — `src/tool-registry.ts`.** Capped `frontmatter_search.key`, `frontmatter_get.key`, `filter_frontmatter` record keys, `frontmatter_set` record keys.
|
|
146
|
+
- **AUD-03 + embed-title (LOW) — producer-side frontmatter-key fold — `src/name-fold.ts` (`lookupFoldedAny`) + parser.ts / bases.ts / tools/meta.ts / tools/write.ts / embed-pipeline.ts.** A case/NFC-variant `Tags:`/`Title:` key is now visible to tag retrieval + embedding context.
|
|
147
|
+
- **AUD-05 (LOW) — `frontmatter_set` literal `__proto__` round-trip — `src/tools/write.ts`.** Null-proto maps + `Object.hasOwn` + `defineProperty`.
|
|
148
|
+
- **AUD-02 (LOW, CI-integrity) — synced the stale `src/embeddings.ts` coverage comment — `scripts/check-per-file-coverage.mjs`** (29.41% → 35.29%, clearing the OIA Check 6 RED).
|
|
149
|
+
|
|
150
|
+
### Tests / structural defenses
|
|
151
|
+
|
|
152
|
+
- **`tests/rc13-dual-audit.test.ts`** (NEW) — AUD-01 symlink-escape regression (POSITIVE: external file untouched, note not a symlink; NEGATIVE: normal overwrite still works, no leftover tmp), AUD-03 producer-fold (POSITIVE: `Tags:`/`TAG:` recovered, e2e `list_tags` aggregates upper+lower; NEGATIVE: tag-less note), AUD-05 proto round-trip (POSITIVE: `+__proto__` persists, no pollution; NEGATIVE: normal key).
|
|
153
|
+
- **`tests/parser-input-cap-invariant.test.ts`** — +2 inventory entries (`frontmatter_search`/`frontmatter_get` `key` → `MAX_FRONTMATTER_KEY_LEN`), so a future frontmatter-key tool added without a cap fails CI.
|
|
154
|
+
- **`tests/name-fold-invariant.test.ts`** — +2: a narrow producer-fold guard (no raw `fm.tags ?? fm.tag` / `frontmatter?.title` in the 5 producers) + NEGATIVE control.
|
|
155
|
+
|
|
156
|
+
## [3.11.0-rc.12] — 2026-06-23
|
|
157
|
+
|
|
158
|
+
> **TL;DR:** **external re-audit (rc.11 report) response — re-verified every finding adversarially (workflow `w5fd5cs8x`, verify + skeptic per finding), shipped the 4 confirmed-real, reasoned-rejected the rest.** A user-supplied external auditor graded the rc.11 commit (`5953225`): 0 CRIT / 1 HIGH / 0 MED / 7 LOW / 8 INFO — and, to its credit, **downgraded its own prior-pass H-1** (claimed abs-path leak) to LOW after re-checking that the throw echoes the *user's* input, not the host path (we confirm: no leak). I re-verified each item against the actual code. **Fixed (4):** **H-2 [confirmed; severity LOW not the auditor's HIGH] — the 7th frontmatter-key-lookup site:** `lint_vault_wiki`'s stale pass read `frontmatter.last_reviewed` by RAW exact string (`meta.ts` didn't import `lookupFoldedKey`), so a case-variant `Last_Reviewed` key (the common Obsidian convention) silently fell back to mtime → a recently-reviewed note wrongly flagged stale. A genuine sibling of the rc.10 H1 NFC-key-fold class. Now routes BOTH spellings (`last_reviewed`/`last-reviewed`) through `lookupFoldedKey` (case+NFC-insensitive; the `_`/`-` spelling alias is preserved per the rc.9 narrowing) + a behavioral test (case-variant key + ancient mtime → not stale, + NEGATIVE control) + a narrow regression guard (no raw `parsed.frontmatter?.last_reviewed` access). **L-7 [confirmed MED, write-fidelity] — `writeNote` overwrite was non-atomic:** a crash/SIGKILL mid-`fs.writeFile` could truncate a note (`--enable-write`-gated). Now writes a sibling `.tmp` then `rename(2)`s over the target (same-dir → atomic; mirrors the cacheFile writer), **preserving the destination's existing mode** on overwrite (a brand-new path gets default perms; the auditor's suggested `0o600` would have wrongly locked down user notes — corrected). The `overwrite=false` exclusive-create (`wx`) path + symlink/privacy checks are unchanged. **L-2 [LOW] — offline enforcement test gap:** exported `applyOfflineEnv` + a mock-mod behavioral test asserting a transformers.js-shaped `{ env }` actually gets `allowRemoteModels=false` when offline (closes the "flag set but not wired" gap in the claimed-guarantee class) + a NEGATIVE no-op-when-online control. **I-1 [LOW] — `obsidian_replace_in_notes` (`--enable-write`) `search`/`replace` uncapped:** added `.max(MAX_QUERY_LEN)` (defense-in-depth, consistent with rc.11). **1359 → 1365 tests** (+6). No API changes (additive). **Rejected / reasoned (documented per the v3.5.14 rule):** **L-1 → FALSE POSITIVE** — the auditor's `obsidian_paper_audit.tag_filters` array field **does not exist** anywhere in the codebase (paper_audit has only `tag`/`folder`/`limit`); hallucinated. **L-5 → KNOWN-WAI** — the README "1365 unit tests" is the project's deliberate **source-`it()`** count (gated by `docs-consistency.test.ts`), not the data-driven runtime expansion (~1462); the identical "overclaim" was rejected on the Mavis HIGH in rc.35. **L-6 (PathEscapeError consistency)** — the auditor's own re-analysis confirms **zero host-path leak** (the throw echoes user input); a structured-error refactor across 14 read tools is consistency-only with no security value → ACCEPTED-as-is. **L-3 / L-4 / M-1 (test/detector tightening)** — DOCUMENT-ACCEPT: rc.4 already mutation-verified the feedback concurrency test; broadening the producer-completeness + abs-path-leak detectors increases false-positive surface for zero current risk (per the rc.39 "don't chase EDA-precise detection" rule). All 4 of the auditor's positive claims (NFC producers, K-3 `markUseful`, offline enforcement on both serve paths, SHA-pinned actions) were independently re-confirmed accurate. **Lesson: the auditor's headline HIGH (H-2) was a real, well-found sibling of a known class — but its severity was inflated (correctness on an opt-in convention with an mtime fallback, not HIGH) and 1 of its 7 LOWs was a hallucinated field; per-item adversarial re-verification (verify + skeptic) is what separates the genuine catch from the noise, and the skeptic caught a harmful detail in the auditor's own proposed fix (the `0o600` perm change).**
|
|
159
|
+
|
|
160
|
+
### Fixed
|
|
161
|
+
|
|
162
|
+
- **H-2 (LOW) — `lint_vault_wiki` stale-pass frontmatter-key fold — `src/tools/meta.ts`.** Read `last_reviewed`/`last-reviewed` via `lookupFoldedKey` (the 7th, previously-unwired site of the rc.10 H1 key-fold class) so a case/NFC-variant property resolves instead of silently falling back to mtime. Stale js-yaml@4→@5 comment refreshed (dates load as strings now).
|
|
163
|
+
- **L-7 (MED) — atomic `writeNote` overwrite — `src/vault.ts`.** Sibling `.tmp` + `rename(2)` (same-dir, atomic) replaces the direct `fs.writeFile`, so a crash mid-write can't truncate the note; the existing file's mode is preserved on overwrite. `--enable-write`-gated; `wx` exclusive-create + symlink/privacy checks unchanged.
|
|
164
|
+
- **L-2 (LOW) — offline-enforcement wire-up test — `src/embeddings.ts` (export `applyOfflineEnv`).**
|
|
165
|
+
- **I-1 (LOW) — `obsidian_replace_in_notes` `search`/`replace` `.max(MAX_QUERY_LEN)` — `src/tool-registry.ts`.**
|
|
166
|
+
|
|
167
|
+
### Tests / structural defenses
|
|
168
|
+
|
|
169
|
+
- **`tests/lint.test.ts`** — +2: case-variant `Last_Reviewed` + ancient mtime → not stale (POSITIVE) + a no-key NEGATIVE control (isolated temp vaults so the shared fixture's bucket counts are untouched).
|
|
170
|
+
- **`tests/name-fold-invariant.test.ts`** — +2: a narrow regression guard that the meta.ts staleness key read routes through `lookupFoldedKey` (NOT a broad `frontmatter.<ident>` detector — that would over-flag per the rc.11-audit L-4 verdict), with a NEGATIVE control proving it fires on the pre-rc.12 access shape but not on the legit output-label string.
|
|
171
|
+
- **`tests/embeddings-offline.test.ts`** — +2: `applyOfflineEnv` mutates a mock transformers env when offline (POSITIVE) + no-op when online (NEGATIVE).
|
|
172
|
+
|
|
173
|
+
## [3.11.0-rc.11] — 2026-06-23
|
|
174
|
+
|
|
175
|
+
> **TL;DR:** **rc.9-audit LOW/INFO tail — closes the backlog (defensive DoS caps + a write-linter UX gap), with every remaining finding reasoned to a verdict.** After rc.10 shipped the two HIGHs (M1 producer-NFC + H1 key-fold), this RC clears the rest of the rc.9 external audit. **L1 (LOW, defense-in-depth) — free-form query/tag inputs were uncapped:** the HTTP transport already bounds the request body, but `obsidian_search` / `obsidian_context_pack` (`query`), the diagnostic `obsidian_search_text` (`query`), and `obsidian_paper_audit` (`tag`) had no per-field `.max()`, so an absurd string reached a per-note tokenize/`.toLowerCase()` scan before any bound. Added `.max(MAX_QUERY_LEN=4096)` / `.max(MAX_TAG_ARG_LEN=256)` at the schema boundary (mirrors `MAX_DQL_QUERY_LEN` / `MAX_QUESTION_PATTERN_LEN`) and **extended `tests/parser-input-cap-invariant.test.ts`** (3 new always-on inventory entries) so a future query-fed tool added without a cap fails CI. **L2 (LOW) — `searchText` whole-vault scan uncapped:** added `capScanEntries(…, "obsidian_search_text")` (parity with `findSimilar`/`validateNoteProposal`); the tool is `--diagnostic-search-tools`-gated so this is defense-in-depth, not a default-surface fix. **L4 (LOW, write-linter UX) — `validate_note_proposal` hid the `coerced` signal:** a valid-YAML-but-non-mapping frontmatter (bare scalar / sequence) parses green but `frontmatter_set` REFUSES it (rc.64); the validator now surfaces `yaml.coerced` + a `frontmatter-non-mapping` warning so an agent isn't surprised by a later refusal after a clean validate. **1358 → 1359 tests** (+1: the L4 behavioral test with a NEGATIVE mapping control). No API changes (additive response field + tighter input bounds). **Reasoned-to-verdict (no code change, documented so they're not re-flagged):** **L3 (chmod, multi-user host)** — the feedback sidecar is `0600` + per-write chmod (rc.4); a shared-host adversary is outside the single-user local-vault threat model → ACCEPTED. **L5 (feedback concurrency test)** — rc.4 already reworked it to assert zero tmp-rename collisions (mutation-verified) → ALREADY_OK. **I6 (name-fold detector inverse shape)** — the "inverse" `nfcLower(s.replace(/^#/,""))` pattern is functionally CORRECT (NFC + case-fold); the detector deliberately flags only the BUG shape (strip-then-lowercase WITHOUT NFC), and flagging correct-but-non-canonical code would be a false positive (per the rc.39 "accept the tradeoff, don't chase EDA-precise detection" rule) → WON'T-FIX. **M2 / M3 / I2 / I3** confirmed ALREADY_OK in rc.10's re-verification (backlink rewrite folds via `findBestMatch`; `clear-cache` erases `.tmp` since rc.36; HNSW reader/writer are both synchronous → no interleave). **This CLOSES the rc.9 external-audit cascade (2 HIGH rc.10 + 3 LOW rc.11, all other findings reasoned-rejected or ALREADY_OK).** **Lesson:** the DoS-cap LOWs were genuine-but-bounded (body cap upstream, gated/bearer-auth), so the proportionate fix is a declarative `.max()` + the inventory invariant that keeps the next query-fed tool honest — not a behavioral rewrite; and a LOW finding that's actually ALREADY_OK still earns a written verdict so the next audit doesn't re-litigate it.
|
|
176
|
+
|
|
177
|
+
### Fixed
|
|
178
|
+
|
|
179
|
+
- **L1 (LOW, defense-in-depth) — free-form query / tag input caps — `src/tool-registry.ts`.** `MAX_QUERY_LEN`=4096 on `obsidian_search` / `obsidian_context_pack` / `obsidian_search_text` `query`; `MAX_TAG_ARG_LEN`=256 on `obsidian_paper_audit` `tag`. Fails an absurd input at the schema boundary before a per-note scan, above the HTTP body cap.
|
|
180
|
+
- **L2 (LOW) — `searchText` scan cap — `src/tools/search.ts`.** `capScanEntries(await vault.listMarkdown(folder), "obsidian_search_text")` bounds the whole-vault read+tokenize loop (parity with `findSimilar`); the tool is `--diagnostic-search-tools`-gated.
|
|
181
|
+
- **L4 (LOW) — `validate_note_proposal` surfaces `yaml.coerced` — `src/tools/meta.ts`.** A valid-YAML-non-mapping frontmatter now reports `yaml.coerced: true` + a `frontmatter-non-mapping` warning (matching the `frontmatter_set` refusal behavior from rc.64), so a green validate doesn't mislead about a later set.
|
|
182
|
+
|
|
183
|
+
### Tests / structural defenses
|
|
184
|
+
|
|
185
|
+
- **`tests/parser-input-cap-invariant.test.ts`** — +3 always-on inventory entries (`obsidian_search`, `obsidian_context_pack`, `obsidian_paper_audit`) so the L1 caps are CI-pinned; a future query/tag-fed always-on tool added without a `.max()` fails here. (The detector's NEGATIVE control already proves it isn't vacuous.)
|
|
186
|
+
- **`tests/tools.test.ts`** — +1: `validate_note_proposal` surfaces `yaml.coerced` + the `frontmatter-non-mapping` warning for a bare-scalar frontmatter, with a NEGATIVE control proving a normal key/value mapping is NOT coerced and gets no warning.
|
|
187
|
+
|
|
188
|
+
## [3.11.0-rc.10] — 2026-06-23
|
|
189
|
+
|
|
190
|
+
> **TL;DR:** **rc.9-audit response — the NFC class TRULY closed across the PRODUCER and KEY surfaces (2 HIGH).** A second external audit on the rc.9 commit (re-verified here by a 32-agent adversarial Workflow, 3 skeptics/finding) confirmed all 3 of my rc.9 downgrades — and correctly disputed the rc.9 L-TAG-1 "closed" claim as a **partial fix**: rc.9 hardened the tag *consumer* (compare) side but the *producer* (extraction) side and the frontmatter *key* side were uncovered siblings. **M1 (HIGH) — producer tag regexes dropped `\p{M}` combining marks:** on macOS APFS an NFD inline `#café` (`cafe`+U+0301) was captured as `cafe` BEFORE the rc.9 `nfc()` ran (normalize-after-extraction is too late), corrupting the **persisted FTS5 tag column** and defeating the exact dedup rc.9 added. Three producers — `parser.ts:170` (`TAG_RE`), `tools/meta.ts:221` (byte-identical copy), `bases.ts:652` (ASCII-only — dropped *every* non-Latin tag) — plus the `search.ts` TF-IDF tokenizer (LOW sibling). Fixed by **NFC-normalizing the text BEFORE matching** at every producer (composes the mark into the base letter so the regex captures the full token, and recovers any future combining mark), deduping the copy into a shared exported `INLINE_TAG_RE`, and making the Bases regex Unicode-aware (`\p{L}` + `u`). **H1 (HIGH) — frontmatter KEY lookups not case/NFC-folded:** rc.9 folded the *value* side but `frontmatter[key]` / `key in frontmatter` stayed exact-string at 6 sites (`search` filter_frontmatter, `dql` resolveField, `bases` `==`/`contains`, and `read.ts` `frontmatter_get` + `frontmatter_search` — the last two the auditor itself missed), so a `Status` filter silently missed `status` and an NFC key missed an NFD-on-disk key (Obsidian properties are case-insensitive). Fixed with a shared `lookupFoldedKey` that folds at LOOKUP time (exact-match wins, first-own-key-wins on a fold collision; never destructive at parse time → write fidelity preserved). **Structural defenses (the "improve the audits" ask):** a producer-completeness invariant (behavioral NFD/non-Latin extraction + a static "no ASCII-only `#[A-Za-z]` tag regex" gate) and a key-lookup invariant (`lookupFoldedKey` precedence/collision/NFC units) — converting "did we NFC-normalize at every text-INGEST + identity-KEY boundary?" into self-checking gates (the rc.9 invariant only patrolled the compare SITES). **1350 → 1358 tests** (+8). No API changes. **Re-verified ALREADY_OK (auditor wrong / no action):** M2 (renameNote backlink rewrite already folds via `findBestMatch`/`foldName`), M3 (`clear-cache` already erases `.tmp` — `clearDiskCache` has looped it since rc.36). DoS-cap LOWs (L1/L2: uncapped `search_text` query + `searchText` scan) → rc.11. Documents the rc.9 L-TAG-1 partial as overclaim instance.
|
|
191
|
+
|
|
192
|
+
### Fixed
|
|
193
|
+
|
|
194
|
+
- **M1 (HIGH) — NFC tag PRODUCER class — `src/parser.ts`, `src/tools/meta.ts`, `src/bases.ts`, `src/tools/search.ts`.** Every inline-tag extractor now NFC-normalizes the body BEFORE applying the regex, so an NFD accented tag composes to its base letter and is captured whole (not truncated at the combining mark). `parser.ts` exports a shared `INLINE_TAG_RE`; `meta.ts` imports it (was a byte-identical copy); `bases.ts` is now Unicode-aware (`\p{L}` + `u`, was ASCII-only). The TF-IDF tokenizer (`search.ts`) NFC-normalizes before tokenizing (symmetry between an NFD body and an NFC query). **On-disk FTS5 indexes built before rc.10 hold the corrupted `cafe`; a reindex (re-run `build-embeddings`/`index` or let the watcher re-touch) heals them — newly-indexed notes self-heal.**
|
|
195
|
+
- **H1 (HIGH) — frontmatter KEY fold — `src/name-fold.ts` (`lookupFoldedKey`) + `search.ts`, `dql.ts`, `bases.ts` (×2), `read.ts` (×2).** Case/NFC-insensitive frontmatter key resolution at lookup time, matching Obsidian's case-insensitive property semantics; exact-key wins, first-own-key wins on a fold collision; the raw key map is never mutated (parse-time fold would corrupt write round-trips). `read.ts` `frontmatter_get`/`frontmatter_search` were the two sites the external audit missed.
|
|
196
|
+
|
|
197
|
+
### Tests / structural defenses
|
|
198
|
+
|
|
199
|
+
- **`tests/name-fold-invariant.test.ts`** — +8: a **producer-completeness** invariant (behavioral: `extractInlineTags` recovers an NFD `#café` + a non-Latin `#日本語`; static: no `src/` tag regex uses an ASCII-only `#[A-Za-z]` lead, + NEGATIVE control) and a **key-lookup** invariant (`lookupFoldedKey` case-insensitive / NFC-insensitive / exact-wins / collision-first-wins / absent). These close the why-missed: the rc.9 invariant patrolled fold-*helper* usage at compare sites and was structurally blind to the producer regex char-class AND the key-lookup path.
|
|
200
|
+
|
|
5
201
|
## [3.11.0-rc.9] — 2026-06-23
|
|
6
202
|
|
|
7
203
|
> **TL;DR:** **External re-audit response (rc.5 report, re-verified against HEAD) — the NFC-tag class + its uncovered value siblings, closed structurally.** An external auditor reviewed the rc.5 commit (0 CRIT / 0 HIGH / 1 MED / 1 LOW / 3 INFO). Per the project's per-item re-verification discipline I re-verified every finding against current HEAD with a 4-lens adversarial Workflow (re-verify + concurrency / full-NFC / semantic-fallback sweeps, 3 skeptics per verdict, 20 agents). Outcome: **1 LOW confirmed real and EXPANDED, 1 MED + 2 INFO downgraded with reasons, + 4 new siblings the auditor missed.** **L-TAG-1 (LOW, real):** tag comparisons lowercased WITHOUT NFC across ~13 sites — an accented `#café` tag stored NFD (macOS) silently failed to match its NFC query form. The rc.46 NFC name-fold class was never generalized to the parallel TAG identity surface (three hand-rolled `#`-strip-then-lowercase helpers bypassed `foldName`). Closed via a shared `foldTag`/`nfcLower`/`nfc` in `name-fold.ts` routed through every tag + frontmatter-value comparison (parser producer, meta/read/write/search/dql/bases), **plus the name-fold inventory invariant extended with the tag signature** (the why-missed root cause: its detector matched only the `.md`-strip shape) + behavioral NFD→NFC tests (dql `FROM #tag`, bases `tag ==`/`status ==`). **New siblings (auditor missed):** DQL `FROM #tag` was NFC-blind even though rc.8 fixed the `WHERE`-value path in the same file; `obsidian_search` `filter_frontmatter` value match + `bases.ts` frontmatter `==`/`contains` were NFC-blind (rc.8 DQL-value siblings on other surfaces). **Rejected/downgraded (documented):** **T-MED-1 (MED → FALSE POSITIVE)** — `applyDiff`/`syncHnswForFile` are fully synchronous, so cross-file watcher events CANNOT interleave (JS run-to-completion); the auditor's proposed mutation queue would be redundant. Instead pinned the real property with `tests/hnsw-sync-critical-section.test.ts` (no `await` in the critical section) + a TSDoc contract. **I-HNSW-1 (INFO → WAI)** — `capacity()` has zero production callers; fixed the fallback to an honest `Infinity` sentinel anyway. **I-COV-1 (INFO, mechanism FALSE)** — the spawned `dist/` child is NOT coverage-instrumented (vitest v8 uses the inspector, not `NODE_V8_COVERAGE`); but re-verification surfaced a *real* adjacent risk (coverage CI job `timeout-minutes: 5` vs the test job's 10 while doing more work) → bumped to 10. Plus stale `peek*Meta` "throws only on…" TSDocs corrected (never-throw since rc.33/34). **1340 → 1350 tests** (+10). No API changes. **⚠️ Scope correction (post-ship, rc.9 external audit):** this closed the NFC-tag class on the CONSUMER side (the fold helpers + comparison sites) — a follow-up external audit on this commit confirmed that but found the class is only PARTIALLY closed: the tag-extraction regex (`parser.ts`/`meta.ts`/`bases.ts`) drops `\p{M}` combining marks, so an NFD inline `#café` is captured as `cafe` BEFORE folding (the producer-side leg), and frontmatter KEYS remain NFC/case-blind (HIGH H1). Full producer-side closure + the frontmatter-key fix land in **rc.10** (the rc.9-audit response). The "closed structurally" phrasing above is scoped to the consumer side.
|
package/README.ar.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**كُفّ عن إعادة شرح السياق لـ Claude وCursor وChatGPT وCodex وOpenClaw في كل جلسة. تصبح ملاحظاتك في Obsidian ذاكرةً مشتركةً قابلةً للبحث عبر كل وكيل متوافق مع MCP — معرفتك، وكل نموذج، وتظل ملكاً لك إلى الأبد.**
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/@oomkapwn/enquire-mcp)
|
|
14
|
-
[](./STABILITY.md)
|
|
15
15
|
[](https://slsa.dev/spec/v1.0/levels#build-l2)
|
|
16
16
|
[](https://modelcontextprotocol.io/)
|
|
17
17
|
[](./LICENSE)
|
|
@@ -54,7 +54,7 @@ claude mcp add obsidian -- npx -y @oomkapwn/enquire-mcp serve --vault ~/Document
|
|
|
54
54
|
> 3. **صفر استدعاءات سحابية أثناء التشغيل.** النماذج مخزّنة محلياً (تنزيل لمرة واحدة من HuggingFace). محتوى مكتبتك لا يغادر جهازك أبداً. آمنة للعمل المعزول (air-gap) افتراضياً.
|
|
55
55
|
> 4. **استدعاء واعٍ بالحداثة.** تُبلّغ كل نتيجة عن عمر الملاحظة؛ وإعادة الترتيب الاختيارية بالحداثة تتيح للوكيل تفضيل المعرفة الحديثة ووسم الحقائق القديمة لإعادة التحقق — حدود "الوعي بالنسيان"، مبنيّة على `mtime` الذي تملكه ملفاتك أصلاً.
|
|
56
56
|
|
|
57
|
-
**46 أداة · 19 موجِّه MCP ·
|
|
57
|
+
**46 أداة · 19 موجِّه MCP · 1427+ اختبار وحدة · 50+ لغة · إصدار مستقر v3.11.x · مُقيَّد بالـ semver · MIT · إثبات بناء npm (SLSA L2).**
|
|
58
58
|
|
|
59
59
|
</div>
|
|
60
60
|
|
|
@@ -245,7 +245,7 @@ enquire-mcp doctor --vault <path> # color-coded ✓/⚠/✗ health check
|
|
|
245
245
|
|
|
246
246
|
<div dir="rtl" align="right">
|
|
247
247
|
|
|
248
|
-
القناة: `npm install @oomkapwn/enquire-mcp` ← أحدث إصدار مستقر (`@latest` = v3.
|
|
248
|
+
القناة: `npm install @oomkapwn/enquire-mcp` ← أحدث إصدار مستقر (`@latest` = v3.11.x). الإصدار التجريبي: `npm install @oomkapwn/enquire-mcp@rc` (أحدث مرشّح إصدار). سجل التغييرات الكامل في **[CHANGELOG.md](./CHANGELOG.md)** · خارطة الطريق في **[ROADMAP.md](https://github.com/oomkapwn/enquire-mcp/blob/main/ROADMAP.md)**.
|
|
249
249
|
|
|
250
250
|
</div>
|
|
251
251
|
|
package/README.es.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**Deja de reexplicarle el contexto a Claude, Cursor, ChatGPT, Codex y OpenClaw en cada sesión. Tus notas de Obsidian se convierten en una memoria compartida y consultable entre todos los agentes compatibles con MCP: tu conocimiento, cualquier modelo, tuyo para siempre.**
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/@oomkapwn/enquire-mcp)
|
|
14
|
-
[](./STABILITY.md)
|
|
15
15
|
[](https://slsa.dev/spec/v1.0/levels#build-l2)
|
|
16
16
|
[](https://modelcontextprotocol.io/)
|
|
17
17
|
[](./LICENSE)
|
|
@@ -48,7 +48,7 @@ Tu bóveda de Obsidian se convierte en **memoria a largo plazo persistente y con
|
|
|
48
48
|
> 3. **Cero llamadas a la nube durante el servicio.** Modelos en caché local (una descarga única desde HuggingFace). El contenido de tu bóveda nunca sale de tu máquina. Seguro para entornos aislados por defecto.
|
|
49
49
|
> 4. **Recuperación consciente de la frescura.** Cada resultado informa de la antigüedad de la nota; el reordenamiento por recencia opcional permite que un agente prefiera el conocimiento reciente y marque los hechos obsoletos para reverificación: la frontera consciente del olvido, construida sobre el `mtime` que tus archivos ya tienen.
|
|
50
50
|
|
|
51
|
-
**46 herramientas · 19 prompts MCP ·
|
|
51
|
+
**46 herramientas · 19 prompts MCP · 1427+ pruebas unitarias · 50+ idiomas · v3.11.x estable · ligado a semver · MIT · procedencia de compilación en npm (SLSA L2).**
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
@@ -195,7 +195,7 @@ Postura completa: **[SECURITY.md](./SECURITY.md)** · Superficie de estabilidad:
|
|
|
195
195
|
|
|
196
196
|
## 🚀 Publicaciones
|
|
197
197
|
|
|
198
|
-
Canal: `npm install @oomkapwn/enquire-mcp` → última versión estable (`@latest` = v3.
|
|
198
|
+
Canal: `npm install @oomkapwn/enquire-mcp` → última versión estable (`@latest` = v3.11.x). Versión preliminar: `npm install @oomkapwn/enquire-mcp@rc` (el último candidato a versión). Registro de cambios completo en **[CHANGELOG.md](./CHANGELOG.md)** · Hoja de ruta en **[ROADMAP.md](https://github.com/oomkapwn/enquire-mcp/blob/main/ROADMAP.md)**.
|
|
199
199
|
|
|
200
200
|
## 🤝 Cómo contribuir
|
|
201
201
|
|
package/README.fr.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
[](https://github.com/oomkapwn/enquire-mcp/actions/workflows/ci.yml)
|
|
16
16
|
[](https://www.npmjs.com/package/@oomkapwn/enquire-mcp)
|
|
17
17
|
[](https://www.npmjs.com/package/@oomkapwn/enquire-mcp)
|
|
18
|
-
[](#️-confiance)
|
|
19
|
+
[](./STABILITY.md)
|
|
20
20
|
[](https://slsa.dev/spec/v1.0/levels#build-l2)
|
|
21
21
|
[](https://modelcontextprotocol.io/)
|
|
22
22
|
[](./LICENSE)
|
|
@@ -53,7 +53,7 @@ Votre coffre Obsidian devient une **mémoire à long terme persistante et interr
|
|
|
53
53
|
> 3. **Zéro appel au cloud pendant le service.** Modèles mis en cache localement (téléchargement unique depuis HuggingFace). Le contenu de votre coffre ne quitte jamais votre machine. Sûr en environnement isolé par défaut.
|
|
54
54
|
> 4. **Rappel conscient de la fraîcheur.** Chaque résultat indique l'âge de la note ; le reclassement par récence optionnel permet à un agent de préférer le savoir frais et de signaler les faits périmés à revérifier — la frontière consciente de l'oubli, bâtie sur le `mtime` que vos fichiers possèdent déjà.
|
|
55
55
|
|
|
56
|
-
**46 outils · 19 prompts MCP ·
|
|
56
|
+
**46 outils · 19 prompts MCP · 1427+ tests unitaires · 50+ langues · v3.11.x stable · lié au semver · MIT · provenance de build npm (SLSA L2).**
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
@@ -81,7 +81,7 @@ Votre coffre Obsidian devient une **mémoire à long terme persistante et interr
|
|
|
81
81
|
| **GraphRAG-light** (détection de communautés de wikilinks par modularité de Louvain) | ✅ **uniquement ici** | ❌ | ❌ |
|
|
82
82
|
| **Exécution autonome de requêtes `.base`** (fonctionne sans Obsidian ouvert) | ✅ **uniquement ici** | ❌ | ❌ délègue à Obsidian |
|
|
83
83
|
| **Récupération HyDE** (Gao et al. 2023) + décomposition en sous-questions | ✅ **uniquement ici** | ❌ | ❌ |
|
|
84
|
-
| **
|
|
84
|
+
| **1427 tests unitaires · 9 portes CI requises + 5 indicatives par PR** | ✅ | s.o. | rare |
|
|
85
85
|
| **Provenance de build signée** (npm + Sigstore, SLSA Build L2) | ✅ | s.o. | ❌ |
|
|
86
86
|
| **Surface publique liée au semver** ([STABILITY.md](./STABILITY.md)) | ✅ | s.o. | ❌ |
|
|
87
87
|
| Autonome (aucun plugin Obsidian requis) | ✅ | ❌ requiert Obsidian | variable |
|
|
@@ -311,7 +311,7 @@ Posture complète : **[SECURITY.md](./SECURITY.md)** · Surface de stabilité :
|
|
|
311
311
|
|
|
312
312
|
`v2.0` récupération hybride (BM25+TF-IDF+embeddings via RRF) · `v2.6` MCP distant · `v2.7-2.8` PDF fondus · `v2.9` reranker BGE · `v2.10` OCR · `v2.11` doctor + setup · `v2.12` harnais d'évaluation · `v2.13` HNSW · `v2.14` sessions à état · `v2.15` late-chunking · `v2.16` persistance HNSW · `v2.17` quantification int8 · `v3.8.0` stable · `v3.8.7` durcissement du transport HTTP · **`v3.9.0` stable** : embed-sync du watcher pour les PDF OCRisés, mise à jour HNSW en mémoire en temps réel lors des modifications de fichiers, refill HNSW adaptatif R-10 (clôt la sous-restitution avec >66 % d'exclusion). · **`v3.10` (`@rc`)** : fraîcheur consciente de l'oubli — indicateur `age_days` + `stale` + reclassement optionnel `--recency-weight` + `obsidian_search` conscient du frontmatter.
|
|
313
313
|
|
|
314
|
-
Canal : `npm install @oomkapwn/enquire-mcp` → dernière version stable (`@latest` = v3.
|
|
314
|
+
Canal : `npm install @oomkapwn/enquire-mcp` → dernière version stable (`@latest` = v3.11.x). Préversion : `npm install @oomkapwn/enquire-mcp@rc` (le dernier candidat à la version — voir [CHANGELOG.md](./CHANGELOG.md)). Changelog complet : **[CHANGELOG.md](./CHANGELOG.md)** · Plan à venir : **[ROADMAP.md](https://github.com/oomkapwn/enquire-mcp/blob/main/ROADMAP.md)**.
|
|
315
315
|
|
|
316
316
|
---
|
|
317
317
|
|
|
@@ -320,7 +320,7 @@ Canal : `npm install @oomkapwn/enquire-mcp` → dernière version stable (`@late
|
|
|
320
320
|
```bash
|
|
321
321
|
git clone https://github.com/oomkapwn/enquire-mcp.git
|
|
322
322
|
cd enquire-mcp && npm install
|
|
323
|
-
npm test # suite complète (
|
|
323
|
+
npm test # suite complète (1427 tests, ~12 s)
|
|
324
324
|
npm run lint # zéro avertissement
|
|
325
325
|
npm run build # tsc → dist/
|
|
326
326
|
```
|
package/README.hi.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**हर सत्र में Claude, Cursor, ChatGPT, Codex, OpenClaw को संदर्भ दोबारा समझाना बंद करें। आपके Obsidian नोट्स हर MCP-संगत एजेंट के बीच साझा, खोजने-योग्य स्मृति बन जाते हैं — आपका ज्ञान, हर मॉडल, हमेशा आपका अपना।**
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/@oomkapwn/enquire-mcp)
|
|
14
|
-
[](./STABILITY.md)
|
|
15
15
|
[](https://slsa.dev/spec/v1.0/levels#build-l2)
|
|
16
16
|
[](https://modelcontextprotocol.io/)
|
|
17
17
|
[](./LICENSE)
|
|
@@ -48,7 +48,7 @@ claude mcp add obsidian -- npx -y @oomkapwn/enquire-mcp serve --vault ~/Document
|
|
|
48
48
|
> 3. **serve के दौरान शून्य क्लाउड कॉल।** मॉडल स्थानीय रूप से कैश्ड (HuggingFace से एक-बार डाउनलोड)। आपके vault की सामग्री कभी आपकी मशीन नहीं छोड़ती। डिफ़ॉल्ट रूप से एयर-गैप-सुरक्षित।
|
|
49
49
|
> 4. **ताज़गी-सजग recall।** हर परिणाम बताता है कि नोट कितना पुराना है; वैकल्पिक recency re-ranking एजेंट को ताज़ा ज्ञान को प्राथमिकता देने और बासी तथ्यों को पुनः-सत्यापन हेतु चिह्नित करने देता है — भूलने-के-प्रति-सजग सीमांत, जो आपकी फ़ाइलों में पहले से मौजूद `mtime` पर बना है।
|
|
50
50
|
|
|
51
|
-
**46 टूल · 19 MCP प्रॉम्प्ट ·
|
|
51
|
+
**46 टूल · 19 MCP प्रॉम्प्ट · 1427+ यूनिट टेस्ट · 50+ भाषाएँ · v3.11.x स्थिर · semver-बाध्य · MIT · npm बिल्ड प्रोवेनेंस (SLSA L2)।**
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
@@ -197,7 +197,7 @@ enquire-mcp doctor --vault <path> # रंग-कोडित ✓/⚠/✗ स
|
|
|
197
197
|
|
|
198
198
|
## 🚀 रिलीज़
|
|
199
199
|
|
|
200
|
-
चैनल: `npm install @oomkapwn/enquire-mcp` → नवीनतम स्थिर (`@latest` = v3.
|
|
200
|
+
चैनल: `npm install @oomkapwn/enquire-mcp` → नवीनतम स्थिर (`@latest` = v3.11.x)। प्री-रिलीज़: `npm install @oomkapwn/enquire-mcp@rc` (नवीनतम release candidate)। पूर्ण changelog देखें **[CHANGELOG.md](./CHANGELOG.md)** · रोडमैप देखें **[ROADMAP.md](https://github.com/oomkapwn/enquire-mcp/blob/main/ROADMAP.md)**।
|
|
201
201
|
|
|
202
202
|
## 🤝 योगदान
|
|
203
203
|
|
package/README.ja.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**セッションのたびに Claude、Cursor、ChatGPT、Codex、OpenClaw へコンテキストを説明し直すのはもう終わりです。あなたの Obsidian ノートが、すべての MCP 対応エージェント間で共有・検索可能な記憶になります——あなたの知識を、あらゆるモデルで、永遠にあなたのものに。**
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/@oomkapwn/enquire-mcp)
|
|
14
|
-
[](./STABILITY.md)
|
|
15
15
|
[](https://slsa.dev/spec/v1.0/levels#build-l2)
|
|
16
16
|
[](https://modelcontextprotocol.io/)
|
|
17
17
|
[](./LICENSE)
|
|
@@ -48,7 +48,7 @@ claude mcp add obsidian -- npx -y @oomkapwn/enquire-mcp serve --vault ~/Document
|
|
|
48
48
|
> 3. **serve 中はクラウド呼び出しがゼロ。** モデルはローカルにキャッシュ(HuggingFace から一度だけダウンロード)。あなたのボールトの内容はマシンから決して出ていきません。デフォルトでエアギャップ安全。
|
|
49
49
|
> 4. **鮮度を意識した呼び戻し。** すべてのヒットが、そのノートがどれくらい古いかを報告します。オプトインの鮮度リランキングにより、エージェントは新しい知識を優先し、古くなった事実を再検証対象としてフラグ付けできます——これは忘却を意識したフロンティアであり、あなたのファイルがもともと持っている `mtime` の上に構築されています。
|
|
50
50
|
|
|
51
|
-
**46 ツール · 19 MCP プロンプト ·
|
|
51
|
+
**46 ツール · 19 MCP プロンプト · 1427+ ユニットテスト · 50+ 言語 · v3.11.x 安定版 · semver 準拠 · MIT · npm ビルドプロベナンス(SLSA L2)。**
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
@@ -76,7 +76,7 @@ claude mcp add obsidian -- npx -y @oomkapwn/enquire-mcp serve --vault ~/Document
|
|
|
76
76
|
| **GraphRAG-light**(Louvain モジュラリティによる wikilink コミュニティ検出) | ✅ **ここだけ** | ❌ | ❌ |
|
|
77
77
|
| **スタンドアロンの `.base` クエリ実行**(Obsidian を起動せずに動作) | ✅ **ここだけ** | ❌ | ❌ Obsidian に委譲 |
|
|
78
78
|
| **HyDE 検索**(Gao et al 2023)+ サブクエスチョン分解 | ✅ **ここだけ** | ❌ | ❌ |
|
|
79
|
-
| **
|
|
79
|
+
| **1427 ユニットテスト · PR ごとに 9 個の必須 + 5 個のアドバイザリ CI ゲート** | ✅ | 該当なし | まれ |
|
|
80
80
|
| **署名付きビルドプロベナンス**(npm + Sigstore、SLSA Build L2) | ✅ | 該当なし | ❌ |
|
|
81
81
|
| **semver 準拠の公開サーフェス**([STABILITY.md](./STABILITY.md)) | ✅ | 該当なし | ❌ |
|
|
82
82
|
| スタンドアロン(Obsidian プラグイン不要) | ✅ | ❌ Obsidian が必要 | まちまち |
|
|
@@ -306,7 +306,7 @@ graph LR
|
|
|
306
306
|
|
|
307
307
|
`v2.0` ハイブリッド検索(BM25+TF-IDF+埋め込みを RRF で) · `v2.6` リモート MCP · `v2.7-2.8` PDF 統合 · `v2.9` BGE リランカー · `v2.10` OCR · `v2.11` doctor + setup · `v2.12` 評価ハーネス · `v2.13` HNSW · `v2.14` ステートフルセッション · `v2.15` late-chunking · `v2.16` HNSW 永続化 · `v2.17` int8 量子化 · `v3.8.0` 安定版 · `v3.8.7` HTTP トランスポート強化 · **`v3.9.0` 安定版**:OCR された PDF のウォッチャー embed-sync、ファイル変更時の HNSW インメモリライブ更新、R-10 適応的 HNSW リフィル(>66% 除外時のアンダーリターンを解消)。 · **`v3.10`(`@rc`)**:忘却を意識した鮮度 —— `age_days` + `stale` フラグ + オプトインの `--recency-weight` リランキング + frontmatter を意識した `obsidian_search`。
|
|
308
308
|
|
|
309
|
-
チャネル:`npm install @oomkapwn/enquire-mcp` → 最新安定版(`@latest` = v3.
|
|
309
|
+
チャネル:`npm install @oomkapwn/enquire-mcp` → 最新安定版(`@latest` = v3.11.x)。プレリリース:`npm install @oomkapwn/enquire-mcp@rc`(最新のリリース候補 —— [CHANGELOG.md](./CHANGELOG.md) を参照)。完全な変更履歴:**[CHANGELOG.md](./CHANGELOG.md)** · 今後の計画:**[ROADMAP.md](https://github.com/oomkapwn/enquire-mcp/blob/main/ROADMAP.md)**。
|
|
310
310
|
|
|
311
311
|
---
|
|
312
312
|
|
|
@@ -315,7 +315,7 @@ graph LR
|
|
|
315
315
|
```bash
|
|
316
316
|
git clone https://github.com/oomkapwn/enquire-mcp.git
|
|
317
317
|
cd enquire-mcp && npm install
|
|
318
|
-
npm test # フルスイート(
|
|
318
|
+
npm test # フルスイート(1427 テスト、約 12 秒)
|
|
319
319
|
npm run lint # 警告ゼロ
|
|
320
320
|
npm run build # tsc → dist/
|
|
321
321
|
```
|
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
[](https://github.com/oomkapwn/enquire-mcp/actions/workflows/ci.yml)
|
|
16
16
|
[](https://www.npmjs.com/package/@oomkapwn/enquire-mcp)
|
|
17
17
|
[](https://www.npmjs.com/package/@oomkapwn/enquire-mcp)
|
|
18
|
-
[](#️-trust)
|
|
19
|
+
[](./STABILITY.md)
|
|
20
20
|
[](https://slsa.dev/spec/v1.0/levels#build-l2)
|
|
21
21
|
[](https://modelcontextprotocol.io/)
|
|
22
22
|
[](./LICENSE)
|
|
@@ -51,7 +51,7 @@ Your Obsidian vault becomes **persistent, queryable long-term memory** for any M
|
|
|
51
51
|
> 3. **Zero cloud calls during serve.** Models cached locally (one-time download from HuggingFace). Your vault content never leaves your machine. Air-gap-safe by default.
|
|
52
52
|
> 4. **Freshness-aware recall.** Every hit reports how old the note is; opt-in recency re-ranking lets an agent prefer fresh knowledge and flag stale facts for re-verification — the forgetting-aware frontier, built on the `mtime` your files already have.
|
|
53
53
|
|
|
54
|
-
**46 tools · 19 MCP prompts ·
|
|
54
|
+
**46 tools · 19 MCP prompts · 1427 unit tests · 50+ languages · v3.11.x stable · semver-bound · MIT · npm build provenance (SLSA L2).**
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
@@ -79,7 +79,7 @@ Your Obsidian vault becomes **persistent, queryable long-term memory** for any M
|
|
|
79
79
|
| **GraphRAG-light** (wikilink community detection via Louvain modularity) | ✅ **only here** | ❌ | ❌ |
|
|
80
80
|
| **Standalone `.base` query execution** (works without Obsidian running) | ✅ **only here** | ❌ | ❌ delegates to Obsidian |
|
|
81
81
|
| **HyDE retrieval** (Gao et al 2023) + sub-question decomposition | ✅ **only here** | ❌ | ❌ |
|
|
82
|
-
| **
|
|
82
|
+
| **1427 unit tests · 9 required + 5 advisory CI gates per PR** | ✅ | n/a | rare |
|
|
83
83
|
| **Signed build provenance** (npm + Sigstore, SLSA Build L2) | ✅ | n/a | ❌ |
|
|
84
84
|
| **Semver-bound public surface** ([STABILITY.md](./STABILITY.md)) | ✅ | n/a | ❌ |
|
|
85
85
|
| Standalone (no Obsidian plugin needed) | ✅ | ❌ requires Obsidian | varies |
|
|
@@ -309,7 +309,7 @@ Full posture: **[SECURITY.md](./SECURITY.md)** · Stability surface: **[STABILIT
|
|
|
309
309
|
|
|
310
310
|
`v2.0` hybrid retrieval (BM25+TF-IDF+embeddings via RRF) · `v2.6` remote MCP · `v2.7-2.8` PDFs blended · `v2.9` BGE reranker · `v2.10` OCR · `v2.11` doctor + setup · `v2.12` eval harness · `v2.13` HNSW · `v2.14` stateful sessions · `v2.15` late-chunking · `v2.16` HNSW persistence · `v2.17` int8 quantization · `v3.8.0` stable · `v3.8.7` HTTP transport hardening · **`v3.9.0` stable**: OCR'd PDF watcher embed-sync, HNSW in-memory live update on file changes, R-10 adaptive HNSW refill (closes the >66% excluded under-return). · **`v3.10` (`@rc`)**: forgetting-aware freshness — `age_days` + `stale` flag + opt-in `--recency-weight` re-ranking + frontmatter-aware `obsidian_search`.
|
|
311
311
|
|
|
312
|
-
Channel: `npm install @oomkapwn/enquire-mcp` → latest stable (`@latest` = v3.
|
|
312
|
+
Channel: `npm install @oomkapwn/enquire-mcp` → latest stable (`@latest` = v3.11.x). Pre-release: `npm install @oomkapwn/enquire-mcp@rc` (the latest release candidate — see [CHANGELOG.md](./CHANGELOG.md)). Full changelog: **[CHANGELOG.md](./CHANGELOG.md)** · Forward plan: **[ROADMAP.md](https://github.com/oomkapwn/enquire-mcp/blob/main/ROADMAP.md)**.
|
|
313
313
|
|
|
314
314
|
---
|
|
315
315
|
|
|
@@ -318,7 +318,7 @@ Channel: `npm install @oomkapwn/enquire-mcp` → latest stable (`@latest` = v3.1
|
|
|
318
318
|
```bash
|
|
319
319
|
git clone https://github.com/oomkapwn/enquire-mcp.git
|
|
320
320
|
cd enquire-mcp && npm install
|
|
321
|
-
npm test # full suite (
|
|
321
|
+
npm test # full suite (1427 tests, ~12s)
|
|
322
322
|
npm run lint # zero warnings
|
|
323
323
|
npm run build # tsc → dist/
|
|
324
324
|
```
|