feat(v0.26.5): Destructive operation guard — impact preview, confirmation gate, soft-delete#595
Closed
garrytan-agents wants to merge 1 commit into
Closed
Conversation
…tion gate, soft-delete
Three-layer protection against accidental data loss:
1. **Impact preview**: Every destructive operation (sources remove, purge)
now shows a formatted preview of exactly what will be destroyed —
page count, chunk count, embedding count, file count — BEFORE acting.
2. **--confirm-destructive flag**: `--yes` alone is no longer sufficient
when a source has data. Must pass `--confirm-destructive` to proceed
with permanent deletion. Prevents scripted/reflexive destroys.
3. **Soft-delete with 72h TTL**: New `gbrain sources archive <id>`
hides a source from search and federation without destroying any data.
Data preserved for 72 hours. Restorable via `gbrain sources restore <id>`.
Expired archives purged via `gbrain sources purge`.
New subcommands:
- `gbrain sources archive <id>` — soft-delete (hide, preserve 72h)
- `gbrain sources restore <id>` — un-archive, re-federate
- `gbrain sources archived` — list soft-deleted sources + TTL
- `gbrain sources purge [<id>] [--confirm-destructive]` — permanent delete
Behavioral changes:
- `sources remove` with data now requires `--confirm-destructive` (not just `--yes`)
- `sources remove --dry-run` shows full impact preview without side effects
- Impact box format shows source name, id, and all cascade counts
New files:
- src/core/destructive-guard.ts — impact assessment, confirmation gate,
soft-delete/restore/purge logic, display formatters
Merged
9 tasks
garrytan
added a commit
that referenced
this pull request
May 4, 2026
… + autopilot purge) (#600) * feat(v0.26.5): destructive operation guard — impact preview, confirmation gate, soft-delete Three-layer protection against accidental data loss: 1. **Impact preview**: Every destructive operation (sources remove, purge) now shows a formatted preview of exactly what will be destroyed — page count, chunk count, embedding count, file count — BEFORE acting. 2. **--confirm-destructive flag**: `--yes` alone is no longer sufficient when a source has data. Must pass `--confirm-destructive` to proceed with permanent deletion. Prevents scripted/reflexive destroys. 3. **Soft-delete with 72h TTL**: New `gbrain sources archive <id>` hides a source from search and federation without destroying any data. Data preserved for 72 hours. Restorable via `gbrain sources restore <id>`. Expired archives purged via `gbrain sources purge`. New subcommands: - `gbrain sources archive <id>` — soft-delete (hide, preserve 72h) - `gbrain sources restore <id>` — un-archive, re-federate - `gbrain sources archived` — list soft-deleted sources + TTL - `gbrain sources purge [<id>] [--confirm-destructive]` — permanent delete Behavioral changes: - `sources remove` with data now requires `--confirm-destructive` (not just `--yes`) - `sources remove --dry-run` shows full impact preview without side effects - Impact box format shows source name, id, and all cascade counts New files: - src/core/destructive-guard.ts — impact assessment, confirmation gate, soft-delete/restore/purge logic, display formatters * chore(release): v0.26.5 — destructive operation guard Bump VERSION + package.json to 0.26.5 and add the v0.26.5 CHANGELOG entry on top of the destructive-guard feature commit cherry-picked from PR #595. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(v0.26.5): page-level soft-delete + autopilot purge + search visibility Closes the destructive-guard posture across every gbrain destructive surface. PR #595 cherry-pick covered the CLI source-remove path; this commit closes the higher-velocity MCP `delete_page` agent footgun and the three internal correctness gaps the CEO+Eng review surfaced: - Gap 1: archived sources were not actually filtered from search. Now they are, via `buildVisibilityClause` in `searchKeyword`/`searchKeywordChunks`/ `searchVector` for both engines. - Gap 2: 72h TTL was honor-system. Now wired into a new autopilot `purge` phase (9th in ALL_PHASES) that calls `purgeExpiredSources` + `engine. purgeDeletedPages(72)`. Manual escape hatch: `gbrain pages purge-deleted`. - Gap 3: zero tests for safety-critical code. ~30 cases now in `test/destructive-guard.test.ts`, `test/pages-soft-delete.test.ts`, and `test/sql-ranking.test.ts` covering the boundary truth table, JSONB→column migration, soft-delete/restore/purge round-trip, multi-source isolation, cascade verification, and the Q3 IRON-rule contract test. Schema migration v33 (`destructive_guard_columns`): adds `pages.deleted_at` + partial purge index, promotes `archived` from `sources.config` JSONB to real columns (`sources.archived BOOLEAN`, `archived_at`, `archive_expires_at`), backfills any pre-v0.26.5 JSONB shape. Engine-aware: Postgres uses CREATE INDEX CONCURRENTLY, PGLite uses plain CREATE INDEX. Forward-reference bootstrap extended in both engines so pre-v0.26.5 brains don't crash on the embedded-schema replay. BrainEngine surface: new `softDeletePage` / `restorePage` / `purgeDeletedPages` methods + `includeDeleted` flag on `getPage`/`listPages`. MCP ops: `delete_page` rewired to soft-delete (description string updated); new `restore_page` (scope: write) + `purge_deleted_pages` (scope: admin, localOnly: true). Q3 contract (eng-review lynchpin): `get_page(slug)` returns null for soft-deleted by default; `get_page(slug, {include_deleted: true})` surfaces the row with `deleted_at` populated. Same flag for `list_pages`. Mirrors the search-filter contract end-to-end. Issue 5 (eng-review): `archived` is now a real column on `sources`, not a JSONB key. No reserved-key footgun. Faster filter. Visibility clause compiles to a column lookup, not JSONB containment. Verification: - bun run typecheck: PASS - bun run build:schema + bun run build:llms: regenerated - targeted test runs: 90 pass / 0 fail across destructive-guard, pages-soft-delete, sql-ranking, schema-bootstrap-coverage, build-llms - full bun test: 16 pre-existing failures inherited from v0.26.2 (sync, sync-parallel, queue-child-done, etc — already filed in TODOS.md as "Fix 22 pre-existing test failures unrelated to OAuth") CHANGELOG, CLAUDE.md (Key Files + Commands), TODOS.md updated. The plan file at ~/.claude/plans/take-a-look-and-gentle-pine.md captures the full review trail (CEO=C, Eng-Q3=A, Eng-Issue5=a, 8 defaults applied). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v0.26.5): CI fallout — getStats excludes soft-deleted; tests use --confirm-destructive Two CI failures from the v0.26.5 ship: 1. **Tier 1 (Postgres E2E):** `E2E: Page CRUD > delete_page removes page and others survive` failed because `delete_page` now soft-deletes (sets deleted_at) but `getStats.page_count` was still counting all rows. The test seeds 16 pages, deletes one, and asserts page_count is 15. Fix: `getStats` now filters `WHERE deleted_at IS NULL` for page_count in both engines. This matches the visibility-filter contract — soft-deleted pages are hidden everywhere the user looks (search, get_page, list_pages, stats). Chunks and links stay raw because they still occupy storage until the autopilot purge phase runs. 2. **Test 2 (PGLite unit):** `multi-source-integration.test.ts:184` and `e2e/multi-source.test.ts:274` called `runSources(engine, ['remove', X, '--yes'])` against populated sources. v0.26.5's destructive guard rejects `--yes` alone on populated sources and calls `process.exit(5)`, which killed the bun test runner mid-suite (CI exit 5). Both test sites now pass `--confirm-destructive` per the v0.26.5 contract. Verification: 115/0 pass across destructive-guard, pages-soft-delete, sql-ranking, schema-bootstrap-coverage, sources, repos-alias, and multi-source-integration test files. typecheck PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): cycle phase count is 9 (v0.26.5 added `purge` phase) CI failure: `runCycle — yieldBetweenPhases hook` tests asserted exactly 8 phases. v0.26.5 added the autopilot `purge` phase as the 9th, so: - `test/core/cycle.test.ts:381` — `hookCalls` is now 9 (one yield per phase) - `test/core/cycle.test.ts:392` — `report.phases.length` is now 9 - `test/e2e/cycle.test.ts:101` — same update for the dry-run E2E The `purge` phase invocation was already visible in the failing log output: the cycle ran 9 phases end-to-end; the test assertions hadn't been updated. Verification: bun run typecheck PASS. cycle.test.ts: 28/0 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: garrytan-agents <garrytan-agents@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three-layer protection against accidental data loss in gbrain.
1. Impact Preview (always shown)
Every destructive operation now shows a formatted box with the exact blast radius before acting:
2.
--confirm-destructiveflag--yesalone is no longer sufficient when a source has data. You must pass--confirm-destructiveto proceed with permanent deletion. This prevents scripted or reflexive destroys.3. Soft-delete with 72h TTL
New
gbrain sources archive <id>hides a source from search without destroying data. Preserved for 72 hours. Restorable anytime within that window.New Commands
Behavioral Changes
sources removewith data now requires--confirm-destructive(breaking:--yesalone no longer enough)sources remove --dry-runshows full impact previewarchive→ verify →purge(or let TTL expire)Why
Wintermute accidentally removed a federated source instead of clarifying intent. The data loss gate should be in the tool, not just in the operator instructions.
Files
src/core/destructive-guard.ts— new module: impact assessment, confirmation gate, soft-delete/restore/purge, display formatterssrc/commands/sources.ts— updated remove, added archive/restore/purge/archived subcommandsVersion
Targeting v0.26.5.
Need help on this PR? Tag
@codesmithwith what you need.