Skip to content

feat: ranked memory search + supersede detector#3

Open
smixs wants to merge 3 commits into
mainfrom
feat/ranked-search-supersede
Open

feat: ranked memory search + supersede detector#3
smixs wants to merge 3 commits into
mainfrom
feat/ranked-search-supersede

Conversation

@smixs

@smixs smixs commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Adds two schema-agnostic engine scripts consumed by downstream vaults (Iva, agent-second-brain).

search.py — ranked recall

Replaces raw-grep memory recall with BM25 ranking:

  • stdlib sqlite3 FTS5 (zero extra deps, no native build), column weights title/meta > tags > body
  • link-graph rerank over the nightly .graph/vault-graph.json (BFS proximity + 1-hop graph-recall + incoming-link boost)
  • IDF coverage from the vault's own corpus (language-agnostic, no stopword lists)
  • status: superseded/archived/... demoted ×0.3 (findable but ranked below current)
  • soft degradation: FTS5 missing → naive substring scan, the call never fails
  • reuses common.parse_frontmatter; no hardcoded card types

python3 search.py "<query>" --vault DIR [--json]{count, engine, hits:[{file,score,status,confidence,snippet}]}. Built-in check: search.py --selftest.

supersede.py — contradiction detector

Ported from the production copy running in Iva's nightly doctor. Groups same-entity cards, flags conflicting scalar fields, writes .graph/supersede-candidates.json for the nightly rollup to resolve (dry-run by default).

Verified end-to-end against graph.py health output (node shape, graph-recall).

Summary by CodeRabbit

  • New Features
    • Added a ranked memory vault search tool with relevance-ranked results, proximity-aware reranking, and short matched snippets (human-readable or JSON output).
    • Added a duplicate-content review tool that scans grouped cards for conflicting fields, generates a JSON candidate report, and can mark superseded items when exactly two related cards exist.
  • Bug Fixes
    • Improved search reliability by automatically falling back to simpler matching when advanced ranking/indexing isn’t available or errors occur.

search.py: BM25 ranking via stdlib sqlite3 FTS5 + link-graph rerank over the
nightly vault-graph.json, IDF coverage weighting, stale-status demotion, graph
recall of 1-hop neighbors. Schema/language-agnostic, zero extra deps, degrades
to a naive substring scan if FTS5 is unavailable. Reuses common.parse_frontmatter.
Run 'search.py --selftest' for the built-in assert check.

supersede.py: deterministic same-entity contradiction detector. Groups cards by
entity, flags conflicting scalar fields (company/role/status/...), writes
.graph/supersede-candidates.json for the nightly rollup to resolve (dry-run by
default; --apply stamps status=superseded conservatively).
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 82bc1c3b-2bd3-4a6a-bf64-c11fa50e427b

📥 Commits

Reviewing files that changed from the base of the PR and between 831c5ed and c87395c.

📒 Files selected for processing (2)
  • skills/autograph/scripts/search.py
  • skills/autograph/scripts/supersede.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • skills/autograph/scripts/supersede.py
  • skills/autograph/scripts/search.py

📝 Walkthrough

Walkthrough

Adds two new scripts under skills/autograph/scripts: one ranks vault markdown with graph-aware reranking and CLI/selftest output, and the other scans duplicate cards for conflicting fields and can optionally apply superseded status updates.

Changes

Ranked Memory Search Script

Layer / File(s) Summary
Frontmatter parsing, constants, and doc loading
skills/autograph/scripts/search.py
Defines frontmatter parsing fallback, scope and ignore constants, text normalization, and markdown vault loading into indexed document fields.
Query tokenization and retrieval strategies
skills/autograph/scripts/search.py
Tokenizes queries into FTS terms and implements BM25 sqlite3 ranking plus a naive substring fallback search.
Graph-based reranking and snippet generation
skills/autograph/scripts/search.py
Loads the vault link-graph, computes BFS distances from anchor documents, and generates condensed snippets around matched tokens.
Main ranking pipeline, selftest, and CLI
skills/autograph/scripts/search.py
Combines retrieval, coverage, proximity, and stale-status demotion into final scores; adds a hermetic selftest and a CLI entrypoint with JSON or human-readable output.

Stacked Contradiction Supersede Scan

