Spaces:
Running
Running
L2-R1 Unicode keyword authority and upstream quantitative contract repair
#132
by GuXiaopeng - opened
- core/generation_runner.py +17 -1
- core/quantitative_engine.py +37 -7
- core/study_design_contract.py +83 -16
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,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = "
|
| 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 |
-
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
if not explicit:
|
| 422 |
-
return
|
| 423 |
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
labels = [str(row.get("label") or "") for row in constructs]
|
| 426 |
-
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
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 631 |
|
| 632 |
by_id = {row["construct_id"]: row for row in constructs}
|
| 633 |
def resolve(ref: Any) -> str:
|
| 634 |
-
text =
|
| 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")
|