mceg 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mceg/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Scientific model contracts with explicit v0.1 and v0.2 readers."""
2
+
3
+ from .schema import Contract
4
+ from .schema_v02 import ContractV02
5
+ from .provenance import trace_provenance
6
+ from .validator import load_contract
7
+ from .migration import migrate_contract, restore_v01
8
+ from .assessment import assess_conditions
9
+
10
+ __all__ = ["Contract", "ContractV02", "load_contract", "trace_provenance",
11
+ "migrate_contract", "restore_v01", "assess_conditions"]
mceg/__main__.py ADDED
@@ -0,0 +1,84 @@
1
+ """Command-line structural validation."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+ from .validator import load_contract
10
+ from .provenance import trace_provenance
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> int:
14
+ argv = list(sys.argv[1:] if argv is None else argv)
15
+ if argv and not Path(argv[0]).is_file():
16
+ if argv[0] == "init":
17
+ from .initialization import main as command
18
+ return command(argv[1:])
19
+ if argv[0] == "check":
20
+ from .check import main as command
21
+ return command(argv[1:])
22
+ if argv[0] == "coverage":
23
+ from .coverage import main as command
24
+ return command(argv[1:])
25
+ if argv[0] == "docs":
26
+ from .documentation import main as command
27
+ return command(argv[1:])
28
+ if argv[0] == "graph":
29
+ from .graph import main as command
30
+ return command(argv[1:])
31
+ if argv[0] == "migrate":
32
+ from .migration import main as command
33
+ return command(argv[1:])
34
+ if argv[0] == "assess":
35
+ from .assessment import main as command
36
+ return command(argv[1:])
37
+ if argv[0] == "explain":
38
+ from .explain import main as command
39
+ return command(argv[1:])
40
+ parser = argparse.ArgumentParser(
41
+ prog="mceg",
42
+ usage="%(prog)s COMMAND [OPTIONS]\n %(prog)s [--trace EVIDENCE_ID] FILE [FILE ...]",
43
+ description="Inspect scientific model contracts, evidence and review gaps.",
44
+ formatter_class=argparse.RawDescriptionHelpFormatter,
45
+ epilog=(
46
+ "Commands:\n"
47
+ " init Start a model contract and register it for review.\n"
48
+ " check Discover contracts and summarize available checks.\n"
49
+ " coverage Find model registration and source-review gaps.\n"
50
+ " docs Generate RST sources for a repository's Sphinx docs.\n"
51
+ " graph Export recorded relationships as Graph JSON.\n"
52
+ " explain Inspect gaps, evidence and concrete next steps.\n"
53
+ " migrate Preserve a v0.1 contract in the v0.2 format.\n"
54
+ " assess Evaluate explicit conditions and save scoped assessments.\n\n"
55
+ "Examples:\n"
56
+ " mceg init mypkg.water --source src/mypkg/water.py --symbol concentration\n"
57
+ " mceg check -k curated\n"
58
+ " mceg explain -k curated --temperature 1750 --lang ja\n"
59
+ " mceg explain -k curated --plain\n"
60
+ " mceg contracts/h2_solubility.yaml\n\n"
61
+ "Use mceg COMMAND -h for that command's options and usage."
62
+ ),
63
+ )
64
+ parser.add_argument("paths", nargs="+", type=Path, help="YAML contract files")
65
+ parser.add_argument("--trace", metavar="EVIDENCE_ID", help="Report recorded source ancestry as JSON")
66
+ args = parser.parse_args(argv)
67
+ failed = False
68
+ for path in args.paths:
69
+ try:
70
+ contract = load_contract(path)
71
+ report = trace_provenance(contract, args.trace) if args.trace else None
72
+ except (OSError, ValueError, yaml.YAMLError) as exc:
73
+ print(f"INVALID {path}: {exc}", file=sys.stderr)
74
+ failed = True
75
+ else:
76
+ if report is None:
77
+ print(f"VALID {path} (structure only)")
78
+ else:
79
+ print(json.dumps({"path": str(path), **report}, indent=2))
80
+ return int(failed)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ raise SystemExit(main())
mceg/_curated.py ADDED
@@ -0,0 +1,171 @@
1
+ """Reuse one reviewed numerical declaration under its recorded conditions.
2
+
3
+ This provider-free experiment is not a v0.2 schema or a general support rule.
4
+ It checks archived files; it does not execute ExoGibbs or FastChem.
5
+ """
6
+
7
+ import hashlib
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
13
+
14
+ from mceg import load_contract
15
+ from mceg.schema import Text
16
+
17
+
18
+ HERE = Path(__file__).resolve().parent / "data" / "curated_gas"
19
+ IMPLEMENTATION = "src/exogibbs/presets/curated.py"
20
+ METHOD = "reviewed_curated_receipt_reuse_v1"
21
+
22
+
23
+ class Record(BaseModel):
24
+ model_config = ConfigDict(extra="forbid", strict=True, allow_inf_nan=False)
25
+
26
+
27
+ class Context(Record):
28
+ temperature_K: float | None = Field(default=None, gt=0)
29
+ pressure_bar: float | None = Field(default=None, gt=0)
30
+ network: Literal["ions", "neutral"] | None = None
31
+ composition: Text | None = None
32
+ python: Text | None = None
33
+ jax: Text | None = None
34
+ backend: Text | None = None
35
+ dtype: Text | None = None
36
+ epsilon_crit: float | None = Field(default=None, gt=0)
37
+
38
+
39
+ class FilePin(Record):
40
+ path: Text
41
+ sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
42
+
43
+
44
+ class Review(Record):
45
+ format: Literal["curated_receipt_review_v1"]
46
+ repository: Text
47
+ audited_revision: Text
48
+ contract: FilePin
49
+ receipt: FilePin
50
+ composition: Literal["asplund_2021_normalized_nuclear"]
51
+ numerical_temperature_K: list[float] = Field(min_length=2, max_length=2)
52
+ rationale: Text
53
+
54
+ @model_validator(mode="after")
55
+ def ordered_temperature_bounds(self):
56
+ lower, upper = self.numerical_temperature_K
57
+ if not 0 < lower <= upper:
58
+ raise ValueError("Numerical temperature bounds must be positive and ordered")
59
+ return self
60
+
61
+
62
+ def sha256(path):
63
+ return hashlib.sha256(path.read_bytes()).hexdigest()
64
+
65
+
66
+ def local_path(root, name):
67
+ if Path(name).is_absolute():
68
+ raise ValueError("Evidence paths must be repository-relative")
69
+ path = (root / name).resolve()
70
+ path.relative_to(root.resolve())
71
+ return path
72
+
73
+
74
+ def checked_pin(root, pin):
75
+ path = local_path(root, pin.path)
76
+ if sha256(path) != pin.sha256:
77
+ raise ValueError(f"Reviewed record changed; review scope before repinning: {pin.path}")
78
+ return path
79
+
80
+
81
+ def run(provider, inputs, review_path=HERE / "review.json"):
82
+ provider = Path(provider).resolve()
83
+ context = Context.model_validate(inputs)
84
+ review = Review.model_validate(json.loads(review_path.read_text()))
85
+ contract = load_contract(checked_pin(provider, review.contract))
86
+ receipt_path = checked_pin(provider, review.receipt)
87
+ receipt = json.loads(receipt_path.read_text())
88
+ if contract.model.id != "exogibbs.curated_gas.v1":
89
+ raise ValueError("This experiment requires the curated gas v1 contract")
90
+ if contract.model.implementation.repository != review.repository:
91
+ raise ValueError("The reviewed provider identity differs from the contract")
92
+ claim = next(c for c in contract.claims if c.id == "equilibrium_verification")
93
+ evidence = next(e for e in contract.evidence if e.id == "verification")
94
+ if (claim.kind != "numerical_verification" or evidence.id not in claim.evidence
95
+ or review.receipt.path not in evidence.sources):
96
+ raise ValueError("The reviewed Claim/Evidence/receipt links must be preserved")
97
+ expected = receipt["selected_file_sha256"]
98
+ if contract.model.implementation.revision != "sha256:" + expected[IMPLEMENTATION]:
99
+ raise ValueError("Contract implementation and receipt identities differ")
100
+
101
+ files = []
102
+ for kind, hashes, prefix in (("input", expected, Path()),
103
+ ("output", receipt["output_sha256"], Path(review.receipt.path).parent)):
104
+ if not hashes:
105
+ raise ValueError("A reviewed receipt needs input and output hashes")
106
+ for name, digest in hashes.items():
107
+ path = local_path(provider, str(prefix / name))
108
+ actual = sha256(path) if path.is_file() else None
109
+ files.append({"kind": kind, "path": path.relative_to(provider).as_posix(),
110
+ "expected_sha256": digest, "actual_sha256": actual,
111
+ "matches": actual == digest})
112
+ intact = all(item["matches"] for item in files)
113
+ temperature = context.temperature_K
114
+ # The reviewed preset's continuous evaluation domain is distinct from its grid.
115
+ lower, upper = review.numerical_temperature_K
116
+ interval = ("unknown" if temperature is None else "within_reported_range"
117
+ if lower <= temperature <= upper else "outside_reported_range")
118
+ sampled = ("unknown" if any(getattr(context, key) is None for key in
119
+ ("temperature_K", "pressure_bar", "network"))
120
+ else "at_recorded_sample" if temperature in receipt["temperature_K"]
121
+ and context.pressure_bar in receipt["pressure_bar"] else "not_sampled")
122
+ missing = [key for key in Context.model_fields if getattr(context, key) is None]
123
+ settings = {key: receipt[key] for key in ("python", "jax", "backend", "dtype", "epsilon_crit")}
124
+ settings["composition"] = review.composition
125
+ different = [key for key, value in settings.items()
126
+ if getattr(context, key) is not None and getattr(context, key) != value]
127
+ completed = (receipt["fastchem"] == "passed"
128
+ and receipt["cases_passed"] == receipt["fastchem_cases_passed"] == 70)
129
+ reasons = []
130
+ if not intact:
131
+ reasons.append("selected_files_changed_or_missing")
132
+ if not completed or evidence.result != "passed" or claim.status != "supported":
133
+ reasons.append("recorded_support_incomplete")
134
+ if missing:
135
+ reasons.append("missing_context")
136
+ if different:
137
+ reasons.append("unreviewed_context")
138
+ if sampled != "at_recorded_sample":
139
+ reasons.append("no_recorded_sample")
140
+ if interval == "outside_reported_range":
141
+ reasons.append("outside_evaluation_domain")
142
+ context_record = context.model_dump(exclude_none=True)
143
+ return {
144
+ "format": "curated_receipt_assessment_v1",
145
+ "review": review.model_dump(), "review_sha256": sha256(review_path),
146
+ "evaluator_sha256": sha256(Path(__file__)),
147
+ "context": context_record,
148
+ "context_sha256": hashlib.sha256(json.dumps(context_record, sort_keys=True).encode()).hexdigest(),
149
+ "imported_declaration": {"origin": "imported_v0.1_declaration", "claim": claim.model_dump()},
150
+ "evidence": {"id": evidence.id, "result": evidence.result,
151
+ "fastchem": receipt["fastchem"], "fastchem_cases_passed": receipt["fastchem_cases_passed"],
152
+ "scope": evidence.scope},
153
+ "integrity": {"selected_files_match": intact, "files": files},
154
+ "assessments": [
155
+ {"kind": "range_membership", "condition": "reviewed_numerical_temperature",
156
+ "interval": {"lower": lower, "upper": upper, "unit": "K", "closure": "closed"},
157
+ "classification": interval},
158
+ {"kind": "sample_membership", "evidence": evidence.id,
159
+ "classification": sampled, "interpolation": "not_assessed"},
160
+ {"kind": "evidence_support", "method": METHOD,
161
+ "target": {"claim": claim.id, "contract_sha256": review.contract.sha256,
162
+ "implementation_sha256": {item["path"]: item["actual_sha256"]
163
+ for item in files if item["kind"] == "input"}},
164
+ "evidence": [evidence.id], "status": "unknown" if reasons else "supported",
165
+ "reasons": reasons or ["reviewed_declaration_reused"],
166
+ "missing_context": missing, "different_context": different},
167
+ ],
168
+ "physical_validation": "unknown",
169
+ "not_evaluated": ["new_numerical_execution", "complete_transitive_dependencies",
170
+ "other_claims", "source_adoption_choices", "physical_accuracy"],
171
+ }
mceg/_display.py ADDED
@@ -0,0 +1,268 @@
1
+ """Optional Japanese UI text; scientific records and machine reports stay original."""
2
+
3
+ import re
4
+ from string import Formatter
5
+ import textwrap
6
+ import unicodedata
7
+
8
+
9
+ # Only application-owned messages are translated. Unknown/free-form text is retained.
10
+ JA = {
11
+ "MCEG checks: recorded structure, source audits and scoped evidence reuse": "MCEG確認:記録の構造、出典の確認、条件を指定した証拠の再利用",
12
+ "Structure": "構造",
13
+ "Source audit": "出典の確認",
14
+ "Selected files": "対象ファイル",
15
+ "Temperature range": "温度範囲",
16
+ "Sample coverage": "試験点の対応",
17
+ "Evidence reuse": "証拠の再利用",
18
+ "Physical validation": "物理的妥当性",
19
+ "Curated context": "curated gasの指定条件",
20
+ "archived example defaults with CLI overrides": "保存済み例の既定値とコマンドで指定した値",
21
+ "Invalid contract": "契約の形式に問題があります",
22
+ "Cannot read the reviewed evidence": "レビュー済みの証拠を読み込めません",
23
+ "Use the provider-owned mceg/contracts/curated_gas.yaml": "提供元のmceg/contracts/curated_gas.yamlを指定してください",
24
+ "Requested T/P/network is an archived sample": "指定した温度・圧力・反応ネットワークに一致する試験点が記録されています",
25
+ "No archived sample established for the requested T/P/network": "指定した温度・圧力・反応ネットワークに対応する試験点を確認できません",
26
+ "Recorded files changed or are missing": "記録されたファイルに変更または欠落があります",
27
+ "The recorded numerical comparison is incomplete": "記録された数値比較が未完了です",
28
+ "Requested condition is not covered by an archived sample": "指定した条件は記録済みの試験点に含まれていません",
29
+ "Temperature is outside the numerical evaluation interval": "温度が数値評価の範囲外です",
30
+ "Reviewed evidence supports this context": "レビュー済みの証拠が指定した条件での主張を支持しています",
31
+ "Support is limited to the explicitly reviewed equilibrium_verification claim.": "支持の対象は、明示的にレビューしたequilibrium_verificationの主張に限られます。",
32
+ "Whole-mixture physical accuracy is unestablished": "混合物全体の物理的な精度は確認されていません",
33
+ "No automatic evidence-reuse rule for this model": "このモデルには証拠の再利用を自動判定する規則がありません",
34
+ "Stored support assessments are scoped records; inspect a specific assessment with mceg explain": "支持の評価は対象と条件ごとの記録です。mceg explainで個別の評価を確認してください",
35
+ "Recorded classification differs from the current Condition/Context; review the changed record.": "記録された判定が現在のCondition/Contextと一致しません。変更された記録を確認してください。",
36
+ " Context describes the requested target; the installed numerical environment is not inspected.": " これは評価対象として指定された条件です。インストール済みの計算環境は調査していません。",
37
+ " Recorded v0.1 declarations (not reassessed by structure/source checks):": " 記録済みのv0.1宣言(構造・出典の確認では再評価しません):",
38
+ " Recorded v0.2 assessments (each bound to its own subject and context):": " 記録済みのv0.2評価(それぞれ固有の対象と条件に対応):",
39
+ "UNKNOWN identifies missing support or review gaps. These checks do not run numerical solvers.": "UNKNOWNは証拠や確認が不足している箇所を示します。この確認では数値ソルバーを実行しません。",
40
+ "Next steps": "次の作業",
41
+ "passed": "件 PASS",
42
+ "failed": "件 FAIL",
43
+ "unknown": "件 UNKNOWN",
44
+ "No contracts selected. Use a contract file or a directory containing YAML contracts.": "契約が選択されていません。契約ファイルまたはYAML契約を含むディレクトリを指定してください。",
45
+ "No contracts selected.": "契約が選択されていません。",
46
+ "MCEG next steps: recorded gaps and suggested work; no evidence is changed.": "MCEGの次の作業:記録された未確認事項と作業案を表示します。証拠は変更しません。",
47
+ "Contract text, authored instructions and IDs are shown in their original language.": "契約の本文、記録された作業指示、IDは原文のまま表示します。",
48
+ "Requested curated context (not an inspection of the installed environment):": "curated gasの指定条件(インストール済み環境の調査結果ではありません):",
49
+ "Use --json to inspect all joint samples.": "すべての試験点の組み合わせを見るには--jsonを指定してください。",
50
+ "Recorded next step": "記録された次の作業(原文)",
51
+ "Suggested next step": "提案する次の作業",
52
+ "Comparison target": "比較対象",
53
+ "Comparison note": "比較についての記録",
54
+ "Review progress": "確認作業の進み具合",
55
+ "Follow-up work has a completion record. Its results are listed below.": "この記録に関する作業には完了報告があります。その結果を下に示します。",
56
+ "The sources below have follow-up comparison results. Their old 'not_checked' entries are kept as history.": "下の資料は、その後の比較結果が記録されています。「未比較」は当時の記録として残しています。",
57
+ "The compared parts agree.": "比較済み:確認した範囲では一致しています。",
58
+ "Compared; differences remain to be explained.": "比較済み:違いが見つかり、その理由や扱いの確認が残っています。",
59
+ "The source-reading task has a completion record. The old inspection state is kept as history.": "資料を読んで確認する作業には完了報告があります。元の確認状況は当時の記録として残しています。",
60
+ "Task completion does not establish model accuracy. Use the linked assessments to see what the evidence supports.": "作業の完了だけでは、モデルの精度は保証されません。確認できた精度や条件は、下に案内する評価で確認してください。",
61
+ "Original record (history retained)": "元の記録(当時の状態を保持)",
62
+ "Completed follow-up": "完了した作業(記録)",
63
+ "Recorded task completion only; historical source fields and support judgments are unchanged.": "これは作業の完了報告です。以前の出典確認や精度の評価は、当時の記録として残しています。",
64
+ "Needed": "必要なもの",
65
+ "Source": "出典",
66
+ "Completion": "完了条件",
67
+ "Inspect": "詳しく確認",
68
+ "Evidence gaps (IDs for --evidence)": "証拠の未確認事項(--evidenceで指定するID)",
69
+ "Unknown Claim IDs": "評価がunknownの主張ID",
70
+ "Evidence": "証拠(Evidence)",
71
+ "Claim": "科学的主張(Claim)",
72
+ "Assessment": "評価(Assessment)",
73
+ "Condition": "比較する条件(Condition)",
74
+ "Id": "ID",
75
+ "Kind": "種類",
76
+ "Revision": "版",
77
+ "Summary": "概要",
78
+ "Statement": "主張の内容",
79
+ "Scope": "対象範囲",
80
+ "Sources": "出典",
81
+ "Provenance": "出典の追跡情報",
82
+ "Role": "資料の役割",
83
+ "Location": "該当箇所",
84
+ "Inspection": "確認状況",
85
+ "Note": "記録された補足",
86
+ "Derived from": "元になった証拠",
87
+ "Comparison": "比較結果",
88
+ "Result": "記録された結果",
89
+ "Status": "記録された状態",
90
+ "Origin": "評価の由来",
91
+ "Method": "評価方法",
92
+ "Description": "説明",
93
+ "Subject": "評価対象(Subject)",
94
+ "Context": "計算条件(Context)",
95
+ "Claim revision": "主張の版",
96
+ "Condition revision": "比較する条件の版",
97
+ "Reasons": "理由",
98
+ "Implementation": "実装",
99
+ "Repository": "リポジトリ",
100
+ "Entry points": "実装の入口",
101
+ "Quantities": "物理量",
102
+ "Settings": "設定",
103
+ "Artifacts": "保存された成果物",
104
+ "Samples": "記録された試験点",
105
+ "Value": "値",
106
+ "Unit": "単位",
107
+ "Quantity": "物理量",
108
+ "Interval": "区間",
109
+ "Lower": "下限",
110
+ "Upper": "上限",
111
+ "Closure": "端点の扱い",
112
+ "Coverage": "確認した範囲",
113
+ "Classification": "条件との比較結果",
114
+ "Missing file": "欠落したファイル",
115
+ "Use --plain for the original text layout or --json for complete records.": "従来のテキスト表示は--plain、完全な記録は--jsonで確認できます。",
116
+ "subject": "評価対象(Subject)",
117
+ "context": "計算条件(Context)",
118
+ "claim": "科学的主張(Claim)",
119
+ "condition": "比較する条件(Condition)",
120
+ "missing_provenance": "出典の追跡情報がありません",
121
+ "source_not_fully_inspected": "出典の必要な内容を確認できていません",
122
+ "unknown_source_role": "一次資料か二次資料か未確認です",
123
+ "primary_source_not_identified": "一次資料が特定されていません",
124
+ "comparison_not_checked": "この記録では、元の資料との比較結果がまだ記録されていません",
125
+ "comparison_discrepancy": "比較した結果、元の資料と異なる点が記録されています",
126
+ "An explicitly recorded joint sample matches; other context fields are not evaluated.": "明示的に記録された条件の組み合わせに一致します。他の条件項目は評価していません。",
127
+ "No recorded joint sample matches; interpolation and physical accuracy are not assessed.": "一致する条件の組み合わせは記録されていません。補間と物理的精度は評価していません。",
128
+ "Review the listed source for this precise scope and record its role, location and inspection state.": "この範囲に関係する出典を確認し、資料の役割、該当箇所、確認状況を記録してください。",
129
+ "Add the inspected passage or code location and any upstream evidence/comparison; a source locator alone is insufficient.": "確認した文章やコードの位置と、元の証拠・比較結果を記録してください。出典の場所を示すだけでは確認完了になりません。",
130
+ "Obtain and inspect the relevant full text or data. If access is restricted, a human may need an institutional library or an authorized author copy.": "必要な本文やデータを入手して内容を確認してください。閲覧制限がある場合は、人間が所属機関の図書館や著者から正規の提供を受ける必要があるかもしれません。",
131
+ "Record the exact equation/table/page, relevant values, units and material scope; mark only the content actually inspected.": "該当する式・表・ページ、値、単位、対象物質の範囲を記録し、実際に確認した内容だけを確認済みとしてください。",
132
+ "The listed source content, not only its abstract or citation.": "出典の本文やデータ。要旨や引用情報だけでは足りません。",
133
+ "Determine whether this is an original source or a later account, and identify the original work.": "一次資料か後の解説かを確認し、元の研究を特定してください。",
134
+ "Record the verified source role and link the inspected original evidence.": "確認した資料の役割を記録し、内容を確認した一次資料の証拠へリンクしてください。",
135
+ "Trace the scoped statement to its original theoretical, experimental or observational source.": "対象の記述を元の理論・実験・観測資料までたどってください。",
136
+ "Add a primary evidence record and the inspected derivation/comparison links.": "一次資料の証拠記録と、確認した導出・比較のリンクを追加してください。",
137
+ "Compare the equations, tables or code at the source locations below. Check values, units, definitions and the materials and conditions covered.": "下に示す資料の式・表・コードと、対象の実装や記述を見比べてください。値だけでなく、単位や定義、扱う物質・条件も確認してください。",
138
+ "After inspecting both sources, record where you compared them, what agrees and what differs.": "両方の資料を確認してから、見比べたページ・式・表と、一致した点・異なる点を記録してください。",
139
+ "The comparison found differences. Check whether units, definitions, conditions or transcription explain them before changing a claim.": "比較は済んでいますが、違いが見つかっています。単位・定義・対象条件や転記の誤りで説明できるかを確認してください。",
140
+ "Record how each difference was explained, or which differences still need investigation.": "違いの理由が分かった点と、まだ調べる必要がある点を記録してください。",
141
+ "Define the physical observable, accuracy criterion and calculation conditions, then obtain relevant independent measurements or observations.": "何をどの条件で、どれくらいの誤差まで再現したいかを決め、その条件に合う実験・観測データを用意してください。",
142
+ "Record the comparison as new physical Evidence and a scoped Assessment; numerical solver agreement alone does not establish physical accuracy.": "実験・観測との比較結果と、確認できた精度・条件を新しいEvidenceとAssessmentに記録してください。計算同士の一致だけでは、実験に合うかは分かりません。",
143
+ "Review the claim's scope and evidence for the intended implementation and conditions.": "使用する実装と条件に対して、主張の範囲と証拠を確認してください。",
144
+ "Record a separate scoped support Assessment; preserve the earlier declaration and execution results.": "対象を明示した支持のAssessmentを別に記録し、以前の宣言と実行結果を保持してください。",
145
+ "Review the imported declaration before using it for a new context.": "取り込んだ宣言を新しい条件で使う前に、その適用範囲を確認してください。",
146
+ "Keep the imported Assessment attached to its original Claim revision; add a separate reviewed Assessment for the new target.": "取り込んだAssessmentは元のClaimの版に対応させたまま、新しい対象への評価を別にレビューして追加してください。",
147
+ "Create a new context and run mceg assess with a new output file; preserve this assessment.": "新しいContextと出力ファイルを指定してmceg assessを実行し、この評価も保持してください。",
148
+ "Run the provider verification for the requested conditions and environment, retaining a new execution receipt and outputs.": "指定した条件と環境で提供元の数値検証を実行し、新しい実行記録と出力を保存してください。",
149
+ "Review the new run's coverage and Claim criteria before adding its support Assessment.": "新しい実行が確認した範囲とClaimの判定基準をレビューしてから、支持のAssessmentを追加してください。",
150
+ "Supply the missing quantities or settings identified in the reasons, using a new Context ID.": "理由に示された不足する量や設定を、新しいContext IDで指定してください。",
151
+ "Run mceg assess with the new context and a new output file; retain the old assessment.": "新しい条件と出力ファイルでmceg assessを実行し、以前の評価も保持してください。",
152
+ "Obtain numerical evidence at the requested joint conditions, or review an explicit interpolation method.": "指定した条件の組み合わせで数値的な証拠を取得するか、明示的な補間方法をレビューしてください。",
153
+ "Record actual new samples and their evidence; range inclusion alone is insufficient.": "実際に確認した新しい試験点と証拠を記録してください。範囲内にあるだけでは十分ではありません。",
154
+ "Review whether the intended calculation requires evidence beyond this source's reported interval.": "計算の目的に対して、この出典の報告範囲を超える証拠が必要か確認してください。",
155
+ "Use an evidenced condition or obtain new scoped evidence; do not widen an interval merely to clear this result.": "証拠のある条件を使うか、新しい範囲の証拠を取得してください。この結果を解消するためだけに範囲を広げないでください。",
156
+ "Correct the field or local reference identified in the validation error.": "形式の検証エラーで示された項目や参照先を修正してください。",
157
+ "Run mceg check again; valid structure permits inspection but does not establish scientific support.": "mceg checkを再実行してください。構造が正しくても、科学的な支持が確立したわけではありません。",
158
+ "Select an evidence ID below to see the source, recorded access/inspection notes and upstream comparisons.": "下の証拠IDを指定すると、出典、入手・確認状況の記録、元の資料との比較を見られます。",
159
+ "Complete the scoped inspection and comparisons; obtaining a document alone does not complete the audit.": "必要な範囲の内容確認と比較を完了してください。文献を入手しただけでは確認完了になりません。",
160
+ "Inspect the changed or missing paths listed below.": "下に示された変更・欠落したファイルを確認してください。",
161
+ "Restore the archived files for a replay, or produce and review new evidence for the changed implementation; do not replace hashes to manufacture a pass.": "再現確認には保存時のファイルを復元するか、変更した実装の証拠を新たに作成・レビューしてください。ハッシュの書き換えだけでPASSにしないでください。",
162
+ "Review the requested temperature, pressure and network against the archived scope.": "指定した温度・圧力・反応ネットワークを、記録された検証範囲と照合してください。",
163
+ "For a new condition, obtain actual new numerical evidence and a scoped review; no interpolation support is inferred.": "新しい条件では、実際の数値検証の証拠とその範囲のレビューが必要です。補間の妥当性は自動推定しません。",
164
+ "Use --context with the actual intended quantities, composition, environment and tolerance.": "実際に使用する量、組成、環境、許容誤差を--contextで指定してください。",
165
+ "A provider environment and the relevant numerical comparator; mceg explain does not execute them.": "提供元の計算環境と対応する比較用の数値計算。mceg explain自体はこれらを実行しません。",
166
+ "Inspect the recorded Claim/Assessment scope and any changed contract or receipt.": "記録されたClaim/Assessmentの範囲と、契約や実行記録の変更を確認してください。",
167
+ "Define or review a scoped reuse policy with evidence for the intended target; imported support is not a new evaluation.": "使用する対象への証拠に基づき、再利用できる範囲と規則を定めるかレビューしてください。取り込んだ支持の宣言は新しい評価ではありません。",
168
+ "Inspect the recorded assessment and its context using --assessment ID.": "--assessment IDで記録された評価とその条件を確認してください。",
169
+ "Address the stated reason and append a separately identified assessment; preserve the old record.": "示された理由に対応し、別のIDの評価を追加してください。以前の記録も保持してください。",
170
+ }
171
+
172
+ # Match complete application messages, preserving interpolated IDs, paths and values.
173
+ TEMPLATES = {
174
+ "Collected {count} contracts; selected {selected}": "{count}件の契約を検出、{selected}件を選択",
175
+ "v{version} structure and local references are consistent": "v{version}の構造と内部の参照先は整合しています",
176
+ "{count} recorded input/output hashes match": "記録された入出力ハッシュの{count}件が一致しています",
177
+ "{count} evidence records have complete declared source traces": "証拠の{count}件で、記録上の出典確認が完了しています",
178
+ "{temperature} K; within numerical interval {bounds} K": "{temperature} Kは数値評価の範囲{bounds} Kの内側です",
179
+ "{temperature} K; outside numerical interval {bounds} K": "{temperature} Kは数値評価の範囲{bounds} Kの外側です",
180
+ "Missing temperature K; not compared with numerical interval {bounds} K": "温度が未指定です。数値評価の範囲{bounds} Kとの比較はできません",
181
+ "Missing context: {fields}": "不足する条件項目:{fields}",
182
+ "Different context: {fields}": "記録と異なる条件項目:{fields}",
183
+ "Historical numerical result: {result}": "過去の数値検証結果:{result}",
184
+ "Assessment: {id}": "評価ID:{id}",
185
+ "Missing quantity: {name}": "不足する量:{name}",
186
+ "Missing sample coordinates: {names}": "試験点との比較に必要な条件が不足しています:{names}",
187
+ "{value} compared with closed interval {bounds}; marginal coverage only.": "{value}を両端を含む範囲{bounds}と比較しました。個々の量についての範囲だけを確認しています。",
188
+ "{value} compared with closed interval {bounds}; numerical_evaluation coverage only.": "{value}を両端を含む範囲{bounds}と比較しました。数値評価の範囲だけを確認しています。",
189
+ "context={context}; within_reported_range": "条件={context};報告された範囲内",
190
+ "context={context}; outside_reported_range": "条件={context};報告された範囲外",
191
+ "context={context}; at_recorded_sample": "条件={context};記録された試験点に一致",
192
+ "context={context}; not_sampled": "条件={context};記録された試験点に一致しない",
193
+ "context={context}; unknown": "条件={context};必要な情報が不足",
194
+ "Supply context fields: {fields}": "不足する条件項目を指定してください:{fields}",
195
+ "Supply the actual context fields: {fields}": "実際に使用する条件を指定してください:{fields}",
196
+ "{passed} passed, {failed} failed, {unknown} unknown": "PASS {passed}件、FAIL {failed}件、UNKNOWN {unknown}件",
197
+ }
198
+
199
+
200
+ def _pattern(template):
201
+ return re.compile("".join(re.escape(literal) + (f"(?P<{field}>.*?)" if field else "")
202
+ for literal, field, _, _ in Formatter().parse(template)))
203
+
204
+
205
+ PATTERNS = [(_pattern(source), target) for source, target in TEMPLATES.items()]
206
+
207
+
208
+ def translate(message, lang):
209
+ if lang != "ja":
210
+ return message
211
+ if message in JA:
212
+ return JA[message]
213
+ for pattern, target in PATTERNS:
214
+ match = pattern.fullmatch(message)
215
+ if match:
216
+ return target.format_map(match.groupdict())
217
+ return message
218
+
219
+
220
+ def check_message(check, lang):
221
+ message = check["message"]
222
+ if message in JA:
223
+ return translate(message, lang)
224
+ if check["name"] == "evidence_reuse" and lang == "ja":
225
+ return ";".join(translate(part, lang) for part in message.split("; "))
226
+ return translate(message, lang)
227
+
228
+
229
+ def detail(message, lang):
230
+ prefix, separator, issue = message.rpartition(": ")
231
+ if separator and issue in {"missing_provenance", "source_not_fully_inspected", "unknown_source_role",
232
+ "primary_source_not_identified", "comparison_not_checked", "comparison_discrepancy"}:
233
+ return prefix + separator + translate(issue, lang)
234
+ return translate(message, lang)
235
+
236
+
237
+ def language_command(command, lang):
238
+ return command + " --lang ja" if lang == "ja" else command
239
+
240
+
241
+ def add_language_argument(parser):
242
+ parser.add_argument("--lang", choices=("en", "ja"), default="en",
243
+ help="Language for human-facing output (default: en); JSON and stored text remain original")
244
+
245
+
246
+ def wrap(text, width, indent, lang):
247
+ if lang != "ja":
248
+ return textwrap.fill(text, width=width, subsequent_indent=indent)
249
+ # Count terminal cells so Japanese prose fits without adding a dependency.
250
+ def cell_width(char):
251
+ return 0 if unicodedata.combining(char) else 2 if unicodedata.east_asian_width(char) in {"W", "F"} else 1
252
+
253
+ lines, line, cells = [], "", 0
254
+ for token in re.findall(r"[!-~]+|.", text, re.DOTALL):
255
+ size = sum(cell_width(char) for char in token)
256
+ if cells + size > width and size <= width - len(indent):
257
+ lines.append(line.rstrip())
258
+ line, cells = indent, len(indent)
259
+ for char in token:
260
+ size = cell_width(char)
261
+ if char == "\n" or cells + size > width:
262
+ lines.append(line.rstrip())
263
+ line, cells = indent, len(indent)
264
+ if char.isspace():
265
+ continue
266
+ line += char
267
+ cells += size
268
+ return "\n".join([*lines, line.rstrip()])