Layer / File(s) Summary
Conflict fields, freshness helper, and scan logic
skills/autograph/scripts/supersede.py
Defines conflict field list and card freshness helper, then scans duplicate groups to detect conflicting field values and select the newest card.
Apply updates and CLI entrypoint
skills/autograph/scripts/supersede.py
Implements apply_supersede to mark older two-card groups as superseded, and a CLI main that runs the scan, writes a JSON report, and optionally applies updates.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant search_memory
  participant SQLiteFTS5
  participant VaultGraph

  CLI->>search_memory: query, vault, limit, scope
  search_memory->>search_memory: load_docs and content_tokens
  search_memory->>SQLiteFTS5: bm25_search(fts_query)
  SQLiteFTS5-->>search_memory: ranked docs or empty
  search_memory->>search_memory: naive_search fallback
  search_memory->>VaultGraph: load_graph
  VaultGraph-->>search_memory: nodes and edges
  search_memory->>search_memory: bfs_distances and reranking
  search_memory-->>CLI: ranked results
Loading

Related issues: None referenced.

Related PRs: None referenced.

Suggested labels: enhancement, scripts

Suggested reviewers: None

Poem:
Two scripts arrived in one small sweep,
One ranks the vault, one sorts the deep.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main additions: ranked memory search and a supersede detector.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ranked-search-supersede

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ede141364

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread skills/autograph/scripts/supersede.py Outdated
vault_dir = Path(args[0])
apply = "--apply" in args
verbose = "--verbose" in args
schema = load_schema(vault_dir / ".claude" / "skills" / "autograph" / "schema.json")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the vault schema instead of a hard-coded .claude path

When a vault follows the documented/common layout with schema.json at the vault root, this explicit path raises FileNotFoundError before the scan runs, so supersede.py <vault> never writes .graph/supersede-candidates.json unless the user has also mirrored the schema into .claude/skills/autograph. Please use the same schema resolution as the other scripts or accept a schema argument.

Useful? React with 👍 / 👎.

