L2-R1 Unicode keyword authority and upstream quantitative contract repair

#132
core/generation_runner.py CHANGED
@@ -4374,9 +4374,25 @@ class GenerationRunner:
4374
  agent.global_memory["core_golden_metadata"] = extract_golden_metadata(agent, all_paper_texts)
4375
  if mode == "amj":
4376
  data_file = data_paths[0] if data_paths else None
 
 
 
 
 
 
 
 
 
 
 
4377
  _, user_data_md, _, data_metadata = quant_engine.resolve_empirical_data(
4378
- data_file, interface_language, topic=topic, keywords=keywords, agent=agent
 
 
 
 
4379
  )
 
4380
  else:
4381
  raw_interviews = extract_text_from_files(interview_paths[:1]) if interview_paths else ""
4382
  prewrite_started = time.monotonic()
 
4374
  agent.global_memory["core_golden_metadata"] = extract_golden_metadata(agent, all_paper_texts)
4375
  if mode == "amj":
4376
  data_file = data_paths[0] if data_paths else None
4377
+ # L2-R1: establish the validated English identity projection
4378
+ # BEFORE a system-generated quantitative construct/statistical
4379
+ # contract is frozen. The immutable source Title/Keywords remain
4380
+ # semantic authority and are passed separately below.
4381
+ upstream_projection = self._resolve_english_front_matter_projection(
4382
+ agent, state, data_metadata, topic, keywords
4383
+ )
4384
+ internal_topic_en = str(
4385
+ upstream_projection.get("english_title") or topic or ""
4386
+ ).strip()
4387
+ internal_keywords_en = list(upstream_projection.get("english_keywords") or [])
4388
  _, user_data_md, _, data_metadata = quant_engine.resolve_empirical_data(
4389
+ data_file, interface_language,
4390
+ topic=topic, keywords=keywords,
4391
+ internal_topic=internal_topic_en,
4392
+ internal_keywords=internal_keywords_en,
4393
+ agent=agent,
4394
  )
4395
+ data_metadata["front_matter_language_projection"] = dict(upstream_projection)
4396
  else:
4397
  raw_interviews = extract_text_from_files(interview_paths[:1]) if interview_paths else ""
4398
  prewrite_started = time.monotonic()
core/quantitative_engine.py CHANGED
@@ -474,7 +474,9 @@ class QuantitativeEngine:
474
  if isinstance(item, dict) and str(item.get("label") or "").strip()
475
  ]
476
 
477
- def _derive_system_design(self, agent, topic="", keywords=""):
 
 
478
  """Interpret a parsimonious topic-aligned quantitative graph adaptively.
479
 
480
  The existing ``structured_json`` logical node is intentionally reused so
@@ -497,7 +499,10 @@ class QuantitativeEngine:
497
  "background_concepts":[],
498
  }
499
  if agent is None:
500
- return build_quantitative_contract(topic, keywords, fallback_candidate)
 
 
 
501
  prompt = f"""Design the minimum coherent quantitative management-study graph for the topic below.
502
  Return ONLY valid JSON with this schema:
