endoxa 0.0.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.
- endoxa/__init__.py +23 -0
- endoxa/governance/__init__.py +122 -0
- endoxa/governance/derive.py +528 -0
- endoxa/governance/knowledge.py +20 -0
- endoxa/governance/ledger.py +231 -0
- endoxa/governance/provenance.py +84 -0
- endoxa/governance/resolution.py +380 -0
- endoxa/governance/revision/__init__.py +50 -0
- endoxa/governance/revision/engine.py +533 -0
- endoxa/governance/revision/facts.py +41 -0
- endoxa/governance/revision/links.py +509 -0
- endoxa/governance/revision/preference.py +153 -0
- endoxa/governance/revision/tie.py +214 -0
- endoxa/governance/support.py +102 -0
- endoxa/governance/view.py +458 -0
- endoxa/instruments/__init__.py +23 -0
- endoxa/instruments/calibration/__init__.py +44 -0
- endoxa/instruments/calibration/ask_policy.py +50 -0
- endoxa/instruments/calibration/competence.py +45 -0
- endoxa/instruments/calibration/knowledge.py +83 -0
- endoxa/instruments/calibration/replay.py +111 -0
- endoxa/instruments/calibration/snapshot.py +31 -0
- endoxa/instruments/calibration/windowed.py +273 -0
- endoxa/instruments/coverage/__init__.py +9 -0
- endoxa/instruments/coverage/graph.py +122 -0
- endoxa/instruments/coverage/snapshot.py +85 -0
- endoxa/solver/__init__.py +71 -0
- endoxa/solver/api.py +103 -0
- endoxa/solver/ast/__init__.py +0 -0
- endoxa/solver/ast/context.py +105 -0
- endoxa/solver/ast/expr.py +151 -0
- endoxa/solver/ast/sorts.py +30 -0
- endoxa/solver/ast/utils.py +46 -0
- endoxa/solver/engine.py +268 -0
- endoxa/solver/parsers/__init__.py +3 -0
- endoxa/solver/parsers/dimacs.py +34 -0
- endoxa/solver/parsers/tptp.lark +53 -0
- endoxa/solver/parsers/tptp.py +118 -0
- endoxa/solver/quantifiers/__init__.py +3 -0
- endoxa/solver/quantifiers/ematch.py +230 -0
- endoxa/solver/sat/__init__.py +3 -0
- endoxa/solver/sat/solver.py +743 -0
- endoxa/solver/sat/types.py +25 -0
- endoxa/solver/tactic/__init__.py +5 -0
- endoxa/solver/tactic/encoder.py +109 -0
- endoxa/solver/tactic/preprocess.py +102 -0
- endoxa/solver/tactic/skolemize.py +100 -0
- endoxa/solver/theories/__init__.py +3 -0
- endoxa/solver/theories/base.py +22 -0
- endoxa/solver/theories/euf.py +260 -0
- endoxa/syntax/__init__.py +15 -0
- endoxa/syntax/atoms.py +64 -0
- endoxa/trace/__init__.py +20 -0
- endoxa/trace/models.py +49 -0
- endoxa/trace/store.py +33 -0
- endoxa-0.0.1.dist-info/METADATA +121 -0
- endoxa-0.0.1.dist-info/RECORD +60 -0
- endoxa-0.0.1.dist-info/WHEEL +4 -0
- endoxa-0.0.1.dist-info/licenses/LICENSE +201 -0
- endoxa-0.0.1.dist-info/licenses/NOTICE +14 -0
endoxa/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""endoxa -- governed beliefs for agents.
|
|
2
|
+
|
|
3
|
+
An agent that asserts things needs somewhere for those assertions to be checked,
|
|
4
|
+
recorded, and given up when they turn out to conflict. This package is that
|
|
5
|
+
somewhere: hand it beliefs and the constraints they live under, and it answers in
|
|
6
|
+
operations -- what to retract, what to hold, what stands -- and writes each one
|
|
7
|
+
to an append-only ledger you can read back.
|
|
8
|
+
|
|
9
|
+
The five packages are a DAG, listed here bottom-up:
|
|
10
|
+
|
|
11
|
+
- ``endoxa.syntax`` -- the shape of an atom: predicate, arity, arguments.
|
|
12
|
+
- ``endoxa.solver`` -- a self-contained SMT engine deciding satisfiability.
|
|
13
|
+
- ``endoxa.governance`` -- the decision surface, the ledger it writes, and the
|
|
14
|
+
revision machinery that picks what to give up.
|
|
15
|
+
- ``endoxa.trace`` -- the ordered series of an agent's conscious propositions.
|
|
16
|
+
- ``endoxa.instruments`` -- calibration and coverage measures, imported by nothing
|
|
17
|
+
else, because a measure its subject can reach is a measure its subject can
|
|
18
|
+
move.
|
|
19
|
+
|
|
20
|
+
The exports arrive with the port; this module is the facade they will land in.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Belief governance: the decision, the ledger, and the machinery underneath.
|
|
2
|
+
|
|
3
|
+
Hand this package beliefs and the constraints they live under, and it answers in
|
|
4
|
+
operations -- what to retract, what to hold, what stands. The answer is data
|
|
5
|
+
rather than a mutation: you append it to the ledger and apply it to your own
|
|
6
|
+
store.
|
|
7
|
+
|
|
8
|
+
- :mod:`~endoxa.governance.resolution` is the decision surface. Reading a ledger is
|
|
9
|
+
not yet being governed; this is the part that makes a host governable.
|
|
10
|
+
- :mod:`~endoxa.governance.ledger` declares the seven operations as an append-only
|
|
11
|
+
schema.
|
|
12
|
+
- :mod:`~endoxa.governance.derive` recovers the operation series from a host's
|
|
13
|
+
audit log, read-only.
|
|
14
|
+
- :mod:`~endoxa.governance.view` folds the series back into a current view, in
|
|
15
|
+
which an unsettleable conflict is a state with a name
|
|
16
|
+
(:data:`~endoxa.governance.view.UNRESOLVED`) rather than a silent choice.
|
|
17
|
+
- :mod:`~endoxa.governance.support` reads a belief's footing off what became of the
|
|
18
|
+
things supporting it: it had none, they still stand, they are all gone, or they
|
|
19
|
+
are gone because the state no longer holds what they rested on. That last case
|
|
20
|
+
is why "gone" and "refuted" must not share a name.
|
|
21
|
+
- :mod:`~endoxa.governance.revision` is the machinery every operation above is
|
|
22
|
+
decided by -- the consistency check, the culprit searches, the preference
|
|
23
|
+
ordering, and the detection of a conflict that cannot be settled from inside.
|
|
24
|
+
- :mod:`~endoxa.governance.provenance` is the fixed set of names for where a belief
|
|
25
|
+
came from, and for what brought it back, that the ledger records.
|
|
26
|
+
- :mod:`~endoxa.governance.knowledge` names where a belief sits relative to the
|
|
27
|
+
knowledge boundary.
|
|
28
|
+
|
|
29
|
+
The submodules are residents of this namespace rather than flattened into it.
|
|
30
|
+
What ``__all__`` exports is the API: the ledger schema, the derived view, and the
|
|
31
|
+
decision surface.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from endoxa.governance.derive import LEDGER_EVENT_TYPES, DerivedLedger, derive_ledger
|
|
35
|
+
from endoxa.governance.knowledge import EpistemicStatus
|
|
36
|
+
from endoxa.governance.ledger import (
|
|
37
|
+
EVIDENCE_REASONS,
|
|
38
|
+
LEDGER_OPS,
|
|
39
|
+
REASON_REASSERTION,
|
|
40
|
+
REASON_REVISION_SURVIVED,
|
|
41
|
+
REASON_RULE_RETRACTED,
|
|
42
|
+
REASON_SUPPORT_LOST,
|
|
43
|
+
EvidenceReason,
|
|
44
|
+
LedgerOp,
|
|
45
|
+
OpKind,
|
|
46
|
+
SupportKind,
|
|
47
|
+
SupportRef,
|
|
48
|
+
TargetKind,
|
|
49
|
+
)
|
|
50
|
+
from endoxa.governance.resolution import (
|
|
51
|
+
GOVERNANCE_ACTOR,
|
|
52
|
+
RETRACTED_RULE_CONFIDENCE,
|
|
53
|
+
Belief,
|
|
54
|
+
Constraints,
|
|
55
|
+
ContradictionTie,
|
|
56
|
+
GovernanceOutcome,
|
|
57
|
+
Rule,
|
|
58
|
+
govern,
|
|
59
|
+
)
|
|
60
|
+
from endoxa.governance.support import (
|
|
61
|
+
ABSENT,
|
|
62
|
+
ALIVE,
|
|
63
|
+
DEAD,
|
|
64
|
+
IN,
|
|
65
|
+
INDETERMINATE,
|
|
66
|
+
OUT,
|
|
67
|
+
UNSUPPORTED,
|
|
68
|
+
SupportState,
|
|
69
|
+
SupportVerdict,
|
|
70
|
+
support_verdict,
|
|
71
|
+
)
|
|
72
|
+
from endoxa.governance.view import (
|
|
73
|
+
HELD,
|
|
74
|
+
UNRESOLVED,
|
|
75
|
+
BeliefState,
|
|
76
|
+
ViewEquivalence,
|
|
77
|
+
compare_to_state,
|
|
78
|
+
reconstruct_view,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"ABSENT",
|
|
83
|
+
"ALIVE",
|
|
84
|
+
"DEAD",
|
|
85
|
+
"EVIDENCE_REASONS",
|
|
86
|
+
"GOVERNANCE_ACTOR",
|
|
87
|
+
"HELD",
|
|
88
|
+
"IN",
|
|
89
|
+
"INDETERMINATE",
|
|
90
|
+
"LEDGER_EVENT_TYPES",
|
|
91
|
+
"LEDGER_OPS",
|
|
92
|
+
"OUT",
|
|
93
|
+
"REASON_REASSERTION",
|
|
94
|
+
"REASON_REVISION_SURVIVED",
|
|
95
|
+
"REASON_RULE_RETRACTED",
|
|
96
|
+
"REASON_SUPPORT_LOST",
|
|
97
|
+
"RETRACTED_RULE_CONFIDENCE",
|
|
98
|
+
"UNRESOLVED",
|
|
99
|
+
"UNSUPPORTED",
|
|
100
|
+
"Belief",
|
|
101
|
+
"BeliefState",
|
|
102
|
+
"Constraints",
|
|
103
|
+
"ContradictionTie",
|
|
104
|
+
"DerivedLedger",
|
|
105
|
+
"EpistemicStatus",
|
|
106
|
+
"EvidenceReason",
|
|
107
|
+
"GovernanceOutcome",
|
|
108
|
+
"LedgerOp",
|
|
109
|
+
"OpKind",
|
|
110
|
+
"Rule",
|
|
111
|
+
"SupportKind",
|
|
112
|
+
"SupportRef",
|
|
113
|
+
"SupportState",
|
|
114
|
+
"SupportVerdict",
|
|
115
|
+
"TargetKind",
|
|
116
|
+
"ViewEquivalence",
|
|
117
|
+
"compare_to_state",
|
|
118
|
+
"derive_ledger",
|
|
119
|
+
"govern",
|
|
120
|
+
"reconstruct_view",
|
|
121
|
+
"support_verdict",
|
|
122
|
+
]
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""Recovering the ledger from what the host already records.
|
|
2
|
+
|
|
3
|
+
The separation this rests on is between *the source of truth on the API* and
|
|
4
|
+
*the source of truth in the implementation*: the ledger is the former, a host's
|
|
5
|
+
own stores stay the latter. This module is what makes that separation cost
|
|
6
|
+
nothing at the write side -- it is a **read-only derivation** of the operation
|
|
7
|
+
series from a host's persisted audit log. No write path changes; the ledger is a
|
|
8
|
+
way of reading what already happened.
|
|
9
|
+
|
|
10
|
+
**Why the event log and not the current state.** The current state alone cannot
|
|
11
|
+
yield the series: a retraction is a flip that leaves no trace of the flip, and
|
|
12
|
+
checking a reconstruction needs a series to reconstruct *from*. An audit log is
|
|
13
|
+
the only place a host keeps the order of what it did, so it is the primary
|
|
14
|
+
input; the host's own state is what the derived view is then checked against
|
|
15
|
+
(:func:`~endoxa.governance.view.compare_to_state`).
|
|
16
|
+
|
|
17
|
+
**The horizon.** A host that prunes its audit log by event type can have had
|
|
18
|
+
governance operations swept out from under it, and a derivation that stayed
|
|
19
|
+
silent about that would be claiming a completeness it does not have.
|
|
20
|
+
:class:`DerivedLedger` therefore reports the horizon -- the earliest row it saw
|
|
21
|
+
-- rather than pretending the series starts at the beginning of time.
|
|
22
|
+
|
|
23
|
+
**Keeping the ledger-bearing types does not retire the horizon.** A retention
|
|
24
|
+
policy acts *forward*: what was already swept cannot be un-swept, and a host
|
|
25
|
+
remains free to configure one that keeps less. So the horizon keeps meaning
|
|
26
|
+
exactly what it always meant -- the earliest row this derivation could read,
|
|
27
|
+
which is not a claim of completeness.
|
|
28
|
+
|
|
29
|
+
**Reading the host's event names.** This package may not import
|
|
30
|
+
a host's own event definitions (this package has to travel to
|
|
31
|
+
another host without the control plane), so the event type names live here as
|
|
32
|
+
string constants. A host-side test pins them against the real classes'
|
|
33
|
+
``__name__`` so a rename cannot silently empty the ledger.
|
|
34
|
+
|
|
35
|
+
Pure and dependency-free: the input is the raw row shape a host's event store
|
|
36
|
+
returns.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from datetime import datetime
|
|
42
|
+
from typing import TYPE_CHECKING, Any
|
|
43
|
+
|
|
44
|
+
from endoxa.governance.ledger import EVIDENCE_REASONS, EvidenceReason, LedgerOp, SupportRef
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from collections.abc import Mapping, Sequence
|
|
48
|
+
|
|
49
|
+
#: Host event class names the derivation reads. Pinned against the real classes
|
|
50
|
+
#: by a host-side test (see the module docstring).
|
|
51
|
+
ATOM_ADDED = "AtomAddedEvent"
|
|
52
|
+
BELIEF_EVIDENCE_BOOKED = "BeliefEvidenceBookedEvent"
|
|
53
|
+
BELIEF_EVIDENCE_RECORDED = "BeliefEvidenceRecordedEvent"
|
|
54
|
+
BELIEF_SUPPORT_RECORDED = "BeliefSupportRecordedEvent"
|
|
55
|
+
CONTRADICTION_TIE_DETECTED = "ContradictionTieDetectedEvent"
|
|
56
|
+
MEMORY_BATCH_GET_RESPONSE = "MemoryBatchGetResponseEvent"
|
|
57
|
+
MEMORY_BATCH_UPDATE_REQUEST = "MemoryBatchUpdateRequestEvent"
|
|
58
|
+
|
|
59
|
+
#: The event types a ledger derivation reads. Handed to the store's type-filtered
|
|
60
|
+
#: read so a diagnostic run does not drag the whole audit log into memory.
|
|
61
|
+
#:
|
|
62
|
+
#: ``BeliefSupportRecordedEvent`` is the odd one: it yields **no operation at
|
|
63
|
+
#: all**. A derivation reaching a belief already on the beliefs changes
|
|
64
|
+
#: no claim and no credence, so it is read for the support state it
|
|
65
|
+
#: carries and for nothing else -- the first row type here that is state without
|
|
66
|
+
#: being an operation.
|
|
67
|
+
#:
|
|
68
|
+
#: ``BeliefEvidenceBookedEvent`` is the write side breaking its own silence
|
|
69
|
+
#: (increment 1). It maps onto exactly the same operations as
|
|
70
|
+
#: ``BeliefEvidenceRecordedEvent``; the two are separate classes only because
|
|
71
|
+
#: the beliefs subscribes to the latter, so re-using it would fold the evidence a
|
|
72
|
+
#: second time. The distinction is a host wiring detail, and the derivation is
|
|
73
|
+
#: deliberately blind to it.
|
|
74
|
+
LEDGER_EVENT_TYPES: frozenset[str] = frozenset(
|
|
75
|
+
{
|
|
76
|
+
ATOM_ADDED,
|
|
77
|
+
BELIEF_EVIDENCE_BOOKED,
|
|
78
|
+
BELIEF_EVIDENCE_RECORDED,
|
|
79
|
+
BELIEF_SUPPORT_RECORDED,
|
|
80
|
+
CONTRADICTION_TIE_DETECTED,
|
|
81
|
+
MEMORY_BATCH_GET_RESPONSE,
|
|
82
|
+
MEMORY_BATCH_UPDATE_REQUEST,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
#: Property name the beliefs writes its support record under, and the keys of one
|
|
87
|
+
#: record (a host's own support record). String constants for the
|
|
88
|
+
#: same reason the event names are: this package cannot import a host.
|
|
89
|
+
SUPPORTED_BY_KEY = "supported_by"
|
|
90
|
+
_SUPPORT_KIND_KEY = "kind"
|
|
91
|
+
_SUPPORT_REF_KEY = "ref"
|
|
92
|
+
|
|
93
|
+
#: Correlation id a host stamps on its axiom batch-get, pinned by the same
|
|
94
|
+
#: host-side test as the event names. The response to *this* request is the moment
|
|
95
|
+
#: a rule store's content enters the ledger as beliefs: the file or table is host
|
|
96
|
+
#: initialisation data, and its content becomes governance.
|
|
97
|
+
AXIOM_LOAD_CORRELATION_ID = "reasoning_axiom_load"
|
|
98
|
+
|
|
99
|
+
#: Memory type of a learned or base rule row.
|
|
100
|
+
_AXIOM_MEMORY_TYPE = "axiom"
|
|
101
|
+
|
|
102
|
+
#: Default for the confidence below which a defeasible rule stops constraining
|
|
103
|
+
#: (``settings.reasoning_rule_active_confidence_threshold``). Passed in by the
|
|
104
|
+
#: host rather than imported, so this package carries no configuration dependency.
|
|
105
|
+
DEFAULT_RULE_ACTIVE_THRESHOLD = 0.5
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class DerivedLedger:
|
|
110
|
+
"""The operation series recovered from a run's audit log, and what it cost.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
ops: The operations in the order they happened.
|
|
114
|
+
horizon: Epoch seconds of the earliest row read, or ``None`` when no row
|
|
115
|
+
carried a readable timestamp. Operations before it are unrecoverable
|
|
116
|
+
(see the module docstring on retention).
|
|
117
|
+
rows_read: How many rows the derivation was handed.
|
|
118
|
+
rows_unreadable: Rows whose payload could not be parsed as an object.
|
|
119
|
+
Counted rather than dropped silently: a reader that quietly skipped
|
|
120
|
+
them would be reporting a series it cannot vouch for.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
ops: tuple[LedgerOp, ...]
|
|
124
|
+
horizon: float | None
|
|
125
|
+
rows_read: int
|
|
126
|
+
rows_unreadable: int
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def derive_ledger(
|
|
130
|
+
rows: Sequence[Mapping[str, Any]],
|
|
131
|
+
*,
|
|
132
|
+
rule_active_threshold: float = DEFAULT_RULE_ACTIVE_THRESHOLD,
|
|
133
|
+
) -> DerivedLedger:
|
|
134
|
+
"""Derive the ledger's operation series from persisted event rows.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
rows: Raw ``event_store`` rows (``id``/``timestamp``/``event_type``/
|
|
138
|
+
``payload``) in any order; they are sorted here.
|
|
139
|
+
rule_active_threshold: Confidence at or above which a defeasible rule
|
|
140
|
+
still constrains. An axiom update below it is a ``retract``.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The derived series together with what it could and could not see.
|
|
144
|
+
"""
|
|
145
|
+
parsed = [record for record in (_parse(row) for row in rows) if record is not None]
|
|
146
|
+
unreadable = len(rows) - len(parsed)
|
|
147
|
+
parsed.sort(key=lambda record: (record.at if record.at is not None else 0.0, record.event_id))
|
|
148
|
+
|
|
149
|
+
ops: list[LedgerOp] = []
|
|
150
|
+
state = _FoldState()
|
|
151
|
+
for record in parsed:
|
|
152
|
+
ops.extend(_operations(record, state, rule_active_threshold=rule_active_threshold))
|
|
153
|
+
|
|
154
|
+
stamps = [record.at for record in parsed if record.at is not None]
|
|
155
|
+
return DerivedLedger(
|
|
156
|
+
ops=tuple(ops),
|
|
157
|
+
horizon=min(stamps) if stamps else None,
|
|
158
|
+
rows_read=len(rows),
|
|
159
|
+
rows_unreadable=unreadable,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True, slots=True)
|
|
164
|
+
class _Record:
|
|
165
|
+
"""One audit-log row normalized into what the derivation reads."""
|
|
166
|
+
|
|
167
|
+
event_id: str
|
|
168
|
+
event_type: str
|
|
169
|
+
at: float | None
|
|
170
|
+
payload: dict[str, Any]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse(row: Mapping[str, Any]) -> _Record | None:
|
|
174
|
+
"""Normalize a raw store row, or return ``None`` when its payload is unreadable.
|
|
175
|
+
|
|
176
|
+
Two row shapes are accepted, because a store may hand back either: a JSON
|
|
177
|
+
string payload with a ``datetime`` timestamp, or a plain dict payload with an
|
|
178
|
+
epoch float.
|
|
179
|
+
"""
|
|
180
|
+
payload = row.get("payload")
|
|
181
|
+
if isinstance(payload, str):
|
|
182
|
+
try:
|
|
183
|
+
payload = json.loads(payload)
|
|
184
|
+
except TypeError, ValueError:
|
|
185
|
+
return None
|
|
186
|
+
if not isinstance(payload, dict):
|
|
187
|
+
return None
|
|
188
|
+
return _Record(
|
|
189
|
+
event_id=str(row.get("id") or payload.get("event_id") or ""),
|
|
190
|
+
event_type=str(row.get("event_type", "")),
|
|
191
|
+
at=_as_epoch(row.get("timestamp", payload.get("timestamp"))),
|
|
192
|
+
payload=payload,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _as_epoch(value: object) -> float | None:
|
|
197
|
+
"""Coerce a row timestamp to epoch seconds, or ``None`` when unreadable."""
|
|
198
|
+
if isinstance(value, datetime):
|
|
199
|
+
return value.timestamp()
|
|
200
|
+
if isinstance(value, bool):
|
|
201
|
+
return None
|
|
202
|
+
if isinstance(value, (int, float)):
|
|
203
|
+
return float(value)
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(slots=True)
|
|
208
|
+
class _FoldState:
|
|
209
|
+
"""What the fold has to remember between rows.
|
|
210
|
+
|
|
211
|
+
``truth`` is the running truth value per belief: the host's ``AtomAddedEvent``
|
|
212
|
+
carries the properties that were *written*, not the resulting node, so whether
|
|
213
|
+
a write flipped a belief is only visible against what the series says it held
|
|
214
|
+
(a host's own atom writer).
|
|
215
|
+
|
|
216
|
+
``supports`` is the same idea for the support seat: a belief's
|
|
217
|
+
footing accumulates across rows (a materialisation writes one, a later
|
|
218
|
+
derivation reaching the same belief adds another), and each operation is
|
|
219
|
+
stamped with the set as it stood *at that moment*. Mutable and threaded rather
|
|
220
|
+
than recomputed, because the series is the only thing that has the order.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
truth: dict[str, bool] = field(default_factory=dict)
|
|
224
|
+
supports: dict[str, tuple[SupportRef, ...]] = field(default_factory=dict)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _operations(
|
|
228
|
+
record: _Record,
|
|
229
|
+
state: _FoldState,
|
|
230
|
+
*,
|
|
231
|
+
rule_active_threshold: float,
|
|
232
|
+
) -> list[LedgerOp]:
|
|
233
|
+
"""Map one event row onto the ledger operations it stands for.
|
|
234
|
+
|
|
235
|
+
A row may legitimately stand for none: ``BeliefSupportRecordedEvent`` updates
|
|
236
|
+
the running support state and returns nothing, because gaining a second
|
|
237
|
+
footing is not a governance operation on the belief: neither the claim nor the
|
|
238
|
+
credence is touched.
|
|
239
|
+
"""
|
|
240
|
+
if record.event_type == ATOM_ADDED:
|
|
241
|
+
return _atom_operations(record, state)
|
|
242
|
+
if record.event_type == BELIEF_SUPPORT_RECORDED:
|
|
243
|
+
_absorb_support(record, state)
|
|
244
|
+
return []
|
|
245
|
+
if record.event_type in (BELIEF_EVIDENCE_RECORDED, BELIEF_EVIDENCE_BOOKED):
|
|
246
|
+
return _evidence_operations(record, state)
|
|
247
|
+
if record.event_type == CONTRADICTION_TIE_DETECTED:
|
|
248
|
+
return _hold_operations(record)
|
|
249
|
+
if record.event_type == MEMORY_BATCH_GET_RESPONSE:
|
|
250
|
+
return _rule_load_operations(record)
|
|
251
|
+
if record.event_type == MEMORY_BATCH_UPDATE_REQUEST:
|
|
252
|
+
return _rule_update_operations(record, rule_active_threshold=rule_active_threshold)
|
|
253
|
+
return []
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _support_refs(records: object) -> tuple[SupportRef, ...]:
|
|
257
|
+
"""Read a beliefs support record into typed references, skipping malformed entries.
|
|
258
|
+
|
|
259
|
+
Tolerant in the same measure as the beliefs's own reader
|
|
260
|
+
(a host's own support record): support is read on paths that also run for
|
|
261
|
+
beliefs that never had any, and an audit log is not a place where raising is
|
|
262
|
+
useful. An unknown ``kind`` is dropped rather than guessed -- the whole point
|
|
263
|
+
of carrying the kind is that it is not inferred.
|
|
264
|
+
"""
|
|
265
|
+
if not isinstance(records, list):
|
|
266
|
+
return ()
|
|
267
|
+
refs: list[SupportRef] = []
|
|
268
|
+
for record in records:
|
|
269
|
+
if not isinstance(record, dict):
|
|
270
|
+
continue
|
|
271
|
+
kind = record.get(_SUPPORT_KIND_KEY)
|
|
272
|
+
ref = record.get(_SUPPORT_REF_KEY)
|
|
273
|
+
if kind in ("derivation", "rule") and ref:
|
|
274
|
+
refs.append(SupportRef(kind=kind, ref=str(ref)))
|
|
275
|
+
return tuple(refs)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _absorb_support(record: _Record, state: _FoldState) -> None:
|
|
279
|
+
"""Fold a ``BeliefSupportRecordedEvent`` into the running support state.
|
|
280
|
+
|
|
281
|
+
Appended, not replaced, and never duplicated -- mirroring the beliefs's
|
|
282
|
+
``add_support``: a derivation that runs again over the same pair is
|
|
283
|
+
the same footing, not a second one.
|
|
284
|
+
"""
|
|
285
|
+
node_id = str(record.payload.get("node_id", ""))
|
|
286
|
+
added = _support_refs([record.payload.get("support")])
|
|
287
|
+
if not node_id or not added:
|
|
288
|
+
return
|
|
289
|
+
current = state.supports.get(node_id, ())
|
|
290
|
+
state.supports[node_id] = current + tuple(ref for ref in added if ref not in current)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _atom_operations(record: _Record, state: _FoldState) -> list[LedgerOp]:
|
|
294
|
+
"""Map an ``AtomAddedEvent`` onto ``assert`` / ``retract`` / ``supersede`` / ``ground``.
|
|
295
|
+
|
|
296
|
+
Four cases, in the order they are tested:
|
|
297
|
+
|
|
298
|
+
- ``ask_grounding`` -> ``ground``. The ask-user closed loop is the only writer
|
|
299
|
+
of confidence 1.0, so it is its own operation whatever else the
|
|
300
|
+
write does.
|
|
301
|
+
- an unseen belief -> ``assert``. Birth defaults to true, matching ``add_atom``.
|
|
302
|
+
- a flip -> ``retract`` or ``supersede``. **The discriminator is the one the
|
|
303
|
+
beliefs itself uses**: a host's belief store re-attributes the belief's
|
|
304
|
+
evidence to its new claim (``swap_evidence``) exactly when the
|
|
305
|
+
write carries no explicit ``confidence``. A revision flip writes the truth
|
|
306
|
+
value alone; recency supersession restates the confidence to say "the world
|
|
307
|
+
moved, the belief was not miscalibrated". So the
|
|
308
|
+
presence of that key is not a guessed proxy for the distinction -- it *is*
|
|
309
|
+
the host's own test for it.
|
|
310
|
+
- anything else -> ``assert``, a restatement (the host publishes nothing at
|
|
311
|
+
all for an idempotent re-assertion, so a row here always changed something).
|
|
312
|
+
|
|
313
|
+
A written support record **replaces** what the belief is known to rest on,
|
|
314
|
+
because that is what the beliefs does: ``add_atom`` overwrites the property, so
|
|
315
|
+
a re-derivation names the antecedent it actually rode on this time.
|
|
316
|
+
Accumulation is the other writer's job (``_absorb_support``).
|
|
317
|
+
"""
|
|
318
|
+
payload = record.payload
|
|
319
|
+
node_id = str(payload.get("node_id", ""))
|
|
320
|
+
if not node_id:
|
|
321
|
+
return []
|
|
322
|
+
properties = payload.get("properties") or {}
|
|
323
|
+
if not isinstance(properties, dict):
|
|
324
|
+
properties = {}
|
|
325
|
+
role = str(payload.get("role", ""))
|
|
326
|
+
stated_truth = properties.get("truth_value")
|
|
327
|
+
stated_truth = None if stated_truth is None else bool(stated_truth)
|
|
328
|
+
confidence = _as_confidence(properties.get("confidence"))
|
|
329
|
+
known = state.truth.get(node_id)
|
|
330
|
+
if SUPPORTED_BY_KEY in properties:
|
|
331
|
+
state.supports[node_id] = _support_refs(properties.get(SUPPORTED_BY_KEY))
|
|
332
|
+
|
|
333
|
+
if payload.get("ask_grounding"):
|
|
334
|
+
op = "ground"
|
|
335
|
+
elif known is None:
|
|
336
|
+
op = "assert"
|
|
337
|
+
stated_truth = True if stated_truth is None else stated_truth
|
|
338
|
+
elif stated_truth is not None and stated_truth != known:
|
|
339
|
+
op = "supersede" if "confidence" in properties else "retract"
|
|
340
|
+
else:
|
|
341
|
+
op = "assert"
|
|
342
|
+
|
|
343
|
+
if stated_truth is not None:
|
|
344
|
+
state.truth[node_id] = stated_truth
|
|
345
|
+
return [
|
|
346
|
+
LedgerOp(
|
|
347
|
+
op=op, # type: ignore[arg-type] -- one of the seven, by construction above
|
|
348
|
+
target=node_id,
|
|
349
|
+
actor=role,
|
|
350
|
+
truth_value=stated_truth,
|
|
351
|
+
confidence=confidence,
|
|
352
|
+
origin_event_id=record.event_id,
|
|
353
|
+
at=record.at,
|
|
354
|
+
supported_by=state.supports.get(node_id, ()),
|
|
355
|
+
),
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _evidence_operations(record: _Record, state: _FoldState) -> list[LedgerOp]:
|
|
360
|
+
"""Map an evidence row onto ``confirm`` or ``refute``.
|
|
361
|
+
|
|
362
|
+
Both evidence event types land here (see :data:`LEDGER_EVENT_TYPES`): which
|
|
363
|
+
class the host used says only whether the beliefs had already folded the
|
|
364
|
+
evidence before publishing, which is not a governance distinction.
|
|
365
|
+
|
|
366
|
+
No confidence is carried: the event says only which way the evidence points,
|
|
367
|
+
and the resulting credence is the Laplace fold the view replays.
|
|
368
|
+
|
|
369
|
+
This is the operation the support seat was worth filling for. Counter-evidence
|
|
370
|
+
is booked against a derived belief exactly when its footing goes,
|
|
371
|
+
and stamping the entry with what it was resting on at that moment is what lets
|
|
372
|
+
a reader tell "refuted while still supported" from "refuted because the
|
|
373
|
+
support died" -- from the series alone, with no beliefs in hand. The reason
|
|
374
|
+
says the same thing from the other side: the support set is
|
|
375
|
+
the *state* the booking found, the reason is the *event* that caused it, and
|
|
376
|
+
a reader who has to infer one from the other is guessing again.
|
|
377
|
+
"""
|
|
378
|
+
node_id = str(record.payload.get("node_id", ""))
|
|
379
|
+
if not node_id:
|
|
380
|
+
return []
|
|
381
|
+
supports = bool(record.payload.get("supports", True))
|
|
382
|
+
return [
|
|
383
|
+
LedgerOp(
|
|
384
|
+
op="confirm" if supports else "refute",
|
|
385
|
+
target=node_id,
|
|
386
|
+
origin_event_id=record.event_id,
|
|
387
|
+
at=record.at,
|
|
388
|
+
supported_by=state.supports.get(node_id, ()),
|
|
389
|
+
reason=_reason(record.payload.get("reason")),
|
|
390
|
+
),
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _reason(value: object) -> EvidenceReason | None:
|
|
395
|
+
"""Read a booking's reason, or ``None`` when it is absent or unrecognised.
|
|
396
|
+
|
|
397
|
+
Dropped rather than guessed, exactly as :func:`_support_refs` drops an
|
|
398
|
+
unknown support ``kind``: the whole value of carrying the word is that it was
|
|
399
|
+
not inferred, so an audit log written by an older host (or by a writer this
|
|
400
|
+
set of names has not caught up with) reads as "no reason stated" instead of as
|
|
401
|
+
a reason invented here.
|
|
402
|
+
"""
|
|
403
|
+
if isinstance(value, str) and value in EVIDENCE_REASONS:
|
|
404
|
+
return value # type: ignore[return-value] -- membership above is the Literal
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _hold_operations(record: _Record) -> list[LedgerOp]:
|
|
409
|
+
"""Map a ``ContradictionTieDetectedEvent`` onto one ``hold`` over the pair.
|
|
410
|
+
|
|
411
|
+
One operation, not two: a tie *is* a pair (``_TIE_ARITY``), and
|
|
412
|
+
counting it twice would make the ledger's own statistics say the system held
|
|
413
|
+
twice as often as it did. The view marks both members from ``partner``.
|
|
414
|
+
|
|
415
|
+
The detection event is what the ledger records, not the self-model's
|
|
416
|
+
``RevisionTieAskEvent``: being unable to settle is the governance layer's
|
|
417
|
+
judgement, while deciding to *ask* about it is the host's dialogue policy
|
|
418
|
+
The two are kept apart deliberately: a ledger should not require its host to
|
|
419
|
+
have someone to talk to.
|
|
420
|
+
"""
|
|
421
|
+
node_a = str(record.payload.get("node_a", ""))
|
|
422
|
+
node_b = str(record.payload.get("node_b", ""))
|
|
423
|
+
if not node_a or not node_b:
|
|
424
|
+
return []
|
|
425
|
+
return [
|
|
426
|
+
LedgerOp(
|
|
427
|
+
op="hold",
|
|
428
|
+
target=node_a,
|
|
429
|
+
partner=node_b,
|
|
430
|
+
origin_event_id=record.event_id,
|
|
431
|
+
at=record.at,
|
|
432
|
+
),
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _rule_load_operations(record: _Record) -> list[LedgerOp]:
|
|
437
|
+
"""Map the axiom batch-get response onto one ``assert`` per rule.
|
|
438
|
+
|
|
439
|
+
A rule store is host initialisation data, and the moment its content is loaded
|
|
440
|
+
is the moment it enters the ledger as beliefs: learned rules are in the
|
|
441
|
+
ledger's primary scope. A base, non-defeasible axiom is asserted and never
|
|
442
|
+
retracted; only a defeasible rule has a retraction path.
|
|
443
|
+
|
|
444
|
+
Only the response to Reasoning's own axiom load is read: other batch-gets
|
|
445
|
+
(memory recall, paging) return the same row shape but are not the rule set
|
|
446
|
+
being taken into the ledger.
|
|
447
|
+
"""
|
|
448
|
+
if record.payload.get("correlation_id") != AXIOM_LOAD_CORRELATION_ID:
|
|
449
|
+
return []
|
|
450
|
+
results = record.payload.get("results")
|
|
451
|
+
if not isinstance(results, list):
|
|
452
|
+
return []
|
|
453
|
+
ops: list[LedgerOp] = []
|
|
454
|
+
for result in results:
|
|
455
|
+
if not isinstance(result, dict):
|
|
456
|
+
continue
|
|
457
|
+
memory_id = str(result.get("id", ""))
|
|
458
|
+
if not memory_id:
|
|
459
|
+
continue
|
|
460
|
+
ops.append(
|
|
461
|
+
LedgerOp(
|
|
462
|
+
op="assert",
|
|
463
|
+
target=memory_id,
|
|
464
|
+
target_kind="rule",
|
|
465
|
+
actor=str(result.get("source_kind") or _AXIOM_MEMORY_TYPE),
|
|
466
|
+
truth_value=True,
|
|
467
|
+
confidence=_as_confidence(result.get("confidence")),
|
|
468
|
+
origin_event_id=record.event_id,
|
|
469
|
+
at=record.at,
|
|
470
|
+
),
|
|
471
|
+
)
|
|
472
|
+
return ops
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _rule_update_operations(record: _Record, *, rule_active_threshold: float) -> list[LedgerOp]:
|
|
476
|
+
"""Map an axiom confidence update onto ``retract`` (or a restating ``assert``).
|
|
477
|
+
|
|
478
|
+
Soft retraction of a learned rule is a confidence driven below the active
|
|
479
|
+
threshold: the row is kept so the
|
|
480
|
+
rule can be re-learned, which is precisely the ledger's own stance -- the
|
|
481
|
+
entry does not disappear, it stops counting. An update that leaves the rule
|
|
482
|
+
active is a restatement, not a withdrawal.
|
|
483
|
+
"""
|
|
484
|
+
if record.payload.get("memory_type") != _AXIOM_MEMORY_TYPE:
|
|
485
|
+
return []
|
|
486
|
+
updates = record.payload.get("updates")
|
|
487
|
+
if not isinstance(updates, list):
|
|
488
|
+
return []
|
|
489
|
+
ops: list[LedgerOp] = []
|
|
490
|
+
for update in updates:
|
|
491
|
+
if not isinstance(update, dict):
|
|
492
|
+
continue
|
|
493
|
+
memory_id = str(update.get("id", ""))
|
|
494
|
+
confidence = _as_confidence(update.get("confidence"))
|
|
495
|
+
if not memory_id or confidence is None:
|
|
496
|
+
continue
|
|
497
|
+
ops.append(
|
|
498
|
+
LedgerOp(
|
|
499
|
+
op="retract" if confidence < rule_active_threshold else "assert",
|
|
500
|
+
target=memory_id,
|
|
501
|
+
target_kind="rule",
|
|
502
|
+
actor=_AXIOM_MEMORY_TYPE,
|
|
503
|
+
truth_value=True,
|
|
504
|
+
confidence=confidence,
|
|
505
|
+
origin_event_id=record.event_id,
|
|
506
|
+
at=record.at,
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
return ops
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _as_confidence(value: object) -> float | None:
|
|
513
|
+
"""Read a confidence field, or ``None`` when it is absent or not a number."""
|
|
514
|
+
if isinstance(value, bool) or value is None:
|
|
515
|
+
return None
|
|
516
|
+
if isinstance(value, (int, float)):
|
|
517
|
+
return float(value)
|
|
518
|
+
return None
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
__all__ = [
|
|
522
|
+
"AXIOM_LOAD_CORRELATION_ID",
|
|
523
|
+
"DEFAULT_RULE_ACTIVE_THRESHOLD",
|
|
524
|
+
"LEDGER_EVENT_TYPES",
|
|
525
|
+
"SUPPORTED_BY_KEY",
|
|
526
|
+
"DerivedLedger",
|
|
527
|
+
"derive_ledger",
|
|
528
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""The knowledge boundary as a schema.
|
|
2
|
+
|
|
3
|
+
What an agent knows, is uncertain about, and does not know is a fact *about its
|
|
4
|
+
belief set*, so the names for saying it belong with the belief set rather than
|
|
5
|
+
with whoever happens to do the classifying.
|
|
6
|
+
|
|
7
|
+
**Only the schema lives here.** The thresholds, the classifier and the policy for
|
|
8
|
+
when to ask belong to the host: deciding whether a mid-confidence belief is worth
|
|
9
|
+
a question is a policy of a particular agent, not a property of any belief set.
|
|
10
|
+
What this module provides is the three names an outside reader needs in order to
|
|
11
|
+
read a knowledge boundary at all.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
#: Where a belief sits relative to the agent's knowledge boundary: held with
|
|
17
|
+
#: confidence (``known``), attended to but not confidently held (``uncertain``),
|
|
18
|
+
#: or absent/too weak to count as knowledge (``unknown``). The latter two are the
|
|
19
|
+
#: known-unknowns -- calibration's subject matter.
|
|
20
|
+
EpistemicStatus = Literal["known", "uncertain", "unknown"]
|