@oomkapwn/enquire-mcp 3.11.0-rc.2 → 3.11.0-rc.20
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 +305 -0
- package/README.ar.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +5 -5
- package/README.hi.md +1 -1
- package/README.ja.md +4 -4
- package/README.md +5 -5
- package/README.pt.md +5 -5
- package/README.ru.md +6 -6
- package/README.zh.md +1 -1
- package/SECURITY.md +2 -2
- package/dist/bases.d.ts.map +1 -1
- package/dist/bases.js +39 -16
- package/dist/bases.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/dql.d.ts.map +1 -1
- package/dist/dql.js +15 -6
- package/dist/dql.js.map +1 -1
- package/dist/embed-db.d.ts +4 -1
- package/dist/embed-db.d.ts.map +1 -1
- package/dist/embed-db.js +6 -2
- 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.map +1 -1
- package/dist/feedback.js +21 -3
- package/dist/feedback.js.map +1 -1
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +41 -33
- package/dist/frontmatter.js.map +1 -1
- package/dist/fts5.d.ts +4 -1
- package/dist/fts5.d.ts.map +1 -1
- package/dist/fts5.js +18 -9
- package/dist/fts5.js.map +1 -1
- package/dist/hnsw.d.ts.map +1 -1
- package/dist/hnsw.js +7 -3
- package/dist/hnsw.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 +75 -0
- package/dist/name-fold.d.ts.map +1 -1
- package/dist/name-fold.js +94 -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 +113 -15
- 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 +60 -11
- 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 +66 -21
- 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 +47 -10
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +27 -9
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +102 -38
- 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/watcher.d.ts +13 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +13 -0
- package/dist/watcher.js.map +1 -1
- package/dist/wildcard-match.d.ts +28 -0
- package/dist/wildcard-match.d.ts.map +1 -1
- package/dist/wildcard-match.js +66 -0
- package/dist/wildcard-match.js.map +1 -1
- package/docs/COMPARISON.md +2 -2
- package/docs/api.md +1 -1
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,311 @@
|
|
|
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-rc.20] — 2026-06-26
|
|
6
|
+
|
|
7
|
+
> **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.**
|
|
8
|
+
|
|
9
|
+
## [3.11.0-rc.19] — 2026-06-25
|
|
10
|
+
|
|
11
|
+
> **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.**
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **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).
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
### Tests
|
|
19
|
+
|
|
20
|
+
- **`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).
|
|
21
|
+
|
|
22
|
+
### Re-verified — rejected with reason (no code)
|
|
23
|
+
|
|
24
|
+
- **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.
|
|
25
|
+
|
|
26
|
+
### Audit response — rc.20 (INFO/hygiene tail, verified + queued)
|
|
27
|
+
|
|
28
|
+
- 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).
|
|
29
|
+
|
|
30
|
+
## [3.11.0-rc.18] — 2026-06-25
|
|
31
|
+
|
|
32
|
+
> **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.**
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **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).
|
|
37
|
+
- **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).
|
|
38
|
+
- **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).
|
|
39
|
+
- **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).
|
|
40
|
+
|
|
41
|
+
### Tests
|
|
42
|
+
|
|
43
|
+
- **`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.
|
|
44
|
+
- **`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.
|
|
45
|
+
|
|
46
|
+
### Audit response — deferred to rc.19/rc.20 (verified, sequenced)
|
|
47
|
+
|
|
48
|
+
- **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).
|
|
49
|
+
- **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).
|
|
50
|
+
|
|
51
|
+
## [3.11.0-rc.17] — 2026-06-24
|
|
52
|
+
|
|
53
|
+
> **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.**
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **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).
|
|
58
|
+
- **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).
|
|
59
|
+
|
|
60
|
+
### Tests / structural defenses
|
|
61
|
+
|
|
62
|
+
- **`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).
|
|
63
|
+
- **`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).
|
|
64
|
+
|
|
65
|
+
### Reasoned verdicts on the auditor's optional items (no code this RC)
|
|
66
|
+
|
|
67
|
+
- **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.
|
|
68
|
+
- **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**.
|
|
69
|
+
- **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**.
|
|
70
|
+
|
|
71
|
+
## [3.11.0-rc.16] — 2026-06-24
|
|
72
|
+
|
|
73
|
+
> **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.**
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- **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.
|
|
78
|
+
- **`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).
|
|
79
|
+
|
|
80
|
+
### Tests / structural defenses
|
|
81
|
+
|
|
82
|
+
- **`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.
|
|
83
|
+
- **`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.
|
|
84
|
+
|
|
85
|
+
## [3.11.0-rc.15] — 2026-06-24
|
|
86
|
+
|
|
87
|
+
> **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.**
|
|
88
|
+
|
|
89
|
+
### Changed
|
|
90
|
+
|
|
91
|
+
- **`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.
|
|
92
|
+
|
|
93
|
+
### Tests / structural defenses
|
|
94
|
+
|
|
95
|
+
- **`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.
|
|
96
|
+
|
|
97
|
+
### Reasoned rejections (no code)
|
|
98
|
+
|
|
99
|
+
- **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.
|
|
100
|
+
- **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.
|
|
101
|
+
|
|
102
|
+
## [3.11.0-rc.14] — 2026-06-24
|
|
103
|
+
|
|
104
|
+
> **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.**
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
|
|
108
|
+
- **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.
|
|
109
|
+
|
|
110
|
+
### Tests / structural defenses
|
|
111
|
+
|
|
112
|
+
- **`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+$`).
|
|
113
|
+
|
|
114
|
+
## [3.11.0-rc.13] — 2026-06-23
|
|
115
|
+
|
|
116
|
+
> **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."**
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
|
|
120
|
+
- **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.
|
|
121
|
+
- **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.
|
|
122
|
+
- **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.
|
|
123
|
+
- **AUD-05 (LOW) — `frontmatter_set` literal `__proto__` round-trip — `src/tools/write.ts`.** Null-proto maps + `Object.hasOwn` + `defineProperty`.
|
|
124
|
+
- **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).
|
|
125
|
+
|
|
126
|
+
### Tests / structural defenses
|
|
127
|
+
|
|
128
|
+
- **`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).
|
|
129
|
+
- **`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.
|
|
130
|
+
- **`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.
|
|
131
|
+
|
|
132
|
+
## [3.11.0-rc.12] — 2026-06-23
|
|
133
|
+
|
|
134
|
+
> **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).**
|
|
135
|
+
|
|
136
|
+
### Fixed
|
|
137
|
+
|
|
138
|
+
- **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).
|
|
139
|
+
- **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.
|
|
140
|
+
- **L-2 (LOW) — offline-enforcement wire-up test — `src/embeddings.ts` (export `applyOfflineEnv`).**
|
|
141
|
+
- **I-1 (LOW) — `obsidian_replace_in_notes` `search`/`replace` `.max(MAX_QUERY_LEN)` — `src/tool-registry.ts`.**
|
|
142
|
+
|
|
143
|
+
### Tests / structural defenses
|
|
144
|
+
|
|
145
|
+
- **`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).
|
|
146
|
+
- **`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.
|
|
147
|
+
- **`tests/embeddings-offline.test.ts`** — +2: `applyOfflineEnv` mutates a mock transformers env when offline (POSITIVE) + no-op when online (NEGATIVE).
|
|
148
|
+
|
|
149
|
+
## [3.11.0-rc.11] — 2026-06-23
|
|
150
|
+
|
|
151
|
+
> **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.
|
|
152
|
+
|
|
153
|
+
### Fixed
|
|
154
|
+
|
|
155
|
+
- **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.
|
|
156
|
+
- **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.
|
|
157
|
+
- **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.
|
|
158
|
+
|
|
159
|
+
### Tests / structural defenses
|
|
160
|
+
|
|
161
|
+
- **`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.)
|
|
162
|
+
- **`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.
|
|
163
|
+
|
|
164
|
+
## [3.11.0-rc.10] — 2026-06-23
|
|
165
|
+
|
|
166
|
+
> **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.
|
|
167
|
+
|
|
168
|
+
### Fixed
|
|
169
|
+
|
|
170
|
+
- **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.**
|
|
171
|
+
- **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.
|
|
172
|
+
|
|
173
|
+
### Tests / structural defenses
|
|
174
|
+
|
|
175
|
+
- **`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.
|
|
176
|
+
|
|
177
|
+
## [3.11.0-rc.9] — 2026-06-23
|
|
178
|
+
|
|
179
|
+
> **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.
|
|
180
|
+
|
|
181
|
+
### Fixed
|
|
182
|
+
|
|
183
|
+
- **NFC-tag class (L-TAG-1) — `src/name-fold.ts` + parser/meta/read/write/search/dql/bases.** New `foldTag(t)` (strip `#` + NFC + lowercase), `nfcLower(v)` (NFC + lowercase, value surface — centralizes the rc.8 DQL-local copy), and `nfc(v)` (NFC only, case-preserving). The parser producer NFC-normalizes stored tags (display case kept) so `collectTags` dedups two Unicode forms to one; every tag comparison + every frontmatter-value comparison routes through the shared helpers. Bases keeps case-sensitivity (NFC-only, per Obsidian Bases semantics); DQL/search keep case-insensitivity (NFC + lowercase, matching their prior `.toLowerCase()` contract).
|
|
184
|
+
- **DQL `FROM #tag` NFC (new sibling) — `src/dql.ts`.** rc.8 NFC-fixed the `WHERE`-value comparators but left the dedicated `#tag` source path on `.toLowerCase()`; now folds via `foldTag`.
|
|
185
|
+
- **`obsidian_search` `filter_frontmatter` value NFC (new sibling) — `src/tools/search.ts`.** `frontmatterScalarEq` NFC-folds both operands (`nfcLower`) — the rc.8 DQL value fix's search-surface sibling.
|
|
186
|
+
- **`.base` frontmatter-value NFC (new sibling) — `src/bases.ts`.** `<key> ==` / `contains` NFC-normalize both sides (case-preserved), parity with the rc.73 path fix + the DQL twin.
|
|
187
|
+
- **`capacity()` honest fallback (I-HNSW-1) — `src/hnsw.ts`.** The `!hasLiveUpdate` fallback returned `maxElements: size` (cap == count → "0 free slots"); now `Number.POSITIVE_INFINITY` (capacity unknown / unbounded). Zero production callers; latent only.
|
|
188
|
+
- **Coverage CI job timeout (I-COV-1 re-verify) — `.github/workflows/ci.yml`.** `coverage` job `timeout-minutes` 5 → 10 (parity with `test`, which does less); removes a latent flake-blocks-release risk on a slow runner.
|
|
189
|
+
- **Stale `peek*Meta` TSDocs — `src/embed-db.ts`, `src/fts5.ts`.** "Throws only on actual SQLite open/read errors" corrected to "never throws — any failure → null" (true since rc.33/34 wrapped `new Database()` in the catch).
|
|
190
|
+
|
|
191
|
+
### Tests / structural defenses
|
|
192
|
+
|
|
193
|
+
- **`tests/name-fold-invariant.test.ts`** — extended with a TAG-signature detector (`#`-strip-then-lowercase, both orderings) + POSITIVE class gate (0 unfolded tag sites in `src/`) + NEGATIVE control + `foldTag`/`nfc` unit tests. Closes the root cause the auditor exploited: the rc.46 inventory invariant's detector was scoped to the note-NAME shape and was structurally blind to the tag surface.
|
|
194
|
+
- **`tests/hnsw-sync-critical-section.test.ts`** (new) — asserts `applyDiff` + `syncHnswForFile` contain no `await` in the shared-state critical section (the real T-MED-1 safety property) + NEGATIVE control. A future `async` refactor that opens a cross-file interleave window now fails CI.
|
|
195
|
+
- Behavioral NFD→NFC nets: dql `FROM #<nfd-tag>`, bases `tag ==`/`taggedWith`/`status ==`/`contains`.
|
|
196
|
+
|
|
197
|
+
## [3.11.0-rc.8] — 2026-06-23
|
|
198
|
+
|
|
199
|
+
> **TL;DR:** **Pre-promotion audit response — 5 confirmed findings (1 MED + 4 LOW), 0 CRIT / 0 HIGH.** Before commissioning the external v3.11.0 → `@latest` audit, I ran a from-scratch state-driven full-project audit (7 lenses, 3-skeptic adversarial verify, 25 agents) on the rc.7 commit. It was exceptionally clean — but the one MED validated running it: a **remotely-triggerable prototype-pollution** in the freshest v3.11.0 surface (`obsidian_mark_useful`). All 5 fixed here. The MED: `FeedbackStore`'s `entries` map was a plain object, so an agent calling `obsidian_mark_useful` with `paths:["__proto__"]` (when `--feedback-weight > 0`, bearer-reachable on serve-http) resolved `entries["__proto__"]` to `Object.prototype` and polluted it process-wide; now a **null-prototype map** (`Object.create(null)`) makes such keys harmless own keys. The 4 LOW: DQL frontmatter-value `=`/`contains` weren't NFC-folded (the rc.69 NFC class's last uncovered sibling — the value surface); `renameNote`'s rc.60 WRITE-1 dest-exclusion was case-sensitive (a case-variant overwrite dest reopened the data-loss on a case-insensitive FS); `CITATION.cff` was stale at v3.9.1 across two stable promotions (now pinned to the latest stable by a new docs-consistency guard); `docs/COMPARISON.md` tied the 46-tool count to "v3.8.x stable" (which had 44). **1336 → 1340 tests** (+4: proto-pollution NEGATIVE control, DQL NFC regression, case-variant-rename regression, CITATION drift guard). No API changes. AUDIT-REQUEST doc for the external pass committed under `docs/audits/`.
|
|
200
|
+
|
|
201
|
+
### Security
|
|
202
|
+
|
|
203
|
+
- **`src/feedback.ts` [MED] — prototype-pollution via `obsidian_mark_useful`.** `FeedbackStore.record()` writes agent-supplied path strings directly as keys of `this.data.entries`. When that map is a plain object, `paths:["__proto__"]` makes `entries["__proto__"]` resolve to `Object.prototype`, and `e.useful += 1` / `e.lastMarked = …` mutate the shared prototype for the whole process (remotely reachable by any bearer-authenticated serve-http client when `--feedback-weight > 0`; the zod `z.array(z.string())` schema does not sanitize string VALUES — the unique tool where agent strings become object keys without `z.record()`'s key-stripping). Fixed by initializing `entries` as a **null-prototype map** (`Object.create(null)`) at both the default-data site and `open()`'s rebuild loop, so `__proto__`/`constructor` become harmless own keys (a note literally named `__proto__.md` still round-trips). NEGATIVE control asserts `record(["__proto__","constructor","Real.md"])` leaves `Object.prototype` unpolluted and stores all three as own keys.
|
|
204
|
+
|
|
205
|
+
### Fixed
|
|
206
|
+
|
|
207
|
+
- **`src/dql.ts` [LOW] — DQL frontmatter-value `=` / `!=` / `contains` were not NFC-normalized.** The rc.69 NFC fix covered the virtual `file.name`/`file.path` projections + the predicate literal, but arbitrary frontmatter values flow through `resolveField`'s `default` branch un-normalized, so `WHERE author = "café"` (NFC literal) silently missed an NFD-stored value. Added an `nfcLower` helper and routed both `looseEq`'s string branch and the string-`contains` branch through it (the shared sinks), matching the already-NFC literal. The `name-fold-invariant` couldn't see this — its signature targets note-NAME comparisons, not value equality. Bounded impact (degrades to fewer matches, never wrong/leaked results). +1 regression test (NFD value vs NFC literal, with a non-matching NEGATIVE control).
|
|
208
|
+
- **`src/tools/write.ts` [LOW] — `renameNote` rc.60 WRITE-1 dest-exclusion was case-sensitive.** `isDest = e.absPath === toAbsCheck` compared the on-disk (canonical-case) `e.absPath` against the user-cased `toAbsCheck`, so on a case-insensitive FS a case-variant `to` (e.g. `Posts/Existing.md` for on-disk `posts/existing.md`) wasn't excluded from the backlink-rewrite plan — and with `overwrite=true` + the dest backlinking the source, the plan clobbered the just-moved source (rc.60 data-loss, reopened for that scenario). Now also matches against the realpath-canonical `canonicalToRel` already computed for the privacy check. +1 regression test (case-insensitive-FS-gated, exercised on the macOS CI leg).
|
|
209
|
+
- **`CITATION.cff` [LOW] — version stale at v3.9.1 across two stable promotions.** The file's own contract is "`version` tracks the @latest STABLE line," but it was never bumped at v3.10.0 / v3.10.1 (it's deliberately excluded from `check-version-consistency.mjs`, and no invariant pinned it). Bumped to `version: "3.10.1"` / `date-released: "2026-06-22"`, and added a `docs-consistency.test.ts` **drift guard** asserting `CITATION.cff` version equals the latest STABLE (non-rc) CHANGELOG heading — so the next stable promotion can't forget it.
|
|
210
|
+
- **`docs/COMPARISON.md` [LOW] — 46-tool count tied to the wrong version.** Line 60 said the 46-tool count "is exact for v3.8.x stable" — but v3.8.x had 44 tools (44→45 in v3.10, 45→46 in v3.11.0). `docs-consistency` keeps the NUMBER correct but nothing gated the version LABEL. Decoupled: "…exact as of the current release…".
|
|
211
|
+
|
|
212
|
+
## [3.11.0-rc.7] — 2026-06-23
|
|
213
|
+
|
|
214
|
+
> **TL;DR:** **CI-guard flake hardening — a transient gh-auth blip can no longer block a release.** During the rc.6 ship, `tests/github-metadata-invariant.test.ts`'s "CI GUARD — gh is actually authenticated" test flaked: `gh auth status` makes a network call to validate the token, and it transiently failed on the main-push run (the identical commit had PASSED on the PR run minutes earlier). That failed CI and blocked the rc.6 publish (`release.yml`'s "assert CI green on main" guard correctly refused) until a manual re-run. Same flake-blocks-a-release class as the rc.20 `npm ci` incident. Fix: a bounded retry (`retryUntil`, 3 attempts / 750 ms backoff) around the network-y `gh auth status` + `gh api` calls — recovering a momentary blip **without** masking a genuine auth failure (every attempt must fail before concluding "unavailable"). Test-infra only. **1335 → 1336 tests.**
|
|
215
|
+
|
|
216
|
+
### Changed
|
|
217
|
+
|
|
218
|
+
- **`tests/github-metadata-invariant.test.ts`** — extracted `ghAuthStatusOnce` / `fetchRepoMetaOnce` and wrapped both in a generic `retryUntil(attempt, ok, attempts, backoffMs, sleep)` helper. `ghIsAvailable()` + `fetchRepoMeta()` now retry **only in a CI/token context** (`CI || GH_TOKEN || GITHUB_TOKEN`), so pure local dev without a token still fails fast (1 attempt, no backoff penalty on every `npm test`). The CI-GUARD reuses the once-computed `available` (no second retry storm) and now fails only after the retry is exhausted — a genuine broken-token regression, not a single transient blip. Synchronous backoff via `Atomics.wait` (no busy-spin).
|
|
219
|
+
|
|
220
|
+
### Tests (1336)
|
|
221
|
+
|
|
222
|
+
- New control (`+1`): `retryUntil recovers a transient blip but still fails a genuine no-auth` — NEGATIVE control proves a truly-unauthenticated gh (every attempt fails) still returns `false` *after exhausting all retries* (the guard still fails loudly); POSITIVE controls prove a fail-twice-then-succeed sequence recovers to `true`, and a first-try success makes exactly one probe (no wasted retries). `sleep` is injected so the controls run instantly.
|
|
223
|
+
|
|
224
|
+
### Notes
|
|
225
|
+
|
|
226
|
+
- Preserves the guard's purpose: it still hard-fails in CI when `GH_TOKEN` is set but `gh` is genuinely unauthenticated (the About/Topics invariants would otherwise silently no-op). Only transient network/API errors are now tolerated. Closes the spawn-task flagged after the rc.6 publish-recovery.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## [3.11.0-rc.6] — 2026-06-23
|
|
231
|
+
|
|
232
|
+
> **TL;DR:** **js-yaml 4 → 5 migration (#275) — the deferred major, done deliberately.** Closes the last open dependabot major. js-yaml@5 is a *behavioral* major in the frontmatter/`.base` area, so this is a real migration, not a bump: (1) v5 no longer coerces YAML timestamps to `Date` — bare dates / timestamps load as **strings** that round-trip FAITHFULLY, which **root-fixes** the rc.58 date-mutation bug (no more silent time-appending) and dissolves the rc.66 midnight/timestamp collision; (2) v5 **`load("")` throws** ("expected a document") where v4 returned `undefined` → `bases.ts parseBase` now guards an empty `.base` body; (3) v5 **does not resolve YAML merge keys (`<<`)** — which is *why* GHSA-h67p-54hq-rp68 is gone at the ROOT in v5, not merely patched; (4) the now-redundant `@types/js-yaml` devDep is **removed** (v5 ships its own types). Scalar resolution (octal/sexagesimal/underscore — the rc.54 contracts) is **identical** on @4 and @5, re-verified. **1333 → 1335 tests.**
|
|
233
|
+
|
|
234
|
+
### Changed
|
|
235
|
+
|
|
236
|
+
- **`js-yaml`** `^4.2.0` → `^5.0.0` (resolved 5.1.0; direct dep; closes dependabot #275). The frontmatter port + `.base` parser run unchanged on v5's `load`/`dump`; behavioral deltas handled below.
|
|
237
|
+
- **Removed `@types/js-yaml`** (devDep) — js-yaml@5 bundles its own type declarations; `tsc` strict builds clean on them.
|
|
238
|
+
- **`src/bases.ts`** `parseBase` guards an empty/whitespace-only `.base` body to `{}` before `load` (v5 `load("")` throws; preserves the v4 `load(body) ?? {}` contract).
|
|
239
|
+
- **`src/frontmatter.ts`** header + `normalizeDateOnly` TSDoc rewritten to v5 reality: dates load as strings (faithful round-trip), `normalizeDateOnly` is now DEFENSIVE-ONLY (fires only for a directly-passed `Date`), merge keys dropped, empty-input throw documented. `parseFrontmatter` is unaffected (its `block !== ""` guard never calls `load` on empty input).
|
|
240
|
+
|
|
241
|
+
### Tests
|
|
242
|
+
|
|
243
|
+
- `tests/frontmatter.test.ts`: the rc.58/rc.66 date contracts re-derived for v5 — an explicit midnight-UTC timestamp is now **preserved verbatim** (no Date coercion → no collapse); a direct-`Date` unit pins `normalizeDateOnly`'s now-defensive midnight branch; a new **merge-key security contract** test pins that `<<` stays a literal key (a future engine that silently re-enables merge-key expansion fails CI). Stale `Date`-era comments refreshed across the suite.
|
|
244
|
+
|
|
245
|
+
### Notes
|
|
246
|
+
|
|
247
|
+
- The whole `js-yaml@4` → `@5` reference sweep landed across SECURITY.md (the `.base` threat-model now states merge keys are removed in v5, not "fixed in v4"), README ×5, `docs/api.md`, `read.ts`/`write.ts`/`bases.ts` TSDoc, and the test comments. `node scripts/check-audit.mjs` is clean on js-yaml@5 (allowlist still empty). The migration was verified on the installed **5.1.0** with a fresh v4-vs-v5 differential (scalar contracts unchanged; the date/merge-key/empty-input deltas are the three handled above) before any code change — the rc.54 differential discipline.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## [3.11.0-rc.5] — 2026-06-23
|
|
252
|
+
|
|
253
|
+
> **TL;DR:** **Dependency majors — the two GO ones, after isolated-worktree evaluation.** Each of the three open dependabot majors was checked out in its own worktree and run through the full gate battery before deciding. **Merged:** `@types/node` 25.9.3 → **26.0.0** (#273 — types-only; `tsc` strict + 1427 tests clean) and `actions/checkout` 6.0.2 → **7.0.0** (#271 — SHA verified against the real v7.0.0 tag; v7's `node24` runtime is supported on our GitHub-hosted runners). **Deferred (NOT merged):** `js-yaml` 4 → **5** (#275) — a genuine behavioral major in the frontmatter/bases area (v5 drops `Date` coercion + merge-key resolution and makes `load("")` throw), breaking 2 pinned contracts; it needs a deliberate migration RC, not an auto-bump. Deps/CI-config only — no source/API change. **1333 tests unchanged.**
|
|
254
|
+
|
|
255
|
+
### Changed
|
|
256
|
+
|
|
257
|
+
- **`@types/node`** `^25.9.3` → `^26.0.0` (dev; closes dependabot #273). Verified in an isolated worktree: `npm ci`, `tsc` (strict + `noUncheckedIndexedAccess`), the full 1427-test suite, and lint all clean — a types-only bump with no runtime surface.
|
|
258
|
+
- **`actions/checkout`** `6.0.2` → `7.0.0` across all 4 workflows (13 pins; closes dependabot #271). Pinned to the verified v7.0.0 commit SHA `9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0`. **Also corrected the `# vN` comment** dependabot left stale — all 13 pins now read `# v7` (the bump had kept `# v6` on the v7 SHA). checkout v7 runs on `node24`, supported by our GitHub-hosted `ubuntu-latest`/`macos-latest` runners.
|
|
259
|
+
|
|
260
|
+
### Notes
|
|
261
|
+
|
|
262
|
+
- **`js-yaml` 5.0.0 (#275) deferred — evaluated, NO-GO as a bump.** A v4-vs-v5 differential + the test suite found four behavioral changes in the project's most-hardened area: (1) timestamps/bare dates load as **strings**, not `Date` (so the rc.58/rc.66 `normalizeDateOnly` date-collapse contract fails); (2) **`load("")` throws** `YAMLException: expected a document` where v4 returned `undefined` (breaks `parseBase`'s `load(body) ?? {}`); (3) **merge keys (`<<`) are no longer resolved** — which is *why* GHSA-h67p-54hq-rp68 is gone in v5 (fixed at the root, not version-bumped); (4) the bump leaves `@types/js-yaml@4` redundant (v5 bundles its own types). 2 pinned tests fail under v5. The upside (v5 removes the Date-mutation root cause and the merge-key advisory at the source) makes it worth a **deliberate migration RC** later — guarding `load("")`, re-deriving the date contracts under string semantics, dropping `@types/js-yaml`, re-running the differential corpus.
|
|
263
|
+
- `node scripts/check-audit.mjs` clean after re-resolution (allowlist still empty).
|
|
264
|
+
- Synced 3 stale `// current X%` coverage annotations in `scripts/check-per-file-coverage.mjs` (OIA Check 6 drift, pre-existing, surfaced when rc.4's coverage run refreshed the summary): `ocr.ts` branches 74.41→71.11, `http-transport.ts` 77.61→75.23, `media.ts` 67.93→69.17. The floors themselves are unchanged.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## [3.11.0-rc.4] — 2026-06-22
|
|
269
|
+
|
|
270
|
+
> **TL;DR:** **Post-rc.1–rc.3 audit response (11-lens adversarial workflow; 0 CRIT / 0 HIGH-code).** The feedback feature re-swept **clean** (provably loss-free / tmp-leak-free); the 45→46 cascade + dep bumps + i18n invariants verified sound. Real findings were concentrated in the **9-language README surface**: (1) **6 broken in-file anchors** across 4 READMEs — incl. a **pre-existing bug in the canonical README.md** (the tests badge linked `#trust`, but "🛡️ Trust" slugs to `️-trust` with a leading variation-selector, so it never resolved), plus README.ru.md's 3 un-localized nav/badge anchors and fr/pt's `#trust`. Fixed to exact github-slugger slugs + a new **anchor-integrity invariant** (`tests/readme-anchor-invariant.test.ts`) that resolves every in-file `(#anchor)` in all 9 READMEs against its heading slugs. (2) **Test-count drift** — the rc.2 translations were born at `1329` and zh/es/hi/ar/AGENTS carried a stale `1311+`; all surfaces synced to the canonical **1333**. (3) the rc.3 CHANGELOG mischaracterized `markdown-it` as a direct dev bump (it is transitive via typedoc). (4) the `prune --help` text omitted the `.feedback.json` family it now erases. (5) the feedback concurrency test was **vacuous** (passed even with serialization removed) → reworked to assert zero tmp-rename collisions, mutation-verified to discriminate. Plus two INFO hardenings on `feedback.ts` (per-write `chmod 0600` defense-in-depth; `record()` entry-count hoisted out of its loop). **1331 → 1333 tests.**
|
|
271
|
+
|
|
272
|
+
### Fixed
|
|
273
|
+
|
|
274
|
+
- **README anchors (HIGH).** All in-file `(#anchor)` links in the 9 READMEs now resolve. `README.md`'s tests badge `#trust` → `#️-trust` (the Trust heading's true slug; pre-existing since the section was added — no gate checked link targets). `README.ru.md` nav/badge `#-quick-start`/`#-use-cases`/`#trust` → localized slugs; `README.fr.md` / `README.pt.md` `#trust` → localized. Verified with `github-slugger` (the exact algorithm GitHub's renderer uses).
|
|
275
|
+
- **Test-count sync (MED/LOW).** Every test-count surface set to the canonical **1333**: `README.md` (badge + stat + table + npm-test), `llms.txt`, `package.json`, `ROADMAP.md`, `docs/COMPARISON.md`; the 8 translations (stat lower-bound `1333+`, fr/ru/pt badges `tests-1333`, ru/fr/pt/ja table + contributing rows); `AGENTS.md` `1311+` → `1333+`. `CLAUDE.md` rc.1 bullet corrected `1313 → 1329` → `1313 → 1331` (the seed of the `1329` propagation).
|
|
276
|
+
- **rc.3 CHANGELOG accuracy (MED).** `markdown-it 14.2.0` reframed as a **transitive** resolution under typedoc's `^14.1.1` range (lockfile-only), not a declared dev-dependency bump.
|
|
277
|
+
- **`prune --help` (LOW).** The `prune` command description now lists `feedback.json` among the erased artifact families, matching `ENQUIRE_CACHE_ARTIFACT` and SECURITY.md's right-to-erasure claim.
|
|
278
|
+
- **feedback concurrency test (MED).** The "serialized writes" test was vacuous (the shared in-memory map made the file coherent regardless of serialization). Reworked to assert **zero** `feedback persist failed` stderr lines — mutation-verified: removing the `persistChain` makes it fail with the exact ENOENT tmp-rename collisions.
|
|
279
|
+
|
|
280
|
+
### Changed
|
|
281
|
+
|
|
282
|
+
- **`src/feedback.ts` (INFO hardenings).** `writeOnce()` re-asserts `chmod 0600` on the landed sidecar after rename (defense-in-depth parity with the fts5/embed-db writers, so SECURITY.md's "0600 sidecar" is an enforced guard, not only writeFile's create-time mode). `record()` hoists the entry count out of its per-path loop (was re-allocating `Object.keys()` per path at the cap).
|
|
283
|
+
- **devDependency** `github-slugger` `^2.0.0` added — the canonical, zero-dependency GitHub slug implementation, used only by the new anchor-integrity invariant. `scripts/check-audit.mjs` clean after the add (allowlist still empty).
|
|
284
|
+
|
|
285
|
+
### Notes
|
|
286
|
+
|
|
287
|
+
- The audit's INFO/verified-clean items are recorded without action: the feedback `FeedbackStore` is sound (serialized persists are loss-free and tmp-leak-free), the 45→46 tool cascade + feedback gating are fully self-consistent, the rc.3 direct dep bumps match package.json + lockfile, and the 9-language numeric/switcher invariants genuinely discriminate.
|
|
288
|
+
- New structural defense: `tests/readme-anchor-invariant.test.ts` (+ NEGATIVE control) closes the anchor-drift class for the i18n surface — a future heading/anchor edit that breaks a link fails CI.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## [3.11.0-rc.3] — 2026-06-22
|
|
293
|
+
|
|
294
|
+
> **TL;DR:** **Dependency hygiene — safe-patch bundle.** Applied the three **direct** non-major dependabot updates: `better-sqlite3` 12.10.1 → **12.11.1**, `vitest` + `@vitest/coverage-v8` → **4.1.9**, `sharp` → **0.35.2**. A fourth (`markdown-it` → **14.2.0**, dependabot #242) is a **transitive** resolution under typedoc's existing `^14.1.1` range — lockfile-only, not a direct dependency. All patch/minor, in-range; `npm audit` clean after re-resolution (no new transitive advisories); full suite green under vitest 4.1.9. Deps only — no source/API change. **1331 tests unchanged.** The 3 **major** dependabot PRs (js-yaml 4→5, @types/node 26, actions/checkout 7) are deliberately **not** included — they need isolated verification and are maintainer-gated.
|
|
295
|
+
|
|
296
|
+
### Changed
|
|
297
|
+
|
|
298
|
+
- **better-sqlite3** `^12.10.1` → `^12.11.1` (optional dep; closes dependabot #274).
|
|
299
|
+
- **@vitest/coverage-v8** + **vitest** `4.1.8` → `4.1.9` (matched dev pair; part of dependabot #272).
|
|
300
|
+
- **sharp** `^0.35.1` → `^0.35.2` (dev; part of dependabot #272).
|
|
301
|
+
- **markdown-it** `14.1.1` → `14.2.0` (**transitive** via typedoc's `^14.1.1` range, resolved on lockfile re-resolution — NOT a declared dependency; closes dependabot #242).
|
|
302
|
+
|
|
303
|
+
### Notes
|
|
304
|
+
|
|
305
|
+
- `node scripts/check-audit.mjs` clean (prod ≥ moderate / dev ≥ high; allowlist still empty). The re-resolution surfaced no new advisories — per the recurring lesson, a dep bump's `npm install` re-resolves the whole tree against the live advisory DB, so the scoped audit gate was run as part of this RC.
|
|
306
|
+
- **Deferred (maintainer's call):** js-yaml 4→5 (⚠️ the project just migrated onto js-yaml@4 in rc.53 — a major needs isolated frontmatter verification), @types/node 26, actions/checkout 7.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
5
310
|
## [3.11.0-rc.2] — 2026-06-22
|
|
6
311
|
|
|
7
312
|
> **TL;DR:** **Internationalization round 2 — README now in 9 languages.** Added **Russian** (`README.ru.md`), **Brazilian Portuguese** (`README.pt.md`), **French** (`README.fr.md`), and **Japanese** (`README.ja.md`) alongside English + 中文 + Español + हिन्दी + العربية. Every README carries a **9-way language switcher**; the four new files ship in the npm tarball, and the existing structural guards were extended to cover them. Docs only — no code/API change. **1331 tests unchanged** (the i18n invariants are data-driven, no new `it()`).
|
package/README.ar.md
CHANGED
|
@@ -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 · 1410+ اختبار وحدة · 50+ لغة · إصدار مستقر v3.10.x · مُقيَّد بالـ semver · MIT · إثبات بناء npm (SLSA L2).**
|
|
58
58
|
|
|
59
59
|
</div>
|
|
60
60
|
|
package/README.es.md
CHANGED
|
@@ -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 · 1410+ pruebas unitarias · 50+ idiomas · v3.10.x estable · ligado a semver · MIT · procedencia de compilación en npm (SLSA L2).**
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
package/README.fr.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
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
19
|
[](./STABILITY.md)
|
|
20
20
|
[](https://slsa.dev/spec/v1.0/levels#build-l2)
|
|
21
21
|
[](https://modelcontextprotocol.io/)
|
|
@@ -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 · 1410+ tests unitaires · 50+ langues · v3.10.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
|
+
| **1410 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 |
|
|
@@ -278,7 +278,7 @@ Plus 3 ressources MCP (`obsidian://vault/info`, `obsidian://note/{path}`, `obsid
|
|
|
278
278
|
| **Sécurité des chemins** | Vérification realpath à chaque lecture+écriture ; les liens symboliques sortant du coffre sont rejetés |
|
|
279
279
|
| **Filtre de confidentialité** | Vérifié aux chemins de ressources FTS5 + embed-db + chunk ; fail-closed sur des listes d'autorisation/interdiction vides |
|
|
280
280
|
| **Transport HTTP** | Auth bearer (SHA-256 à temps constant + `timingSafeEqual`), limite de débit par token, CORS strict |
|
|
281
|
-
| **Frontmatter** | `js-yaml@
|
|
281
|
+
| **Frontmatter** | `js-yaml@5` `load` (schéma cœur YAML 1.2, sûr par défaut) — aucune exécution de code |
|
|
282
282
|
| **Fichiers de cache + index** | chmod 0600, répertoire parent 0700 |
|
|
283
283
|
| **CI** | **9 portes** de protection de branche **requises** : (1) `lint`, (2) `test` sur Node 22, (3) `test` sur Node 24, (4) `smoke`, (5) `audit`, (6) `coverage`, (7) `version-consistency`, (8) `docs`, (9) `oia`. **5 indicatives** : `test-macos` + `docker` (build du Dockerfile + smoke d'introspection `tools/list`) via `.github/workflows/ci.yml` ; CodeQL ×2 + actions Analyze via le [default-setup de GitHub](https://docs.github.com/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-default-setup-for-code-scanning) (pas des fichiers de workflow). Le workflow de release revérifie que les 9 portes requises sont passées sur le SHA taggé avant la publication npm. _v3.7.10 — `docs` (porte de génération TypeDoc) ajoutée à l'ensemble requis. v3.7.13 — plancher `engines.node` relevé à `>=22.13.0` pour correspondre à la matrice CI. v3.8.0-rc.6 — `oia` (Outside-In Audit) promue depuis l'ensemble indicatif._ |
|
|
284
284
|
| **Couverture** | Lignes ≥86 % · instructions ≥82 % · fonctions ≥75 % · branches ≥74 % (sous garde) |
|
|
@@ -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 (1410 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
|
@@ -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 प्रॉम्प्ट · 1410+ यूनिट टेस्ट · 50+ भाषाएँ · v3.10.x स्थिर · semver-बाध्य · MIT · npm बिल्ड प्रोवेनेंस (SLSA L2)।**
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
package/README.ja.md
CHANGED
|
@@ -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 プロンプト · 1410+ ユニットテスト · 50+ 言語 · v3.10.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
|
+
| **1410 ユニットテスト · PR ごとに 9 個の必須 + 5 個のアドバイザリ CI ゲート** | ✅ | 該当なし | まれ |
|
|
80
80
|
| **署名付きビルドプロベナンス**(npm + Sigstore、SLSA Build L2) | ✅ | 該当なし | ❌ |
|
|
81
81
|
| **semver 準拠の公開サーフェス**([STABILITY.md](./STABILITY.md)) | ✅ | 該当なし | ❌ |
|
|
82
82
|
| スタンドアロン(Obsidian プラグイン不要) | ✅ | ❌ Obsidian が必要 | まちまち |
|
|
@@ -273,7 +273,7 @@ graph LR
|
|
|
273
273
|
| **パス安全性** | すべての読み書きで realpath チェック;ボールト外を指すシンボリックリンクは拒否 |
|
|
274
274
|
| **プライバシーフィルタ** | FTS5 + embed-db + chunk リソースパスで検証;空の許可 / 拒否リストには fail-closed |
|
|
275
275
|
| **HTTP トランスポート** | Bearer 認証(定数時間 SHA-256 + `timingSafeEqual`)、トークン別レート制限、厳格な CORS |
|
|
276
|
-
| **Frontmatter** | `js-yaml@
|
|
276
|
+
| **Frontmatter** | `js-yaml@5` の `load`(YAML 1.2 コアスキーマ、デフォルトで安全)—— コード実行なし |
|
|
277
277
|
| **キャッシュ + インデックスファイル** | chmod 0600、親ディレクトリ 0700 |
|
|
278
278
|
| **CI** | **9 個の必須**ブランチ保護ゲート:(1) `lint`、(2) Node 22 での `test`、(3) Node 24 での `test`、(4) `smoke`、(5) `audit`、(6) `coverage`、(7) `version-consistency`、(8) `docs`、(9) `oia`。**5 個のアドバイザリ**:`test-macos` + `docker`(Dockerfile ビルド + `tools/list` イントロスペクションスモーク)を `.github/workflows/ci.yml` 経由で;CodeQL ×2 + Analyze アクションを [GitHub の default-setup](https://docs.github.com/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-default-setup-for-code-scanning) 経由で(ワークフローファイルではありません)。リリースワークフローは、npm 公開の前に、タグ付き SHA で 9 個の必須すべてが通過したことを再検証します。_v3.7.10 —— `docs`(TypeDoc 生成ゲート)を必須セットに追加。v3.7.13 —— CI マトリクスに合わせて `engines.node` の下限を `>=22.13.0` に引き上げ。v3.8.0-rc.6 —— `oia`(Outside-In Audit)をアドバイザリから昇格。_ |
|
|
279
279
|
| **カバレッジ** | 行 ≥86% · ステートメント ≥82% · 関数 ≥75% · 分岐 ≥74%(ゲート付き) |
|
|
@@ -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 # フルスイート(1410 テスト、約 12 秒)
|
|
319
319
|
npm run lint # 警告ゼロ
|
|
320
320
|
npm run build # tsc → dist/
|
|
321
321
|
```
|