Comment on lines +83 to +84
old_rel = cand["superseded_candidates"][0]
cur_rel = cand["current"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip --apply when the date winner is ambiguous

If two duplicate cards both lack updated/created/last_accessed values, or their dates tie, scan() still picks current from sorted walk order and this block rewrites the other card as superseded. In that common two-card conflict scenario, --apply silently chooses a winner arbitrarily, contradicting the documented “clear newer-by-date winner” and can mark the wrong card superseded.

Useful? React with 👍 / 👎.

for f in CONFLICT_FIELDS:
vals = {}
for c in cards:
v = str(c["fields"].get(f, "")).strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize missing frontmatter before scanning fields

If any duplicate group contains a markdown file without YAML frontmatter, parse_frontmatter() returns None; this loop then calls .get on that None value and aborts the entire dry-run before writing the report. Vaults can contain ordinary notes without frontmatter, so please coerce missing fields to {} before storing or reading them.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
skills/autograph/scripts/supersede.py (3)

68-68: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer Path.stem/Path operations over string splitting for path/name handling.

.split("/")[-1].replace(".md", "") and cur_rel.replace('.md', '') assume POSIX separators and a single, terminal .md occurrence; Path(rp).stem is more robust and clearer.

♻️ Suggested fix
-                    "entity": ordered[0]["path"].split("/")[-1].replace(".md", ""),
+                    "entity": Path(ordered[0]["path"]).stem,
-        fields["superseded_by"] = f"[[{cur_rel.replace('.md', '')}]]"
+        fields["superseded_by"] = f"[[{Path(cur_rel).stem}]]"

Also applies to: 93-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/autograph/scripts/supersede.py` at line 68, Use Path-based name
extraction in supersede.py instead of manual string splitting/replacement.
Update the logic around ordered[0]["path"] and cur_rel so entity/name derivation
uses Path(...).stem (or equivalent Path operations) rather than
split("/")[-1].replace(".md", "") and .replace(".md", ""), keeping the existing
behavior while making it cross-platform and more robust.

46-50: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Blind except Exception: continue silently swallows read/parse failures.

Both loops silently skip unreadable/malformed cards with no diagnostic output, which could mask corruption or encoding issues in the vault without any trace, as flagged by static analysis (S112/BLE001).

🛡️ Suggested fix
-            try:
-                fields = parse_frontmatter((vault_dir / rp).read_text(encoding="utf-8"))[0]
-            except Exception:
-                continue
+            try:
+                fields = parse_frontmatter((vault_dir / rp).read_text(encoding="utf-8"))[0]
+            except Exception as e:
+                print(f"supersede: skipping unreadable card {rp}: {e}", file=sys.stderr)
+                continue

Also applies to: 86-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/autograph/scripts/supersede.py` around lines 46 - 50, The broad
exception handling in supersede.py is hiding read and frontmatter parse failures
in the loops that load card metadata, so replace the blind except Exception:
continue in the logic around parse_frontmatter and the related loop at the later
location with narrower handling and a diagnostic path. Use the existing
processing flow in the supersede script to log or report the specific error and
the affected path rp before skipping, so unreadable or malformed cards are
visible instead of silently ignored.

Source: Linters/SAST tools


30-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Non-English comments reduce accessibility for the broader team.

Cyrillic comments (flagged by Ruff RUF002/RUF003 as ambiguous characters) are fine functionally but may hinder maintainability for contributors who don't read Russian.

Also applies to: 61-61, 78-78

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/autograph/scripts/supersede.py` at line 30, Replace the
Cyrillic/Russian comments in supersede.py with clear English comments so the
intent is accessible to the whole team and avoids Ruff RUF002/RUF003 ambiguity;
update the comment near the top of the file and the other flagged comment
locations in the same module, keeping the meaning intact while rewriting them in
English.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/autograph/scripts/search.py`:
- Around line 277-278: The no-match test in search_memory is currently
tautological, so it never verifies the graceful empty-result path. Replace the
always-true assertion near search_memory with a real check on the returned count
from the no-match query, using the existing search_memory(v, "zzzznomatch",
limit=5) call, so the test fails if the function does not return zero matches.
- Around line 117-121: The bm25 ranking in search.py is using the wrong weight
alignment because the UNINDEXED path column is still counted, so the intended
title/meta/tags/body priorities are shifted. Update the SELECT query in the
search flow that calls bm25(d, ...) to include a leading 0.0 weight for path,
keeping the remaining weights aligned with the indexed columns and preserving
the current LIMIT behavior.

In `@skills/autograph/scripts/supersede.py`:
- Around line 100-108: The main() flow in supersede.py is hardcoding a schema
path instead of using the loader’s default discovery behavior, which can make
the script fail even when a valid schema exists elsewhere in the vault. Update
the schema loading in main() to rely on load_schema()’s default search logic,
and if needed accept an optional schema path in the same style as the other
scripts so schema resolution stays flexible.

---

Nitpick comments:
In `@skills/autograph/scripts/supersede.py`:
- Line 68: Use Path-based name extraction in supersede.py instead of manual
string splitting/replacement. Update the logic around ordered[0]["path"] and
cur_rel so entity/name derivation uses Path(...).stem (or equivalent Path
operations) rather than split("/")[-1].replace(".md", "") and .replace(".md",
""), keeping the existing behavior while making it cross-platform and more
robust.
- Around line 46-50: The broad exception handling in supersede.py is hiding read
and frontmatter parse failures in the loops that load card metadata, so replace
the blind except Exception: continue in the logic around parse_frontmatter and
the related loop at the later location with narrower handling and a diagnostic
path. Use the existing processing flow in the supersede script to log or report
the specific error and the affected path rp before skipping, so unreadable or
malformed cards are visible instead of silently ignored.
- Line 30: Replace the Cyrillic/Russian comments in supersede.py with clear
English comments so the intent is accessible to the whole team and avoids Ruff
RUF002/RUF003 ambiguity; update the comment near the top of the file and the
other flagged comment locations in the same module, keeping the meaning intact
while rewriting them in English.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcfd0c4f-29d1-4298-a2e0-aac7f8036ee2

📥 Commits

Reviewing files that changed from the base of the PR and between fe62612 and 9ede141.

📒 Files selected for processing (2)
  • skills/autograph/scripts/search.py
  • skills/autograph/scripts/supersede.py

Comment thread skills/autograph/scripts/search.py
Comment thread skills/autograph/scripts/search.py Outdated
Comment thread skills/autograph/scripts/supersede.py Outdated
…bustness

- search.py: bm25() needs one weight per column INCLUDING the UNINDEXED path
  slot; the 4-weight form shifted meta/tags left by one and mis-ranked them
- supersede.py: resolve schema via load_schema() fallback chain (schema.json →
  schema.example.json), degrade to {} instead of raising on a fresh vault so
  the scan actually runs
- supersede.py: tolerate notes without frontmatter (coerce None → {})
- supersede.py: --apply only supersedes on a genuinely newer winner, never on a
  date tie / missing dates

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/autograph/scripts/supersede.py`:
- Around line 86-95: The exception handling in supersede.py around
parse_frontmatter/read_text is too broad and silently hides file read or parse
failures. Update the try/except in the supersede flow to log a diagnostic that
includes the current path and/or old path plus the exception details before
continuing, so skipped candidates from read/parse errors are distinguishable
from the normal newer-date filter in the _card_date comparison.
- Around line 114-120: Pass vault_dir into load_schema() in supersede.py so
schema resolution starts from the current vault instead of CWD or the skill
directory. Update the schema-loading flow to use the vault-specific path first,
then fall back to the shared example schema only if that lookup fails, keeping
the existing graceful {} fallback behavior in the exception path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ec5c632-d399-46b8-b573-c72f54957dec

📥 Commits

Reviewing files that changed from the base of the PR and between 9ede141 and 831c5ed.

📒 Files selected for processing (2)
  • skills/autograph/scripts/search.py
  • skills/autograph/scripts/supersede.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • skills/autograph/scripts/search.py

Comment on lines +86 to +95
cur_path = vault_dir / cur_rel
try:
cur_fields = parse_frontmatter(cur_path.read_text(encoding="utf-8"))[0] or {}
fields, body, lines = parse_frontmatter(old_path.read_text(encoding="utf-8"))
except Exception:
continue
fields = fields or {}
# Only apply on a genuinely newer winner — never guess on a tie or missing dates.
if _card_date(cur_fields) <= _card_date(fields):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Good fix, but silent blind excepts hide read/parse failures.

The strictly-newer date check at Lines 93-95 correctly closes the tie/missing-date gap called out in the commit message. However, Lines 90-91 swallow any exception (missing file, permission error, malformed frontmatter) from reading two files and silently continue, with no diagnostic output. For a --apply run that mutates files, an operator has no way to tell "candidate skipped because winner isn't newer" from "candidate skipped because reading a card raised an exception."

🩹 Suggested logging
         try:
             cur_fields = parse_frontmatter(cur_path.read_text(encoding="utf-8"))[0] or {}
             fields, body, lines = parse_frontmatter(old_path.read_text(encoding="utf-8"))
-        except Exception:
+        except Exception as e:
+            print(f"supersede: skip {old_rel} (read/parse error: {e})", file=sys.stderr)
             continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cur_path = vault_dir / cur_rel
try:
cur_fields = parse_frontmatter(cur_path.read_text(encoding="utf-8"))[0] or {}
fields, body, lines = parse_frontmatter(old_path.read_text(encoding="utf-8"))
except Exception:
continue
fields = fields or {}
# Only apply on a genuinely newer winner — never guess on a tie or missing dates.
if _card_date(cur_fields) <= _card_date(fields):
continue
cur_path = vault_dir / cur_rel
try:
cur_fields = parse_frontmatter(cur_path.read_text(encoding="utf-8"))[0] or {}
fields, body, lines = parse_frontmatter(old_path.read_text(encoding="utf-8"))
except Exception as e:
print(f"supersede: skip {old_rel} (read/parse error: {e})", file=sys.stderr)
continue
fields = fields or {}
# Only apply on a genuinely newer winner — never guess on a tie or missing dates.
if _card_date(cur_fields) <= _card_date(fields):
continue
🧰 Tools
🪛 Ruff (0.15.20)

[error] 90-91: try-except-continue detected, consider logging the exception

(S112)


[warning] 90-90: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/autograph/scripts/supersede.py` around lines 86 - 95, The exception
handling in supersede.py around parse_frontmatter/read_text is too broad and
silently hides file read or parse failures. Update the try/except in the
supersede flow to log a diagnostic that includes the current path and/or old
path plus the exception details before continuing, so skipped candidates from
read/parse errors are distinguishable from the normal newer-date filter in the
_card_date comparison.

Source: Linters/SAST tools

Comment thread skills/autograph/scripts/supersede.py Outdated
- supersede.py: resolve schema from vault_dir first (then example/{}), so a scan
  against an arbitrary vault never picks up a different vault's schema
- supersede.py: exclude already-superseded cards from the conflict report, so
  resolved conflicts don't re-surface every nightly run
- search.py: drop the tautological 'or True' in the no-match selftest assertion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant