Add findings_stale field to GraphPlanningRoadmapArtifact and update ProgressionGraphEditor for state management
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m34s

- Introduced `findings_stale` field in `GraphPlanningRoadmapArtifact` to track the freshness of findings.
- Updated `ProgressionGraphEditor` to manage `findingsStale` state across various functions, ensuring accurate representation of evaluation status.
- Modified related utility functions and tests to accommodate the new state, enhancing overall functionality and user feedback in the progression graph management process.
This commit is contained in:
Lars 2026-06-13 16:29:17 +02:00
parent 5e5f4ca8d4
commit 7265cd5a01
4 changed files with 38 additions and 24 deletions

View File

@ -37,6 +37,7 @@ class GraphPlanningRoadmapArtifact(BaseModel):
path_skill_expectations: Optional[Dict[str, Any]] = None path_skill_expectations: Optional[Dict[str, Any]] = None
slot_contents: Optional[List[SlotContentEntry]] = None slot_contents: Optional[List[SlotContentEntry]] = None
last_findings: Optional[Dict[str, Any]] = None last_findings: Optional[Dict[str, Any]] = None
findings_stale: bool = Field(default=False)
planning_catalog_context: Optional[Dict[str, Any]] = None planning_catalog_context: Optional[Dict[str, Any]] = None
@field_validator("progression_roadmap", "path_skill_expectations", "last_findings", "planning_catalog_context", mode="before") @field_validator("progression_roadmap", "path_skill_expectations", "last_findings", "planning_catalog_context", mode="before")

View File

@ -57,3 +57,15 @@ def test_normalize_slot_contents():
) )
assert len(out["slot_contents"]) == 2 assert len(out["slot_contents"]) == 2
assert out["slot_contents"][1]["primary"]["kind"] == "proposal" assert out["slot_contents"][1]["primary"]["kind"] == "proposal"
def test_normalize_planning_roadmap_with_findings_stale():
out = normalize_planning_roadmap_payload(
{
"goal_query": "Mae Geri",
"last_findings": {"overall_ok": False},
"findings_stale": True,
}
)
assert out["findings_stale"] is True
assert out["last_findings"]["overall_ok"] is False

View File

