Skip to content

Commit 20d8d97

Browse files
committed
fix: route lint-view + sources-view deletes through the unified ref-cleanup helper
Three delete paths existed in the codebase, with three different cleanup behaviours, and only the newest one cleaned everything: Knowledge tree right-click (new) body + index + related: ✓ Sources view source delete cascade body + index, NOT related: ✗ Lint view orphan delete nothing ✗ The user-visible symptom was the FrontmatterPanel rendering an AlertTriangle next to a `related:` slug that pointed at a page deleted via Sources view or Lint view — the page was gone, so the slug couldn't resolve, but no cleanup pass had filtered it out of its referrer's frontmatter array. After deletion the right thing to do is remove the dangling reference, not show the user a broken-link warning that they then have to fix by hand. Both call sites now go through `cascadeDeleteWikiPagesWithRefs` which already handles the full sweep (body wikilinks via stripDeletedWikilinks, index.md listing via cleanIndexListing, and `related:` filtering via parseFrontmatterArray + writeFrontmatterArray on every surviving wiki .md). Sources view's previous inline cleanup loop and the now-orphaned `flattenMdFiles` local helper are removed along with the unused wiki-cleanup imports. Tests: 7 cases on the helper itself were added in the previous commit and cover both the body-wikilink and related-frontmatter paths plus the substring-collision regression. Existing sources-view + lint-view flows have no targeted unit tests, but the full unit suite (984 cases) passes and the helper's tests pin the behaviour now shared by all three call sites.
1 parent 65c1b4f commit 20d8d97

2 files changed

Lines changed: 32 additions & 77 deletions

File tree

