Standardize on the .agents/ directory (shared with project skills): phases/, user_stories/, reports/, screenshots/, validate.sh, and phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves history; runtime artifacts move alongside). Updates every reference in AGENTS.md, README.md, .gitignore, app docstrings, and test story headers. Historical KB content in data/ and the runtime pipeline.log transcript are left untouched.
9.7 KiB
Phase 69 — Full Source Removal: Files, Index, and a Confirmation Modal
Source: owner request (chat, 2026-09-02) — "Removing sources doesn't remove the data from the filesystem or the rag… I need files cleaned up and the rag index automatically synced. When I remove a source it should be totally removed. For that reason, there should be a confirmation modal that pops up asking for confirmation if the user clicks delete on a source." Story: n/a (owner request from chat — full source removal, 2026-09-02) Context:
app/api/git_sources.py—delete_git_sourceis row-only today: its docstring says "Removing does not touch the clones or the index — the next Sync (prune=True) prunes the dropped repo (phase scope boundary)". The module's "Scope boundary" paragraph repeats it. 404 (unknown id) / 422 (bad uuid) / 204 pins.app/rag/importer.py—_prunedeletes a source'sDocumentrows whose files are no longer walked;Document.chunkscarriescascade="all, delete-orphan"(app/models.py:91), so deleting a document drops every chunk row including its pgvector embedding. Sync source naming: git rows index underrepo_name(row.url)(scripts/import_docs.py), local rows underPath(row.path or row.url).expanduser().name— the exact expressions removal must reuse to find the right documents.app/api/sync.py::_run_sync— the pipeline whose pieces removal reuses (not re-implements): thebump_sources_versionshort-lived-session pattern (app/rag/sources_meta.py, phase 53 saved-chat invalidation — any KB change, including a pure prune, bumps exactly once) and the change-gatedregenerate_overview(app/rag/overview.py, phase 31 — best-effort by design: anLLMErrorreturnsFalsewith the previous row intact).- On-disk layout: git checkouts live under
settings.sources_dir(~/bor-sources/<repo>/), unpacked uploads undersettings.upload_dir(~/bor-sources/uploads/<name>/) — both app-managed. Akind='local'row may also point at any owner directory; those are not app-managed and must never be deleted (the page no longer offers the local-dir form — phase 49 — but the API still accepts it, and such rows can exist). frontend/assets/git-sources.js— the remove flow callswindow.confirm(the onlywindow.confirmin the frontend); module docstring bullets "remove" (L69–76) and "Scope boundary" (L82–87) carry the stale "prunes on the next sync" copy.frontend/git-sources.html—#git-sources-hint(L237–243) carries the same stale copy.tests/e2e/test_git_sources_admin.py— test 4 pins the remove lifecycle through a Playwrightpage.on("dialog")handler (accept → one DELETE; dismiss → none).tests/integration/test_git_sources_api.py— the existing DELETE pins (test_delete_removes_row_and_falls_back_to_env,test_delete_unknown_id_returns_404,test_delete_invalid_id_returns_422).tests/integration/test_git_sources_upload.pyis the house pattern for pointing the router at tmp dirs (the_point_atmonkeypatch) and faking the LLM (FakeEmbedder, spiedimport_sources).README.md— git-sources section (~L129–142: "Adding/removing does not clone…") and local-sources section (~L404–410: removal semantics) carry the stale contract.
Objective
Removing a source (admin page or API) is a total removal: the stored row, every
indexed document of that source (chunks + embeddings), and — for app-managed sources
— the files on disk (the git checkout or the unpacked upload folder), all in one
action. The page confirms the removal first through an accessible, in-app modal
(replacing window.confirm) that spells out exactly what will be deleted.
Dependencies
68_search_tool(complete; ordering by number — no functional dependency).- Functional foundations, all complete:
28_git_based_sources/ phase 35 (sources registry + CRUD),38_local_directory_sources(local rows), phase 49/64_sync_upload_progress(archive uploads + upload dir), phase 32 (sync +prune=True),53_stale_saved_chats(sources version), phase 31 (KB overview).
Tasks
01_full_removal_backend.md— theapp/rag/source_removal.pyhelper (source-name resolver, managed-dir mapping, sibling guard, disk removal) and the rewiredDELETE /api/git-sources/{id}(row + index + managed files + version bump + best-effort overview).02_confirmation_modal.md— the accessible confirmation modal on/git-sources.html(replaceswindow.confirm), the updated hint-box + docstring copy, frontend unit pins.03_e2e_and_commit.md— the dedicated E2E suite (modal → API → disk + DB),test_git_sources_admin.pyupdated to the modal, README copy, full gates, commit.
Testing & Quality
- Unit:
tests/unit/test_source_removal.py— the resolver (git URL shapes incl..gitsuffix + scp-stylegit@,~expansion), the managed-dir mapping (git / upload-under-root / foreign-local →None; the containment check so a sibling nameduploads-foonever counts as underupload_dir), disk removal (absent dir no-op, present dir removed,OSErrorlogged not fatal), the sibling-guard decision. - Unit (frontend):
tests/unit/test_remove_confirm_modal.py—window.confirmgone fromgit-sources.js; the dialog ids +role="alertdialog"+ aria wiring + Esc/Cancel/focus-return wiring present; the stale "stays indexed until the next sync" copy gone fromgit-sources.html/.js; the new hint copy present. - Integration:
tests/integration/test_source_removal_api.py— the full-removal matrix (task 01 step 3), tmp dirs + faked/spied LLM per thetest_git_sources_upload.pypatterns; existing 404/422/204 pins stay green. - E2E (mandatory, house rule):
tests/e2e/test_source_removal_cleanup.py, run in isolation — the modal flow end-to-end for uploaded, git, and local-directory sources, incl. disk assertions (same-hostpathlibagainst the settings-resolved dirs) and the cancel/Esc paths. - Coverage: >90% on
app/(validate.sh gate).
Completion Criteria
DELETE /api/git-sources/{id}removes the row and prunes the source's documents (+chunks/embeddings) in one commit, deletes the app-managed on-disk dir when present (git checkout / upload folder), bumpssources_versionwhen docs were pruned, and best-effort-regenerates the overview when pruned > 0. 404/422/204 pins unchanged. Foreign local directories are never touched. Sibling rows sharing a source name keep their documents and files./git-sources.htmlremoval is a page-localrole="alertdialog"modal (nowindow.confirmanywhere infrontend/): names the source, states the removal policy, Cancel/Esc/backdrop cancel, "Removing…" lifecycle, in-modalrole="alert"error, focus return to the trigger, WCAG 2.1 AA basics (labelled, focus-visible, contrast ≥4.5:1, ≥44px targets).uv run pytestgreen;uv run pytest --cov=appTOTAL >90%;uv run ruff check . && uv run pyrightclean.uv run pytest tests/e2e/test_source_removal_cleanup.py -v --no-covgreen in isolation (DB up); regression suites green in isolation:test_git_sources_admin.py,test_archive_upload_sources.py.- README removal-semantics copy updated (git-sources + local-sources sections).
- One
--no-gpg-signcommit (message in the Commit block); phase dir moved to.agents/phases/complete/.
Locked decisions
- Owner (chat, 2026-09-02): removal is a total removal — row + RAG index +
app-managed files, immediately (not deferred to the next sync); and a real
confirmation modal (not
window.confirm) pops up when Remove is clicked, stating what will be deleted before it happens. - DB first, disk second. The row + document prune commit atomically first (the
RAG is always consistent with the registry — this is the owner's core ask); the
disk removal runs after the commit, and an
OSErroris logged (logger.exception) but does not fail the 204. A leftover dir is inert (no row → never imported) and self-heals on re-add (git re-clones —clone_or_pullclones when.gitis absent; a re-upload recreates the folder). The reverse order is forbidden: a disk failure must never leave a row pointing at deleted files. - App-managed files only. Removal deletes
sources_dir/<repo>/for git rows and the stored dir for local rows when it isupload_diritself or nested under it (containment via resolved paths +parents, so…/uploads-foonever counts). Any otherkind='local'path — the owner's own directory — is never touched on disk; only its row + index entries are removed. - Sibling guard. If another stored row resolves to the same source name (e.g.
https://e.com/randhttps://e.com/r.git→ bothr), only the row is deleted — the shared documents and files still belong to the sibling. Logged loudly. - Version bump + overview gates.
sources_versionbumps exactly once when pruned > 0 (the phase-53 saved-chat invalidation gate, same as sync). Overview regeneration runs when pruned > 0 — deliberately broader than sync's added+updated gate, because a whole-source removal changes the KB's face — and is best-effort: an LLM failure logs and never fails the delete (the next added/updated change refreshes it, as today). - Contract preserved.
DELETEkeeps 204 with no body (the UI success path and the 404/422 pins are unchanged); the per-operation INFO log line (PLAN §9 / AGENTS.md rule 10) carries the counts.
Commit
git add -A .agents/ app/ tests/ frontend/ README.md && git commit --no-gpg-sign -m "feat(sources): removing a source deletes its files and index entries behind a confirmation modal"