expAge Claude Opus 4.7 commited on
Commit
f6b8585
·
1 Parent(s): 31eca78

fix(.enrich): SKIP les triplets non-inférés au lieu d'écrire un placeholder

Browse files

Énorme bug logique de la version précédente : écrire un triplet
dans un .enrich avec « non inférable depuis JDM » comme explication
= soumettre du garbage à JDM ! Le triplet aurait quand même atterri
dans le fichier de soumission.

Maintenant : pour un fichier .enrich, si le triplet n'est PAS dans le
registry de consolidation (= pas passé par consolidate_candidate ou
pas inféré), on le SKIP entièrement. Pas d'écriture, pas de garbage.

Les skipped sont remontés à l'agent via `skipped_no_inference_proof`
+ `skipped_count` + `skipped_note` (« re-passe par consolidate_candidate
avant »). L'agent peut soit ré-inférer, soit retirer ces triplets.

Tests : passé de .enrich à .txt pour les tests d'upload qui ne sont
pas dans un contexte agent (pas de registry peuplé).

183 tests verts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

src/jdm_agent/tools/jdm_tools.py CHANGED
@@ -1792,35 +1792,39 @@ def write_submission_file(
1792
  # Mode TRIPLETS canonique (que des dicts).
1793
  # Pour .enrich : l'explication DOIT venir du registry de
1794
  # consolidation (mise là par consolidate_candidate après inférence).
1795
- # Toute formulation libre du LLM est REJETÉE on n'écrit que
1796
- # ce qui a été PROUVÉ par le moteur d'inférence. Si le triplet
1797
- # n'est pas dans le registry, on met « non inférable depuis JDM »
1798
- # (explicite, ne laisse pas croire à une preuve).
1799
- # Pour les autres extensions (.audit, .err, .stat) qui utiliseraient
1800
- # quand même le schéma triplet : le texte libre du LLM est accepté
1801
- # (fallback registry → LLM text).
1802
  from jdm_agent.enrich.validators import get_consolidation
1803
  is_enrich_file = str(path).lower().endswith(".enrich")
1804
- def _resolve_explanation(t: dict) -> str:
1805
- from_registry = get_consolidation(
1806
- str(t["term"]), str(t["relation"]), str(t["target"])
1807
- )
 
 
 
1808
  if from_registry and from_registry.get("explanation"):
1809
- return from_registry["explanation"]
1810
- # Pas dans le registry.
1811
- if is_enrich_file:
1812
- # STRICT : pas de texte libre LLM pour .enrich.
1813
- return "non inférable depuis JDM"
1814
- # Autres extensions : le texte libre LLM est OK.
1815
- return str(t.get("explanation") or "")
1816
- cands = [Candidate(
1817
- term=str(t["term"]), relation=str(t["relation"]), target=str(t["target"]),
1818
- annotation=str(t.get("annotation") or ""),
1819
- consolidation_explanation=_resolve_explanation(t),
1820
- confidence=0.7, source="agent",
1821
- validation_status="ok", consolidation_status="consolidated",
1822
- ) for t in dict_items]
1823
- n = _write_sub(path, cands, client=c)
 
 
1824
  out = {
1825
  "path": path, "count": n,
1826
  "lines": [
@@ -1830,6 +1834,13 @@ def write_submission_file(
1830
  ],
1831
  "mode": "triplets",
1832
  }
 
 
 
 
 
 
 
1833
 
1834
  if upload:
1835
  from jdm_agent.enrich.uploader import submit_to_jdm
 
1792
  # Mode TRIPLETS canonique (que des dicts).
1793
  # Pour .enrich : l'explication DOIT venir du registry de
1794
  # consolidation (mise là par consolidate_candidate après inférence).
1795
+ # Si le triplet N'EST PAS dans le registry on le SKIP entièrement.
1796
+ # Un .enrich ne doit contenir QUE des triplets prouvés par
1797
+ # inférence pas de garbage à JDM. Les skipped sont remontés
1798
+ # dans `skipped_no_inference_proof` pour que l'agent les voie
1799
+ # et puisse soit les ré-inférer, soit les retirer de sa pile.
1800
+ # Pour .audit / .err / .stat (si schéma triplet) : texte libre OK.
 
1801
  from jdm_agent.enrich.validators import get_consolidation
1802
  is_enrich_file = str(path).lower().endswith(".enrich")
1803
+ cands: list[Candidate] = []
1804
+ skipped_no_proof: list[dict] = []
1805
+ for t in dict_items:
1806
+ term_v = str(t.get("term") or "")
1807
+ rel_v = str(t.get("relation") or "")
1808
+ tgt_v = str(t.get("target") or "")
1809
+ from_registry = get_consolidation(term_v, rel_v, tgt_v)
1810
  if from_registry and from_registry.get("explanation"):
1811
+ explanation = from_registry["explanation"]
1812
+ elif is_enrich_file:
1813
+ # Pas de preuve d'inférence pour un .enrich → SKIP.
1814
+ skipped_no_proof.append({
1815
+ "term": term_v, "relation": rel_v, "target": tgt_v,
1816
+ })
1817
+ continue
1818
+ else:
1819
+ explanation = str(t.get("explanation") or "")
1820
+ cands.append(Candidate(
1821
+ term=term_v, relation=rel_v, target=tgt_v,
1822
+ annotation=str(t.get("annotation") or ""),
1823
+ consolidation_explanation=explanation,
1824
+ confidence=0.7, source="agent",
1825
+ validation_status="ok", consolidation_status="consolidated",
1826
+ ))
1827
+ n = _write_sub(path, cands, client=c) if cands else 0
1828
  out = {
1829
  "path": path, "count": n,
1830
  "lines": [
 
1834
  ],
1835
  "mode": "triplets",
1836
  }
1837
+ if skipped_no_proof:
1838
+ out["skipped_no_inference_proof"] = skipped_no_proof
1839
+ out["skipped_count"] = len(skipped_no_proof)
1840
+ out["skipped_note"] = (
1841
+ "Triplets non écrits car absents du registry d'inférence. "
1842
+ "Re-passe-les par consolidate_candidate avant write_submission_file."
1843
+ )
1844
 
1845
  if upload:
1846
  from jdm_agent.enrich.uploader import submit_to_jdm
tests/test_tools.py CHANGED
@@ -381,7 +381,7 @@ def test_write_submission_file_local_only_default(tmp_path, monkeypatch):
381
  "term": "chat", "relation": "r_isa", "target": "mammifère",
382
  "annotation": "constitutif", "explanation": "trivialement",
383
  }],