@ -129,9 +129,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const [comparing, setComparing] = useState(false) const [comparing, setComparing] = useState(false)
const [compareApplying, setCompareApplying] = useState(false) const [compareApplying, setCompareApplying] = useState(false)
const [proposedPathQa, setProposedPathQa] = useState(null) const [proposedPathQa, setProposedPathQa] = useState(null)
const [evaluationStale, setEvaluationStale] = useState(false)
const loadGraph = useCallback(async ({ preserveEvaluationStale = false } = {}) => { const loadGraph = useCallback(async () => {
if (!graphId) return if (!graphId) return
setBusy(true) setBusy(true)
setLoadErr('') setLoadErr('')
@ -154,7 +153,6 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
) )
const findings = graph?.planning_roadmap?.last_findings const findings = graph?.planning_roadmap?.last_findings
if (findings) setPathQa(findings) if (findings) setPathQa(findings)
if (!preserveEvaluationStale) setEvaluationStale(false)
} catch (e) { } catch (e) {
setLoadErr(e.message || 'Graph konnte nicht geladen werden') setLoadErr(e.message || 'Graph konnte nicht geladen werden')
setDraft(null) setDraft(null)
@ -202,9 +200,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
setDraft((prev) => { setDraft((prev) => {
if (!prev) return prev if (!prev) return prev
const next = patchFn(prev) const next = patchFn(prev)
return { ...next, dirty: true } return { ...next, dirty: true, findingsStale: true }
}) })
setEvaluationStale(true)
}, []) }, [])
const gapContextParams = useMemo(() => { const gapContextParams = useMemo(() => {
@ -367,9 +364,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
{ ...prev, progressionRoadmap: roadmap }, { ...prev, progressionRoadmap: roadmap },
roadmap, roadmap,
) )
return { ...structured, dirty: true } return { ...structured, dirty: true, findingsStale: true }
}) })
setEvaluationStale(true)
setStartTargetReady(true) setStartTargetReady(true)
setSemanticBrief(res?.semantic_brief_summary || null) setSemanticBrief(res?.semantic_brief_summary || null)
} catch (e) { } catch (e) {
@ -437,8 +433,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
graphName: draft.graphName, graphName: draft.graphName,
}) })
const withPhases = syncSlotPhasesFromRoadmap(hydrated, roadmap) const withPhases = syncSlotPhasesFromRoadmap(hydrated, roadmap)
setDraft({ ...withPhases, goalQuery: q, maxSteps: majorCount || withPhases.maxSteps, dirty: true }) setDraft({ ...withPhases, goalQuery: q, maxSteps: majorCount || withPhases.maxSteps, dirty: true, findingsStale: true })
setEvaluationStale(true)
setSemanticBrief(res?.semantic_brief_summary || null) setSemanticBrief(res?.semantic_brief_summary || null)
} catch (e) { } catch (e) {
setActionErr(e.message || 'Roadmap-Generierung fehlgeschlagen') setActionErr(e.message || 'Roadmap-Generierung fehlgeschlagen')
@ -473,9 +468,11 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const applyEvaluateResult = (synced, res) => { const applyEvaluateResult = (synced, res) => {
setSemanticBrief(res?.semantic_brief_summary || null) setSemanticBrief(res?.semantic_brief_summary || null)
setPathQa(res?.path_qa || null) setPathQa(res?.path_qa || null)
setEvaluationStale(false)
const { draft: evaluated, remainingOffers } = applyEvaluateResponseToDraft(synced, res) const { draft: evaluated, remainingOffers } = applyEvaluateResponseToDraft(synced, res)
return { draft: { ...evaluated, lastFindings: res?.path_qa || null }, remainingOffers } return {
draft: { ...evaluated, lastFindings: res?.path_qa || null, findingsStale: false },
remainingOffers,
}
} }
const buildMatchRequestBase = (synced) => { const buildMatchRequestBase = (synced) => {
@ -651,13 +648,12 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
) )
const syncedNext = syncProgressionRoadmapFromSlots(nextDraft) const syncedNext = syncProgressionRoadmapFromSlots(nextDraft)
setDraft({ ...syncedNext, dirty: false }) setDraft({ ...syncedNext, dirty: false, findingsStale: true })
setEvaluationStale(true)
setCompareOpen(false) setCompareOpen(false)
setComparePayload(null) setComparePayload(null)
setProposedPathQa(null) setProposedPathQa(null)
await saveProgressionGraphDraft(api, graphId, syncedNext) await saveProgressionGraphDraft(api, graphId, { ...syncedNext, findingsStale: true })
setMatchNotice( setMatchNotice(
'Übernommen und gespeichert. Bewertung bezieht sich noch auf den vorherigen Stand — bitte „Graph bewerten“.', 'Übernommen und gespeichert. Bewertung bezieht sich noch auf den vorherigen Stand — bitte „Graph bewerten“.',
) )
@ -697,7 +693,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
setActionErr('') setActionErr('')
try { try {
await saveProgressionGraphDraft(api, graphId, { ...draft, lastFindings: pathQa }) await saveProgressionGraphDraft(api, graphId, { ...draft, lastFindings: pathQa })
await loadGraph({ preserveEvaluationStale: true }) await loadGraph()
if (typeof onSaved === 'function') await onSaved() if (typeof onSaved === 'function') await onSaved()
alert('Progressionsgraph gespeichert.') alert('Progressionsgraph gespeichert.')
} catch (e) { } catch (e) {
@ -710,9 +706,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const handleApplyGapOffer = (offer, slotIndex) => { const handleApplyGapOffer = (offer, slotIndex) => {
setDraft((prev) => { setDraft((prev) => {
const next = applyGapOfferToDraft(prev, offer, { slotIndex }) const next = applyGapOfferToDraft(prev, offer, { slotIndex })
return { ...next, dirty: true } return { ...next, dirty: true, findingsStale: true }
}) })
setEvaluationStale(true)
setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id)) setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id))
} }
@ -723,9 +718,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
} }
setDraft((prev) => { setDraft((prev) => {
const next = applyGapOfferToDraft(prev, offer, { insertNewSlot: true }) const next = applyGapOfferToDraft(prev, offer, { insertNewSlot: true })
return { ...next, dirty: true } return { ...next, dirty: true, findingsStale: true }
}) })
setEvaluationStale(true)
setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id)) setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id))
} }
@ -838,8 +832,10 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
: null : null
if (resolvedSlot != null) { if (resolvedSlot != null) {
setSlotQuickCreateIndex(resolvedSlot) setSlotQuickCreateIndex(resolvedSlot)
setDraft((prev) => applyGapOfferToDraft(prev, enrichedOffer, { slotIndex: resolvedSlot })) setDraft((prev) => ({
setEvaluationStale(true) ...applyGapOfferToDraft(prev, enrichedOffer, { slotIndex: resolvedSlot }),
findingsStale: true,
}))
} }
setSlotQuickCreateDraft(aiDraft) setSlotQuickCreateDraft(aiDraft)
setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id)) setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id))
@ -887,8 +883,11 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const payload = buildQuickCreateExercisePayloadFromDraft(slotQuickCreateDraft) const payload = buildQuickCreateExercisePayloadFromDraft(slotQuickCreateDraft)
const created = await api.createExercise(payload) const created = await api.createExercise(payload)
if (!created?.id) throw new Error('Anlegen fehlgeschlagen') if (!created?.id) throw new Error('Anlegen fehlgeschlagen')
setDraft((prev) => setSlotPrimaryLibrary(prev, slotQuickCreateIndex, created)) setDraft((prev) => ({
setEvaluationStale(true) ...setSlotPrimaryLibrary(prev, slotQuickCreateIndex, created),
dirty: true,
findingsStale: true,
}))
setSlotQuickCreateDraft(null) setSlotQuickCreateDraft(null)
setSlotQuickCreateIndex(null) setSlotQuickCreateIndex(null)
setActiveOffer(null) setActiveOffer(null)
@ -1177,7 +1176,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
slotCount={draft.slots.length} slotCount={draft.slots.length}
loading={evaluating} loading={evaluating}
error="" error=""
evaluationStale={evaluationStale} evaluationStale={Boolean(draft?.findingsStale)}
onEvaluate={runEvaluate} onEvaluate={runEvaluate}
onApplyGapOffer={handleApplyGapOffer} onApplyGapOffer={handleApplyGapOffer}
onInsertGapSlot={handleInsertGapSlot} onInsertGapSlot={handleInsertGapSlot}

