fix: bescherm excluded en locked topics tegen AI-deletion/merge
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 46s
On Pull Request to Main / publish (pull_request) Successful in 1m26s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m49s

Topics met learning_relevance="exclude" of relevance_locked=true werden
door de Full Analysis stilletjes verwijderd: het model zag exclude als
"irrelevant" en stelde ze voor in actions.deletions / actions.merges,
en analyzeGraph paste die zonder filter toe. Het locked-vlaggetje werd
in full-scope payload niet eens meegestuurd, dus zelfs een nieuwe prompt
kon niet helpen.

Defense-in-depth fixes:

1. Pure graphGuard.filterAiActions strips elke deletion/merge waarvan de
   target excluded of locked is, vóór bulkSave. Merges waarin een
   protected topic juist de keepId is (canonical survivor) blijven door.

2. SYSTEM_PROMPTS.full krijgt een expliciete "PROTECTED TOPICS" sectie
   die exclude en locked topics als nooit-te-verwijderen markeert.

3. relevance_locked wordt nu ook in de full-scope compactTopics payload
   meegestuurd, zodat het model de vlag überhaupt ziet.

4. UI-feedback: GraphControls toont een ShieldCheck banner met aantal
   geblokkeerde acties na de analyze, en het excluded-aantal naast de
   "Show Excluded Nodes" toggle (visible/hidden).

5. 13 nieuwe Vitest cases dekken protected detection en alle drop/keep
   paden van filterAiActions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
RaymondVerhoef
2026-06-04 08:22:12 +02:00
parent e310b6d85b
commit 9395ea11fe
4 changed files with 267 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { RotateCcw, RefreshCw, AlertCircle, ChevronDown } from 'lucide-react';
import { RotateCcw, RefreshCw, AlertCircle, ChevronDown, ShieldCheck } from 'lucide-react';
import Button from '../../ui/Button';
import SuggestionsQueue from '../SuggestionsQueue';
@@ -24,9 +24,11 @@ const SCOPE_OPTIONS = [
export default function GraphControls({
showExcludeNodes,
onShowExcludeChange,
excludedCount = 0,
onAnalyze,
isAnalyzing,
analyzeError,
analyzeNotice,
disabled,
onApplied,
snapshotMeta,
@@ -50,7 +52,14 @@ export default function GraphControls({
onChange={e => onShowExcludeChange(e.target.checked)}
className="rounded bg-bg-warm border-transparent focus:ring-0 text-teal"
/>
Show Excluded Nodes (Reference Material)
<span>
Show Excluded Nodes (Reference Material)
{excludedCount > 0 && (
<span className="ml-1 text-xs text-fg-muted/80">
· {excludedCount} {showExcludeNodes ? 'visible' : 'hidden'}
</span>
)}
</span>
</label>
<div className="space-y-2">
@@ -106,6 +115,16 @@ export default function GraphControls({
{analyzeError}
</p>
)}
{analyzeNotice && (
<p
className="text-xs text-teal flex items-start gap-1 bg-teal/5 border border-teal/20 rounded-[var(--r-sm)] p-2"
title="The AI suggested removing excluded/locked topics. Those suggestions were dropped client-side before saving."
>
<ShieldCheck size={14} className="shrink-0 mt-0.5" />
{analyzeNotice}
</p>
)}
</div>
<SuggestionsQueue onApplied={onApplied} />