') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix: hashing to avoid doc with duplicate name to collide by saccharin98 · Pull Request #30 · VectifyAI/OpenKB · GitHub
Skip to content

fix: hashing to avoid doc with duplicate name to collide - #30

Closed
saccharin98 wants to merge 1 commit into
VectifyAI:devfrom
saccharin98:dev
Closed

fix: hashing to avoid doc with duplicate name to collide#30
saccharin98 wants to merge 1 commit into
VectifyAI:devfrom
saccharin98:dev

Conversation

@saccharin98

Copy link
Copy Markdown
Collaborator

No description provided.

@saccharin98

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs (race conditions, data corruption, logic errors, edge cases) — no significant problems introduced by this PR.

Notes:

  • The two potential concerns (parallel indexing race, raw/wiki name mismatch when file is already in raw/) were evaluated and determined to be false positives: the hash suffix prevents collisions in the normal code path, and lint.py's registry lookup correctly handles the naming indirection.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@rejojer

Copy link
Copy Markdown
Member

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@saccharin98

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@KylinMountain

Copy link
Copy Markdown
Collaborator

Code review

Found 1 issue:

  1. Pre-existing registry entries (from versions before this PR) have no doc_name, path, raw_path, or source_path fields, so neither get_by_path nor remove_by_doc_name can match them. On re-ingest of a file whose content has changed, the upgrade path silently leaks orphans: the new hashed doc_name is generated, remove_by_doc_name matches nothing (old entry has no doc_name to compare), and the new entry is added alongside the stale one. The old wiki/sources/<stem>.md / wiki/summaries/<stem>.md files are also left in place next to the new <stem>-<hash>.md outputs. No migration code is included.

remove_by_doc_name only matches entries whose metadata["doc_name"] == doc_name:

defremove_by_doc_name(self, doc_name: str) ->None:
"""Remove older content-hash entries for the same document."""
stale_hashes= [
file_hash
forfile_hash, metadatainself._data.items()
ifmetadata.get("doc_name") ==doc_name
]
ifnotstale_hashes:
return
forfile_hashinstale_hashes:
delself._data[file_hash]
self._persist()

get_by_path only matches entries with the new path fields:

defget_by_path(self, path: str) ->dict|None:
"""Return metadata registered for a source, raw, or wiki path."""
formetadatainself._data.values():
ifpathin {
metadata.get("path"),
metadata.get("raw_path"),
metadata.get("source_path"),
}:
returnmetadata
returnNone

The cleanup call in add_single_file therefore no-ops for any pre-PR entry:

OpenKB/openkb/cli.py

Lines 208 to 223 in 870c947

# Register hash only after successful compilation
ifresult.file_hash:
doc_type="long_pdf"ifresult.is_long_docelsefile_path.suffix.lstrip(".")
metadata= {
"name": file_path.name,
"doc_name": doc_name,
"type": doc_type,
"path": _registry_path(file_path, kb_dir),
}
ifresult.raw_pathisnotNone:
metadata["raw_path"] =_registry_path(result.raw_path, kb_dir)
ifresult.source_pathisnotNone:
metadata["source_path"] =_registry_path(result.source_path, kb_dir)
registry.remove_by_doc_name(doc_name)
registry.add(result.file_hash, metadata)

A one-time migration (backfill path/doc_name for existing entries based on name) or a fallback match by name in remove_by_doc_name would close this gap.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

kdush added a commit to kdush/OpenKB that referenced this pull request May 13, 2026
- 用 schema_meta 完成标记替代 DB 文件存在性判断,避免中断后永不重试
- get_registry() 只要 hashes.json 存在即传递 migrate_from,让 DbRegistry 自主判断
- 新增 get_by_path / remove_by_doc_name 到 HashRegistry 与 DbRegistry,
防止后续 PR VectifyAI#30 合并时 SQLite 后端出现 AttributeError
kdush added a commit to kdush/OpenKB that referenced this pull request May 23, 2026
修复 JSON 到 SQLite 迁移完成标记的原子写入
对齐 get_by_path 和 remove_by_doc_name 与 PR VectifyAI#30 的字段契约
补充 HashRegistry 与 DbRegistry 的回归测试
@KylinMountain

Copy link
Copy Markdown
Collaborator

Thanks for surfacing and fixing this, @saccharin98 — the duplicate-name collision was a real bug. We ended up addressing it in #96 with a slightly different approach: document identity is keyed by path, and a colliding same-stem source gets a deterministic -{sha256(path)[:8]} suffix (plus legacy-entry backfill and a full regression test suite). Since #96 covers this case, we're closing it in favor of #96. Thanks again for the original report and patch! 🙏

KylinMountain added a commit that referenced this pull request Jun 14, 2026
…rwrite each other (#96)
* feat(state): index registry entries by path for doc identity
* feat(state): match legacy registry entries by stem for path backfill
* docs(state): document first-match-wins + truthy-path semantics in find_legacy_by_stem
* feat(converter): portable registry path key
* feat(converter): collision-resistant doc_name resolution (Scheme A)
* fix(converter,state): NFKC-normalize both sides of name comparisons + edge tests
* feat(converter): name all artifacts by collision-resistant doc_name
* fix(converter): registry-authoritative names; read-only dedup skip
Unclaimed on-disk artifacts no longer force a suffix (fixes doc_name
drift on retry after a failed compile), and the dedup early-return now
derives the name from the stored entry without invoking the resolver
(no legacy path backfill from duplicate copies).
* feat(indexer): accept explicit doc_name for long-doc artifacts
* feat(cli): persist path identity metadata on add
* fix(cli): construct registry after convert so legacy backfills aren't clobbered
* fix(lint): resolve raw files through the registry before stem matching
* fix(cli): remove locates raw copies via recorded raw_path
* fix(cli): restrict raw-name fallback to legacy entries without raw_path
* credit: collision-resistant doc_name groundwork
The identity model (path-keyed registry metadata, collision-resistant
naming) builds on the approach pioneered in PR #30.
Co-authored-by: Xinyan Zhou <xinyanzhou938@gmail.com>
---------
Co-authored-by: Xinyan Zhou <xinyanzhou938@gmail.com>
Sign up for freeto 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.

3 participants

@saccharin98@rejojer@KylinMountain