Enhance Path Exclusion Logic and Semantic Brief Enrichment
Some checks failed
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Has been cancelled
Some checks failed
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Has been cancelled
- Introduced `resolve_path_anti_patterns` to improve handling of path exclusions based on explicit negations and semantic briefs. - Updated `enrich_brief_with_path_constraints` to incorporate path-specific exclusions into semantic briefs, enhancing exercise relevance. - Modified roadmap step annotation to allow for anti-pattern overrides, improving flexibility in exercise selection. - Enhanced tests to validate new path exclusion features and ensure correct functionality against learning goals. - Incremented application version to reflect these updates.
This commit is contained in:
parent
07e147bc76
commit
3c12363b8f
|
|
@ -36,7 +36,9 @@ from planning_exercise_semantics import (
|
|||
brief_to_summary_dict,
|
||||
build_semantic_brief,
|
||||
build_stage_match_brief,
|
||||
enrich_brief_with_path_constraints,
|
||||
enrich_target_with_semantic_expectations,
|
||||
resolve_path_anti_patterns,
|
||||
exercise_passes_path_semantic_gate,
|
||||
pick_best_path_hit,
|
||||
resolve_semantic_skill_weights,
|
||||
|
|
@ -487,6 +489,7 @@ def _annotate_roadmap_step(
|
|||
stage_spec: StageSpecArtifact,
|
||||
major_step: Optional[MajorStep],
|
||||
skill_expectations: Optional[Dict[str, Any]] = None,
|
||||
anti_patterns_override: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
reasons = list(step.get("reasons") or [])
|
||||
learning_goal = (stage_spec.learning_goal or "").strip()
|
||||
|
|
@ -508,8 +511,9 @@ def _annotate_roadmap_step(
|
|||
step["roadmap_major_step_index"] = stage_spec.major_step_index
|
||||
step["roadmap_phase"] = major_step.phase if major_step else None
|
||||
step["roadmap_learning_goal"] = learning_goal or None
|
||||
if stage_spec.anti_patterns:
|
||||
step["roadmap_anti_patterns"] = list(stage_spec.anti_patterns)
|
||||
anti = list(anti_patterns_override or stage_spec.anti_patterns or [])
|
||||
if anti:
|
||||
step["roadmap_anti_patterns"] = anti
|
||||
step["roadmap_match_source"] = "stage_spec"
|
||||
if skill_expectations:
|
||||
step["skill_expectations"] = skill_expectations
|
||||
|
|
@ -589,7 +593,6 @@ def _build_steps_roadmap_first(
|
|||
)
|
||||
step_kind = resolve_step_exercise_kind_filter(stage_spec, body.exercise_kind_any)
|
||||
stage_goal = (stage_spec.learning_goal or "").strip()
|
||||
stage_anti = list(stage_spec.anti_patterns or [])
|
||||
path_context_note = None
|
||||
if rs_dump:
|
||||
ctx_parts = [
|
||||
|
|
@ -598,6 +601,12 @@ def _build_steps_roadmap_first(
|
|||
str(rs_dump.get("roadmap_notes") or "").strip()[:120],
|
||||
]
|
||||
path_context_note = " ".join(p for p in ctx_parts if p)[:240] or None
|
||||
path_anti = resolve_path_anti_patterns(
|
||||
goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
extra_context=path_context_note,
|
||||
)
|
||||
stage_anti = list(dict.fromkeys([*(stage_spec.anti_patterns or []), *path_anti]))
|
||||
stage_match_brief = build_stage_match_brief(
|
||||
learning_goal=stage_goal,
|
||||
anti_patterns=stage_anti,
|
||||
|
|
@ -605,6 +614,7 @@ def _build_steps_roadmap_first(
|
|||
load_profile=list(stage_spec.load_profile or []),
|
||||
phase=major.phase if major else None,
|
||||
path_context_note=path_context_note,
|
||||
path_anti_patterns=path_anti,
|
||||
)
|
||||
|
||||
hits, _, _, _ = _run_path_step_retrieval(
|
||||
|
|
@ -652,6 +662,7 @@ def _build_steps_roadmap_first(
|
|||
stage_spec=stage_spec,
|
||||
major_step=major,
|
||||
skill_expectations=skill_exp_api,
|
||||
anti_patterns_override=stage_anti,
|
||||
)
|
||||
steps.append(step)
|
||||
eid = int(step["exercise_id"])
|
||||
|
|
@ -795,7 +806,13 @@ def _run_evaluate_only_path_qa(
|
|||
bridge_inserts=bridge_inserts,
|
||||
)
|
||||
|
||||
off_topic_steps = detect_off_topic_steps(cur, steps, brief=semantic_brief)
|
||||
off_topic_steps = detect_off_topic_steps(
|
||||
cur,
|
||||
steps,
|
||||
brief=semantic_brief,
|
||||
goal_query=goal_query,
|
||||
)
|
||||
steps, stripped_off_topic = strip_off_topic_steps_from_path(steps, off_topic_steps)
|
||||
llm_gap_specs = parse_llm_suggested_new_exercises(
|
||||
llm_qa,
|
||||
brief=semantic_brief,
|
||||
|
|
@ -807,7 +824,7 @@ def _run_evaluate_only_path_qa(
|
|||
gap_specs = collect_gap_fill_specs(
|
||||
steps=steps,
|
||||
unfilled_gaps=fresh_large_gaps or unfilled_gaps,
|
||||
off_topic_steps=off_topic_steps,
|
||||
off_topic_steps=off_topic_steps if not stripped_off_topic else [],
|
||||
llm_specs=llm_gap_specs,
|
||||
brief=semantic_brief,
|
||||
goal_query=goal_query,
|
||||
|
|
@ -902,6 +919,20 @@ def suggest_progression_path(
|
|||
semantic_brief, semantic_llm_applied = try_enrich_semantic_brief_with_llm(
|
||||
cur, goal_query, semantic_brief
|
||||
)
|
||||
extra_path_ctx = " ".join(
|
||||
p
|
||||
for p in (
|
||||
(body.start_situation or "").strip(),
|
||||
(body.target_state or "").strip(),
|
||||
(body.roadmap_notes or "").strip(),
|
||||
)
|
||||
if p
|
||||
)
|
||||
semantic_brief = enrich_brief_with_path_constraints(
|
||||
semantic_brief,
|
||||
goal_query,
|
||||
extra_context=extra_path_ctx or None,
|
||||
)
|
||||
|
||||
roadmap_first = bool(body.roadmap_first)
|
||||
roadmap_only = bool(body.roadmap_only)
|
||||
|
|
@ -1222,7 +1253,12 @@ def suggest_progression_path(
|
|||
if llm_qa.get("overall_ok") or (q_val is not None and q_val >= 0.45):
|
||||
steps, reorder_applied, reorder_notes = apply_llm_path_reorder(steps, llm_qa)
|
||||
|
||||
off_topic_steps = detect_off_topic_steps(cur, steps, brief=semantic_brief)
|
||||
off_topic_steps = detect_off_topic_steps(
|
||||
cur,
|
||||
steps,
|
||||
brief=semantic_brief,
|
||||
goal_query=goal_query,
|
||||
)
|
||||
steps, stripped_off_topic = strip_off_topic_steps_from_path(steps, off_topic_steps)
|
||||
if stripped_off_topic:
|
||||
off_topic_steps = []
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ from openrouter_chat import (
|
|||
|
||||
from planning_exercise_semantics import (
|
||||
PlanningSemanticBrief,
|
||||
_blob_from_fields,
|
||||
_blob_matches_stage_excludes,
|
||||
brief_to_summary_dict,
|
||||
exercise_passes_path_semantic_gate,
|
||||
exercise_passes_stage_learning_goal_gate,
|
||||
resolve_path_anti_patterns,
|
||||
score_exercise_semantic_relevance,
|
||||
semantic_brief_for_stage,
|
||||
step_phase_for_index,
|
||||
|
|
@ -398,17 +401,39 @@ def detect_off_topic_steps(
|
|||
steps: Sequence[Mapping[str, Any]],
|
||||
*,
|
||||
brief: PlanningSemanticBrief,
|
||||
goal_query: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Schritte ohne Bezug zum Pfad-Thema (z. B. reine Kraftübungen bei Mae Geri)."""
|
||||
if brief.semantic_strength < 0.55 or len(steps) < 2:
|
||||
return []
|
||||
|
||||
path_anti = resolve_path_anti_patterns(goal_query or "", semantic_brief=brief)
|
||||
off_topic: List[Dict[str, Any]] = []
|
||||
total = len(steps)
|
||||
for idx, step in enumerate(steps):
|
||||
if step.get("is_ai_proposal") or step.get("exercise_id") is None:
|
||||
continue
|
||||
bundle = _load_exercise_text_bundle(cur, int(step["exercise_id"]))
|
||||
blob = _blob_from_fields(
|
||||
bundle["title"],
|
||||
bundle["summary"],
|
||||
bundle["goal"],
|
||||
bundle["variant_names"],
|
||||
)
|
||||
step_anti = list(step.get("roadmap_anti_patterns") or []) + path_anti
|
||||
if step_anti and _blob_matches_stage_excludes(blob, step_anti):
|
||||
off_topic.append(
|
||||
{
|
||||
"step_index": idx,
|
||||
"exercise_id": int(step["exercise_id"]),
|
||||
"title": step.get("title") or bundle["title"],
|
||||
"semantic_score": 0.0,
|
||||
"expected_phase": (step.get("roadmap_phase") or "").strip().lower() or None,
|
||||
"issue": "path_exclude",
|
||||
"reasons": ["Widerspricht Pfad-Ausschlüssen (z. B. Kumite)"],
|
||||
}
|
||||
)
|
||||
continue
|
||||
stage_goal = (step.get("roadmap_learning_goal") or "").strip()
|
||||
phase = (step.get("roadmap_phase") or "").strip().lower() or step_phase_for_index(
|
||||
brief, idx, total
|
||||
|
|
@ -529,9 +554,10 @@ def strip_off_topic_steps_from_path(
|
|||
return steps, []
|
||||
|
||||
by_index = {int(o["step_index"]): dict(o) for o in off_topic_steps if o.get("step_index") is not None}
|
||||
indices = sorted(by_index.keys(), reverse=True)
|
||||
if len(steps) - len(indices) < min_remaining:
|
||||
max_remove = max(0, len(steps) - min_remaining)
|
||||
if max_remove <= 0:
|
||||
return steps, []
|
||||
indices = sorted(by_index.keys(), reverse=True)[:max_remove]
|
||||
|
||||
out = list(steps)
|
||||
removed: List[Dict[str, Any]] = []
|
||||
|
|
|
|||
|
|
@ -246,6 +246,11 @@ def build_semantic_brief(query: Optional[str]) -> PlanningSemanticBrief:
|
|||
if len(q) >= 24 and not technique:
|
||||
strength = max(strength, 0.4)
|
||||
|
||||
path_constraints = parse_stage_goal_constraints(q)
|
||||
for item in path_constraints.exclude_phrases:
|
||||
if item not in exclude:
|
||||
exclude.append(item)
|
||||
|
||||
return PlanningSemanticBrief(
|
||||
primary_topic=primary,
|
||||
topic_type=topic_type,
|
||||
|
|
@ -775,6 +780,63 @@ def _blob_matches_stage_excludes(blob: str, exclude_phrases: Sequence[str]) -> b
|
|||
return False
|
||||
|
||||
|
||||
def resolve_path_anti_patterns(
|
||||
goal_query: str,
|
||||
*,
|
||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
extra_context: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Pfadweite Ausschlüsse — nur aus expliziten Quellen, kein Themen-Raten.
|
||||
|
||||
Quellen (in dieser Reihenfolge):
|
||||
1. Negationen in Anfrage/Kontext (ohne/kein/nicht …) via parse_stage_goal_constraints
|
||||
2. exclude_phrases im Semantic Brief (inkl. LLM/Technik-Regeln)
|
||||
3. stage_specs.anti_patterns (Roadmap-Stufe, vom Trainer oder LLM)
|
||||
|
||||
Keine stillen Ausschlüsse aus dem Hauptthema (z. B. „Mawashi“ → kein Kumite).
|
||||
"""
|
||||
parts = [str(goal_query or "").strip(), str(extra_context or "").strip()]
|
||||
combined = " ".join(p for p in parts if p)
|
||||
if not combined and not semantic_brief:
|
||||
return []
|
||||
|
||||
constraints = parse_stage_goal_constraints(combined) if combined else StageGoalConstraints()
|
||||
out: List[str] = []
|
||||
for item in constraints.exclude_phrases:
|
||||
if item and item not in out:
|
||||
out.append(item)
|
||||
|
||||
if semantic_brief:
|
||||
for raw in semantic_brief.exclude_phrases or []:
|
||||
for expanded in _expand_stage_exclude_phrase(str(raw or "")):
|
||||
if expanded and expanded not in out:
|
||||
out.append(expanded)
|
||||
|
||||
return out[:24]
|
||||
|
||||
|
||||
def enrich_brief_with_path_constraints(
|
||||
brief: PlanningSemanticBrief,
|
||||
goal_query: str,
|
||||
*,
|
||||
extra_context: Optional[str] = None,
|
||||
) -> PlanningSemanticBrief:
|
||||
"""Negationen/Ausschlüsse aus der Gesamtanfrage in den Semantic Brief übernehmen."""
|
||||
anti = resolve_path_anti_patterns(
|
||||
goal_query,
|
||||
semantic_brief=brief,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
if not anti:
|
||||
return brief
|
||||
exclude = list(brief.exclude_phrases or [])
|
||||
for item in anti:
|
||||
if item not in exclude:
|
||||
exclude.append(item)
|
||||
return brief.model_copy(update={"exclude_phrases": exclude[:16]})
|
||||
|
||||
|
||||
_MIN_STAGE_FIT_SEMANTIC = 0.30
|
||||
_MIN_STAGE_FIT_RELAXED = 0.20
|
||||
|
||||
|
|
@ -787,6 +849,7 @@ def build_stage_match_brief(
|
|||
load_profile: Optional[Sequence[str]] = None,
|
||||
phase: Optional[str] = None,
|
||||
path_context_note: Optional[str] = None,
|
||||
path_anti_patterns: Optional[Sequence[str]] = None,
|
||||
) -> PlanningSemanticBrief:
|
||||
"""
|
||||
Stufen-zentrierter Semantik-Brief — unabhängig vom Gesamt-Pfad-Thema.
|
||||
|
|
@ -797,7 +860,12 @@ def build_stage_match_brief(
|
|||
if len(lg) < 3:
|
||||
return PlanningSemanticBrief(semantic_strength=0.0)
|
||||
|
||||
constraints = parse_stage_goal_constraints(lg, anti_patterns)
|
||||
merged_anti: List[str] = []
|
||||
for raw in list(anti_patterns or []) + list(path_anti_patterns or []):
|
||||
s = str(raw or "").strip()
|
||||
if s and s not in merged_anti:
|
||||
merged_anti.append(s)
|
||||
constraints = parse_stage_goal_constraints(lg, merged_anti)
|
||||
must: List[str] = []
|
||||
norm_lg = _normalize_phrase(lg)
|
||||
for token in constraints.positive_tokens:
|
||||
|
|
@ -1134,7 +1202,9 @@ __all__ = [
|
|||
"StageGoalConstraints",
|
||||
"apply_stage_match_retrieval_weights",
|
||||
"build_stage_match_brief",
|
||||
"enrich_brief_with_path_constraints",
|
||||
"exercise_passes_stage_fit",
|
||||
"resolve_path_anti_patterns",
|
||||
"exercise_passes_stage_learning_goal_gate",
|
||||
"merge_semantic_brief_llm",
|
||||
"parse_stage_goal_constraints",
|
||||
|
|
|
|||
|
|
@ -847,12 +847,24 @@ def build_stage_specs(
|
|||
major_steps: Sequence[MajorStep],
|
||||
*,
|
||||
goal_analysis: GoalAnalysisArtifact,
|
||||
goal_query: str = "",
|
||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
) -> List[StageSpecArtifact]:
|
||||
"""Phase C — Stufenspezifikation je Major Step (heuristisch)."""
|
||||
from planning_exercise_semantics import resolve_path_anti_patterns
|
||||
|
||||
topic = goal_analysis.primary_topic or "Technik"
|
||||
path_anti = resolve_path_anti_patterns(goal_query, semantic_brief=semantic_brief)
|
||||
specs: List[StageSpecArtifact] = []
|
||||
for step in major_steps:
|
||||
phase = (step.phase or "vertiefung").lower()
|
||||
anti = [
|
||||
"reine Kraftübung ohne Technikbezug",
|
||||
f"andere Technik als {topic}" if topic else "themenfremde Übung",
|
||||
]
|
||||
for item in path_anti:
|
||||
if item not in anti:
|
||||
anti.append(item)
|
||||
specs.append(
|
||||
StageSpecArtifact(
|
||||
major_step_index=step.index,
|
||||
|
|
@ -863,10 +875,7 @@ def build_stage_specs(
|
|||
f"Bezug zu {topic}",
|
||||
f"Phase {phase} erkennbar im Übungsziel",
|
||||
],
|
||||
anti_patterns=[
|
||||
"reine Kraftübung ohne Technikbezug",
|
||||
f"andere Technik als {topic}" if topic else "themenfremde Übung",
|
||||
],
|
||||
anti_patterns=anti[:14],
|
||||
)
|
||||
)
|
||||
return specs
|
||||
|
|
@ -927,14 +936,24 @@ def roadmap_context_from_override(
|
|||
)
|
||||
)
|
||||
if not all(s.exercise_type for s in stage_specs):
|
||||
rebuilt = build_stage_specs(majors, goal_analysis=goal_analysis)
|
||||
rebuilt = build_stage_specs(
|
||||
majors,
|
||||
goal_analysis=goal_analysis,
|
||||
goal_query=goal_query.strip(),
|
||||
semantic_brief=semantic_brief,
|
||||
)
|
||||
for i, spec in enumerate(stage_specs):
|
||||
if not spec.exercise_type:
|
||||
spec.exercise_type = rebuilt[i].exercise_type
|
||||
if not spec.load_profile:
|
||||
spec.load_profile = list(rebuilt[i].load_profile)
|
||||
else:
|
||||
stage_specs = build_stage_specs(majors, goal_analysis=goal_analysis)
|
||||
stage_specs = build_stage_specs(
|
||||
majors,
|
||||
goal_analysis=goal_analysis,
|
||||
goal_query=goal_query.strip(),
|
||||
semantic_brief=semantic_brief,
|
||||
)
|
||||
|
||||
return ProgressionRoadmapContext(
|
||||
goal_query=goal_query.strip(),
|
||||
|
|
@ -1103,7 +1122,12 @@ def run_progression_roadmap_pipeline(
|
|||
)
|
||||
ctx.roadmap = roadmap
|
||||
|
||||
stage_specs = build_stage_specs(roadmap.major_steps, goal_analysis=goal_analysis)
|
||||
stage_specs = build_stage_specs(
|
||||
roadmap.major_steps,
|
||||
goal_analysis=goal_analysis,
|
||||
goal_query=goal_query,
|
||||
semantic_brief=brief,
|
||||
)
|
||||
if include_llm_roadmap and cur is not None:
|
||||
llm_specs, spec_ok = try_llm_stage_specs(
|
||||
cur,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
"""Tests Roadmap-Stufen-Match — Gate gegen themenfremde Übungen."""
|
||||
from planning_exercise_semantics import (
|
||||
build_semantic_brief,
|
||||
build_stage_match_brief,
|
||||
enrich_brief_with_path_constraints,
|
||||
exercise_passes_stage_learning_goal_gate,
|
||||
exercise_passes_stage_fit,
|
||||
pick_best_path_hit,
|
||||
resolve_path_anti_patterns,
|
||||
score_exercise_stage_fit,
|
||||
semantic_brief_for_stage,
|
||||
build_semantic_brief,
|
||||
)
|
||||
from planning_exercise_path_qa import strip_off_topic_steps_from_path
|
||||
|
||||
|
||||
def test_stage_gate_accepts_learning_goal_in_title():
|
||||
|
|
@ -174,6 +178,83 @@ def test_pick_best_rejects_mawashi_tritt_precision_for_coordination_slot():
|
|||
assert int(chosen["id"]) == 100
|
||||
|
||||
|
||||
def test_path_anti_patterns_from_keine_kumite_anwendung():
|
||||
q = "gesprungener Mawashi Geri Sprungphase, keine Kumite-Anwendung gewünscht"
|
||||
brief = enrich_brief_with_path_constraints(build_semantic_brief(q), q)
|
||||
anti = resolve_path_anti_patterns(q, semantic_brief=brief)
|
||||
assert any("kumite" in a for a in anti)
|
||||
|
||||
|
||||
def test_stage_fit_rejects_kumite_when_path_excludes_kumite():
|
||||
q = "gesprungener Mawashi Geri, keine Kumite-Anwendung"
|
||||
brief = enrich_brief_with_path_constraints(build_semantic_brief(q), q)
|
||||
path_anti = resolve_path_anti_patterns(q, semantic_brief=brief)
|
||||
stage_goal = "Sprungkraft und Koordination für gesprungenen Mawashi Geri"
|
||||
stage_brief = build_stage_match_brief(
|
||||
learning_goal=stage_goal,
|
||||
anti_patterns=path_anti,
|
||||
path_anti_patterns=path_anti,
|
||||
)
|
||||
assert not exercise_passes_stage_fit(
|
||||
learning_goal=stage_goal,
|
||||
title="Kumite Distanztraining Mawashi",
|
||||
summary="Partner-Kumite mit Trittanwendung",
|
||||
goal="Anwendung im freien Kampf",
|
||||
stage_brief=stage_brief,
|
||||
anti_patterns=path_anti,
|
||||
)
|
||||
|
||||
|
||||
def test_pick_best_skips_kumite_for_mawashi_athletic_path():
|
||||
q = "gesprungener Mawashi Geri Sprungkraft, keine Kumite-Anwendung"
|
||||
brief = enrich_brief_with_path_constraints(build_semantic_brief(q), q)
|
||||
path_anti = resolve_path_anti_patterns(q, semantic_brief=brief)
|
||||
stage_goal = "Athletisches Sprungtraining für Mawashi Geri"
|
||||
stage_brief = build_stage_match_brief(
|
||||
learning_goal=stage_goal,
|
||||
anti_patterns=path_anti,
|
||||
path_anti_patterns=path_anti,
|
||||
)
|
||||
hits = [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Kumite Mawashi Anwendung",
|
||||
"summary": "Partner Kumite",
|
||||
"goal": "Kampfanwendung",
|
||||
"score": 0.95,
|
||||
"semantic_score": 0.55,
|
||||
"stage_semantic_score": 0.55,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Sprungkraft Plyometrie",
|
||||
"summary": "Absprung und Landung",
|
||||
"goal": "Sprungkraft für Tritttechnik",
|
||||
"score": 0.62,
|
||||
"semantic_score": 0.38,
|
||||
"stage_semantic_score": 0.38,
|
||||
},
|
||||
]
|
||||
chosen = pick_best_path_hit(
|
||||
hits,
|
||||
set(),
|
||||
stage_learning_goal=stage_goal,
|
||||
stage_anti_patterns=path_anti,
|
||||
roadmap_stage_match=True,
|
||||
stage_match_brief=stage_brief,
|
||||
)
|
||||
assert chosen is not None
|
||||
assert int(chosen["id"]) == 2
|
||||
|
||||
|
||||
def test_strip_off_topic_removes_partial_when_most_steps_bad():
|
||||
steps = [{"exercise_id": i, "title": f"E{i}"} for i in range(1, 8)]
|
||||
off_topic = [{"step_index": i, "issue": "path_exclude"} for i in range(5)]
|
||||
out, removed = strip_off_topic_steps_from_path(steps, off_topic, min_remaining=2)
|
||||
assert len(out) == 2
|
||||
assert len(removed) == 5
|
||||
|
||||
|
||||
def test_parse_stage_goal_constraints_extracts_ohne_tritttechnik():
|
||||
from planning_exercise_semantics import parse_stage_goal_constraints
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.220"
|
||||
APP_VERSION = "0.8.222"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260607088"
|
||||
|
||||
|
|
@ -53,6 +53,21 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.222",
|
||||
"date": "2026-06-07",
|
||||
"changes": [
|
||||
"Pfad-Ausschlüsse: athletic→Kumite-Heuristik entfernt — nur explizite Negationen und anti_patterns.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.221",
|
||||
"date": "2026-06-07",
|
||||
"changes": [
|
||||
"Pfad-Ausschlüsse (keine Kumite etc.) aus Anfrage in Brief, stage_specs und Matching-Gates.",
|
||||
"QS entfernt path_exclude-Schritte; partielles Strippen wenn die meisten Slots falsch sind.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.220",
|
||||
"date": "2026-06-07",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user