arbiter-engine 0.1.0__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.
- arbiter_engine/__init__.py +31 -0
- arbiter_engine/api.py +473 -0
- arbiter_engine/axiom_thresholds.py +118 -0
- arbiter_engine/envelope.py +270 -0
- arbiter_engine/examples/water_tank.yaml +112 -0
- arbiter_engine/fire_frequency.py +252 -0
- arbiter_engine/history/__init__.py +12 -0
- arbiter_engine/history/observation.py +596 -0
- arbiter_engine/history/observation_production.py +335 -0
- arbiter_engine/history/observation_source_wiring.py +120 -0
- arbiter_engine/history/readiness.py +288 -0
- arbiter_engine/interfaces.py +1353 -0
- arbiter_engine/mcp/__init__.py +1 -0
- arbiter_engine/mcp/server.py +221 -0
- arbiter_engine/ontology/__init__.py +18 -0
- arbiter_engine/ontology/axiom_verdicts_production.py +332 -0
- arbiter_engine/ontology/axioms/__init__.py +26 -0
- arbiter_engine/ontology/axioms/boundedness.py +340 -0
- arbiter_engine/ontology/axioms/connectivity.py +396 -0
- arbiter_engine/ontology/axioms/conservation.py +266 -0
- arbiter_engine/ontology/axioms/consistency.py +422 -0
- arbiter_engine/ontology/axioms/extensions.py +106 -0
- arbiter_engine/ontology/axioms/homeostasis.py +783 -0
- arbiter_engine/ontology/axioms/monotonicity.py +444 -0
- arbiter_engine/ontology/axioms/responsiveness.py +752 -0
- arbiter_engine/ontology/axioms/roles.py +207 -0
- arbiter_engine/ontology/axioms/stability.py +327 -0
- arbiter_engine/ontology/domain_loader.py +485 -0
- arbiter_engine/ontology/loader.py +802 -0
- arbiter_engine/ontology/reasoner.py +1069 -0
- arbiter_engine/propagation/__init__.py +1 -0
- arbiter_engine/propagation/impact_estimator.py +305 -0
- arbiter_engine/propagation/lp_confidence.py +123 -0
- arbiter_engine/propagation/mcts_root_cause.py +266 -0
- arbiter_engine/propagation/root_cause.py +559 -0
- arbiter_engine/propagation/weight_learner.py +216 -0
- arbiter_engine/rca/__init__.py +4 -0
- arbiter_engine/rca/greedy_set_cover.py +291 -0
- arbiter_engine/residual/__init__.py +1 -0
- arbiter_engine/residual/predict_vs_mirror.py +542 -0
- arbiter_engine/temporal/__init__.py +1 -0
- arbiter_engine/temporal/temporal_edge.py +584 -0
- arbiter_engine/temporal/trend_projection.py +430 -0
- arbiter_engine/twin/__init__.py +23 -0
- arbiter_engine/twin/action_clears_problem.py +217 -0
- arbiter_engine/twin/builder.py +479 -0
- arbiter_engine/twin/gap.py +132 -0
- arbiter_engine/twin/hypothesis_generator.py +530 -0
- arbiter_engine/twin/hypothesis_production.py +236 -0
- arbiter_engine/twin/kernel_pipeline_executor.py +571 -0
- arbiter_engine/twin/monte_carlo_predictor.py +1001 -0
- arbiter_engine/twin/optimization_production.py +233 -0
- arbiter_engine/twin/pipeline_production.py +176 -0
- arbiter_engine/twin/topology.py +450 -0
- arbiter_engine/twin/topology_optimizer.py +412 -0
- arbiter_engine/twin/traverser.py +1260 -0
- arbiter_engine/twin/traverser_production.py +283 -0
- arbiter_engine/types.py +427 -0
- arbiter_engine-0.1.0.dist-info/METADATA +278 -0
- arbiter_engine-0.1.0.dist-info/RECORD +63 -0
- arbiter_engine-0.1.0.dist-info/WHEEL +4 -0
- arbiter_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
- arbiter_engine-0.1.0.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""arbiter-engine v0.1 — arbiter_engine/mcp."""
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""The MCP transport shim.
|
|
2
|
+
|
|
3
|
+
Deliberately thin. All five tools live in :mod:`arbiter_engine.api`
|
|
4
|
+
as plain functions over the engine (moved there, because they are
|
|
5
|
+
engine API rather than transport); this module only registers them with the SDK and
|
|
6
|
+
serialises the envelope. Everything worth testing is testable without
|
|
7
|
+
importing ``mcp`` at all, which is why the tool tests need no protocol
|
|
8
|
+
round-trip.
|
|
9
|
+
|
|
10
|
+
The SDK import is **lazy**, inside :func:`build_server`. Importing this module
|
|
11
|
+
must not require ``mcp`` to be installed — the engine is the dependency, the
|
|
12
|
+
transport is not (the scope ruling holds the platform; the server
|
|
13
|
+
holds neither).
|
|
14
|
+
|
|
15
|
+
Run with:
|
|
16
|
+
|
|
17
|
+
python3 -m arbiter_engine.mcp.server
|
|
18
|
+
|
|
19
|
+
(Inside the orchestrator repo the same module runs at its in-repo path, from
|
|
20
|
+
the repo parent on the path. That path is deliberately not spelled out here.)
|
|
21
|
+
|
|
22
|
+
This module now SHIPS -- the public-API ruling decided the
|
|
23
|
+
surface is a permanent public contract -- so its own docstring is public text. It previously gave only
|
|
24
|
+
the in-repo invocation, with an absolute operator path in front of it: a private
|
|
25
|
+
package name and a private filesystem path, both in a docstring, where the
|
|
26
|
+
build's stray scan cannot see them because that scan matches import STATEMENTS
|
|
27
|
+
and this is prose. Name what a reader actually has, and nothing else.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from typing import Any, Dict, List
|
|
34
|
+
|
|
35
|
+
from arbiter_engine.api import (
|
|
36
|
+
EngineSession, attest, check, gaps, model_describe, traverse,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
#: The five primitives, in the order an internal ruling lists them. Each entry is the
|
|
40
|
+
#: name, a one-line description for the client, and the JSON-Schema input.
|
|
41
|
+
TOOL_SPECS: List[Dict[str, Any]] = [
|
|
42
|
+
{
|
|
43
|
+
"name": "model_describe",
|
|
44
|
+
"description": (
|
|
45
|
+
"What domain model is loaded: entity types, indicators, and the "
|
|
46
|
+
"axioms each indicator DECLARES. Call this before reasoning — it "
|
|
47
|
+
"is the vocabulary, and an entity type absent from it does not "
|
|
48
|
+
"exist in this model. Declared is not the same as evaluated."
|
|
49
|
+
),
|
|
50
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "check",
|
|
54
|
+
"description": (
|
|
55
|
+
"Evaluate the declared invariants over the supplied observations. "
|
|
56
|
+
"Returns findings AND what could not be evaluated, with reasons — "
|
|
57
|
+
"an empty findings list with a populated not_checked list means "
|
|
58
|
+
"nothing was measured, not that nothing is wrong."
|
|
59
|
+
),
|
|
60
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "traverse",
|
|
64
|
+
"description": (
|
|
65
|
+
"Walk the entity graph. One primitive subsumes root-cause "
|
|
66
|
+
"(direction=reverse), impact (forward), and what-if "
|
|
67
|
+
"(value_mode=hypothetical with overrides)."
|
|
68
|
+
),
|
|
69
|
+
"inputSchema": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"start_nodes": {"type": "array", "items": {"type": "string"}},
|
|
73
|
+
"direction": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"enum": ["forward", "reverse", "bidirectional"],
|
|
76
|
+
},
|
|
77
|
+
# An internal ruling fed PROJECTED (2026-08-04), so the enum offers it.
|
|
78
|
+
# It was withheld while the mode was inert; advertising a
|
|
79
|
+
# capability nothing implements is the shape, and so
|
|
80
|
+
# is leaving a withholding note whose stated reason has since
|
|
81
|
+
# been resolved.
|
|
82
|
+
"value_mode": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"enum": ["current", "hypothetical", "projected"],
|
|
85
|
+
},
|
|
86
|
+
"max_hops": {"type": "integer"},
|
|
87
|
+
"overrides": {"type": "object"},
|
|
88
|
+
},
|
|
89
|
+
"required": ["start_nodes"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "gaps",
|
|
94
|
+
"description": (
|
|
95
|
+
"What the model is missing, as priority-ranked questions. This is "
|
|
96
|
+
"what the engine needs to know next in order to answer better."
|
|
97
|
+
),
|
|
98
|
+
"inputSchema": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": {"start_node": {"type": "string"}},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "attest",
|
|
105
|
+
"description": (
|
|
106
|
+
"The evidence behind a finding: which axiom, which threshold, how "
|
|
107
|
+
"many observations. Engine-side evidence only."
|
|
108
|
+
),
|
|
109
|
+
"inputSchema": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"problem_type": {"type": "string"},
|
|
113
|
+
"entity_id": {"type": "string"},
|
|
114
|
+
},
|
|
115
|
+
"required": ["problem_type"],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
_HANDLERS = {
|
|
121
|
+
"model_describe": lambda s, a: model_describe(s),
|
|
122
|
+
"check": lambda s, a: check(s),
|
|
123
|
+
"traverse": lambda s, a: traverse(
|
|
124
|
+
s, a["start_nodes"], a.get("direction", "forward"),
|
|
125
|
+
a.get("value_mode", "current"), a.get("max_hops", 4),
|
|
126
|
+
a.get("overrides")),
|
|
127
|
+
"gaps": lambda s, a: gaps(s, a.get("start_node")),
|
|
128
|
+
"attest": lambda s, a: attest(s, a["problem_type"], a.get("entity_id")),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def dispatch(session: EngineSession, name: str,
|
|
133
|
+
arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
134
|
+
"""Route one tool call and return the envelope as a dict.
|
|
135
|
+
|
|
136
|
+
Kept separate from the SDK registration so the routing table itself is
|
|
137
|
+
testable — a name present in TOOL_SPECS but missing from _HANDLERS is the
|
|
138
|
+
kind of gap that otherwise surfaces only at runtime.
|
|
139
|
+
"""
|
|
140
|
+
handler = _HANDLERS.get(name)
|
|
141
|
+
if handler is None:
|
|
142
|
+
raise KeyError(f"unknown tool: {name}")
|
|
143
|
+
return handler(session, arguments or {}).to_dict()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def build_server(session: EngineSession | None = None):
|
|
147
|
+
"""Construct the MCP server. Imports the SDK lazily.
|
|
148
|
+
|
|
149
|
+
this targets ``mcp.server.MCPServer``, the high-level class in
|
|
150
|
+
the 2.x SDK, which derives each tool's input schema from the wrapper's
|
|
151
|
+
signature. The first version registered handlers with
|
|
152
|
+
``@server.list_tools()`` / ``@server.call_tool()`` on the low-level
|
|
153
|
+
``Server``; those decorators do not exist in 2.x, so **it raised
|
|
154
|
+
``AttributeError`` on the first line of registration and had never once
|
|
155
|
+
been run.** Nothing caught it because the SDK cannot be installed in this
|
|
156
|
+
repo's environment, so the shim had no test at all — see the module note
|
|
157
|
+
below.
|
|
158
|
+
|
|
159
|
+
The wrappers are deliberately trivial: each one calls :func:`dispatch`,
|
|
160
|
+
which stays the single routing table and stays testable without the SDK.
|
|
161
|
+
``TOOL_SPECS`` remains the source of the descriptions, because those are
|
|
162
|
+
what an agent actually reads.
|
|
163
|
+
"""
|
|
164
|
+
from mcp.server import MCPServer # noqa: PLC0415 — deliberate lazy import
|
|
165
|
+
|
|
166
|
+
state = session or EngineSession()
|
|
167
|
+
server = MCPServer("arbiter-engine")
|
|
168
|
+
spec_by_name = {spec["name"]: spec for spec in TOOL_SPECS}
|
|
169
|
+
|
|
170
|
+
def _emit(name: str, arguments: Dict[str, Any]) -> str:
|
|
171
|
+
return json.dumps(dispatch(state, name, arguments), indent=2)
|
|
172
|
+
|
|
173
|
+
async def model_describe_tool() -> str:
|
|
174
|
+
return _emit("model_describe", {})
|
|
175
|
+
|
|
176
|
+
async def check_tool() -> str:
|
|
177
|
+
return _emit("check", {})
|
|
178
|
+
|
|
179
|
+
async def traverse_tool(
|
|
180
|
+
start_nodes: List[str],
|
|
181
|
+
direction: str = "forward",
|
|
182
|
+
value_mode: str = "current",
|
|
183
|
+
max_hops: int = 4,
|
|
184
|
+
overrides: Dict[str, Any] | None = None,
|
|
185
|
+
) -> str:
|
|
186
|
+
return _emit("traverse", {
|
|
187
|
+
"start_nodes": start_nodes, "direction": direction,
|
|
188
|
+
"value_mode": value_mode, "max_hops": max_hops,
|
|
189
|
+
"overrides": overrides,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
async def gaps_tool(start_node: str | None = None) -> str:
|
|
193
|
+
return _emit("gaps", {"start_node": start_node})
|
|
194
|
+
|
|
195
|
+
async def attest_tool(problem_type: str,
|
|
196
|
+
entity_id: str | None = None) -> str:
|
|
197
|
+
return _emit("attest", {"problem_type": problem_type,
|
|
198
|
+
"entity_id": entity_id})
|
|
199
|
+
|
|
200
|
+
for name, fn in (
|
|
201
|
+
("model_describe", model_describe_tool),
|
|
202
|
+
("check", check_tool),
|
|
203
|
+
("traverse", traverse_tool),
|
|
204
|
+
("gaps", gaps_tool),
|
|
205
|
+
("attest", attest_tool),
|
|
206
|
+
):
|
|
207
|
+
server.add_tool(fn, name=name,
|
|
208
|
+
description=spec_by_name[name]["description"])
|
|
209
|
+
|
|
210
|
+
return server
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main() -> int:
|
|
214
|
+
import asyncio
|
|
215
|
+
|
|
216
|
+
asyncio.run(build_server().run_stdio_async())
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ontology Reasoning - Layer 2 Detection.
|
|
3
|
+
|
|
4
|
+
The ontology layer uses semantic reasoning based on health indicators
|
|
5
|
+
defined in RDF/OWL ontologies. It maps entities to health concepts
|
|
6
|
+
and checks the 6 System Health Axioms.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- RDF/TTL ontology support via rdflib
|
|
10
|
+
- Health meta-ontology for domain-agnostic concepts
|
|
11
|
+
- Per-axiom reasoning with configurable thresholds
|
|
12
|
+
- History-aware detection for temporal axioms
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .reasoner import UnifiedAxiomReasoner
|
|
16
|
+
from .loader import OntologyLoader
|
|
17
|
+
|
|
18
|
+
__all__ = ['UnifiedAxiomReasoner', 'OntologyLoader']
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""AxiomVerdict production-readiness substrate (chain head).
|
|
2
|
+
|
|
3
|
+
Production-readiness substrate for the 8 canonical axiom checkers at
|
|
4
|
+
arbiter_engine/ontology/axioms/ (HOMEOSTASIS / RESPONSIVENESS / MONOTONICITY /
|
|
5
|
+
BOUNDEDNESS / CONSISTENCY / CONSERVATION / CAUSALITY / PREDICTION_QUALITY).
|
|
6
|
+
|
|
7
|
+
Per sibling-within-module precedent: axioms/ dir hosts 8
|
|
8
|
+
sibling checker modules with no single shared parent module suitable for
|
|
9
|
+
sibling extension. ships substrate at new standalone module
|
|
10
|
+
(parent = `arbiter_engine/ontology/`, sibling to `axioms/` package) rather
|
|
11
|
+
than mutate any individual checker. the established pattern sibling-substrate
|
|
12
|
+
discipline preserved (substrate sibling at axis-parent level rather than
|
|
13
|
+
single-module level).
|
|
14
|
+
|
|
15
|
+
Adds per-verdict production recording + 5 production-readiness public
|
|
16
|
+
functions + an established pattern env-gate. Composes hybrid
|
|
17
|
+
emit-policy decision (transition OR confidence_threshold gate) +
|
|
18
|
+
attestation severity floor + NaturalCategoryDispatcher
|
|
19
|
+
(emit_policy axis dispatch via existing 9th canonical axis added; no new axis).
|
|
20
|
+
|
|
21
|
+
the established pattern sibling-substrate-within-existing-module variant.
|
|
22
|
+
|
|
23
|
+
Domain-agnostic: axiom_name + entity_id + verdict + confidence scalars
|
|
24
|
+
opaque; no per-domain dispatch.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import threading
|
|
31
|
+
from contextlib import contextmanager
|
|
32
|
+
from contextvars import ContextVar
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
from typing import Dict, Iterator, List, Optional
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _env_bool(name: str, default: bool = False) -> bool:
|
|
39
|
+
raw = os.environ.get(name, "1" if default else "0").strip().lower()
|
|
40
|
+
return raw in ("1", "true", "yes", "on")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
DT_AXIOM_VERDICT_PRODUCTION_ENABLED: bool = _env_bool(
|
|
44
|
+
"DT_AXIOM_VERDICT_PRODUCTION_ENABLED", default=False
|
|
45
|
+
)
|
|
46
|
+
DT_AXIOM_VERDICT_PRODUCTION_RING_CAP: int = int(
|
|
47
|
+
os.environ.get("DT_AXIOM_VERDICT_PRODUCTION_RING_CAP", "10000")
|
|
48
|
+
)
|
|
49
|
+
DT_AXIOM_VERDICT_CONFIDENCE_THRESHOLD: float = float(
|
|
50
|
+
os.environ.get("DT_AXIOM_VERDICT_CONFIDENCE_THRESHOLD", "0.5")
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Per hybrid emit-policy default
|
|
54
|
+
PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_HYBRID: str = "hybrid"
|
|
55
|
+
PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_FULL_EMIT: str = "full_emit"
|
|
56
|
+
PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_SUPPRESSED: str = "suppressed"
|
|
57
|
+
KNOWN_PRODUCTION_AXIOM_VERDICT_EMIT_POLICIES = frozenset([
|
|
58
|
+
PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_HYBRID,
|
|
59
|
+
PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_FULL_EMIT,
|
|
60
|
+
PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_SUPPRESSED,
|
|
61
|
+
])
|
|
62
|
+
DEFAULT_PRODUCTION_AXIOM_VERDICT_EMIT_POLICY: str = (
|
|
63
|
+
PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_HYBRID
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# The 8 canonical axiom names per arbiter_engine/ontology/axioms/*.py
|
|
67
|
+
KNOWN_CANONICAL_AXIOMS = frozenset([
|
|
68
|
+
"HOMEOSTASIS",
|
|
69
|
+
"RESPONSIVENESS",
|
|
70
|
+
"MONOTONICITY",
|
|
71
|
+
"BOUNDEDNESS",
|
|
72
|
+
"CONSISTENCY",
|
|
73
|
+
"CONSERVATION",
|
|
74
|
+
"CAUSALITY",
|
|
75
|
+
"PREDICTION_QUALITY",
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ProductionAxiomVerdict:
|
|
81
|
+
""" per-verdict production-readiness axiom-verdict event.
|
|
82
|
+
|
|
83
|
+
5 opaque fields domain-agnostic invariant. Frozen for
|
|
84
|
+
audit-trail provenance emit-policy decision.
|
|
85
|
+
|
|
86
|
+
(Bucket A; 2nd worked example after RCA):
|
|
87
|
+
added optional ``cluster_id`` for per-axis cluster-scope filtering.
|
|
88
|
+
Default None preserves previously behavior; emission callsites that
|
|
89
|
+
pass cluster_id stamp the record so
|
|
90
|
+
``get_axiom_verdicts(cluster_id=X)`` can filter. Field added with
|
|
91
|
+
default so existing emissions are backward-compat.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
axiom_name: str
|
|
95
|
+
entity_id: str
|
|
96
|
+
verdict: str # "PASS" | "FAIL" | "UNKNOWN"
|
|
97
|
+
confidence: float
|
|
98
|
+
emit_policy_per_cd1098: str
|
|
99
|
+
cluster_id: Optional[str] = None # (Bucket A)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def resolve_production_axiom_verdict_emit_policy(value: Optional[str]) -> str:
|
|
103
|
+
"""Safe-default to hybrid."""
|
|
104
|
+
if value is None or value not in KNOWN_PRODUCTION_AXIOM_VERDICT_EMIT_POLICIES:
|
|
105
|
+
return DEFAULT_PRODUCTION_AXIOM_VERDICT_EMIT_POLICY
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_PRODUCTION_VERDICTS: List[ProductionAxiomVerdict] = []
|
|
110
|
+
_PRODUCTION_LOCK = threading.RLock()
|
|
111
|
+
_PRODUCTION_LAST_VERDICT: Dict[tuple, str] = {} # (entity_id, axiom_name) -> verdict
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------- Layer 3 ContextVar emission-scope stamping ----------
|
|
115
|
+
#
|
|
116
|
+
# `record_axiom_verdict` is called from `arbiter_engine/ontology/reasoner.py`
|
|
117
|
+
# inside the per-axiom check loop. That code path does NOT know cluster_id
|
|
118
|
+
# at the function-signature level — DetectorInterface.detect() takes
|
|
119
|
+
# (entities, graph, history) without cluster_id, and threading cluster_id
|
|
120
|
+
# through the detector interface chain is out-of-scope (would touch every
|
|
121
|
+
# detector implementation).
|
|
122
|
+
#
|
|
123
|
+
# Instead, callers in the full system (live loop + Monte Carlo path) wrap
|
|
124
|
+
# the detect-call with `cluster_scope(cluster_id)`; the ContextVar
|
|
125
|
+
# propagates correctly through async/await chains (asyncio.create_task
|
|
126
|
+
# copies the context by default). Inside `record_axiom_verdict`, if the
|
|
127
|
+
# explicit `cluster_id` parameter is None, we fall back to reading the
|
|
128
|
+
# ContextVar. This is minimal-invasive Layer-3 stamping without rewriting
|
|
129
|
+
# the detector contract.
|
|
130
|
+
#
|
|
131
|
+
# substrate-callsite-gap discipline (post-promotion to
|
|
132
|
+
# reference architecture): the substrate-side ContextVar +
|
|
133
|
+
# the caller-side `with cluster_scope(cluster_id):` block together form
|
|
134
|
+
# the bridge between substrate-emission and cluster-aware caller context.
|
|
135
|
+
_CURRENT_CLUSTER_ID: ContextVar[Optional[str]] = ContextVar(
|
|
136
|
+
"axiom_verdict_cluster_id", default=None,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@contextmanager
|
|
141
|
+
def cluster_scope(cluster_id: Optional[str]) -> Iterator[None]:
|
|
142
|
+
""" Layer 3 helper: wrap a detect-call so emissions stamp
|
|
143
|
+
cluster_id automatically.
|
|
144
|
+
|
|
145
|
+
Usage in the full system:
|
|
146
|
+
|
|
147
|
+
with cluster_scope(cluster_id):
|
|
148
|
+
detection_result = await bridge.detect(entities, graph)
|
|
149
|
+
|
|
150
|
+
Any `record_axiom_verdict()` calls inside the block — including those
|
|
151
|
+
several frames deep in the detector chain — will pick up cluster_id
|
|
152
|
+
from the ContextVar when their own `cluster_id` parameter is None.
|
|
153
|
+
Explicit parameter still wins (precedence: explicit > ContextVar >
|
|
154
|
+
None). Safe for nested scopes via ContextVar reset.
|
|
155
|
+
|
|
156
|
+
cluster_id=None is also valid (no-op scope; preserves backward compat).
|
|
157
|
+
"""
|
|
158
|
+
token = _CURRENT_CLUSTER_ID.set(cluster_id)
|
|
159
|
+
try:
|
|
160
|
+
yield
|
|
161
|
+
finally:
|
|
162
|
+
_CURRENT_CLUSTER_ID.reset(token)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _resolve_cluster_id(explicit: Optional[str]) -> Optional[str]:
|
|
166
|
+
"""Resolve effective cluster_id: explicit param > ContextVar > None."""
|
|
167
|
+
if explicit is not None:
|
|
168
|
+
return explicit
|
|
169
|
+
return _CURRENT_CLUSTER_ID.get()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _wire_composition_for_entity(entity_id: str) -> None:
|
|
173
|
+
"""feed an entity's full per-axiom verdict snapshot to the
|
|
174
|
+
axiom-composition wiring at the recording callsite (was callsite-less —
|
|
175
|
+
the established pattern substrate-callsite gap, review Part 66 / ARCHITECTURE Part 65.3).
|
|
176
|
+
|
|
177
|
+
Reads the wire-up gate dynamically so it stays byte-identical when
|
|
178
|
+
``DT_AXIOM_COMPOSITION_WIRING_ENABLED`` is OFF (default): early-return, no
|
|
179
|
+
snapshot build, no state change. When ON, rebuilds the entity's per-axiom
|
|
180
|
+
snapshot from ``_PRODUCTION_LAST_VERDICT`` and registers it so
|
|
181
|
+
``get_axiom_composition_wired_entity_count()`` becomes nonzero (which is
|
|
182
|
+
what flips ``/dt-axiom-verdicts`` from ``warming_up`` to ``ready``).
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
from arbiter_engine.ontology import (
|
|
186
|
+
axiom_composition_wiring as _acw,
|
|
187
|
+
)
|
|
188
|
+
except Exception: # noqa: BLE001 — composition wiring not deployed
|
|
189
|
+
return
|
|
190
|
+
if not _acw.DT_AXIOM_COMPOSITION_WIRING_ENABLED:
|
|
191
|
+
return
|
|
192
|
+
with _PRODUCTION_LOCK:
|
|
193
|
+
snapshot = {
|
|
194
|
+
ax: v
|
|
195
|
+
for (eid, ax), v in _PRODUCTION_LAST_VERDICT.items()
|
|
196
|
+
if eid == entity_id
|
|
197
|
+
}
|
|
198
|
+
if snapshot:
|
|
199
|
+
_acw.wire_axiom_composition_for_entity(entity_id, snapshot)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def record_axiom_verdict(
|
|
203
|
+
axiom_name: str,
|
|
204
|
+
entity_id: str,
|
|
205
|
+
verdict: str,
|
|
206
|
+
confidence: float,
|
|
207
|
+
emit_policy: Optional[str] = None,
|
|
208
|
+
cluster_id: Optional[str] = None,
|
|
209
|
+
) -> Optional[ProductionAxiomVerdict]:
|
|
210
|
+
"""Record an axiom-verdict event at production-readiness shape.
|
|
211
|
+
|
|
212
|
+
Returns the stored ProductionAxiomVerdict when gate enabled AND
|
|
213
|
+
emit_policy admits the event; returns None when gate off OR
|
|
214
|
+
emit_policy suppressed OR hybrid mode rejects (verdict-transition OR confidence >= threshold) gate.
|
|
215
|
+
|
|
216
|
+
Hybrid mode gate: admits if (a) prior verdict for (entity_id,
|
|
217
|
+
axiom_name) differs from current, OR (b) confidence >= threshold.
|
|
218
|
+
|
|
219
|
+
(Bucket A): optional ``cluster_id`` stamps the record
|
|
220
|
+
so per-cluster retrieval queries can filter. Default None preserves
|
|
221
|
+
previously emission behavior (callsites that don't pass cluster_id
|
|
222
|
+
still record verdicts — they're then queryable via the global path
|
|
223
|
+
`get_axiom_verdicts()` but excluded from filtered queries).
|
|
224
|
+
"""
|
|
225
|
+
if not DT_AXIOM_VERDICT_PRODUCTION_ENABLED:
|
|
226
|
+
return None
|
|
227
|
+
policy = resolve_production_axiom_verdict_emit_policy(emit_policy)
|
|
228
|
+
if policy == PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_SUPPRESSED:
|
|
229
|
+
return None
|
|
230
|
+
key = (entity_id, axiom_name)
|
|
231
|
+
if policy == PRODUCTION_AXIOM_VERDICT_EMIT_POLICY_HYBRID:
|
|
232
|
+
with _PRODUCTION_LOCK:
|
|
233
|
+
prior_verdict = _PRODUCTION_LAST_VERDICT.get(key)
|
|
234
|
+
verdict_transition = (prior_verdict is None) or (prior_verdict != verdict)
|
|
235
|
+
confidence_crosses = float(confidence) >= DT_AXIOM_VERDICT_CONFIDENCE_THRESHOLD
|
|
236
|
+
if not (verdict_transition or confidence_crosses):
|
|
237
|
+
return None
|
|
238
|
+
# Layer 3: explicit cluster_id wins; fall back to ContextVar
|
|
239
|
+
# set by `cluster_scope(cluster_id)` in the caller (typically the full system
|
|
240
|
+
# detect-loop wrapper). None when neither is set (previously path).
|
|
241
|
+
effective_cluster_id = _resolve_cluster_id(cluster_id)
|
|
242
|
+
record = ProductionAxiomVerdict(
|
|
243
|
+
axiom_name=axiom_name,
|
|
244
|
+
entity_id=entity_id,
|
|
245
|
+
verdict=verdict,
|
|
246
|
+
confidence=float(confidence),
|
|
247
|
+
emit_policy_per_cd1098=policy,
|
|
248
|
+
cluster_id=effective_cluster_id,
|
|
249
|
+
)
|
|
250
|
+
with _PRODUCTION_LOCK:
|
|
251
|
+
_PRODUCTION_VERDICTS.append(record)
|
|
252
|
+
_PRODUCTION_LAST_VERDICT[key] = verdict
|
|
253
|
+
if len(_PRODUCTION_VERDICTS) > DT_AXIOM_VERDICT_PRODUCTION_RING_CAP:
|
|
254
|
+
del _PRODUCTION_VERDICTS[
|
|
255
|
+
: len(_PRODUCTION_VERDICTS) - DT_AXIOM_VERDICT_PRODUCTION_RING_CAP
|
|
256
|
+
]
|
|
257
|
+
# land the previously-callsite-less axiom-composition wire so
|
|
258
|
+
# composition_wired_count can be nonzero. Self-gated on the established pattern
|
|
259
|
+
# DT_AXIOM_COMPOSITION_WIRING_ENABLED (default OFF -> no-op, byte-identical).
|
|
260
|
+
_wire_composition_for_entity(entity_id)
|
|
261
|
+
return record
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _filter_by_cluster_id(
|
|
265
|
+
verdicts: List[ProductionAxiomVerdict],
|
|
266
|
+
cluster_id: Optional[str],
|
|
267
|
+
) -> List[ProductionAxiomVerdict]:
|
|
268
|
+
""" helper: filter verdicts by cluster_id when non-None.
|
|
269
|
+
|
|
270
|
+
cluster_id=None returns the full list (backward compat).
|
|
271
|
+
cluster_id="X" returns only verdicts with v.cluster_id == "X".
|
|
272
|
+
Verdicts emitted previously have cluster_id=None and are
|
|
273
|
+
excluded from any non-None filter (signaling "not scoped").
|
|
274
|
+
Mirror of the RCA `_filter_by_cluster_id` pattern.
|
|
275
|
+
"""
|
|
276
|
+
if cluster_id is None:
|
|
277
|
+
return verdicts
|
|
278
|
+
return [v for v in verdicts if v.cluster_id == cluster_id]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_axiom_verdicts(
|
|
282
|
+
cluster_id: Optional[str] = None,
|
|
283
|
+
) -> List[ProductionAxiomVerdict]:
|
|
284
|
+
"""All recorded production axiom-verdict records. Empty when gate off.
|
|
285
|
+
|
|
286
|
+
optional ``cluster_id`` filter. None = all (backward compat);
|
|
287
|
+
string value returns only verdicts stamped with that cluster_id.
|
|
288
|
+
"""
|
|
289
|
+
if not DT_AXIOM_VERDICT_PRODUCTION_ENABLED:
|
|
290
|
+
return []
|
|
291
|
+
with _PRODUCTION_LOCK:
|
|
292
|
+
all_verdicts = list(_PRODUCTION_VERDICTS)
|
|
293
|
+
return _filter_by_cluster_id(all_verdicts, cluster_id)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_axiom_verdict_count(cluster_id: Optional[str] = None) -> int:
|
|
297
|
+
"""Aggregate count of recorded production axiom-verdict records.
|
|
298
|
+
|
|
299
|
+
Dashboard-data defensive-accessor entry point + Pattern
|
|
300
|
+
171. Returns 0 when gate off.
|
|
301
|
+
|
|
302
|
+
optional ``cluster_id`` filter. None = aggregate count;
|
|
303
|
+
string value returns count of verdicts stamped with that cluster_id.
|
|
304
|
+
"""
|
|
305
|
+
if not DT_AXIOM_VERDICT_PRODUCTION_ENABLED:
|
|
306
|
+
return 0
|
|
307
|
+
return len(get_axiom_verdicts(cluster_id=cluster_id))
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def get_latest_verdict_for_entity(
|
|
311
|
+
entity_id: str, axiom_name: str,
|
|
312
|
+
) -> Optional[str]:
|
|
313
|
+
"""Last-known verdict for (entity_id, axiom_name); None when unknown
|
|
314
|
+
or gate off."""
|
|
315
|
+
if not DT_AXIOM_VERDICT_PRODUCTION_ENABLED:
|
|
316
|
+
return None
|
|
317
|
+
with _PRODUCTION_LOCK:
|
|
318
|
+
return _PRODUCTION_LAST_VERDICT.get((entity_id, axiom_name))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def known_verdict_axioms() -> List[str]:
|
|
322
|
+
"""Diagnostic accessor — sorted unique axiom_name values."""
|
|
323
|
+
if not DT_AXIOM_VERDICT_PRODUCTION_ENABLED:
|
|
324
|
+
return []
|
|
325
|
+
with _PRODUCTION_LOCK:
|
|
326
|
+
return sorted({r.axiom_name for r in _PRODUCTION_VERDICTS})
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _reset_production_verdicts_for_tests() -> None:
|
|
330
|
+
with _PRODUCTION_LOCK:
|
|
331
|
+
_PRODUCTION_VERDICTS.clear()
|
|
332
|
+
_PRODUCTION_LAST_VERDICT.clear()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Per-axiom checker implementations.
|
|
3
|
+
|
|
4
|
+
Each axiom has its own checker that implements the detection logic
|
|
5
|
+
based on the mathematical definitions in the architecture document.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .stability import StabilityChecker
|
|
9
|
+
from .boundedness import BoundednessChecker
|
|
10
|
+
from .connectivity import ConnectivityChecker
|
|
11
|
+
from .consistency import ConsistencyChecker
|
|
12
|
+
from .responsiveness import ResponsivenessChecker
|
|
13
|
+
from .homeostasis import HomeostasisChecker
|
|
14
|
+
from .conservation import ConservationChecker
|
|
15
|
+
from .monotonicity import MonotonicityChecker
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'StabilityChecker',
|
|
19
|
+
'BoundednessChecker',
|
|
20
|
+
'ConnectivityChecker',
|
|
21
|
+
'ConsistencyChecker',
|
|
22
|
+
'ResponsivenessChecker',
|
|
23
|
+
'HomeostasisChecker',
|
|
24
|
+
'ConservationChecker',
|
|
25
|
+
'MonotonicityChecker',
|
|
26
|
+
]
|