phm-lite-check 1.0.1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PHM Audit Cloud contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: phm-lite-check
3
+ Version: 1.0.1
4
+ Summary: Deterministic physics plausibility checking for LLM outputs and robotics datasets (offline subset of the PHM engine). Zero dependencies.
5
+ Author: PHM-Audit
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/PHM-Audit/phm-lite-check
8
+ Project-URL: Repository, https://github.com/PHM-Audit/phm-lite-check
9
+ Keywords: llm,agent,validation,physics,hallucination,robotics,dataset-quality,embodied-ai,guardrail
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: langchain
25
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
26
+ Provides-Extra: autogen
27
+ Requires-Dist: autogen-agentchat>=0.4; extra == "autogen"
28
+ Provides-Extra: mcp
29
+ Requires-Dist: mcp>=1.0; extra == "mcp"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # phm-lite-check
35
+
36
+ <!-- mcp-name: io.github.phm-audit/phm-lite-check -->
37
+
38
+ Deterministic physics plausibility checking for **LLM outputs** and
39
+ **robotics / embodied-AI datasets**. Zero dependencies, pure standard
40
+ library, Python >= 3.8.
41
+
42
+ This package is an **independent, community-provided library**: it is the
43
+ offline subset of the PHM physical-judgment engine (proprietary core
44
+ methods are intentionally excluded). A companion plugin
45
+ (`phm-physics-check`) is listed on the Dify Marketplace.
46
+
47
+ > Not affiliated with, endorsed by, or sponsored by LangChain, AutoGen,
48
+ > or Dify. Integration adapters are community-provided.
49
+
50
+ ## MCP server
51
+
52
+ This package doubles as a local MCP server (`stdio` transport). After
53
+ installing, point your MCP client at:
54
+
55
+ ```
56
+ python3 -m phm_lite_check.mcp_server
57
+ ```
58
+
59
+ Exposing three tools: `physics_text_check`, `physics_quantity_check`,
60
+ `robotics_data_check`.
61
+
62
+ ## What it checks
63
+
64
+ ### Text / quantities (`check_text`, `full_check`)
65
+ - Dimensional / unit consistency (SI + common units)
66
+ - Universal speed limit (c)
67
+ - Absolute temperature floor (0 K)
68
+ - Non-negativity of mass / length / energy
69
+ - Probability range [0, 1]
70
+ - Kinematic relations (free-fall family, tolerance-checked)
71
+ - Conservation checks (energy / momentum, tolerance-checked)
72
+
73
+ ### Robotics records (`check_robotics_record`)
74
+ - Quaternion normalization (|q| within tolerance of 1.0)
75
+ - Timestamp sanity: monotonicity, duplicates, NaN/Inf, outlier gaps
76
+ (possible frame loss)
77
+ - NaN / Inf scan over numeric arrays
78
+ - Unit annotation presence for common motion fields
79
+
80
+ ## Design guarantees
81
+
82
+ - **Deterministic**: same input -> same output, always reproducible.
83
+ - **Zero dependencies**: pure standard library; framework adapters import
84
+ their host lazily.
85
+ - **Verdicts**: PASS / WARN / FAIL with structured violation records.
86
+ - **No network calls**: everything runs offline in-process.
87
+
88
+ > NOTE: this package contains only the deterministic offline subset of the
89
+ > PHM physical-judgment engine. Proprietary core methods are intentionally
90
+ > excluded. (Deployment-specific deep checks are available separately via a
91
+ > configurable remote engine endpoint, not included in this package.)
92
+
93
+ ## Install
94
+
95
+ **Option A — from the GitHub repo** (no package index needed):
96
+
97
+ ```bash
98
+ pip install git+https://github.com/PHM-Audit/phm-lite-check
99
+ ```
100
+
101
+ **Option B — from a Release wheel** (faster, no git clone):
102
+
103
+ ```bash
104
+ pip install https://github.com/PHM-Audit/phm-lite-check/releases/download/v1.0.0/phm_lite_check-1.0.0-py3-none-any.whl
105
+ ```
106
+
107
+ **Optional extras** (once installed):
108
+
109
+ ```bash
110
+ pip install "phm-lite-check[langchain]" # + langchain-core (adapter tests)
111
+ pip install "phm-lite-check[autogen]" # + autogen-agentchat
112
+ ```
113
+
114
+ PyPI listing is planned; both options above work today.
115
+
116
+ ## Quickstart — library
117
+
118
+ ```python
119
+ from phm_lite_check import check_text, check_robotics_record
120
+
121
+ check_text("The drone descends at 450 km/h and confidence is 1.8.")
122
+ # {"verdict": "FAIL", "violations": [...], ...}
123
+
124
+ check_robotics_record({
125
+ "quaternion": [1.395, 0.0, 0.0, 0.0], # |q| >> 1 -> FAIL
126
+ "timestamps": [0.0, 0.1, 0.05], # not monotonic -> FAIL
127
+ "position": [1.0, 2.0], # no unit annotated -> WARN
128
+ })
129
+ ```
130
+
131
+ ## Quickstart — LangChain
132
+
133
+ ```python
134
+ from phm_lite_check.integrations import langchain_tool
135
+
136
+ tools = langchain_tool.get_langchain_tools() # 3 @tool callables
137
+ agent = create_react_agent(llm, tools) # your usual wiring
138
+ ```
139
+
140
+ ## Quickstart — AutoGen (v0.4+)
141
+
142
+ ```python
143
+ from autogen_agentchat.agents import AssistantAgent
144
+ from phm_lite_check.integrations import autogen_tool
145
+
146
+ assistant = AssistantAgent(
147
+ name="checked_assistant",
148
+ model_client=..., # your model client
149
+ tools=autogen_tool.get_autogen_tools(),
150
+ )
151
+ ```
152
+
153
+ ## Tools exposed by both adapters
154
+
155
+ | Tool | Input | Checks |
156
+ |------|-------|--------|
157
+ | `physics_text_check` | free text with quantities | unit consistency, limits, probabilities |
158
+ | `physics_quantity_check` | value + unit | one quantity against physical law |
159
+ | `robotics_data_check` | JSON record | quaternions, timestamps, NaN/Inf, units |
160
+
161
+ ## Tests
162
+
163
+ ```bash
164
+ pip install -e ".[dev]"
165
+ pytest tests/ -q
166
+ ```
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,137 @@
1
+ # phm-lite-check
2
+
3
+ <!-- mcp-name: io.github.phm-audit/phm-lite-check -->
4
+
5
+ Deterministic physics plausibility checking for **LLM outputs** and
6
+ **robotics / embodied-AI datasets**. Zero dependencies, pure standard
7
+ library, Python >= 3.8.
8
+
9
+ This package is an **independent, community-provided library**: it is the
10
+ offline subset of the PHM physical-judgment engine (proprietary core
11
+ methods are intentionally excluded). A companion plugin
12
+ (`phm-physics-check`) is listed on the Dify Marketplace.
13
+
14
+ > Not affiliated with, endorsed by, or sponsored by LangChain, AutoGen,
15
+ > or Dify. Integration adapters are community-provided.
16
+
17
+ ## MCP server
18
+
19
+ This package doubles as a local MCP server (`stdio` transport). After
20
+ installing, point your MCP client at:
21
+
22
+ ```
23
+ python3 -m phm_lite_check.mcp_server
24
+ ```
25
+
26
+ Exposing three tools: `physics_text_check`, `physics_quantity_check`,
27
+ `robotics_data_check`.
28
+
29
+ ## What it checks
30
+
31
+ ### Text / quantities (`check_text`, `full_check`)
32
+ - Dimensional / unit consistency (SI + common units)
33
+ - Universal speed limit (c)
34
+ - Absolute temperature floor (0 K)
35
+ - Non-negativity of mass / length / energy
36
+ - Probability range [0, 1]
37
+ - Kinematic relations (free-fall family, tolerance-checked)
38
+ - Conservation checks (energy / momentum, tolerance-checked)
39
+
40
+ ### Robotics records (`check_robotics_record`)
41
+ - Quaternion normalization (|q| within tolerance of 1.0)
42
+ - Timestamp sanity: monotonicity, duplicates, NaN/Inf, outlier gaps
43
+ (possible frame loss)
44
+ - NaN / Inf scan over numeric arrays
45
+ - Unit annotation presence for common motion fields
46
+
47
+ ## Design guarantees
48
+
49
+ - **Deterministic**: same input -> same output, always reproducible.
50
+ - **Zero dependencies**: pure standard library; framework adapters import
51
+ their host lazily.
52
+ - **Verdicts**: PASS / WARN / FAIL with structured violation records.
53
+ - **No network calls**: everything runs offline in-process.
54
+
55
+ > NOTE: this package contains only the deterministic offline subset of the
56
+ > PHM physical-judgment engine. Proprietary core methods are intentionally
57
+ > excluded. (Deployment-specific deep checks are available separately via a
58
+ > configurable remote engine endpoint, not included in this package.)
59
+
60
+ ## Install
61
+
62
+ **Option A — from the GitHub repo** (no package index needed):
63
+
64
+ ```bash
65
+ pip install git+https://github.com/PHM-Audit/phm-lite-check
66
+ ```
67
+
68
+ **Option B — from a Release wheel** (faster, no git clone):
69
+
70
+ ```bash
71
+ pip install https://github.com/PHM-Audit/phm-lite-check/releases/download/v1.0.0/phm_lite_check-1.0.0-py3-none-any.whl
72
+ ```
73
+
74
+ **Optional extras** (once installed):
75
+
76
+ ```bash
77
+ pip install "phm-lite-check[langchain]" # + langchain-core (adapter tests)
78
+ pip install "phm-lite-check[autogen]" # + autogen-agentchat
79
+ ```
80
+
81
+ PyPI listing is planned; both options above work today.
82
+
83
+ ## Quickstart — library
84
+
85
+ ```python
86
+ from phm_lite_check import check_text, check_robotics_record
87
+
88
+ check_text("The drone descends at 450 km/h and confidence is 1.8.")
89
+ # {"verdict": "FAIL", "violations": [...], ...}
90
+
91
+ check_robotics_record({
92
+ "quaternion": [1.395, 0.0, 0.0, 0.0], # |q| >> 1 -> FAIL
93
+ "timestamps": [0.0, 0.1, 0.05], # not monotonic -> FAIL
94
+ "position": [1.0, 2.0], # no unit annotated -> WARN
95
+ })
96
+ ```
97
+
98
+ ## Quickstart — LangChain
99
+
100
+ ```python
101
+ from phm_lite_check.integrations import langchain_tool
102
+
103
+ tools = langchain_tool.get_langchain_tools() # 3 @tool callables
104
+ agent = create_react_agent(llm, tools) # your usual wiring
105
+ ```
106
+
107
+ ## Quickstart — AutoGen (v0.4+)
108
+
109
+ ```python
110
+ from autogen_agentchat.agents import AssistantAgent
111
+ from phm_lite_check.integrations import autogen_tool
112
+
113
+ assistant = AssistantAgent(
114
+ name="checked_assistant",
115
+ model_client=..., # your model client
116
+ tools=autogen_tool.get_autogen_tools(),
117
+ )
118
+ ```
119
+
120
+ ## Tools exposed by both adapters
121
+
122
+ | Tool | Input | Checks |
123
+ |------|-------|--------|
124
+ | `physics_text_check` | free text with quantities | unit consistency, limits, probabilities |
125
+ | `physics_quantity_check` | value + unit | one quantity against physical law |
126
+ | `robotics_data_check` | JSON record | quaternions, timestamps, NaN/Inf, units |
127
+
128
+ ## Tests
129
+
130
+ ```bash
131
+ pip install -e ".[dev]"
132
+ pytest tests/ -q
133
+ ```
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ """phm_lite_check -- deterministic physics plausibility checking for
3
+ LLM outputs and robotics datasets. Offline subset of the PHM engine.
4
+
5
+ Public API:
6
+ check_text(text) -> scan text for implausible quantities
7
+ full_check(text, kinematics, conservation) -> text + kinematics + conservation
8
+ check_quantity(value, unit) -> one quantity against physical law
9
+ check_robotics_record(record) -> robotics/embodied-AI dataset record
10
+ ENGINE_ID, __version__
11
+ """
12
+
13
+ from .checker import (check_text, full_check, check_quantity,
14
+ check_probability, check_kinematics, check_conservation,
15
+ ENGINE_ID, __version__ as _checker_version)
16
+ from .robotics import check_robotics_record
17
+
18
+ __version__ = "1.0.0"
19
+ __all__ = [
20
+ "check_text", "full_check", "check_quantity", "check_probability",
21
+ "check_kinematics", "check_conservation", "check_robotics_record",
22
+ "ENGINE_ID", "__version__",
23
+ ]
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PHM-Lite: Deterministic physics plausibility checker (offline subset).
4
+
5
+ Three-time-calibrated rules from common physical law:
6
+ - Dimensional / unit consistency
7
+ - Universal speed limit (c)
8
+ - Absolute temperature floor (0 K)
9
+ - Non-negativity of mass / length / energy
10
+ - Probability range [0, 1]
11
+ - Kinematic relations (free-fall family, tolerance-checked)
12
+ - Conservation checks (energy / momentum, tolerance-checked)
13
+
14
+ Design guarantees:
15
+ - Deterministic: same input -> same output, always reproducible.
16
+ - Zero dependencies (pure standard library), Python >= 3.8.
17
+ - Verdicts: PASS / WARN / FAIL with structured violation records.
18
+
19
+ NOTE: This is the OFFLINE SUBSET of the PHM physical-judgment engine.
20
+ It intentionally excludes proprietary core methods; deployment-specific
21
+ deep checks are exposed via a configurable remote engine endpoint.
22
+ """
23
+
24
+ import math
25
+ import re
26
+ from typing import Any, Dict, List, Optional, Tuple
27
+
28
+ __version__ = "1.0.0"
29
+ ENGINE_ID = "PHM-Lite"
30
+ DETERMINISTIC = True
31
+
32
+ # --- Physical constants (SI) ---
33
+ C_LIGHT = 299_792_458.0 # m/s, exact
34
+ ABSOLUTE_ZERO_C = -273.15 # degC
35
+ G_EARTH = 9.80665 # m/s^2, standard gravity
36
+
37
+ # Sanity thresholds for WARN (orders of magnitude, engineering judgment)
38
+ WARN_ACC_G = 100.0 # sustained >100 g is suspicious outside impact events
39
+ WARN_SPEED_FRAC = 0.5 # >0.5 c is suspicious for macroscopic objects
40
+
41
+
42
+ class Violation:
43
+ __slots__ = ("rule", "severity", "detail", "location")
44
+
45
+ def __init__(self, rule: str, severity: str, detail: str, location: str = ""):
46
+ self.rule = rule
47
+ self.severity = severity # "FAIL" | "WARN"
48
+ self.detail = detail
49
+ self.location = location
50
+
51
+ def to_dict(self) -> Dict[str, str]:
52
+ return {"rule": self.rule, "severity": self.severity,
53
+ "detail": self.detail, "location": self.location}
54
+
55
+
56
+ def _verdict(violations: List[Violation]) -> str:
57
+ if any(v.severity == "FAIL" for v in violations):
58
+ return "FAIL"
59
+ if any(v.severity == "WARN" for v in violations):
60
+ return "WARN"
61
+ return "PASS"
62
+
63
+
64
+ # ---------------------------------------------------------------- units ---
65
+
66
+ # unit -> (dimension, to_si_factor, si_offset_for_temperature)
67
+ _UNIT_TABLE = {
68
+ # length
69
+ "m": ("L", 1.0, 0.0), "km": ("L", 1000.0, 0.0), "cm": ("L", 0.01, 0.0),
70
+ "mm": ("L", 0.001, 0.0), "mile": ("L", 1609.344, 0.0),
71
+ "ft": ("L", 0.3048, 0.0), "au": ("L", 1.495978707e11, 0.0),
72
+ "ly": ("L", 9.4607e15, 0.0),
73
+ # time
74
+ "s": ("T", 1.0, 0.0), "ms": ("T", 0.001, 0.0), "min": ("T", 60.0, 0.0),
75
+ "h": ("T", 3600.0, 0.0), "hour": ("T", 3600.0, 0.0),
76
+ "day": ("T", 86400.0, 0.0), "yr": ("T", 3.15576e7, 0.0),
77
+ # mass
78
+ "kg": ("M", 1.0, 0.0), "g": ("M", 0.001, 0.0), "mg": ("M", 1e-6, 0.0),
79
+ "t": ("M", 1000.0, 0.0), "ton": ("M", 1000.0, 0.0),
80
+ "lb": ("M", 0.45359237, 0.0),
81
+ # speed
82
+ "m/s": ("L/T", 1.0, 0.0), "km/h": ("L/T", 1.0 / 3.6, 0.0),
83
+ "km/s": ("L/T", 1000.0, 0.0), "mph": ("L/T", 0.44704, 0.0),
84
+ "knot": ("L/T", 0.514444, 0.0), "kn": ("L/T", 0.514444, 0.0),
85
+ "c": ("L/T", C_LIGHT, 0.0),
86
+ # acceleration
87
+ "m/s2": ("L/T2", 1.0, 0.0), "m/s^2": ("L/T2", 1.0, 0.0),
88
+ "g0": ("L/T2", G_EARTH, 0.0), "gal": ("L/T2", 0.01, 0.0),
89
+ # force / energy / power
90
+ "N": ("M*L/T2", 1.0, 0.0), "kN": ("M*L/T2", 1000.0, 0.0),
91
+ "J": ("M*L2/T2", 1.0, 0.0), "kJ": ("M*L2/T2", 1000.0, 0.0),
92
+ "MJ": ("M*L2/T2", 1e6, 0.0), "Wh": ("M*L2/T2", 3600.0, 0.0),
93
+ "kWh": ("M*L2/T2", 3.6e6, 0.0), "eV": ("M*L2/T2", 1.602176634e-19, 0.0),
94
+ "MeV": ("M*L2/T2", 1.602176634e-13, 0.0),
95
+ "GeV": ("M*L2/T2", 1.602176634e-10, 0.0),
96
+ "W": ("M*L2/T3", 1.0, 0.0), "kW": ("M*L2/T3", 1000.0, 0.0),
97
+ "MW": ("M*L2/T3", 1e6, 0.0), "GW": ("M*L2/T3", 1e9, 0.0),
98
+ "hp": ("M*L2/T3", 745.69987158227, 0.0),
99
+ # temperature (special: offset conversions)
100
+ "K": ("THETA", 1.0, 0.0), "degC": ("THETA", 1.0, 273.15),
101
+ "°C": ("THETA", 1.0, 273.15),
102
+ # pressure
103
+ "Pa": ("M/(L*T2)", 1.0, 0.0), "kPa": ("M/(L*T2)", 1000.0, 0.0),
104
+ "MPa": ("M/(L*T2)", 1e6, 0.0), "bar": ("M/(L*T2)", 1e5, 0.0),
105
+ "atm": ("M/(L*T2)", 101325.0, 0.0),
106
+ # angle
107
+ "deg": ("A", 1.0, 0.0), "rad": ("A", 1.0, 0.0),
108
+ }
109
+
110
+ _NUM = r"[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?(?:[eE][-+]?\d+)?"
111
+ _QTY_RE = re.compile(
112
+ r"(?P<num>" + _NUM + r")\s?(?P<unit>" +
113
+ "|".join(re.escape(u) for u in sorted(_UNIT_TABLE, key=len, reverse=True)) +
114
+ r")(?![A-Za-z0-9/^])"
115
+ )
116
+
117
+
118
+ def to_si(value: float, unit: str) -> Tuple[Optional[float], Optional[str]]:
119
+ """Convert a value in a known unit to SI base. Returns (si_value, dim) or (None, None)."""
120
+ u = _UNIT_TABLE.get(unit)
121
+ if u is None:
122
+ return None, None
123
+ dim, factor, offset = u
124
+ v = value * factor + (offset if dim == "THETA" else 0.0)
125
+ return v, dim
126
+
127
+
128
+ def check_quantity(value: float, unit: str, location: str = "") -> List[Violation]:
129
+ """Universal per-quantity checks. Applicable to ANY domain."""
130
+ out: List[Violation] = []
131
+ si, dim = to_si(value, unit)
132
+ if si is None:
133
+ return out
134
+ disp = f"{value} {unit}"
135
+
136
+ if dim == "L/T": # speed
137
+ if si < 0:
138
+ out.append(Violation("speed_negative", "FAIL",
139
+ f"Negative speed {disp}", location))
140
+ elif si > C_LIGHT:
141
+ out.append(Violation("speed_exceeds_light", "FAIL",
142
+ f"Speed {disp} exceeds c = 299792458 m/s", location))
143
+ elif si > WARN_SPEED_FRAC * C_LIGHT:
144
+ out.append(Violation("speed_relativistic_warn", "WARN",
145
+ f"Speed {disp} > 0.5c, implausible for macroscopic bodies", location))
146
+ elif dim == "THETA":
147
+ if si < 0:
148
+ out.append(Violation("below_absolute_zero", "FAIL",
149
+ f"Temperature {disp} below absolute zero", location))
150
+ elif dim == "M":
151
+ if si < 0:
152
+ out.append(Violation("negative_mass", "FAIL",
153
+ f"Negative mass {disp}", location))
154
+ elif dim == "L":
155
+ if si < 0:
156
+ out.append(Violation("negative_length", "FAIL",
157
+ f"Negative length {disp}", location))
158
+ elif dim == "L/T2":
159
+ if si < 0:
160
+ out.append(Violation("negative_acceleration_mag", "FAIL",
161
+ f"Negative acceleration magnitude {disp}", location))
162
+ elif si > WARN_ACC_G * G_EARTH:
163
+ out.append(Violation("acceleration_extreme", "WARN",
164
+ f"Acceleration {disp} > {WARN_ACC_G:.0f} g", location))
165
+ elif dim == "M*L/T2":
166
+ if si < 0:
167
+ out.append(Violation("negative_force", "WARN",
168
+ f"Negative force {disp} (check sign convention)", location))
169
+ return out
170
+
171
+
172
+ def check_probability(value: float, location: str = "") -> Optional[Violation]:
173
+ if 0.0 <= value <= 1.0:
174
+ return None
175
+ return Violation("probability_out_of_range", "FAIL",
176
+ f"Probability {value} outside [0, 1]", location)
177
+
178
+
179
+ def check_kinematics(data: Dict[str, float], location: str = "") -> List[Violation]:
180
+ """Cross-check kinematic descriptions for internal consistency.
181
+
182
+ Accepts any subset of keys (SI units): v0, v, a, t, s (displacement), h (height).
183
+ Verifies v = v0 + a t and s = v0 t + 0.5 a t^2 within tolerance when present.
184
+ """
185
+ out: List[Violation] = []
186
+ v0, v, a, t = data.get("v0"), data.get("v"), data.get("a"), data.get("t")
187
+ s, h = data.get("s"), data.get("h")
188
+ tol = 0.02 # 2%
189
+
190
+ if None not in (v0, v, a, t) and t != 0:
191
+ pred = v0 + a * t
192
+ if abs(v - pred) > tol * max(abs(v), abs(pred), 1e-12):
193
+ out.append(Violation("kinematics_v_equation", "FAIL",
194
+ f"v = v0 + a t violated: given v={v} m/s, "
195
+ f"predicted {pred:.6g} m/s", location))
196
+ if None not in (v0, a, t, s) and t != 0:
197
+ pred = v0 * t + 0.5 * a * t * t
198
+ if abs(s - pred) > tol * max(abs(s), abs(pred), 1e-12):
199
+ out.append(Violation("kinematics_s_equation", "FAIL",
200
+ f"s = v0 t + a t^2/2 violated: given s={s} m, "
201
+ f"predicted {pred:.6g} m", location))
202
+ if h is not None and t is not None and a is None and t != 0:
203
+ # free-fall consistency: h = 0.5 g t^2 (if user supplied drop height + time)
204
+ pred = 0.5 * G_EARTH * t * t
205
+ if abs(h - pred) > max(tol * max(abs(h), abs(pred), 1e-12), 0.05):
206
+ out.append(Violation("kinematics_freefall", "WARN",
207
+ f"Free-fall check: h={h} m with t={t} s implies "
208
+ f"{pred:.4g} m at standard gravity", location))
209
+ return out
210
+
211
+
212
+ def check_conservation(before: Dict[str, float], after: Dict[str, float],
213
+ location: str = "") -> List[Violation]:
214
+ """Energy (M*L2/T2) and momentum (M*L/T) conservation with 2% tolerance."""
215
+ out: List[Violation] = []
216
+ tol = 0.02
217
+ for key in ("energy", "momentum"):
218
+ b, a = before.get(key), after.get(key)
219
+ if b is None or a is None:
220
+ continue
221
+ if abs(a - b) > tol * max(abs(b), abs(a), 1e-12):
222
+ out.append(Violation(f"conservation_{key}", "FAIL",
223
+ f"{key.capitalize()} not conserved: {b} -> {a} "
224
+ f"(delta {(a - b) / max(abs(b), 1e-12) * 100:.1f}%)",
225
+ location))
226
+ return out
227
+
228
+
229
+ def check_text(text: str) -> Dict[str, Any]:
230
+ """Main gate for LLM outputs: extract quantities with known units and check each.
231
+
232
+ Deterministic regex extraction; unknown units are ignored (no false FAILs).
233
+ """
234
+ violations: List[Violation] = []
235
+ scanned = 0
236
+ for m in _QTY_RE.finditer(text):
237
+ raw_num = m.group("num").replace(",", "")
238
+ unit = m.group("unit")
239
+ try:
240
+ val = float(raw_num)
241
+ except ValueError:
242
+ continue
243
+ # bare 'g' is ambiguous (gram vs gravity); require explicit unit-like usage
244
+ if unit == "g" and not re.search(r"\d\s?g(?:\s|$|[,.;)])", text):
245
+ continue
246
+ loc = text[max(0, m.start() - 24):m.end() + 8].replace("\n", " ")
247
+ violations.extend(check_quantity(val, unit, location=loc))
248
+ scanned += 1
249
+ # probabilities written as "probability/likelihood/confidence 0.xx"
250
+ for m in re.finditer(r"(probability|likelihood|confidence)[^0-9\-+]{0,12}(" + _NUM + r")",
251
+ text, re.I):
252
+ try:
253
+ val = float(m.group(2).replace(",", ""))
254
+ except ValueError:
255
+ continue
256
+ v = check_probability(val, location=m.group(0)[:40])
257
+ if v:
258
+ violations.append(v)
259
+ scanned += 1
260
+
261
+ return {
262
+ "verdict": _verdict(violations),
263
+ "n_quantities_checked": scanned,
264
+ "violations": [v.to_dict() for v in violations],
265
+ "engine": ENGINE_ID,
266
+ "version": __version__,
267
+ "deterministic": DETERMINISTIC,
268
+ }
269
+
270
+
271
+ def full_check(text: str, kinematics: Optional[Dict[str, float]] = None,
272
+ conservation: Optional[Tuple[Dict[str, float], Dict[str, float]]] = None
273
+ ) -> Dict[str, Any]:
274
+ """Combined check: text scan + optional kinematic + optional conservation."""
275
+ result = check_text(text)
276
+ extra: List[Violation] = []
277
+ if kinematics:
278
+ extra.extend(check_kinematics(kinematics))
279
+ if conservation:
280
+ b, a = conservation
281
+ extra.extend(check_conservation(b, a))
282
+ if extra:
283
+ result["violations"] = result["violations"] + [v.to_dict() for v in extra]
284
+ result["verdict"] = _verdict(
285
+ [Violation(v["rule"], v["severity"], v["detail"], v["location"])
286
+ for v in result["violations"]])
287
+ return result
288
+
289
+
290
+ if __name__ == "__main__":
291
+ import json
292
+ demo = ("The drone descends at 450 km/h and lands after 12 s. "
293
+ "The battery stores 5 kWh. Confidence: 1.8 probability of success.")
294
+ print(json.dumps(full_check(demo, kinematics={"v0": 0.0, "a": 9.8, "t": 3.0, "v": 50.0}),
295
+ indent=2, ensure_ascii=False))
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Framework adapter layer for phm_lite_check.
3
+
4
+ Each adapter lazily imports its host framework, so the base package
5
+ stays zero-dependency. Only import the adapter you need.
6
+ """
7
+
8
+ __all__ = ["langchain_tool", "autogen_tool"]