@@ -246,6 +246,26 @@ def format_proof_ledger(
246246 obligations : list [ProofObligation ] | None = None ,
247247) -> str :
248248 selected = obligations if obligations is not None else pending_obligations (ledger )
249+ ancestry = []
250+ if len (selected ) == 1 :
251+ by_id = {
252+ item .obligation_id : item
253+ for item in ledger .obligations
254+ }
255+ cursor = selected [0 ].parent_id
256+ visited = set ()
257+ while cursor and cursor not in visited :
258+ visited .add (cursor )
259+ ancestor = by_id .get (cursor )
260+ if ancestor is None :
261+ break
262+ ancestry .append (ancestor )
263+ cursor = ancestor .parent_id
264+ ancestry .reverse ()
265+ ancestry_text = "\n " .join (
266+ f"- { item .obligation_id } : { item .statement } "
267+ for item in ancestry
268+ )
249269 items = "\n " .join (
250270 f"- { item .obligation_id } "
251271 f"{ f' (parent={ item .parent_id } )' if item .parent_id else '' } : "
@@ -254,7 +274,9 @@ def format_proof_ledger(
254274 )
255275 return (
256276 f"PROOF OBLIGATION LEDGER id={ ledger .ledger_id } "
257- f"version={ ledger .version } \n { items } \n "
277+ f"version={ ledger .version } \n "
278+ f"COMPLETE ANCESTOR CHAIN:\n { ancestry_text or '(root target)' } \n "
279+ f"CURRENT TARGET:\n { items } \n "
258280 "Generator requirement: emit `### ISSUE_RESPONSE <ID>` for every "
259281 "pending ID, with `Correction:`, `Derivation:`, and `Remaining gap:`. "
260282 "Critic requirement: emit `### ISSUE_VERDICT <ID>` for every pending "
@@ -411,6 +433,144 @@ def _obligation_terms(statement: str) -> set[str]:
411433 }
412434
413435
436+ def _canonical_claim (statement : str ) -> str :
437+ text = _normalize_obligation_statement (statement )
438+ replacements = (
439+ (r"\blocal accumulation rate\b|\blocal density\b" , "local_density" ),
440+ (
441+ r"\bglobal exponent of convergence\b|\bglobal growth order\b|"
442+ r"\bglobal order\b|\bgrowth order\b|\bglobal growth\b" ,
443+ "global_order" ,
444+ ),
445+ (
446+ r"\blower bound\b|\bimposes a lower bound\b|\bmust satisfy\b|"
447+ r"\bforces\b|\bforce\b" ,
448+ "implies_bound" ,
449+ ),
450+ (r"\bcritical density\b|\bdensity threshold\b" , "density_threshold" ),
451+ (r"\baccumulation\b|\bclump(?:ing)?\b" , "concentration" ),
452+ (r"\bsingularity\b|\bpole\b" , "singularity" ),
453+ (r"\bsequence of zeros\b|\bzero sequence\b" , "zero_sequence" ),
454+ (r"\bfunction\b|\bfunctional relationship\b" , "mapping" ),
455+ (r"\bequivalence\b|\bsaturation\b|\bgap\b" , "relation" ),
456+ )
457+ for pattern , replacement in replacements :
458+ text = re .sub (pattern , replacement , text )
459+ # Alpha-normalize mathematical variable names while preserving operators
460+ # and semantic nouns. This makes rho/delta/lambda renamings comparable.
461+ text = re .sub (
462+ r"\b(?:rho|delta|lambda|epsilon|phi|sigma|p|m|s|z|r|n)\d*\b" ,
463+ "var" ,
464+ text ,
465+ )
466+ return " " .join (text .split ())
467+
468+
469+ def _claim_structure (statement : str ) -> tuple [set [str ], set [str ]]:
470+ canonical = _canonical_claim (statement )
471+ markers = (
472+ " without implies_bound " ,
473+ " then " ,
474+ " implies_bound " ,
475+ " such that " ,
476+ " implies " ,
477+ )
478+ split_at = - 1
479+ marker_size = 0
480+ for marker in markers :
481+ position = canonical .find (marker )
482+ if position >= 0 and (split_at < 0 or position < split_at ):
483+ split_at = position
484+ marker_size = len (marker )
485+ if split_at < 0 :
486+ terms = set (canonical .split ())
487+ return terms , terms
488+ premise = set (canonical [:split_at ].split ())
489+ conclusion = set (canonical [split_at + marker_size :].split ())
490+ return premise , conclusion
491+
492+
493+ def _semantic_concepts (statement : str ) -> set [str ]:
494+ text = _canonical_claim (statement )
495+ concepts = set ()
496+ checks = {
497+ "density" : ("density" , "concentration" ),
498+ "global_order" : ("global_order" ,),
499+ "singularity" : ("singularity" , "residue" ),
500+ "genus_or_order" : ("genus" , " order " ),
501+ "convergence" : ("converge" , "diverge" , "limit" ),
502+ "zero_sequence" : ("zero_sequence" , "zeros" ),
503+ "threshold_or_bound" : ("threshold" , "bound" , "above" , "below" ),
504+ "local_global_relation" : ("local" , "global" ),
505+ }
506+ padded = f" { text } "
507+ for concept , needles in checks .items ():
508+ if all (needle in padded for needle in needles ) if (
509+ concept == "local_global_relation"
510+ ) else any (needle in padded for needle in needles ):
511+ concepts .add (concept )
512+ return concepts
513+
514+
515+ def _semantic_equivalence (left : str , right : str ) -> tuple [bool , float ]:
516+ left_canonical = _canonical_claim (left )
517+ right_canonical = _canonical_claim (right )
518+ left_terms = set (left_canonical .split ())
519+ right_terms = set (right_canonical .split ())
520+ union = left_terms | right_terms
521+ jaccard = (
522+ len (left_terms & right_terms ) / len (union )
523+ if union else 1.0
524+ )
525+ sequence = difflib .SequenceMatcher (
526+ None ,
527+ left_canonical ,
528+ right_canonical ,
529+ ).ratio ()
530+ left_premise , left_conclusion = _claim_structure (left )
531+ right_premise , right_conclusion = _claim_structure (right )
532+
533+ def overlap (first : set [str ], second : set [str ]) -> float :
534+ denominator = max (1 , min (len (first ), len (second )))
535+ return len (first & second ) / denominator
536+
537+ structural = min (
538+ overlap (left_premise , right_premise ),
539+ overlap (left_conclusion , right_conclusion ),
540+ )
541+ left_concepts = _semantic_concepts (left )
542+ right_concepts = _semantic_concepts (right )
543+ concept_overlap = overlap (left_concepts , right_concepts )
544+ score = max (jaccard , sequence , structural , concept_overlap )
545+ equivalent = (
546+ (jaccard >= 0.52 and structural >= 0.60 )
547+ or sequence >= 0.68
548+ or structural >= 0.78
549+ or (
550+ min (len (left_concepts ), len (right_concepts )) >= 4
551+ and concept_overlap >= 0.80
552+ )
553+ )
554+ return equivalent , score
555+
556+
557+ def _child_has_structural_delta (parent : str , child : str ) -> bool :
558+ parent_premise , parent_conclusion = _claim_structure (parent )
559+ child_premise , child_conclusion = _claim_structure (child )
560+ new_premise = child_premise - parent_premise
561+ new_conclusion = child_conclusion - parent_conclusion
562+ concrete = {
563+ "compact" , "counterexample" , "exists" , "fixed" , "forall" , "limsup" ,
564+ "neighborhood" , "explicit" , "boundary" , "constant" , "inequality" ,
565+ "converges" , "diverges" , "residue" , "genus" ,
566+ }
567+ return bool (
568+ len (new_premise ) >= 3
569+ or len (new_conclusion ) >= 3
570+ or (child_premise | child_conclusion ) & concrete
571+ )
572+
573+
414574def _frontier_rejection_reason (
415575 ledger : ProofObligationLedger ,
416576 parent_id : str ,
@@ -435,8 +595,27 @@ def _frontier_rejection_reason(
435595 existing_signature = _lemma_signature (item .statement )
436596 if signature and signature == existing_signature :
437597 return f"repeats existing lemma { item .obligation_id } "
598+ if item .obligation_id in ancestors :
599+ equivalent , score = _semantic_equivalence (
600+ item .statement ,
601+ statement ,
602+ )
603+ if equivalent :
604+ return (
605+ f"bidirectionally entails ancestor { item .obligation_id } "
606+ f"after variable normalization (score={ score :.2f} )"
607+ )
438608 if len (terms ) < 6 :
439609 return "frontier is too vague to be a falsifiable smaller obligation"
610+ parent = next (
611+ item for item in ledger .obligations
612+ if item .obligation_id == parent_id
613+ )
614+ if not _child_has_structural_delta (parent .statement , statement ):
615+ return (
616+ "does not declare a new assumption, narrower domain, or "
617+ "falsifiable conclusion"
618+ )
440619 for item in ledger .obligations :
441620 existing_terms = _obligation_terms (item .statement )
442621 union = terms | existing_terms
@@ -450,6 +629,51 @@ def _frontier_rejection_reason(
450629 return ""
451630
452631
632+ def audit_ledger_semantic_duplicates (
633+ ledger : ProofObligationLedger ,
634+ ) -> list [tuple [str , str , float ]]:
635+ by_id = {
636+ item .obligation_id : item
637+ for item in ledger .obligations
638+ }
639+ rejected : list [tuple [str , str , float ]] = []
640+ rejected_ids = set ()
641+ for item in ledger .obligations :
642+ if item .status != "UNRESOLVED" or not item .parent_id :
643+ continue
644+ cursor = item .parent_id
645+ visited = set ()
646+ duplicate_of = ""
647+ duplicate_score = 0.0
648+ while cursor and cursor not in visited :
649+ visited .add (cursor )
650+ ancestor = by_id .get (cursor )
651+ if ancestor is None :
652+ break
653+ equivalent , score = _semantic_equivalence (
654+ ancestor .statement ,
655+ item .statement ,
656+ )
657+ if equivalent :
658+ duplicate_of = ancestor .obligation_id
659+ duplicate_score = score
660+ break
661+ cursor = ancestor .parent_id
662+ if duplicate_of or item .parent_id in rejected_ids :
663+ duplicate_of = duplicate_of or item .parent_id
664+ item .status = "REJECTED_DUPLICATE"
665+ item .last_evidence = (
666+ f"Semantic duplicate/cyclic descendant of { duplicate_of } ."
667+ )
668+ rejected_ids .add (item .obligation_id )
669+ rejected .append (
670+ (item .obligation_id , duplicate_of , duplicate_score ),
671+ )
672+ if rejected :
673+ ledger .version += 1
674+ return rejected
675+
676+
453677def create_child_obligations (
454678 ledger : ProofObligationLedger ,
455679 critic_text : str ,
@@ -1244,6 +1468,19 @@ def get_stats():
12441468 if critic_issue_batch is not None else ""
12451469 )
12461470 proof_ledger = load_proof_ledger (proof_ledger_path )
1471+ semantic_rejections = (
1472+ audit_ledger_semantic_duplicates (proof_ledger )
1473+ if proof_ledger is not None else []
1474+ )
1475+ if proof_ledger is not None and semantic_rejections :
1476+ save_proof_ledger (proof_ledger_path , proof_ledger )
1477+ for obligation_id , ancestor_id , score in semantic_rejections :
1478+ print (
1479+ "[proof-obligation-retro-rejected] "
1480+ f"id={ obligation_id } duplicate_of={ ancestor_id } "
1481+ f"score={ score :.2f} " ,
1482+ flush = True ,
1483+ )
12471484 turn_obligations = pending_obligations (proof_ledger )
12481485 if research_candidate is not None and turn_obligations :
12491486 target_id = str (research_candidate .TARGET_OBLIGATION_ID )
@@ -1504,16 +1741,22 @@ def get_stats():
15041741 },
15051742 id_repairs ,
15061743 )
1507- created_obligations = create_child_obligations (
1508- proof_ledger ,
1509- critic_text ,
1510- run_id ,
1511- {
1512- item .obligation_id
1513- for item in turn_obligations
1514- },
1515- rejected_frontiers ,
1516- )
1744+ if missing_issues :
1745+ rejected_frontiers .append (
1746+ "Generator coverage incomplete; child creation "
1747+ f"forbidden for { ',' .join (sorted (missing_issues ))} " ,
1748+ )
1749+ else :
1750+ created_obligations = create_child_obligations (
1751+ proof_ledger ,
1752+ critic_text ,
1753+ run_id ,
1754+ {
1755+ item .obligation_id
1756+ for item in turn_obligations
1757+ },
1758+ rejected_frontiers ,
1759+ )
15171760 for model_id , target_id in id_repairs :
15181761 print (
15191762 "[critic-id-repaired] "
0 commit comments