Spaces:
Running
Running
S0-R1-C5.5-R6-R1 Unified Semantic Claim Frame
#89
by GuXiaopeng - opened
- core/pipeline_agent.py +93 -5
- core/qualitative_engine.py +87 -0
- core/quantitative_engine.py +173 -3
core/pipeline_agent.py
CHANGED
|
@@ -75,6 +75,7 @@ class SSciPipelineAgent:
|
|
| 75 |
MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
|
| 76 |
PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
|
| 77 |
CANONICAL_GLOSSARY_QUALITY_VERSION = 'canonical-glossary-quality-v1'
|
|
|
|
| 78 |
|
| 79 |
@staticmethod
|
| 80 |
def _translation_defect_family(codes):
|
|
@@ -4014,12 +4015,21 @@ INPUT:
|
|
| 4014 |
External literature and participant quotations are excluded.
|
| 4015 |
"""
|
| 4016 |
value = str(text or "")
|
| 4017 |
-
|
| 4018 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4019 |
allowed_by_actor = {}
|
| 4020 |
for row in relations:
|
| 4021 |
role = re.sub(r"\s+", " ", str(row.get("actor_role") or "")).strip()
|
| 4022 |
-
family = cls._qual_source_family(row.get("data_source"))
|
| 4023 |
if not role or not family:
|
| 4024 |
continue
|
| 4025 |
for variant in cls._qual_actor_variants(role):
|
|
@@ -6214,6 +6224,63 @@ TEXT:
|
|
| 6214 |
r'|\|'
|
| 6215 |
)
|
| 6216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6217 |
@classmethod
|
| 6218 |
def _translation_claim_units(cls, value):
|
| 6219 |
"""Create stable source-side claim IDs without constraining target punctuation."""
|
|
@@ -7165,6 +7232,7 @@ SECTION: {section_name}
|
|
| 7165 |
'claim_id': claim_id,
|
| 7166 |
'english': item['text'],
|
| 7167 |
'target': target_claims[claim_id],
|
|
|
|
| 7168 |
})
|
| 7169 |
|
| 7170 |
issues = {}
|
|
@@ -7182,12 +7250,12 @@ SECTION: {section_name}
|
|
| 7182 |
'target_language': target_lang,
|
| 7183 |
'glossary': relevant_glossary,
|
| 7184 |
'allowed_issue_codes': sorted(allowed_codes),
|
| 7185 |
-
'items': [{'id': row['id'], 'english': row['english'], 'target': row['target']} for row in rows],
|
| 7186 |
}
|
| 7187 |
profile = self._translation_language_profile(target_lang)
|
| 7188 |
lexical_contract = self._translation_lexical_retention_contract(target_lang)
|
| 7189 |
system_prompt = f"""You are a bilingual academic translation QA classifier for {target_lang}.
|
| 7190 |
-
Review EACH claim independently. Do not rewrite and do not provide free-form explanations. Check semantic equivalence and language quality: omissions/additions, negation, uncertainty, association-versus-causation strength, terminology, grammar, language contamination, citation attachment, and acronym attachment.
|
| 7191 |
LANGUAGE ADAPTER: {profile['adapter_id']}; morphology={profile['morphology']}. {profile['term_policy']} {lexical_contract} {profile['contamination_policy']}
|
| 7192 |
For Russian, do not flag a terminology error merely because a translated term is inflected for grammatical case/number/gender; sustained non-Russian prose remains an additional contamination signal. For Kazakh, do not flag a terminology error merely because productive suffixes attach to the correct term stem; clearly Russian lexical/syntactic intrusion remains an additional language-specific signal when the pair supports that conclusion. Cyrillic script or absence of Kazakh-specific letters alone is never sufficient evidence.
|
| 7193 |
Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[],"evidence_codes":[]}}]}}.
|
|
@@ -7599,10 +7667,21 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 7599 |
block_id: self._model_safe_translation_manifest(protected_maps.get(block_id, {}))
|
| 7600 |
for block_id, _source, _protected in protected_batch
|
| 7601 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7602 |
effective_prompt = system_prompt + (
|
| 7603 |
'\n<SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 7604 |
+ json.dumps(semantic_manifest, ensure_ascii=False)
|
| 7605 |
+ '\n</SEMANTIC_ANCHOR_MANIFEST>\n'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7606 |
'This manifest intentionally contains no factual values. Preserve every SP-P, SP-C, and SP-T marker exactly. '
|
| 7607 |
'Each SP-C marker identifies one source claim. A target claim may contain one or more grammatical sentences, but every protected fact must remain inside its original SP-C claim. '
|
| 7608 |
'Never infer, spell out, calculate, or recreate a hidden number, date, citation, acronym, name, formula, or identifier. Do not print the manifest.'
|
|
@@ -7935,6 +8014,8 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
|
|
| 7935 |
'block_id': str(block_id),
|
| 7936 |
'claim_id': str(claim_id),
|
| 7937 |
'source_hash': hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest(),
|
|
|
|
|
|
|
| 7938 |
'target_text': target_text,
|
| 7939 |
'qa_status': 'repair_pending' if codes else 'passed',
|
| 7940 |
'issue_codes': [str(code or '').strip().upper() for code in codes if str(code or '').strip()][:5],
|
|
@@ -7964,8 +8045,15 @@ Each code must be one of: {sorted(allowed)}. Return one or more specific dimensi
|
|
| 7964 |
key = f'SP-P{block_id}-{claim_id}'
|
| 7965 |
row = states.get(key)
|
| 7966 |
expected_hash = hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest()
|
|
|
|
| 7967 |
if not isinstance(row, dict) or row.get('source_hash') != expected_hash or not str(row.get('target_text') or '').strip():
|
| 7968 |
return {}, {}, False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7969 |
segments[claim_id] = str(row.get('target_text') or '').strip()
|
| 7970 |
if str(row.get('qa_status') or '') == 'repair_pending':
|
| 7971 |
codes = [str(code or '').strip().upper() for code in (row.get('issue_codes') or []) if str(code or '').strip()]
|
|
|
|
| 75 |
MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
|
| 76 |
PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
|
| 77 |
CANONICAL_GLOSSARY_QUALITY_VERSION = 'canonical-glossary-quality-v1'
|
| 78 |
+
SEMANTIC_CLAIM_FRAME_VERSION = 'smartpaper-core-semantic-claim-frame-v1'
|
| 79 |
|
| 80 |
@staticmethod
|
| 81 |
def _translation_defect_family(codes):
|
|
|
|
| 4015 |
External literature and participant quotations are excluded.
|
| 4016 |
"""
|
| 4017 |
value = str(text or "")
|
| 4018 |
+
meta = dict(metadata or {})
|
| 4019 |
+
facts = dict(meta.get("study_facts") or {})
|
| 4020 |
+
semantic_frame = meta.get("semantic_claim_frame") if isinstance(meta.get("semantic_claim_frame"), dict) else None
|
| 4021 |
+
if not semantic_frame:
|
| 4022 |
+
try:
|
| 4023 |
+
from core.qualitative_engine import QualitativeEngine
|
| 4024 |
+
semantic_frame = QualitativeEngine.build_qualitative_semantic_claim_frame(meta)
|
| 4025 |
+
except Exception:
|
| 4026 |
+
semantic_frame = None
|
| 4027 |
+
frame_relations = list((semantic_frame or {}).get("actor_data_source_relations") or [])
|
| 4028 |
+
relations = frame_relations or [row for row in (facts.get("data_source_roles") or []) if isinstance(row, dict)]
|
| 4029 |
allowed_by_actor = {}
|
| 4030 |
for row in relations:
|
| 4031 |
role = re.sub(r"\s+", " ", str(row.get("actor_role") or "")).strip()
|
| 4032 |
+
family = str(row.get("source_family") or "").strip() or cls._qual_source_family(row.get("data_source"))
|
| 4033 |
if not role or not family:
|
| 4034 |
continue
|
| 4035 |
for variant in cls._qual_actor_variants(role):
|
|
|
|
| 6224 |
r'|\|'
|
| 6225 |
)
|
| 6226 |
|
| 6227 |
+
@classmethod
|
| 6228 |
+
def _source_semantic_claim_frame(cls, source_claim):
|
| 6229 |
+
"""Derive a conservative language-neutral frame once from frozen English.
|
| 6230 |
+
|
| 6231 |
+
This frame is not a substitute for bilingual QA. It is an immutable
|
| 6232 |
+
source-side contract that target-language QA must preserve, preventing
|
| 6233 |
+
ZH/RU/KK from independently reinterpreting polarity, causal strength, or
|
| 6234 |
+
modality on every retry/resume.
|
| 6235 |
+
"""
|
| 6236 |
+
text = re.sub(r"\s+", " ", str(source_claim or "")).strip()
|
| 6237 |
+
folded = text.casefold()
|
| 6238 |
+
causal = bool(re.search(
|
| 6239 |
+
r"\b(?:causes?|caused|causing|leads?\s+to|led\s+to|drives?|drove|driven|determines?|determined|"
|
| 6240 |
+
r"produces?|produced|results?\s+in|resulted\s+in|yields?|yielded|generates?|generated|"
|
| 6241 |
+
r"causal(?:ity|\s+(?:effect|effects|mediation|mechanism|pathway|relationship|relationships)))\b",
|
| 6242 |
+
folded, flags=re.I,
|
| 6243 |
+
))
|
| 6244 |
+
association = bool(re.search(
|
| 6245 |
+
r"\b(?:association|associations|associated|correlation|correlations|correlated|covariation|covariance|indirect\s+association)\b",
|
| 6246 |
+
folded, flags=re.I,
|
| 6247 |
+
))
|
| 6248 |
+
scalar_not = bool(re.search(r"\bnot\s+(?:only|merely|solely|exclusively|primarily|mainly)\b", folded, flags=re.I))
|
| 6249 |
+
negative = False
|
| 6250 |
+
if not scalar_not:
|
| 6251 |
+
negative = bool(re.search(
|
| 6252 |
+
r"\b(?:cannot|can't|could\s+not|couldn't|does\s+not|doesn't|do\s+not|don't|did\s+not|didn't|never|"
|
| 6253 |
+
r"is\s+not|are\s+not|was\s+not|were\s+not|no\s+(?:evidence|basis|support|proof)|without\s+(?:evidence|support|identifying|establishing|demonstrating|showing|inferring))\b",
|
| 6254 |
+
folded, flags=re.I,
|
| 6255 |
+
))
|
| 6256 |
+
if causal and negative:
|
| 6257 |
+
polarity = "NEGATIVE"
|
| 6258 |
+
causal_strength = "CAUSAL_LIMITATION"
|
| 6259 |
+
elif causal:
|
| 6260 |
+
polarity = "AFFIRMATIVE"
|
| 6261 |
+
causal_strength = "CAUSAL_ASSERTION"
|
| 6262 |
+
elif association:
|
| 6263 |
+
polarity = "NONE"
|
| 6264 |
+
causal_strength = "ASSOCIATIONAL"
|
| 6265 |
+
else:
|
| 6266 |
+
polarity = "NONE"
|
| 6267 |
+
causal_strength = "DESCRIPTIVE"
|
| 6268 |
+
if re.search(r"\b(?:may|might|could|appears?\s+to|seems?\s+to|suggests?|consistent\s+with)\b", folded) and not re.search(r"\bcould\s+not\b", folded):
|
| 6269 |
+
modality = "HEDGED"
|
| 6270 |
+
elif negative and causal:
|
| 6271 |
+
modality = "LIMITATION"
|
| 6272 |
+
else:
|
| 6273 |
+
modality = "ASSERTED"
|
| 6274 |
+
frame = {
|
| 6275 |
+
"version": cls.SEMANTIC_CLAIM_FRAME_VERSION,
|
| 6276 |
+
"polarity": polarity,
|
| 6277 |
+
"causal_strength": causal_strength,
|
| 6278 |
+
"modality": modality,
|
| 6279 |
+
}
|
| 6280 |
+
canonical = json.dumps(frame, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
| 6281 |
+
frame["frame_hash"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
| 6282 |
+
return frame
|
| 6283 |
+
|
| 6284 |
@classmethod
|
| 6285 |
def _translation_claim_units(cls, value):
|
| 6286 |
"""Create stable source-side claim IDs without constraining target punctuation."""
|
|
|
|
| 7232 |
'claim_id': claim_id,
|
| 7233 |
'english': item['text'],
|
| 7234 |
'target': target_claims[claim_id],
|
| 7235 |
+
'source_semantic_frame': self._source_semantic_claim_frame(item['text']),
|
| 7236 |
})
|
| 7237 |
|
| 7238 |
issues = {}
|
|
|
|
| 7250 |
'target_language': target_lang,
|
| 7251 |
'glossary': relevant_glossary,
|
| 7252 |
'allowed_issue_codes': sorted(allowed_codes),
|
| 7253 |
+
'items': [{'id': row['id'], 'english': row['english'], 'target': row['target'], 'source_semantic_frame': row['source_semantic_frame']} for row in rows],
|
| 7254 |
}
|
| 7255 |
profile = self._translation_language_profile(target_lang)
|
| 7256 |
lexical_contract = self._translation_lexical_retention_contract(target_lang)
|
| 7257 |
system_prompt = f"""You are a bilingual academic translation QA classifier for {target_lang}.
|
| 7258 |
+
Review EACH claim independently. Do not rewrite and do not provide free-form explanations. The supplied source_semantic_frame is server-frozen source authority: the target must preserve its polarity, causal_strength, and modality. Check semantic equivalence and language quality: omissions/additions, negation, uncertainty, association-versus-causation strength, terminology, grammar, language contamination, citation attachment, and acronym attachment.
|
| 7259 |
LANGUAGE ADAPTER: {profile['adapter_id']}; morphology={profile['morphology']}. {profile['term_policy']} {lexical_contract} {profile['contamination_policy']}
|
| 7260 |
For Russian, do not flag a terminology error merely because a translated term is inflected for grammatical case/number/gender; sustained non-Russian prose remains an additional contamination signal. For Kazakh, do not flag a terminology error merely because productive suffixes attach to the correct term stem; clearly Russian lexical/syntactic intrusion remains an additional language-specific signal when the pair supports that conclusion. Cyrillic script or absence of Kazakh-specific letters alone is never sufficient evidence.
|
| 7261 |
Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[],"evidence_codes":[]}}]}}.
|
|
|
|
| 7667 |
block_id: self._model_safe_translation_manifest(protected_maps.get(block_id, {}))
|
| 7668 |
for block_id, _source, _protected in protected_batch
|
| 7669 |
}
|
| 7670 |
+
claim_semantic_manifest = {
|
| 7671 |
+
block_id: {
|
| 7672 |
+
claim_id: self._source_semantic_claim_frame(source_claim)
|
| 7673 |
+
for claim_id, source_claim in dict(claim_sources_by_block.get(block_id) or {}).items()
|
| 7674 |
+
}
|
| 7675 |
+
for block_id, _source, _protected in protected_batch
|
| 7676 |
+
}
|
| 7677 |
effective_prompt = system_prompt + (
|
| 7678 |
'\n<SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 7679 |
+ json.dumps(semantic_manifest, ensure_ascii=False)
|
| 7680 |
+ '\n</SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 7681 |
+
+ '<SOURCE_SEMANTIC_CLAIM_FRAMES>\n'
|
| 7682 |
+
+ json.dumps(claim_semantic_manifest, ensure_ascii=False)
|
| 7683 |
+
+ '\n</SOURCE_SEMANTIC_CLAIM_FRAMES>\n'
|
| 7684 |
+
'The source semantic claim frames are server-frozen. Preserve polarity, causal strength, and modality for each SP-C claim exactly; do not strengthen an association into causation or drop a negation/hedge. '
|
| 7685 |
'This manifest intentionally contains no factual values. Preserve every SP-P, SP-C, and SP-T marker exactly. '
|
| 7686 |
'Each SP-C marker identifies one source claim. A target claim may contain one or more grammatical sentences, but every protected fact must remain inside its original SP-C claim. '
|
| 7687 |
'Never infer, spell out, calculate, or recreate a hidden number, date, citation, acronym, name, formula, or identifier. Do not print the manifest.'
|
|
|
|
| 8014 |
'block_id': str(block_id),
|
| 8015 |
'claim_id': str(claim_id),
|
| 8016 |
'source_hash': hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest(),
|
| 8017 |
+
'semantic_frame': cls._source_semantic_claim_frame(item['text']),
|
| 8018 |
+
'semantic_frame_hash': cls._source_semantic_claim_frame(item['text']).get('frame_hash', ''),
|
| 8019 |
'target_text': target_text,
|
| 8020 |
'qa_status': 'repair_pending' if codes else 'passed',
|
| 8021 |
'issue_codes': [str(code or '').strip().upper() for code in codes if str(code or '').strip()][:5],
|
|
|
|
| 8045 |
key = f'SP-P{block_id}-{claim_id}'
|
| 8046 |
row = states.get(key)
|
| 8047 |
expected_hash = hashlib.sha256(str(item['text']).encode('utf-8')).hexdigest()
|
| 8048 |
+
expected_semantic_hash = cls._source_semantic_claim_frame(item['text']).get('frame_hash', '')
|
| 8049 |
if not isinstance(row, dict) or row.get('source_hash') != expected_hash or not str(row.get('target_text') or '').strip():
|
| 8050 |
return {}, {}, False
|
| 8051 |
+
# R6-R1: new states are semantic-frame bound. Pre-R6 states do
|
| 8052 |
+
# not carry this optional field and retain legacy fail-closed
|
| 8053 |
+
# repair history instead of being silently "revived".
|
| 8054 |
+
stored_semantic_hash = str(row.get('semantic_frame_hash') or '')
|
| 8055 |
+
if stored_semantic_hash and stored_semantic_hash != expected_semantic_hash:
|
| 8056 |
+
return {}, {}, False
|
| 8057 |
segments[claim_id] = str(row.get('target_text') or '').strip()
|
| 8058 |
if str(row.get('qa_status') or '') == 'repair_pending':
|
| 8059 |
codes = [str(code or '').strip().upper() for code in (row.get('issue_codes') or []) if str(code or '').strip()]
|
core/qualitative_engine.py
CHANGED
|
@@ -14,6 +14,7 @@ from config import APP_CONFIG
|
|
| 14 |
class QualitativeEngine:
|
| 15 |
QUALITATIVE_APPENDIX_MANIFEST_VERSION = "qualitative-appendix-manifest-v1"
|
| 16 |
TARGET_APPENDIX_BINDING_VERSION = "target-qualitative-appendix-binding-v1"
|
|
|
|
| 17 |
APPENDIX_FIELDS = ("theme", "concept", "stage", "case_label", "evidence_role")
|
| 18 |
APPENDIX_HEADERS = (
|
| 19 |
"Higher-order Theme", "Empirical Concept", "Process Stage",
|
|
@@ -94,6 +95,91 @@ class QualitativeEngine:
|
|
| 94 |
rows.append(row)
|
| 95 |
return rows
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
@classmethod
|
| 98 |
def build_scoped_evidence_md(
|
| 99 |
cls,
|
|
@@ -862,6 +948,7 @@ Rules:
|
|
| 862 |
"evidence": clean_evidence,
|
| 863 |
"themes": themes,
|
| 864 |
}
|
|
|
|
| 865 |
status_message = "✅ 定性研究事实��分析设计与证据结构已冻结。"
|
| 866 |
return md, status_message, metadata
|
| 867 |
|
|
|
|
| 14 |
class QualitativeEngine:
|
| 15 |
QUALITATIVE_APPENDIX_MANIFEST_VERSION = "qualitative-appendix-manifest-v1"
|
| 16 |
TARGET_APPENDIX_BINDING_VERSION = "target-qualitative-appendix-binding-v1"
|
| 17 |
+
SEMANTIC_CLAIM_FRAME_VERSION = "smartpaper-core-semantic-claim-frame-v1"
|
| 18 |
APPENDIX_FIELDS = ("theme", "concept", "stage", "case_label", "evidence_role")
|
| 19 |
APPENDIX_HEADERS = (
|
| 20 |
"Higher-order Theme", "Empirical Concept", "Process Stage",
|
|
|
|
| 95 |
rows.append(row)
|
| 96 |
return rows
|
| 97 |
|
| 98 |
+
@classmethod
|
| 99 |
+
def build_qualitative_semantic_claim_frame(cls, metadata):
|
| 100 |
+
"""Freeze actor/source, process-evidence, and quote identities in one frame.
|
| 101 |
+
|
| 102 |
+
The frame is server-side authority. It contains no model-inferred prose
|
| 103 |
+
meaning and no verbatim quotation payload, only typed identities/hashes.
|
| 104 |
+
"""
|
| 105 |
+
meta = dict(metadata or {})
|
| 106 |
+
facts = dict(meta.get("study_facts") or {})
|
| 107 |
+
relations = []
|
| 108 |
+
for row in cls._normalize_data_source_roles(facts.get("data_source_roles") or []):
|
| 109 |
+
actor = cls._clean_cell(row.get("actor_role"), "")
|
| 110 |
+
source = cls._clean_cell(row.get("data_source"), "")
|
| 111 |
+
folded = source.casefold()
|
| 112 |
+
if "interview" in folded:
|
| 113 |
+
family = "interview"
|
| 114 |
+
elif "focus" in folded and "group" in folded:
|
| 115 |
+
family = "focus_group"
|
| 116 |
+
elif "observ" in folded:
|
| 117 |
+
family = "observation"
|
| 118 |
+
elif "log" in folded:
|
| 119 |
+
family = "log"
|
| 120 |
+
elif "document" in folded or "archiv" in folded or "record" in folded:
|
| 121 |
+
family = "document"
|
| 122 |
+
else:
|
| 123 |
+
family = "other"
|
| 124 |
+
relation = {
|
| 125 |
+
"actor_role": actor,
|
| 126 |
+
"data_source": source,
|
| 127 |
+
"source_family": family,
|
| 128 |
+
"count": row.get("count"),
|
| 129 |
+
"relation": "EVIDENCED_BY",
|
| 130 |
+
}
|
| 131 |
+
relation_id_payload = json.dumps(relation, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
| 132 |
+
relation["relation_id"] = hashlib.sha256(relation_id_payload.encode("utf-8")).hexdigest()[:16]
|
| 133 |
+
relations.append(relation)
|
| 134 |
+
|
| 135 |
+
quotes = []
|
| 136 |
+
for item in meta.get("evidence") or []:
|
| 137 |
+
if not isinstance(item, dict):
|
| 138 |
+
continue
|
| 139 |
+
quote_id = str(item.get("tag") or "").strip()
|
| 140 |
+
if not re.fullmatch(r"QUOTE-\d+", quote_id):
|
| 141 |
+
continue
|
| 142 |
+
quote_hash = str(item.get("quote_hash") or "").strip()
|
| 143 |
+
if not quote_hash and str(item.get("quote") or "").strip():
|
| 144 |
+
normalized = re.sub(r"\s+", " ", str(item.get("quote") or "")).strip()
|
| 145 |
+
quote_hash = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
| 146 |
+
quotes.append({
|
| 147 |
+
"quote_id": quote_id,
|
| 148 |
+
"quote_hash": quote_hash,
|
| 149 |
+
"theme_id": str(item.get("theme_id") or "").strip(),
|
| 150 |
+
"stage": cls._clean_cell(item.get("stage")),
|
| 151 |
+
"case_label": cls._clean_cell(item.get("case_label")),
|
| 152 |
+
"evidence_role": str(item.get("evidence_role") or "typical").strip().lower(),
|
| 153 |
+
})
|
| 154 |
+
|
| 155 |
+
process_evidence = []
|
| 156 |
+
for theme in meta.get("themes") or []:
|
| 157 |
+
if not isinstance(theme, dict):
|
| 158 |
+
continue
|
| 159 |
+
theme_id = str(theme.get("theme_id") or "").strip()
|
| 160 |
+
if not theme_id:
|
| 161 |
+
continue
|
| 162 |
+
process_evidence.append({
|
| 163 |
+
"theme_id": theme_id,
|
| 164 |
+
"stages": [cls._clean_cell(x, "") for x in (theme.get("stages") or []) if cls._clean_cell(x, "")],
|
| 165 |
+
"has_deviant_evidence": bool(theme.get("has_deviant_evidence")),
|
| 166 |
+
"quote_ids": [
|
| 167 |
+
str(row.get("tag") or "").strip()
|
| 168 |
+
for row in (theme.get("evidence") or []) if isinstance(row, dict) and str(row.get("tag") or "").strip()
|
| 169 |
+
],
|
| 170 |
+
})
|
| 171 |
+
|
| 172 |
+
frame = {
|
| 173 |
+
"version": cls.SEMANTIC_CLAIM_FRAME_VERSION,
|
| 174 |
+
"mode": "qualitative",
|
| 175 |
+
"actor_data_source_relations": sorted(relations, key=lambda row: (row["actor_role"].casefold(), row["data_source"].casefold())),
|
| 176 |
+
"process_evidence": sorted(process_evidence, key=lambda row: row["theme_id"]),
|
| 177 |
+
"quote_identities": sorted(quotes, key=lambda row: row["quote_id"]),
|
| 178 |
+
}
|
| 179 |
+
canonical = json.dumps(frame, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
| 180 |
+
frame["frame_hash"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
| 181 |
+
return frame
|
| 182 |
+
|
| 183 |
@classmethod
|
| 184 |
def build_scoped_evidence_md(
|
| 185 |
cls,
|
|
|
|
| 948 |
"evidence": clean_evidence,
|
| 949 |
"themes": themes,
|
| 950 |
}
|
| 951 |
+
metadata["semantic_claim_frame"] = self.build_qualitative_semantic_claim_frame(metadata)
|
| 952 |
status_message = "✅ 定性研究事实��分析设计与证据结构已冻结。"
|
| 953 |
return md, status_message, metadata
|
| 954 |
|
core/quantitative_engine.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
|
|
|
| 6 |
import json
|
| 7 |
import logging
|
| 8 |
import re
|
|
@@ -21,6 +22,7 @@ class QuantitativeEngine:
|
|
| 21 |
QUANTITATIVE_TABLE_MANIFEST_VERSION = "quantitative-table-manifest-v1"
|
| 22 |
HYPOTHESIS_LEDGER_VERSION = "quantitative-hypothesis-ledger-v1"
|
| 23 |
TARGET_TABLE_BINDING_VERSION = "target-quantitative-table-binding-v1"
|
|
|
|
| 24 |
|
| 25 |
_TARGET_TABLE_LOCALIZATION = {
|
| 26 |
"zh": {
|
|
@@ -859,6 +861,130 @@ Keywords: {keywords}"""
|
|
| 859 |
] if strict else [],
|
| 860 |
}
|
| 861 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
@classmethod
|
| 863 |
def _classify_quantitative_causal_clause(cls, clause):
|
| 864 |
"""Classify one clause by causal *assertion scope*, not token presence.
|
|
@@ -987,6 +1113,33 @@ Keywords: {keywords}"""
|
|
| 987 |
|
| 988 |
def direct_match_is_disclaimed(match):
|
| 989 |
prefix = value[max(0, match.start() - 220):match.start()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 990 |
if re.search(
|
| 991 |
r"(?:\b(?:does|do|did|can|could|would|should|must|may|might)\s+not|"
|
| 992 |
r"\bcannot|\bcan't|\bcouldn't|\bnever)\s+(?:\w+\s+){0,2}$",
|
|
@@ -1074,6 +1227,22 @@ Keywords: {keywords}"""
|
|
| 1074 |
def classify_quantitative_causal_sentence(cls, sentence):
|
| 1075 |
"""Three-state causal contract shared by inspection, repair, and tests."""
|
| 1076 |
value = str(sentence or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
# Contrast clauses must not let an early disclaimer shield a later assertion.
|
| 1078 |
clauses = re.split(
|
| 1079 |
r"(?i)(?:;|\bbut\b|\bhowever\b|\bwhereas\b|\byet\b|\bnevertheless\b|\balthough\b|\bthough\b)",
|
|
@@ -1206,10 +1375,10 @@ Keywords: {keywords}"""
|
|
| 1206 |
methodological_bias_scope = cls._is_methodological_bias_mechanism_sentence(
|
| 1207 |
sentence, known_constructs
|
| 1208 |
)
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
else cls.classify_quantitative_causal_sentence(sentence)
|
| 1212 |
)
|
|
|
|
| 1213 |
if causal_state == "AFFIRMATIVE_CAUSAL":
|
| 1214 |
violations.append({
|
| 1215 |
"type":"causal_language_violation",
|
|
@@ -1580,6 +1749,7 @@ Keywords: {keywords}"""
|
|
| 1580 |
"Frozen bootstrap and collinearity checks",
|
| 1581 |
],
|
| 1582 |
}
|
|
|
|
| 1583 |
metadata["hypothesis_ledger"] = self.build_hypothesis_ledger(construct_names)
|
| 1584 |
metadata["quantitative_table_manifest"] = self.build_table_manifest(
|
| 1585 |
validity_df, fl_matrix_df, path_table, spearman
|
|
|
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
+
import hashlib
|
| 7 |
import json
|
| 8 |
import logging
|
| 9 |
import re
|
|
|
|
| 22 |
QUANTITATIVE_TABLE_MANIFEST_VERSION = "quantitative-table-manifest-v1"
|
| 23 |
HYPOTHESIS_LEDGER_VERSION = "quantitative-hypothesis-ledger-v1"
|
| 24 |
TARGET_TABLE_BINDING_VERSION = "target-quantitative-table-binding-v1"
|
| 25 |
+
SEMANTIC_CLAIM_FRAME_VERSION = "smartpaper-core-semantic-claim-frame-v1"
|
| 26 |
|
| 27 |
_TARGET_TABLE_LOCALIZATION = {
|
| 28 |
"zh": {
|
|
|
|
| 861 |
] if strict else [],
|
| 862 |
}
|
| 863 |
|
| 864 |
+
@staticmethod
|
| 865 |
+
def _semantic_frame_hash(payload):
|
| 866 |
+
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
| 867 |
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
| 868 |
+
|
| 869 |
+
@classmethod
|
| 870 |
+
def build_quantitative_semantic_claim_frame(cls, sentence, metadata=None, section_name=""):
|
| 871 |
+
"""Freeze a compact server-side semantic frame for one quantitative claim.
|
| 872 |
+
|
| 873 |
+
R6-R1 does not ask the model to decide this frame. It records the
|
| 874 |
+
source-side claim role, causal polarity/strength, methodological domain,
|
| 875 |
+
hypothesis identity, and explicit result direction so downstream
|
| 876 |
+
validation/translation can inherit one stable semantic authority.
|
| 877 |
+
"""
|
| 878 |
+
text = re.sub(r"\s+", " ", str(sentence or "")).strip()
|
| 879 |
+
folded = text.casefold()
|
| 880 |
+
meta = dict(metadata or {})
|
| 881 |
+
ledger = dict(meta.get("quantitative_evidence_ledger") or {})
|
| 882 |
+
if not ledger and meta:
|
| 883 |
+
try:
|
| 884 |
+
ledger = cls.build_quantitative_evidence_ledger(meta)
|
| 885 |
+
except Exception:
|
| 886 |
+
ledger = {}
|
| 887 |
+
constructs = dict(ledger.get("constructs") or {})
|
| 888 |
+
known_constructs = [str(value) for value in constructs.values() if str(value)]
|
| 889 |
+
ownership = cls._quantitative_sentence_ownership(text, section_name, known_constructs) if text else "other"
|
| 890 |
+
|
| 891 |
+
relation_specs = (
|
| 892 |
+
(r"\b(?:causes?|caused|causing)\b", "CAUSE"),
|
| 893 |
+
(r"\b(?:leads?|led)\s+to\b", "LEAD_TO"),
|
| 894 |
+
(r"\b(?:drives?|drove|driven)\b", "DRIVE"),
|
| 895 |
+
(r"\b(?:determines?|determined)\b", "DETERMINE"),
|
| 896 |
+
(r"\b(?:produces?|produced)\b", "PRODUCE"),
|
| 897 |
+
(r"\b(?:results?|resulted)\s+in\b", "RESULT_IN"),
|
| 898 |
+
(r"\b(?:yields?|yielded)\b", "YIELD"),
|
| 899 |
+
(r"\b(?:generates?|generated)\b", "GENERATE"),
|
| 900 |
+
(r"\bcausal(?:ity|\s+(?:effect|effects|mediation|mechanism|pathway|relationship|relationships|identification))\b", "CAUSAL_NOUN"),
|
| 901 |
+
)
|
| 902 |
+
relation = "NONE"
|
| 903 |
+
for pattern, label in relation_specs:
|
| 904 |
+
if re.search(pattern, folded, flags=re.I):
|
| 905 |
+
relation = label
|
| 906 |
+
break
|
| 907 |
+
|
| 908 |
+
methodological_bias = cls._is_methodological_bias_mechanism_sentence(text, known_constructs)
|
| 909 |
+
robustness_scope = bool(re.search(
|
| 910 |
+
r"\b(?:robustness|sensitivity|spearman|rank[- ]order|extreme\s+values?|outliers?|influential\s+(?:cases|observations)|"
|
| 911 |
+
r"response\s+styles?|common\s+method|same[- ]source|single\s+questionnaire|measurement\s+(?:error|bias))\b",
|
| 912 |
+
folded, flags=re.I,
|
| 913 |
+
))
|
| 914 |
+
if methodological_bias:
|
| 915 |
+
domain = "METHODOLOGICAL_BIAS"
|
| 916 |
+
elif robustness_scope:
|
| 917 |
+
domain = "METHODOLOGICAL_ROBUSTNESS"
|
| 918 |
+
elif re.search(r"\b(?:analysis|analyses|bootstrap|regression|correlation|estimation|statistical\s+(?:test|model|analysis)|vif)\b", folded):
|
| 919 |
+
domain = "ANALYTICAL_REPORTING"
|
| 920 |
+
elif any(name.casefold() in folded for name in known_constructs):
|
| 921 |
+
domain = "SUBSTANTIVE_CONSTRUCT"
|
| 922 |
+
else:
|
| 923 |
+
domain = "GENERAL"
|
| 924 |
+
|
| 925 |
+
raw_causal_state = cls.classify_quantitative_causal_sentence(text) if text else "NON_CAUSAL"
|
| 926 |
+
validation_state = "NON_CAUSAL" if methodological_bias else raw_causal_state
|
| 927 |
+
if raw_causal_state == "EXPLICIT_CAUSAL_LIMITATION":
|
| 928 |
+
polarity = "NEGATIVE"
|
| 929 |
+
causal_strength = "CAUSAL_LIMITATION"
|
| 930 |
+
elif raw_causal_state == "AFFIRMATIVE_CAUSAL":
|
| 931 |
+
polarity = "AFFIRMATIVE"
|
| 932 |
+
causal_strength = "CAUSAL_ASSERTION"
|
| 933 |
+
elif re.search(r"\b(?:association|associations|associated|correlation|correlations|correlated|covariation|covariance)\b", folded):
|
| 934 |
+
polarity = "NONE"
|
| 935 |
+
causal_strength = "ASSOCIATIONAL"
|
| 936 |
+
else:
|
| 937 |
+
polarity = "NONE"
|
| 938 |
+
causal_strength = "NON_CAUSAL"
|
| 939 |
+
|
| 940 |
+
if re.search(r"\b(?:may|might|could|appears?\s+to|seems?\s+to|suggests?|consistent\s+with)\b", folded) and not re.search(r"\bcould\s+not\b", folded):
|
| 941 |
+
modality = "HEDGED"
|
| 942 |
+
elif raw_causal_state == "EXPLICIT_CAUSAL_LIMITATION":
|
| 943 |
+
modality = "LIMITATION"
|
| 944 |
+
else:
|
| 945 |
+
modality = "ASSERTED"
|
| 946 |
+
|
| 947 |
+
hypothesis_ids = sorted(set(re.findall(r"(?i)\bH\d+[A-Za-z]?\b", text)), key=str.casefold)
|
| 948 |
+
if re.search(r"\b(?:non[- ]?significant|not\s+significant|did\s+not\s+support|was\s+not\s+supported)\b", folded):
|
| 949 |
+
result_direction = "NOT_SUPPORTED_OR_NONSIGNIFICANT"
|
| 950 |
+
elif re.search(r"\b(?:positive|positively)\b[^.;]{0,80}\b(?:association|associated|relationship|correlation|estimate|coefficient)\b", folded):
|
| 951 |
+
result_direction = "POSITIVE"
|
| 952 |
+
elif re.search(r"\b(?:negative|negatively)\b[^.;]{0,80}\b(?:association|associated|relationship|correlation|estimate|coefficient)\b", folded):
|
| 953 |
+
result_direction = "NEGATIVE"
|
| 954 |
+
else:
|
| 955 |
+
result_direction = "UNSPECIFIED"
|
| 956 |
+
|
| 957 |
+
if hypothesis_ids:
|
| 958 |
+
claim_type = "HYPOTHESIS_CLAIM"
|
| 959 |
+
elif domain in {"METHODOLOGICAL_BIAS", "METHODOLOGICAL_ROBUSTNESS"}:
|
| 960 |
+
claim_type = "METHODOLOGICAL_CLAIM"
|
| 961 |
+
elif ownership == "result":
|
| 962 |
+
claim_type = "RESULT_CLAIM"
|
| 963 |
+
elif causal_strength == "ASSOCIATIONAL":
|
| 964 |
+
claim_type = "ASSOCIATION_CLAIM"
|
| 965 |
+
elif raw_causal_state != "NON_CAUSAL":
|
| 966 |
+
claim_type = "CAUSAL_SCOPE_CLAIM"
|
| 967 |
+
else:
|
| 968 |
+
claim_type = "OTHER"
|
| 969 |
+
|
| 970 |
+
frame = {
|
| 971 |
+
"version": cls.SEMANTIC_CLAIM_FRAME_VERSION,
|
| 972 |
+
"mode": "quantitative",
|
| 973 |
+
"claim_type": claim_type,
|
| 974 |
+
"ownership": ownership,
|
| 975 |
+
"domain": domain,
|
| 976 |
+
"relation": relation,
|
| 977 |
+
"polarity": polarity,
|
| 978 |
+
"causal_strength": causal_strength,
|
| 979 |
+
"causal_state": raw_causal_state,
|
| 980 |
+
"validation_state": validation_state,
|
| 981 |
+
"modality": modality,
|
| 982 |
+
"hypothesis_ids": hypothesis_ids,
|
| 983 |
+
"result_direction": result_direction,
|
| 984 |
+
}
|
| 985 |
+
frame["frame_hash"] = cls._semantic_frame_hash(frame)
|
| 986 |
+
return frame
|
| 987 |
+
|
| 988 |
@classmethod
|
| 989 |
def _classify_quantitative_causal_clause(cls, clause):
|
| 990 |
"""Classify one clause by causal *assertion scope*, not token presence.
|
|
|
|
| 1113 |
|
| 1114 |
def direct_match_is_disclaimed(match):
|
| 1115 |
prefix = value[max(0, match.start() - 220):match.start()]
|
| 1116 |
+
suffix = value[match.end():min(len(value), match.end() + 220)]
|
| 1117 |
+
|
| 1118 |
+
# R6-R1 predicate polarity closure: passive negation is a semantic
|
| 1119 |
+
# limitation ("associations are not driven by outliers"), not an
|
| 1120 |
+
# affirmative causal claim. Do not exempt contrastive constructions
|
| 1121 |
+
# that merely replace one asserted cause with another ("not driven by
|
| 1122 |
+
# X but rather by Y") or scalar forms such as "not only driven".
|
| 1123 |
+
passive_negative = bool(re.search(
|
| 1124 |
+
r"\b(?:is|are|was|were|be|been|being)\s+"
|
| 1125 |
+
r"(?:(?:clearly|necessarily|directly|statistically)\s+){0,2}"
|
| 1126 |
+
r"(?:not|never)\s+"
|
| 1127 |
+
r"(?:(?:clearly|necessarily|directly|statistically)\s+){0,2}$",
|
| 1128 |
+
prefix, flags=re.I,
|
| 1129 |
+
))
|
| 1130 |
+
if passive_negative:
|
| 1131 |
+
alternate_cause = re.search(
|
| 1132 |
+
r"\b(?:but|rather|instead)\b\s*(?:rather\s+|instead\s+|also\s+)?by\b",
|
| 1133 |
+
suffix, flags=re.I,
|
| 1134 |
+
)
|
| 1135 |
+
alternate_negated = re.search(
|
| 1136 |
+
r"\b(?:but|rather|instead)\b\s*(?:also\s+)?(?:not|never)\s+by\b",
|
| 1137 |
+
suffix, flags=re.I,
|
| 1138 |
+
)
|
| 1139 |
+
if alternate_cause and not alternate_negated:
|
| 1140 |
+
return False
|
| 1141 |
+
return True
|
| 1142 |
+
|
| 1143 |
if re.search(
|
| 1144 |
r"(?:\b(?:does|do|did|can|could|would|should|must|may|might)\s+not|"
|
| 1145 |
r"\bcannot|\bcan't|\bcouldn't|\bnever)\s+(?:\w+\s+){0,2}$",
|
|
|
|
| 1227 |
def classify_quantitative_causal_sentence(cls, sentence):
|
| 1228 |
"""Three-state causal contract shared by inspection, repair, and tests."""
|
| 1229 |
value = str(sentence or "")
|
| 1230 |
+
folded = re.sub(r"\s+", " ", value).strip().casefold()
|
| 1231 |
+
# A negated passive can still assert an alternative cause: "not driven
|
| 1232 |
+
# by X but rather by Y". Detect this before contrast-clause splitting,
|
| 1233 |
+
# otherwise the second fragment loses the causal predicate surface.
|
| 1234 |
+
alternate_cause = re.search(
|
| 1235 |
+
r"\b(?:is|are|was|were|be|been|being)\s+"
|
| 1236 |
+
r"(?:(?:clearly|necessarily|directly|statistically)\s+){0,2}"
|
| 1237 |
+
r"(?:not|never)\s+"
|
| 1238 |
+
r"(?:(?:clearly|necessarily|directly|statistically)\s+){0,2}"
|
| 1239 |
+
r"(?:caused|driven|determined|produced|generated|yielded)\b"
|
| 1240 |
+
r"[^.;]{0,180}\b(?:but|rather|instead)\b\s*"
|
| 1241 |
+
r"(?:rather\s+|instead\s+|also\s+)?by\b",
|
| 1242 |
+
folded, flags=re.I,
|
| 1243 |
+
)
|
| 1244 |
+
if alternate_cause:
|
| 1245 |
+
return "AFFIRMATIVE_CAUSAL"
|
| 1246 |
# Contrast clauses must not let an early disclaimer shield a later assertion.
|
| 1247 |
clauses = re.split(
|
| 1248 |
r"(?i)(?:;|\bbut\b|\bhowever\b|\bwhereas\b|\byet\b|\bnevertheless\b|\balthough\b|\bthough\b)",
|
|
|
|
| 1375 |
methodological_bias_scope = cls._is_methodological_bias_mechanism_sentence(
|
| 1376 |
sentence, known_constructs
|
| 1377 |
)
|
| 1378 |
+
semantic_frame = cls.build_quantitative_semantic_claim_frame(
|
| 1379 |
+
sentence, meta, section_name
|
|
|
|
| 1380 |
)
|
| 1381 |
+
causal_state = str(semantic_frame.get("validation_state") or "NON_CAUSAL")
|
| 1382 |
if causal_state == "AFFIRMATIVE_CAUSAL":
|
| 1383 |
violations.append({
|
| 1384 |
"type":"causal_language_violation",
|
|
|
|
| 1749 |
"Frozen bootstrap and collinearity checks",
|
| 1750 |
],
|
| 1751 |
}
|
| 1752 |
+
metadata["semantic_claim_frame_version"] = self.SEMANTIC_CLAIM_FRAME_VERSION
|
| 1753 |
metadata["hypothesis_ledger"] = self.build_hypothesis_ledger(construct_names)
|
| 1754 |
metadata["quantitative_table_manifest"] = self.build_table_manifest(
|
| 1755 |
validity_df, fl_matrix_df, path_table, spearman
|