src/components/lint/lint-view.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,16 @@ export function LintView() {
181181
if (!confirmed) return
182182

183183
try {
184-
// Cascade = deleteFile(...) + drop the orphan page's embedding
185-
// chunks so it stops showing up as a phantom vector hit.
186-
const { cascadeDeleteWikiPage } = await import("@/lib/wiki-page-delete")
187-
await cascadeDeleteWikiPage(pp, pagePath)
184+
// Full cascade: file + embedding chunks + every reference to
185+
// the page across the wiki (body wikilinks, index.md listing,
186+
// `related:` frontmatter arrays). Even though "orphan" by lint
187+
// means no incoming wikilinks were detected, `related:` slugs
188+
// and index.md entries can still point at it — the orphan
189+
// detector only walks body refs.
190+
const { cascadeDeleteWikiPagesWithRefs } = await import(
191+
"@/lib/wiki-page-delete"
192+
)
193+
await cascadeDeleteWikiPagesWithRefs(pp, [pagePath])
188194
setResults((prev) => prev.filter((_, i) => i !== index))
189195
const tree = await listDirectory(pp)
190196
setFileTree(tree)

src/components/sources/sources-view.tsx

Lines changed: 22 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ import { enqueueIngest, enqueueBatch } from "@/lib/ingest-queue"
1111
import { hasUsableLlm } from "@/lib/has-usable-llm"
1212
import { useTranslation } from "react-i18next"
1313
import { normalizePath, getFileName } from "@/lib/path-utils"
14-
import {
15-
buildDeletedKeys,
16-
cleanIndexListing,
17-
stripDeletedWikilinks,
18-
extractFrontmatterTitle,
19-
type DeletedPageInfo,
20-
} from "@/lib/wiki-cleanup"
2114
import { parseSources, writeSources } from "@/lib/sources-merge"
2215
import { decidePageFate } from "@/lib/source-delete-decision"
2316
import { removeFromIngestCache } from "@/lib/ingest-cache"
@@ -347,8 +340,11 @@ export function SourcesView() {
347340
// elsewhere in the frontmatter). Leaving the page
348341
// alone prevents silent data loss when a filename
349342
// happens to appear in an unrelated page's metadata.
350-
const actuallyDeleted: string[] = []
351-
const deletedInfos: DeletedPageInfo[] = []
343+
// Pass 1: keep / skip — rewrite sources for shared pages, no
344+
// deletion needed. The "delete" decisions are deferred to a
345+
// single batch call after the loop so we can route them
346+
// through the unified cascade helper.
347+
const pagesToDelete: string[] = []
352348
for (const pagePath of relatedPages) {
353349
try {
354350
const content = await readFile(pagePath)
@@ -369,64 +365,28 @@ export function SourcesView() {
369365
continue
370366
}
371367

372-
// action === "delete": the page's sole source was this file.
373-
// Capture slug + title before deletion so stale references
374-
// can be cleaned from index / overview / sibling pages.
375-
const slug = getFileName(pagePath).replace(/\.md$/, "")
376-
const title = extractFrontmatterTitle(content)
377-
deletedInfos.push({ slug, title })
378-
// cascadeDeleteWikiPage = deleteFile(...) + drop the page's
379-
// embedding chunks so future searches don't return phantom
380-
// hits pointing at a file that no longer exists.
381-
const { cascadeDeleteWikiPage } = await import("@/lib/wiki-page-delete")
382-
await cascadeDeleteWikiPage(pp, pagePath)
383-
actuallyDeleted.push(pagePath)
368+
// action === "delete" → defer.
369+
pagesToDelete.push(pagePath)
384370
} catch (err) {
385371
console.error(`Failed to process wiki page ${pagePath}:`, err)
386372
}
387373
}
388374

389-
// Steps 5 & 6: clean stale references from every wiki file.
390-
//
391-
// index.md → drop list-item lines whose primary `[[target]]` is
392-
// a deleted page (title OR slug form matches).
393-
// overview.md + everything else → strip `[[deleted]]` occurrences
394-
// in prose, replacing them with plain text (or with
395-
// the pipe display when present).
396-
//
397-
// Using normalized-key matching rather than the old substring
398-
// `includes` check avoids two classes of real bugs: stale
399-
// title-form refs surviving (`[[KV Cache]]` vs slug `kv-cache`),
400-
// and innocent siblings getting wiped collaterally (deleting
401-
// `ai.md` must not take `[[OpenAI]]` / `[[AI Safety]]` down).
402-
const deletedKeys = buildDeletedKeys(deletedInfos)
403-
if (deletedKeys.size > 0) {
404-
try {
405-
const wikiTree = await listDirectory(`${pp}/wiki`)
406-
const allMdFiles = flattenMdFiles(wikiTree)
407-
for (const file of allMdFiles) {
408-
try {
409-
const content = await readFile(file.path)
410-
const isIndex = file.path === `${pp}/wiki/index.md` ||
411-
file.name === "index.md"
412-
// For index: first drop whole entry lines for deleted
413-
// pages, then still strip any secondary `[[...]]` refs
414-
// to deleted pages that may appear in surviving rows.
415-
const afterListing = isIndex
416-
? cleanIndexListing(content, deletedKeys)
417-
: content
418-
const updated = stripDeletedWikilinks(afterListing, deletedKeys)
419-
if (updated !== content) {
420-
await writeFile(file.path, updated)
421-
}
422-
} catch {
423-
// skip individual file failures — best-effort cleanup
424-
}
425-
}
426-
} catch {
427-
// non-critical
428-
}
429-
}
375+
// Pass 2: full cascade for every page whose sole source was
376+
// this file. The helper deletes the file + drops embeddings
377+
// + sweeps every other wiki .md to clean stale body
378+
// wikilinks, index.md listings, AND `related:` frontmatter
379+
// arrays. The previous inline cleanup loop did 1 and 2 but
380+
// left `related:` slugs pointing at deleted pages, which
381+
// FrontmatterPanel renders as a broken-ref warning icon.
382+
const { cascadeDeleteWikiPagesWithRefs } = await import(
383+
"@/lib/wiki-page-delete"
384+
)
385+
const cascadeResult =
386+
pagesToDelete.length > 0
387+
? await cascadeDeleteWikiPagesWithRefs(pp, pagesToDelete)
388+
: { deletedPaths: [], rewrittenFiles: 0 }
389+
const actuallyDeleted = cascadeResult.deletedPaths
430390

431391
// Step 7: Append deletion record to log.md
432392
try {
@@ -803,14 +763,3 @@ function DeleteButton({
803763
)
804764
}
805765

806-
function flattenMdFiles(nodes: FileNode[]): FileNode[] {
807-
const files: FileNode[] = []
808-
for (const node of nodes) {
809-
if (node.is_dir && node.children) {
810-
files.push(...flattenMdFiles(node.children))
811-
} else if (!node.is_dir && node.name.endsWith(".md")) {
812-
files.push(node)
813-
}
814-
}
815-
return files
816-
}

0 commit comments

Comments
 (0)