View File

@ -835,6 +835,7 @@ export function hydrateProgressionGraphDraft({
progressionRoadmap: artifact?.progression_roadmap || null, progressionRoadmap: artifact?.progression_roadmap || null,
planningCatalogContext: parsePlanningCatalogContextFromArtifact(artifact), planningCatalogContext: parsePlanningCatalogContextFromArtifact(artifact),
lastFindings: artifact?.last_findings || null, lastFindings: artifact?.last_findings || null,
findingsStale: Boolean(artifact?.findings_stale),
primaryChainEdgeIds: primaryChain?.edges?.map((e) => e.id) || [], primaryChainEdgeIds: primaryChain?.edges?.map((e) => e.id) || [],
siblingEdgeIds: siblingEdges.map((e) => e.id), siblingEdgeIds: siblingEdges.map((e) => e.id),
dirty: false, dirty: false,
@ -871,6 +872,7 @@ export function buildPlanningArtifactFromDraft(draft, { lastFindings = undefined
const findings = lastFindings !== undefined ? lastFindings : draft.lastFindings const findings = lastFindings !== undefined ? lastFindings : draft.lastFindings
if (findings) artifact.last_findings = findings if (findings) artifact.last_findings = findings
artifact.findings_stale = Boolean(draft.findingsStale)
return artifact return artifact
} }