503
  {{
@@ -532,11 +537,17 @@ Keywords: {keywords}"""
532
  logical_node='structured_json',
533
  )
534
  parsed = agent._extract_json_from_text(raw)
535
- contract = build_quantitative_contract(topic, keywords, parsed if isinstance(parsed, dict) else fallback_candidate)
 
 
 
536
  issues = validate_quantitative_contract(contract)
537
  if issues:
538
  logging.warning("Adaptive quantitative design validation requires intent-preserving fallback: %s", issues[:3])
539
- fallback = build_quantitative_contract(topic, keywords, fallback_candidate)
 
 
 
540
  fallback_issues = validate_quantitative_contract(fallback)
541
  if fallback_issues:
542
  raise RuntimeError(f"quantitative_user_intent_unresolved:{fallback_issues[:3]}")
@@ -550,11 +561,17 @@ Keywords: {keywords}"""
550
  candidate = parsed if isinstance(parsed, dict) else fallback_candidate
551
  candidate = dict(candidate)
552
  candidate["research_context"] = fallback_context
553
- contract = build_quantitative_contract(topic, keywords, candidate)
 
 
 
554
  return contract
555
  except Exception as exc:
556
  logging.warning("Quantitative adaptive study-design freeze fell back safely: %s", exc)
557
- return build_quantitative_contract(topic, keywords, fallback_candidate)
 
 
 
558
 
559
  def generate_empirical_data_robust(
560
  self,
@@ -2335,6 +2352,8 @@ Keywords: {keywords}"""
2335
  *,
2336
  topic="",
2337
  keywords="",
 
 
2338
  agent=None,
2339
  ):
2340
  if uploaded_files:
@@ -2358,9 +2377,20 @@ Keywords: {keywords}"""
2358
  metadata["quantitative_evidence_ledger"] = self.build_quantitative_evidence_ledger(metadata, user_md)
2359
  return None, user_md, tr(language, "actual_stats_parsed"), metadata
2360
 
2361
- design_contract = self._derive_system_design(agent, topic, keywords)
 
 
 
 
 
2362
  issues = validate_quantitative_contract(design_contract)
2363
  if issues:
 
 
 
 
 
 
2364
  raise RuntimeError(f"adaptive_quantitative_study_design_invalid:{issues[:4]}")
2365
  quant = dict(design_contract.get("quantitative") or {})
2366
  construct_items = [dict(item) for item in (quant.get("constructs") or []) if isinstance(item, dict)]
 
474
  if isinstance(item, dict) and str(item.get("label") or "").strip()
475
  ]
476
 
477
+ def _derive_system_design(
478
+ self, agent, topic="", keywords="", *, source_topic=None, source_keywords=None
479
+ ):
480
  """Interpret a parsimonious topic-aligned quantitative graph adaptively.
481
 
482
  The existing ``structured_json`` logical node is intentionally reused so
 
499
  "background_concepts":[],
500
  }
501
  if agent is None:
502
+ return build_quantitative_contract(
503
+ topic, keywords, fallback_candidate,
504
+ source_topic=source_topic, source_keywords=source_keywords,
505
+ )
506
  prompt = f"""Design the minimum coherent quantitative management-study graph for the topic below.
507
  Return ONLY valid JSON with this schema:
508
  {{
 
537
  logical_node='structured_json',
538
  )
539
  parsed = agent._extract_json_from_text(raw)
540
+ contract = build_quantitative_contract(
541
+ topic, keywords, parsed if isinstance(parsed, dict) else fallback_candidate,
542
+ source_topic=source_topic, source_keywords=source_keywords,
543
+ )
544
  issues = validate_quantitative_contract(contract)
545
  if issues:
546
  logging.warning("Adaptive quantitative design validation requires intent-preserving fallback: %s", issues[:3])
547
+ fallback = build_quantitative_contract(
548
+ topic, keywords, fallback_candidate,
549
+ source_topic=source_topic, source_keywords=source_keywords,
550
+ )
551
  fallback_issues = validate_quantitative_contract(fallback)
552
  if fallback_issues:
553
  raise RuntimeError(f"quantitative_user_intent_unresolved:{fallback_issues[:3]}")
 
561
  candidate = parsed if isinstance(parsed, dict) else fallback_candidate
562
  candidate = dict(candidate)
563
  candidate["research_context"] = fallback_context
564
+ contract = build_quantitative_contract(
565
+ topic, keywords, candidate,
566
+ source_topic=source_topic, source_keywords=source_keywords,
567
+ )
568
  return contract
569
  except Exception as exc:
570
  logging.warning("Quantitative adaptive study-design freeze fell back safely: %s", exc)
571
+ return build_quantitative_contract(
572
+ topic, keywords, fallback_candidate,
573
+ source_topic=source_topic, source_keywords=source_keywords,
574
+ )
575
 
576
  def generate_empirical_data_robust(
577
  self,
 
2352
  *,
2353
  topic="",
2354
  keywords="",
2355
+ internal_topic="",
2356
+ internal_keywords=None,
2357
  agent=None,
2358
  ):
2359
  if uploaded_files:
 
2377
  metadata["quantitative_evidence_ledger"] = self.build_quantitative_evidence_ledger(metadata, user_md)
2378
  return None, user_md, tr(language, "actual_stats_parsed"), metadata
2379
 
2380
+ design_topic = str(internal_topic or topic or "").strip()
2381
+ design_keywords = internal_keywords if internal_keywords not in (None, "", []) else keywords
2382
+ design_contract = self._derive_system_design(
2383
+ agent, design_topic, design_keywords,
2384
+ source_topic=topic, source_keywords=keywords,
2385
+ )
2386
  issues = validate_quantitative_contract(design_contract)
2387
  if issues:
2388
+ p1_identity_issues = [
2389
+ issue for issue in issues
2390
+ if issue.startswith("internal_english_") or issue.startswith("quantitative_construct_")
2391
+ ]
2392
+ if p1_identity_issues:
2393
+ raise RuntimeError(f"quantitative_construct_identity_corruption:{p1_identity_issues[:4]}")
2394
  raise RuntimeError(f"adaptive_quantitative_study_design_invalid:{issues[:4]}")
2395
  quant = dict(design_contract.get("quantitative") or {})
2396
  construct_items = [dict(item) for item in (quant.get("constructs") or []) if isinstance(item, dict)]
core/study_design_contract.py CHANGED
@@ -28,7 +28,7 @@ def _clean(value: Any, maximum: int = 240) -> str:
28
 
29
 
30
  KEYWORD_POLICY_VERSION = "smartpaper-keyword-policy-v1"
31
- KEYWORD_SEPARATOR_POLICY = "SEMICOLON_NEWLINE_PIPE_PRIMARY_COMMA_FALLBACK"
32
  _KEYWORD_COUNT_MODES = {"UNSPECIFIED", "PREFERRED", "REQUIRED"}
33
  _KEYWORD_AUTHORITY_SOURCES = {"USER_EXPLICIT", "SYSTEM_GENERATED", "LEGACY"}
34
 
@@ -45,10 +45,10 @@ def parse_keyword_terms(value: Any, *, explicit_user: bool = False, diagnostics:
45
  raw = list(value)
46
  else:
47
  text = str(value or "")
48
- if explicit_user and re.search(r"[;\n|]", text):
49
- raw = re.split(r"[;\n|]+", text)
50
  else:
51
- raw = re.split(r"[,;\n|]+", text)
52
  out: list[str] = []
53
  seen: set[str] = set()
54
  for item in raw:
@@ -406,7 +406,8 @@ def _append_relation(relations: list[dict[str, Any]], *, rtype: str, source: str
406
 
407
 
408
  def _reconcile_explicit_quantitative_intent(
409
- *, topic: str, keywords: Any, constructs: list[dict[str, Any]], relations: list[dict[str, Any]], decisions: list[dict[str, Any]]
 
410
  ) -> dict[str, Any]:
411
  """Make explicit user design intent canonical rather than merely additive.
412
 
@@ -416,14 +417,27 @@ def _reconcile_explicit_quantitative_intent(
416
  assigned alternative moderators are advisory proposals and are removed
417
  before the canonical user relation is compiled.
418
  """
419
- intent = build_user_research_intent(topic, keywords, mode="quantitative")
420
- explicit = set(intent.get("explicit_relation_types") or [])
 
 
 
 
 
421
  if not explicit:
422
- return intent
423
 
424
- intent_text = " ; ".join([intent.get("source_topic") or ""] + list(intent.get("source_keywords") or []))
 
 
 
 
 
425
  labels = [str(row.get("label") or "") for row in constructs]
426
- labels += [kw for kw in (intent.get("source_keywords") or []) if kw.casefold() not in {x.casefold() for x in labels}]
 
 
 
427
 
428
  def ids_for(role: str) -> list[str]:
429
  return [str(row.get("construct_id") or "") for row in constructs if role in (row.get("roles") or [])]
@@ -526,7 +540,7 @@ def _reconcile_explicit_quantitative_intent(
526
  before = len(relations); _append_relation(relations, rtype="direct_association", source=a, target=b)
527
  if len(relations) > before:
528
  decisions.append({"reason":"explicit_user_mediation_edge_reconciled","source":a,"target":b})
529
- return intent
530
 
531
  def _stable_hash(payload: dict[str, Any]) -> str:
532
  encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
@@ -594,6 +608,8 @@ def build_quantitative_contract(
594
  *,
595
  source_type: str = "system_generated",
596
  policy_version: str = "adaptive-quantitative-v1",
 
 
597
  ) -> dict[str, Any]:
598
  """Normalize a model-proposed quantitative graph into deterministic authority."""
599
  raw = dict(candidate or {}) if isinstance(candidate, dict) else {}
@@ -602,12 +618,36 @@ def build_quantitative_contract(
602
  raw = _legacy_quant_candidate(list(raw_constructs), _clean(raw.get("research_context")))
603
  raw_constructs = raw["constructs"]
604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  constructs: list[dict[str, Any]] = []
606
  label_to_id: dict[str, str] = {}
 
607
  for item in raw_constructs[:8]:
608
  if not isinstance(item, dict):
609
  continue
610
- label = _clean(item.get("label") or item.get("name"), 80)
 
 
 
 
 
 
611
  if not label or label.casefold() in label_to_id:
612
  continue
613
  roles = _normalize_roles(item.get("roles") or item.get("role"))
@@ -627,17 +667,19 @@ def build_quantitative_contract(
627
  # Safe compatibility fallback; importantly this is a fallback, not the normal schema.
628
  if len(constructs) < 2:
629
  raw = _legacy_quant_candidate(["Focal Predictor", "Focal Mediator", "Focal Outcome"], _clean(raw.get("research_context")))
630
- return build_quantitative_contract(topic, keywords, raw, source_type=source_type, policy_version=policy_version)
 
 
 
631
 
632
  by_id = {row["construct_id"]: row for row in constructs}
633
  def resolve(ref: Any) -> str:
634
- text = _clean(ref, 80)
635
  if text in by_id:
636
  return text
637
  return label_to_id.get(text.casefold(), "")
638
 
639
  relations: list[dict[str, Any]] = []
640
- rejected: list[dict[str, Any]] = []
641
  for item in (raw.get("relations") or [])[:24]:
642
  if not isinstance(item, dict):
643
  continue
@@ -671,7 +713,8 @@ def build_quantitative_contract(
671
  # explicitly requested mediation/moderation design. Reconciliation is
672
  # deterministic and never invents literature evidence.
673
  user_intent = _reconcile_explicit_quantitative_intent(
674
- topic=topic, keywords=keywords, constructs=constructs, relations=relations, decisions=rejected
 
675
  )
676
  by_id = {row["construct_id"]: row for row in constructs}
677
  label_to_id = {str(row.get("label") or "").casefold(): str(row.get("construct_id") or "") for row in constructs}
@@ -779,6 +822,14 @@ def build_quantitative_contract(
779
  "compiler_decisions": rejected,
780
  "policy_version": policy_version,
781
  }
 
 
 
 
 
 
 
 
782
  return _finalize(contract)
783
 
784
 
@@ -804,6 +855,22 @@ def validate_quantitative_contract(contract: dict[str, Any]) -> list[str]:
804
  # Semantic completeness is part of contract validity: an internally legal graph
805
  # is still invalid when it silently drops an explicit user-requested relation.
806
  intent = dict(record.get("user_research_intent") or {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
  explicit = set(intent.get("explicit_relation_types") or [])
808
  if "moderation" in explicit and not any(r.get("type") == "moderation" for r in relations):
809
  issues.append("explicit_user_moderation_missing")
 
28
 
29
 
30
  KEYWORD_POLICY_VERSION = "smartpaper-keyword-policy-v1"
31
+ KEYWORD_SEPARATOR_POLICY = "UNICODE_SEMICOLON_NEWLINE_PIPE_PRIMARY_COMMA_FALLBACK"
32
  _KEYWORD_COUNT_MODES = {"UNSPECIFIED", "PREFERRED", "REQUIRED"}
33
  _KEYWORD_AUTHORITY_SOURCES = {"USER_EXPLICIT", "SYSTEM_GENERATED", "LEGACY"}
34
 
 
45
  raw = list(value)
46
  else:
47
  text = str(value or "")
48
+ if explicit_user and re.search(r"[;;\n|]", text):
49
+ raw = re.split(r"[;;\n|]+", text)
50
  else:
51
+ raw = re.split(r"[,,;;\n|]+", text)
52
  out: list[str] = []
53
  seen: set[str] = set()
54
  for item in raw:
 
406
 
407
 
408
  def _reconcile_explicit_quantitative_intent(
409
+ *, topic: str, keywords: Any, constructs: list[dict[str, Any]], relations: list[dict[str, Any]], decisions: list[dict[str, Any]],
410
+ source_topic: str | None = None, source_keywords: Any = None,
411
  ) -> dict[str, Any]:
412
  """Make explicit user design intent canonical rather than merely additive.
413
 
 
417
  assigned alternative moderators are advisory proposals and are removed
418
  before the canonical user relation is compiled.
419
  """
420
+ source_intent = build_user_research_intent(
421
+ topic if source_topic is None else source_topic,
422
+ keywords if source_keywords is None else source_keywords,
423
+ mode="quantitative",
424
+ )
425
+ identity_intent = build_user_research_intent(topic, keywords, mode="quantitative")
426
+ explicit = set(source_intent.get("explicit_relation_types") or [])
427
  if not explicit:
428
+ return source_intent
429
 
430
+ # Construct/hypothesis identity is compiled from the validated English
431
+ # projection, while the immutable source-language user intent remains the
432
+ # semantic authority recorded in the contract.
433
+ intent_text = " ; ".join(
434
+ [identity_intent.get("source_topic") or ""] + list(identity_intent.get("source_keywords") or [])
435
+ )
436
  labels = [str(row.get("label") or "") for row in constructs]
437
+ labels += [
438
+ kw for kw in (identity_intent.get("source_keywords") or [])
439
+ if kw.casefold() not in {x.casefold() for x in labels}
440
+ ]
441
 
442
  def ids_for(role: str) -> list[str]:
443
  return [str(row.get("construct_id") or "") for row in constructs if role in (row.get("roles") or [])]
 
540
  before = len(relations); _append_relation(relations, rtype="direct_association", source=a, target=b)
541
  if len(relations) > before:
542
  decisions.append({"reason":"explicit_user_mediation_edge_reconciled","source":a,"target":b})
543
+ return source_intent
544
 
545
  def _stable_hash(payload: dict[str, Any]) -> str:
546
  encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
 
608
  *,
609
  source_type: str = "system_generated",
610
  policy_version: str = "adaptive-quantitative-v1",
611
+ source_topic: str | None = None,
612
+ source_keywords: Any = None,
613
  ) -> dict[str, Any]:
614
  """Normalize a model-proposed quantitative graph into deterministic authority."""
615
  raw = dict(candidate or {}) if isinstance(candidate, dict) else {}
 
618
  raw = _legacy_quant_candidate(list(raw_constructs), _clean(raw.get("research_context")))
619
  raw_constructs = raw["constructs"]
620
 
621
+ has_explicit_source_authority = source_topic is not None or source_keywords is not None
622
+ source_topic_value = topic if source_topic is None else source_topic
623
+ source_keywords_value = keywords if source_keywords is None else source_keywords
624
+ source_terms = parse_keyword_terms(source_keywords_value, explicit_user=True)
625
+ identity_terms = parse_keyword_terms(keywords, explicit_user=True)
626
+ identity_aliases: dict[str, str] = {}
627
+ if has_explicit_source_authority and source_terms and len(source_terms) == len(identity_terms):
628
+ identity_aliases = {
629
+ str(source).casefold(): str(target)
630
+ for source, target in zip(source_terms, identity_terms)
631
+ if str(source).strip() and str(target).strip()
632
+ }
633
+
634
+ def canonical_identity(value: Any) -> str:
635
+ label = _clean(value, 80)
636
+ return identity_aliases.get(label.casefold(), label)
637
+
638
  constructs: list[dict[str, Any]] = []
639
  label_to_id: dict[str, str] = {}
640
+ rejected: list[dict[str, Any]] = []
641
  for item in raw_constructs[:8]:
642
  if not isinstance(item, dict):
643
  continue
644
+ label = canonical_identity(item.get("label") or item.get("name"))
645
+ # A construct label containing a primary keyword separator is a malformed
646
+ # keyword blob, not a legitimate latent-variable identity. Reject it
647
+ # before relations/statistics can be compiled from it.
648
+ if re.search(r"[;;|\n]", label):
649
+ rejected.append({"reason": "keyword_blob_construct_rejected", "label": label})
650
+ continue
651
  if not label or label.casefold() in label_to_id:
652
  continue
653
  roles = _normalize_roles(item.get("roles") or item.get("role"))
 
667
  # Safe compatibility fallback; importantly this is a fallback, not the normal schema.
668
  if len(constructs) < 2:
669
  raw = _legacy_quant_candidate(["Focal Predictor", "Focal Mediator", "Focal Outcome"], _clean(raw.get("research_context")))
670
+ return build_quantitative_contract(
671
+ topic, keywords, raw, source_type=source_type, policy_version=policy_version,
672
+ source_topic=source_topic_value, source_keywords=source_keywords_value,
673
+ )
674
 
675
  by_id = {row["construct_id"]: row for row in constructs}
676
  def resolve(ref: Any) -> str:
677
+ text = canonical_identity(ref)
678
  if text in by_id:
679
  return text
680
  return label_to_id.get(text.casefold(), "")
681
 
682
  relations: list[dict[str, Any]] = []
 
683
  for item in (raw.get("relations") or [])[:24]:
684
  if not isinstance(item, dict):
685
  continue
 
713
  # explicitly requested mediation/moderation design. Reconciliation is
714
  # deterministic and never invents literature evidence.
715
  user_intent = _reconcile_explicit_quantitative_intent(
716
+ topic=topic, keywords=keywords, constructs=constructs, relations=relations, decisions=rejected,
717
+ source_topic=source_topic_value, source_keywords=source_keywords_value,
718
  )
719
  by_id = {row["construct_id"]: row for row in constructs}
720
  label_to_id = {str(row.get("label") or "").casefold(): str(row.get("construct_id") or "") for row in constructs}
 
822
  "compiler_decisions": rejected,
823
  "policy_version": policy_version,
824
  }
825
+ if has_explicit_source_authority:
826
+ contract["internal_english_authority"] = {
827
+ "version": "quantitative-internal-english-authority-v1",
828
+ "title": re.sub(r"\s+", " ", str(topic or "")).strip(),
829
+ "keywords": identity_terms,
830
+ "source_keyword_count": len(list(user_intent.get("source_keywords") or [])),
831
+ "translation_only": True,
832
+ }
833
  return _finalize(contract)
834
 
835
 
 
855
  # Semantic completeness is part of contract validity: an internally legal graph
856
  # is still invalid when it silently drops an explicit user-requested relation.
857
  intent = dict(record.get("user_research_intent") or {})
858
+ internal_authority = dict(record.get("internal_english_authority") or {})
859
+ if internal_authority:
860
+ internal_title = str(internal_authority.get("title") or "")
861
+ internal_keywords = [str(x).strip() for x in (internal_authority.get("keywords") or []) if str(x).strip()]
862
+ source_terms = [str(x).strip() for x in (intent.get("source_keywords") or []) if str(x).strip()]
863
+ if source_terms and len(internal_keywords) != len(source_terms):
864
+ issues.append("internal_english_keyword_cardinality_mismatch")
865
+ foreign_script = re.compile(r"[\u3400-\u9fff\u3040-\u30ff\u0400-\u052f]")
866
+ if foreign_script.search(" ".join([internal_title] + internal_keywords)):
867
+ issues.append("internal_english_authority_language_contamination")
868
+ for row in constructs:
869
+ label = str(row.get("label") or "")
870
+ if re.search(r"[;;|\n]", label):
871
+ issues.append("quantitative_construct_keyword_blob")
872
+ if foreign_script.search(label):
873
+ issues.append("quantitative_construct_identity_language_contamination")
874
  explicit = set(intent.get("explicit_relation_types") or [])
875
  if "moderation" in explicit and not any(r.get("type") == "moderation" for r in relations):
876
  issues.append("explicit_user_moderation_missing")