Enhance Progression Path Suggestion with Stage Specification Refinement
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
- Introduced `auto_refine_stage_spec` to `ProgressionPathSuggestRequest`, enabling optional refinement of stage specifications during the rematch process. - Updated `_run_roadmap_rematch_loop` to incorporate stage specification refinements, logging changes for better tracking of adjustments made during rematching. - Enhanced `suggest_progression_path` to include refine logs in the output, providing clearer insights into the refinement process. - Added utility functions for formatting refine log entries, improving the display of refinement actions in the frontend components. - Updated frontend components to display refine logs, enhancing user feedback on stage specification adjustments during progression analysis. - Bumped version to 0.8.227 to reflect the new features and improvements.
This commit is contained in:
parent
de9fdf3ac0
commit
f36a747efa
|
|
@ -23,6 +23,7 @@ from planning_path_rematch import (
|
|||
prune_stripped_after_rematch,
|
||||
rematch_roadmap_slots,
|
||||
)
|
||||
from planning_path_refine_stage import apply_stage_spec_refinements, collect_refine_stage_targets
|
||||
from planning_stage_context import build_contextualized_stage_goal, resolve_path_start_target
|
||||
from planning_exercise_path_qa import (
|
||||
apply_llm_path_reorder,
|
||||
|
|
@ -108,6 +109,7 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
include_llm_intent: bool = True
|
||||
include_path_qa: bool = True
|
||||
auto_rematch_after_qa: bool = True
|
||||
auto_refine_stage_spec: bool = True
|
||||
max_rematch_rounds: int = Field(default=2, ge=0, le=3)
|
||||
include_llm_path_qa: bool = True
|
||||
include_path_reorder: bool = True
|
||||
|
|
@ -1268,9 +1270,11 @@ def _run_roadmap_rematch_loop(
|
|||
List[Dict[str, Any]],
|
||||
int,
|
||||
List[Tuple[int, StageSpecArtifact]],
|
||||
List[Dict[str, Any]],
|
||||
]:
|
||||
"""Phase A/B: Rematch-Schleife aus Strip, unfilled Slots und optimization_hints."""
|
||||
"""Phase A/B/C: Rematch-Schleife mit optionaler Stufen-Spec-Verfeinerung."""
|
||||
rematch_log: List[Dict[str, Any]] = []
|
||||
refine_log: List[Dict[str, Any]] = []
|
||||
rematch_rounds = 0
|
||||
max_rounds = int(body.max_rematch_rounds or 0)
|
||||
if not body.auto_rematch_after_qa or max_rounds <= 0 or not roadmap_ctx.stage_specs:
|
||||
|
|
@ -1280,7 +1284,15 @@ def _run_roadmap_rematch_loop(
|
|||
brief=semantic_brief,
|
||||
goal_query=goal_query,
|
||||
)
|
||||
return steps, rematch_log, stripped_off_topic, off_topic_steps, rematch_rounds, roadmap_unfilled
|
||||
return (
|
||||
steps,
|
||||
rematch_log,
|
||||
stripped_off_topic,
|
||||
off_topic_steps,
|
||||
rematch_rounds,
|
||||
roadmap_unfilled,
|
||||
refine_log,
|
||||
)
|
||||
|
||||
current_stripped = list(stripped_off_topic or [])
|
||||
use_initial_off_topic = not current_stripped
|
||||
|
|
@ -1297,6 +1309,20 @@ def _run_roadmap_rematch_loop(
|
|||
)
|
||||
optimization_hints = list(mini_qa.get("optimization_hints") or [])
|
||||
|
||||
if body.auto_refine_stage_spec:
|
||||
_, round_refine = apply_stage_spec_refinements(
|
||||
roadmap_ctx,
|
||||
optimization_hints=optimization_hints,
|
||||
off_topic_steps=off_topic_steps if round_idx > 0 else off_topic_before_strip,
|
||||
goal_query=goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
)
|
||||
if round_refine:
|
||||
for entry in round_refine:
|
||||
tagged = dict(entry)
|
||||
tagged["round"] = rematch_rounds + 1
|
||||
refine_log.append(tagged)
|
||||
|
||||
slot_indices, rematch_reasons = collect_rematch_slot_indices(
|
||||
stripped_off_topic=current_stripped if round_idx == 0 else [],
|
||||
off_topic_steps=off_topic_before_strip if round_idx == 0 and use_initial_off_topic else [],
|
||||
|
|
@ -1304,6 +1330,16 @@ def _run_roadmap_rematch_loop(
|
|||
stage_specs=roadmap_ctx.stage_specs,
|
||||
roadmap_unfilled=roadmap_unfilled,
|
||||
)
|
||||
if body.auto_refine_stage_spec:
|
||||
refine_targets = collect_refine_stage_targets(
|
||||
optimization_hints=optimization_hints,
|
||||
off_topic_steps=off_topic_steps if round_idx > 0 else off_topic_before_strip,
|
||||
stage_specs=roadmap_ctx.stage_specs,
|
||||
)
|
||||
for midx in refine_targets:
|
||||
slot_indices.add(int(midx))
|
||||
if int(midx) not in rematch_reasons:
|
||||
rematch_reasons[int(midx)] = "refine_stage_spec"
|
||||
if not slot_indices:
|
||||
break
|
||||
|
||||
|
|
@ -1358,6 +1394,7 @@ def _run_roadmap_rematch_loop(
|
|||
off_topic_steps,
|
||||
rematch_rounds,
|
||||
roadmap_unfilled,
|
||||
refine_log,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1967,6 +2004,7 @@ def suggest_progression_path(
|
|||
off_topic_steps: List[Dict[str, Any]] = []
|
||||
stripped_off_topic: List[Dict[str, Any]] = []
|
||||
rematch_log: List[Dict[str, Any]] = []
|
||||
refine_log: List[Dict[str, Any]] = []
|
||||
rematch_rounds = 0
|
||||
llm_qa: Optional[Dict[str, Any]] = None
|
||||
llm_qa_applied = False
|
||||
|
|
@ -2062,6 +2100,7 @@ def suggest_progression_path(
|
|||
rematch_off_topic,
|
||||
rematch_rounds,
|
||||
roadmap_unfilled,
|
||||
refine_log,
|
||||
) = _run_roadmap_rematch_loop(
|
||||
cur,
|
||||
tenant=tenant,
|
||||
|
|
@ -2162,6 +2201,10 @@ def suggest_progression_path(
|
|||
path_qa["rematch_applied"] = True
|
||||
path_qa["rematch_log"] = rematch_log
|
||||
path_qa["rematch_rounds"] = rematch_rounds
|
||||
if refine_log:
|
||||
path_qa["refine_applied"] = True
|
||||
path_qa["refine_log"] = refine_log
|
||||
path_qa["refine_count"] = len(refine_log)
|
||||
|
||||
if roadmap_first and roadmap_ctx is not None:
|
||||
steps = _normalize_roadmap_steps_coverage(
|
||||
|
|
@ -2261,6 +2304,8 @@ def suggest_progression_path(
|
|||
retrieval_parts.append("roadmap_unfilled")
|
||||
if rematch_log:
|
||||
retrieval_parts.append("path_rematch")
|
||||
if refine_log:
|
||||
retrieval_parts.append("stage_spec_refine")
|
||||
|
||||
return {
|
||||
"goal_query": goal_query,
|
||||
|
|
|
|||
233
backend/planning_path_refine_stage.py
Normal file
233
backend/planning_path_refine_stage.py
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
"""
|
||||
Phase C: Stufen-Spec verfeinern nach stage_mismatch, dann Rematch.
|
||||
|
||||
Deterministisch — keine LLM-Ratelosigkeit. Schärft anti_patterns / success_criteria
|
||||
aus QS-Finding, schließt abgelehnte Übung aus, übernimmt Pfad-Ausschlüsse.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple
|
||||
|
||||
from planning_exercise_semantics import (
|
||||
PlanningSemanticBrief,
|
||||
build_stage_match_brief,
|
||||
parse_stage_goal_constraints,
|
||||
resolve_path_anti_patterns,
|
||||
)
|
||||
from planning_progression_roadmap import ProgressionRoadmapContext, StageSpecArtifact
|
||||
|
||||
|
||||
def _resolve_major_index(
|
||||
item: Mapping[str, Any],
|
||||
stage_specs: Sequence[StageSpecArtifact],
|
||||
) -> Optional[int]:
|
||||
raw = item.get("roadmap_major_step_index")
|
||||
if raw is not None:
|
||||
return int(raw)
|
||||
si = item.get("step_index")
|
||||
if si is not None:
|
||||
pos = int(si)
|
||||
specs = list(stage_specs or [])
|
||||
if 0 <= pos < len(specs):
|
||||
return int(specs[pos].major_step_index)
|
||||
return None
|
||||
|
||||
|
||||
def collect_refine_stage_targets(
|
||||
*,
|
||||
optimization_hints: Sequence[Mapping[str, Any]],
|
||||
off_topic_steps: Sequence[Mapping[str, Any]],
|
||||
stage_specs: Sequence[StageSpecArtifact],
|
||||
) -> Dict[int, Mapping[str, Any]]:
|
||||
"""Major-Step-Indizes mit stage_mismatch / refine_stage_spec + Quell-Finding."""
|
||||
targets: Dict[int, Mapping[str, Any]] = {}
|
||||
|
||||
def _register(midx: int, source: Mapping[str, Any]) -> None:
|
||||
if midx not in targets:
|
||||
targets[int(midx)] = dict(source)
|
||||
|
||||
for hint in optimization_hints or []:
|
||||
if not isinstance(hint, dict):
|
||||
continue
|
||||
if str(hint.get("action") or "") != "refine_stage_spec":
|
||||
continue
|
||||
midx = _resolve_major_index(hint, stage_specs)
|
||||
if midx is not None:
|
||||
_register(midx, hint)
|
||||
|
||||
for item in off_topic_steps or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if str(item.get("issue") or "") != "stage_mismatch":
|
||||
continue
|
||||
midx = _resolve_major_index(item, stage_specs)
|
||||
if midx is not None:
|
||||
_register(midx, item)
|
||||
|
||||
return targets
|
||||
|
||||
|
||||
def _append_unique_strings(dest: List[str], items: Sequence[str], *, limit: int = 14) -> List[str]:
|
||||
out = list(dest or [])
|
||||
for raw in items:
|
||||
s = str(raw or "").strip()
|
||||
if not s or s in out:
|
||||
continue
|
||||
out.append(s[:200])
|
||||
if len(out) >= limit:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def refine_stage_spec_artifact(
|
||||
spec: StageSpecArtifact,
|
||||
*,
|
||||
finding: Mapping[str, Any],
|
||||
goal_query: str,
|
||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
path_anti_patterns: Optional[Sequence[str]] = None,
|
||||
) -> Tuple[StageSpecArtifact, List[str]]:
|
||||
"""
|
||||
Schärft eine StageSpec aus QS-Finding. Returns (neue Spec, Änderungsliste).
|
||||
"""
|
||||
learning_goal = (
|
||||
str(finding.get("roadmap_learning_goal") or spec.learning_goal or "").strip()
|
||||
or spec.learning_goal
|
||||
)
|
||||
anti = list(spec.anti_patterns or [])
|
||||
success = list(spec.success_criteria or [])
|
||||
changes: List[str] = []
|
||||
|
||||
rejected_title = str(finding.get("title") or "").strip()
|
||||
if rejected_title:
|
||||
marker = f"keine Übung wie „{rejected_title[:120]}“"
|
||||
if marker not in anti:
|
||||
anti.append(marker)
|
||||
changes.append(f"Ausschluss abgelehnter Übung: {rejected_title[:80]}")
|
||||
|
||||
path_anti = list(path_anti_patterns or [])
|
||||
if not path_anti and semantic_brief is not None:
|
||||
path_anti = resolve_path_anti_patterns(goal_query, semantic_brief=semantic_brief)
|
||||
merged_anti = _append_unique_strings(anti, path_anti)
|
||||
if len(merged_anti) > len(anti):
|
||||
changes.append("Pfad-Ausschlüsse in Stufen-anti_patterns übernommen")
|
||||
anti = merged_anti
|
||||
|
||||
constraints = parse_stage_goal_constraints(learning_goal, anti)
|
||||
for phrase in constraints.exclude_phrases or []:
|
||||
if phrase and phrase not in anti:
|
||||
anti.append(phrase)
|
||||
changes.append(f"Ausschluss aus Lernziel: {phrase[:60]}")
|
||||
|
||||
stage_brief = build_stage_match_brief(
|
||||
learning_goal=learning_goal,
|
||||
anti_patterns=anti,
|
||||
success_criteria=list(spec.success_criteria or []),
|
||||
load_profile=list(spec.load_profile or []),
|
||||
)
|
||||
for phrase in (stage_brief.must_phrases or [])[:4]:
|
||||
p = str(phrase or "").strip()
|
||||
if len(p) < 4:
|
||||
continue
|
||||
crit = f"Bezug zu Stufen-Lernziel: {p[:100]}"
|
||||
if crit not in success:
|
||||
success.append(crit)
|
||||
changes.append(f"Erfolgskriterium: {p[:60]}")
|
||||
|
||||
for raw in finding.get("reasons") or []:
|
||||
r = str(raw or "").strip()
|
||||
if len(r) < 8:
|
||||
continue
|
||||
crit = f"QS-Hinweis: {r[:120]}"
|
||||
if crit not in success:
|
||||
success.append(crit)
|
||||
if len(changes) < 6:
|
||||
changes.append(f"Kriterium aus QS: {r[:60]}")
|
||||
if len(success) >= 8:
|
||||
break
|
||||
|
||||
if not changes:
|
||||
return spec, []
|
||||
|
||||
refined = StageSpecArtifact(
|
||||
major_step_index=spec.major_step_index,
|
||||
learning_goal=learning_goal or spec.learning_goal,
|
||||
start_state=spec.start_state,
|
||||
target_state=spec.target_state,
|
||||
load_profile=list(spec.load_profile or []),
|
||||
exercise_type=spec.exercise_type,
|
||||
success_criteria=success[:8],
|
||||
anti_patterns=anti[:14],
|
||||
)
|
||||
return refined, changes
|
||||
|
||||
|
||||
def apply_stage_spec_refinements(
|
||||
roadmap_ctx: ProgressionRoadmapContext,
|
||||
*,
|
||||
optimization_hints: Sequence[Mapping[str, Any]],
|
||||
off_topic_steps: Sequence[Mapping[str, Any]],
|
||||
goal_query: str,
|
||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
) -> Tuple[List[StageSpecArtifact], List[Dict[str, Any]]]:
|
||||
"""
|
||||
Wendet refine_stage_spec auf betroffene Slots an (mutiert stage_specs in ctx).
|
||||
|
||||
Returns: (stage_specs, refine_log)
|
||||
"""
|
||||
stage_specs = list(roadmap_ctx.stage_specs or [])
|
||||
if not stage_specs:
|
||||
return stage_specs, []
|
||||
|
||||
targets = collect_refine_stage_targets(
|
||||
optimization_hints=optimization_hints,
|
||||
off_topic_steps=off_topic_steps,
|
||||
stage_specs=stage_specs,
|
||||
)
|
||||
if not targets:
|
||||
return stage_specs, []
|
||||
|
||||
path_anti = resolve_path_anti_patterns(goal_query, semantic_brief=semantic_brief)
|
||||
spec_by_major = {int(s.major_step_index): s for s in stage_specs}
|
||||
refine_log: List[Dict[str, Any]] = []
|
||||
refined_majors: Set[int] = set()
|
||||
|
||||
for midx in sorted(targets):
|
||||
spec = spec_by_major.get(int(midx))
|
||||
if spec is None:
|
||||
continue
|
||||
refined_spec, changes = refine_stage_spec_artifact(
|
||||
spec,
|
||||
finding=targets[midx],
|
||||
goal_query=goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
path_anti_patterns=path_anti,
|
||||
)
|
||||
if not changes:
|
||||
continue
|
||||
spec_by_major[int(midx)] = refined_spec
|
||||
refined_majors.add(int(midx))
|
||||
refine_log.append(
|
||||
{
|
||||
"roadmap_major_step_index": int(midx),
|
||||
"action": "refined",
|
||||
"issue": "stage_mismatch",
|
||||
"rejected_title": targets[midx].get("title"),
|
||||
"changes": changes[:6],
|
||||
"reason": (changes[0] if changes else "refine_stage_spec")[:400],
|
||||
}
|
||||
)
|
||||
|
||||
if not refine_log:
|
||||
return stage_specs, []
|
||||
|
||||
ordered = [spec_by_major[int(s.major_step_index)] for s in stage_specs]
|
||||
roadmap_ctx.stage_specs = ordered
|
||||
return ordered, refine_log
|
||||
|
||||
|
||||
__all__ = [
|
||||
"apply_stage_spec_refinements",
|
||||
"collect_refine_stage_targets",
|
||||
"refine_stage_spec_artifact",
|
||||
]
|
||||
105
backend/tests/test_planning_path_refine_stage.py
Normal file
105
backend/tests/test_planning_path_refine_stage.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""Tests Phase C — refine_stage_spec nach stage_mismatch."""
|
||||
from planning_exercise_semantics import build_semantic_brief
|
||||
from planning_path_refine_stage import (
|
||||
apply_stage_spec_refinements,
|
||||
collect_refine_stage_targets,
|
||||
refine_stage_spec_artifact,
|
||||
)
|
||||
from planning_progression_roadmap import ProgressionRoadmapContext, StageSpecArtifact
|
||||
|
||||
|
||||
def _spec(major=1, goal="Koordination Absprung ohne Tritttechnik"):
|
||||
return StageSpecArtifact(
|
||||
major_step_index=major,
|
||||
learning_goal=goal,
|
||||
load_profile=["koordination"],
|
||||
exercise_type="kihon_einzel",
|
||||
)
|
||||
|
||||
|
||||
def test_collect_refine_stage_targets_from_hint_and_off_topic():
|
||||
specs = [_spec(0, "A"), _spec(1, "B"), _spec(2, "C")]
|
||||
hints = [
|
||||
{
|
||||
"action": "refine_stage_spec",
|
||||
"roadmap_major_step_index": 1,
|
||||
"reason": "Passt nicht zum Stufen-Lernziel",
|
||||
}
|
||||
]
|
||||
off_topic = [
|
||||
{
|
||||
"issue": "stage_mismatch",
|
||||
"step_index": 2,
|
||||
"roadmap_major_step_index": 2,
|
||||
"title": "Kumite Drill",
|
||||
}
|
||||
]
|
||||
targets = collect_refine_stage_targets(
|
||||
optimization_hints=hints,
|
||||
off_topic_steps=off_topic,
|
||||
stage_specs=specs,
|
||||
)
|
||||
assert targets.keys() == {1, 2}
|
||||
|
||||
|
||||
def test_refine_stage_spec_adds_rejected_title_and_criteria():
|
||||
spec = _spec()
|
||||
finding = {
|
||||
"title": "Mawashi Trittpräzision",
|
||||
"roadmap_learning_goal": spec.learning_goal,
|
||||
"reasons": ["Semantik zu schwach für Stufen-Lernziel"],
|
||||
}
|
||||
brief = build_semantic_brief("Mawashi Geri Kumite")
|
||||
refined, changes = refine_stage_spec_artifact(
|
||||
spec,
|
||||
finding=finding,
|
||||
goal_query="Mawashi Geri ohne Kumite",
|
||||
semantic_brief=brief,
|
||||
)
|
||||
assert changes
|
||||
assert any("Mawashi Trittpräzision" in a for a in refined.anti_patterns)
|
||||
assert refined.success_criteria
|
||||
assert refined.anti_patterns != spec.anti_patterns or refined.success_criteria != spec.success_criteria
|
||||
|
||||
|
||||
def test_apply_stage_spec_refinements_mutates_context():
|
||||
specs = [_spec(0, "Stand"), _spec(1, "Sprungkoordination")]
|
||||
ctx = ProgressionRoadmapContext(
|
||||
goal_query="Mawashi Geri",
|
||||
max_steps=2,
|
||||
stage_specs=specs,
|
||||
)
|
||||
_, log = apply_stage_spec_refinements(
|
||||
ctx,
|
||||
optimization_hints=[],
|
||||
off_topic_steps=[
|
||||
{
|
||||
"issue": "stage_mismatch",
|
||||
"roadmap_major_step_index": 1,
|
||||
"title": "Yoko Geri",
|
||||
"roadmap_learning_goal": "Sprungkoordination",
|
||||
}
|
||||
],
|
||||
goal_query="Mawashi Geri",
|
||||
semantic_brief=build_semantic_brief("Mawashi Geri"),
|
||||
)
|
||||
assert len(log) == 1
|
||||
assert log[0]["action"] == "refined"
|
||||
assert ctx.stage_specs[1].anti_patterns
|
||||
assert any("Yoko Geri" in a for a in ctx.stage_specs[1].anti_patterns)
|
||||
|
||||
|
||||
def test_refine_no_op_when_no_finding_data():
|
||||
spec = StageSpecArtifact(
|
||||
major_step_index=1,
|
||||
learning_goal="",
|
||||
load_profile=[],
|
||||
exercise_type="kihon_einzel",
|
||||
)
|
||||
refined, changes = refine_stage_spec_artifact(
|
||||
spec,
|
||||
finding={"issue": "stage_mismatch"},
|
||||
goal_query="x",
|
||||
)
|
||||
assert changes == []
|
||||
assert refined is spec
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.226"
|
||||
APP_VERSION = "0.8.227"
|
||||
BUILD_DATE = "2026-05-22"
|
||||
DB_SCHEMA_VERSION = "20260607090"
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
|||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
||||
"planning_exercise_suggest": "0.23.1", # Phase B: Rematch-Schleife mit optimization_hints + roadmap_unfilled
|
||||
"planning_exercise_suggest": "0.23.2", # Phase C: refine_stage_spec bei stage_mismatch vor Rematch
|
||||
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
offerSourceLabel,
|
||||
optimizationHintActionLabel,
|
||||
formatRematchLogEntry,
|
||||
formatRefineLogEntry,
|
||||
hasRematchSlotHints,
|
||||
resolveHintSlotIndex,
|
||||
resolveOfferSlotIndex,
|
||||
|
|
@ -165,6 +166,7 @@ export default function ProgressionFindingsPanel({
|
|||
}) {
|
||||
const optimizationHints = Array.isArray(pathQa?.optimization_hints) ? pathQa.optimization_hints : []
|
||||
const rematchLog = Array.isArray(pathQa?.rematch_log) ? pathQa.rematch_log : []
|
||||
const refineLog = Array.isArray(pathQa?.refine_log) ? pathQa.refine_log : []
|
||||
const showRematchAction = hasRematchSlotHints(pathQa) && typeof onRematchSlots === 'function'
|
||||
|
||||
return (
|
||||
|
|
@ -246,6 +248,20 @@ export default function ProgressionFindingsPanel({
|
|||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
{pathQa.refine_applied && refineLog.length > 0 ? (
|
||||
<>
|
||||
<p style={{ margin: '8px 0 4px', fontWeight: 600, color: 'var(--text2)' }}>
|
||||
Stufen-Spec verfeinert ({refineLog.length})
|
||||
</p>
|
||||
<ul style={{ margin: 0, paddingLeft: '16px', color: 'var(--text2)' }}>
|
||||
{refineLog.map((entry, i) => (
|
||||
<li key={`refine-${i}-${entry.roadmap_major_step_index}`}>
|
||||
{formatRefineLogEntry(entry)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
{optimizationHints.length > 0 ? (
|
||||
<>
|
||||
<p style={{ margin: '8px 0 4px', fontWeight: 600, color: 'var(--text2)' }}>
|
||||
|
|
|
|||
|
|
@ -446,6 +446,10 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
`Auto-Rematch: ${rematchLog.length} Anpassung(en) in ${rematchRounds} Runde(n).`,
|
||||
)
|
||||
}
|
||||
const refineLog = res?.path_qa?.refine_log
|
||||
if (Array.isArray(refineLog) && refineLog.length > 0) {
|
||||
parts.push(`Stufen-Spec: ${refineLog.length} Slot(s) verfeinert.`)
|
||||
}
|
||||
setMatchNotice(parts.join(' '))
|
||||
}
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -102,6 +102,16 @@ export function formatRematchLogEntry(entry) {
|
|||
return `${slot}${round}: ${entry.reason || entry.action || 'Rematch'}`
|
||||
}
|
||||
|
||||
export function formatRefineLogEntry(entry) {
|
||||
if (!entry || typeof entry !== 'object') return ''
|
||||
const slot = Number.isFinite(Number(entry.roadmap_major_step_index))
|
||||
? `Slot ${Number(entry.roadmap_major_step_index) + 1}`
|
||||
: 'Slot'
|
||||
const round = entry.round != null ? ` (Runde ${entry.round})` : ''
|
||||
const changes = Array.isArray(entry.changes) ? entry.changes.join('; ') : entry.reason
|
||||
return `${slot}${round}: Stufen-Spec geschärft — ${changes || 'refine_stage_spec'}`
|
||||
}
|
||||
|
||||
export function hasRematchSlotHints(pathQa) {
|
||||
return (pathQa?.optimization_hints || []).some((h) => h?.action === 'rematch_slot')
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user