384
- "path": str(tmp_path / "sub.enrich"),
385
  })
386
  assert out["count"] == 1
387
  assert "upload" not in out # pas tenté
@@ -402,7 +402,7 @@ def test_write_submission_file_with_upload_success(tmp_path, monkeypatch):
402
  "term": "chat", "relation": "r_isa", "target": "mammifère",
403
  "annotation": "", "explanation": "trivialement",
404
  }],
405
- "path": str(tmp_path / "sub.enrich"),
406
  "upload": True,
407
  "model_name": "claude-sonnet-4-7",
408
  "api_key": "explicit-key",
@@ -426,7 +426,7 @@ def test_write_submission_file_upload_without_api_key(tmp_path, monkeypatch):
426
  "term": "chat", "relation": "r_isa", "target": "mammifère",
427
  "annotation": "", "explanation": "trivialement",
428
  }],
429
- "path": str(tmp_path / "sub.enrich"),
430
  "upload": True,
431
  "model_name": "claude-haiku",
432
  })
 
381
  "term": "chat", "relation": "r_isa", "target": "mammifère",
382
  "annotation": "constitutif", "explanation": "trivialement",
383
  }],
384
+ "path": str(tmp_path / "sub.txt"),
385
  })
386
  assert out["count"] == 1
387
  assert "upload" not in out # pas tenté
 
402
  "term": "chat", "relation": "r_isa", "target": "mammifère",
403
  "annotation": "", "explanation": "trivialement",
404
  }],
405
+ "path": str(tmp_path / "sub.txt"),
406
  "upload": True,
407
  "model_name": "claude-sonnet-4-7",
408
  "api_key": "explicit-key",
 
426
  "term": "chat", "relation": "r_isa", "target": "mammifère",
427
  "annotation": "", "explanation": "trivialement",
428
  }],
429
+ "path": str(tmp_path / "sub.txt"),
430
  "upload": True,
431
  "model_name": "claude-haiku",
432
  })