fpf-thinking-map 1.9.2__tar.gz → 1.9.3__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.
- {fpf_thinking_map-1.9.2/fpf_thinking_map.egg-info → fpf_thinking_map-1.9.3}/PKG-INFO +1 -1
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/__init__.py +8 -0
- fpf_thinking_map-1.9.3/fpf_thinking_map/reachability.py +109 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/verify.py +59 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3/fpf_thinking_map.egg-info}/PKG-INFO +1 -1
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map.egg-info/SOURCES.txt +1 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/pyproject.toml +1 -1
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/LICENSE +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/NOTICE +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/README.md +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/authorization.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/examples.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/guards.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/logic.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/move_intent.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/pending_input.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/primitives.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/py.typed +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/state.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map/traversal.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map.egg-info/dependency_links.txt +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map.egg-info/top_level.txt +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fpf-thinking-map
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
4
4
|
Summary: Deterministic Python runtime for lawful agent traversal with explicit state, evidence, authorization, waiting, and move identity.
|
|
5
5
|
Author-email: "igareosh.com (@igareosh)" <igareosh@igareosh.com>
|
|
6
6
|
Maintainer-email: "igareosh.com (@igareosh)" <igareosh@igareosh.com>
|
|
@@ -15,6 +15,7 @@ Modules:
|
|
|
15
15
|
pending_input — declared external dependencies distinguishing AWAIT from IDLE
|
|
16
16
|
move_intent — a concrete proposed move, distinct from its transition type
|
|
17
17
|
examples — deploy decision scenarios demonstrating the full system
|
|
18
|
+
reachability — discrete reachability analysis over a map's transition graph
|
|
18
19
|
verify — self-verification harness
|
|
19
20
|
"""
|
|
20
21
|
|
|
@@ -95,3 +96,10 @@ from fpf_thinking_map.pending_input import PendingInput, PendingInputStatus
|
|
|
95
96
|
|
|
96
97
|
# --- Move intent: concrete proposed move, distinct from transition type ---
|
|
97
98
|
from fpf_thinking_map.move_intent import MoveIntent
|
|
99
|
+
|
|
100
|
+
# --- Reachability: discrete reachability analysis over the transition graph ---
|
|
101
|
+
from fpf_thinking_map.reachability import (
|
|
102
|
+
forward_reachable,
|
|
103
|
+
graph_roots,
|
|
104
|
+
unreachable_transitions,
|
|
105
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Discrete reachability analysis over a SemanticMap's declared transition graph.
|
|
2
|
+
|
|
3
|
+
A SemanticMap's transitions form exactly the object M. J. Kochenderfer, S. M.
|
|
4
|
+
Katz, A. L. Corso, and R. J. Moss describe in *Algorithms for Validation*
|
|
5
|
+
(MIT Press, 2026), ch. 10, "Reachability for Discrete Systems": a directed
|
|
6
|
+
graph, one node per state, one edge per transition. Their algorithm 10.1
|
|
7
|
+
(build the graph) and 10.3 (backward/forward reachable sets) are cheap at the
|
|
8
|
+
scale a hand-authored domain map actually runs at -- tens of transitions, not
|
|
9
|
+
thousands -- so there is no excuse for skipping them in favor of hand-picked
|
|
10
|
+
test cases that only exercise the paths someone happened to think of.
|
|
11
|
+
|
|
12
|
+
What this module answers: given the transitions a map declares, is every
|
|
13
|
+
from_state either a named entry point or produced by some other transition's
|
|
14
|
+
to_state? A from_state that is neither is either (a) the map's intentional
|
|
15
|
+
starting point for a bounded context -- the caller sets current_state there
|
|
16
|
+
directly, e.g. because real-world work upstream of this map already happened
|
|
17
|
+
-- or (b) an orphaned typo nobody meant to leave unreachable. This module
|
|
18
|
+
cannot tell those apart on its own, and does not try to: entry_states is a
|
|
19
|
+
required argument, not inferred, because a map's real entry points are a
|
|
20
|
+
domain fact, not something derivable from the graph.
|
|
21
|
+
|
|
22
|
+
Origin story (see check_reachability in verify.py for the worked example):
|
|
23
|
+
a downstream domain map gave a requires_human_authorization transition its
|
|
24
|
+
own dedicated from_state, distinct from its non-destructive twin's. Read cold,
|
|
25
|
+
that dedicated from_state looked unreachable -- nothing in the map produced
|
|
26
|
+
it -- and the obvious "fix" (merge it onto the twin's shared from_state) broke
|
|
27
|
+
four behavior tests, because ThinkingMapTraversal.step() called without an
|
|
28
|
+
explicit transition_id aggregates missing_evidence across every transition
|
|
29
|
+
sharing a from_state, not just the one the caller meant. The state genuinely
|
|
30
|
+
was meant to be entered from outside the graph; the actual defect was that
|
|
31
|
+
nothing recorded that on purpose. entry_states exists so that fact is written
|
|
32
|
+
down once, in code, instead of being tribal knowledge someone has to
|
|
33
|
+
rediscover by breaking tests.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from collections.abc import Iterable
|
|
39
|
+
|
|
40
|
+
from fpf_thinking_map.primitives import TransitionPrimitive
|
|
41
|
+
from fpf_thinking_map.state import SemanticMap
|
|
42
|
+
|
|
43
|
+
__all__ = ["graph_roots", "forward_reachable", "unreachable_transitions"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def graph_roots(transitions: Iterable[TransitionPrimitive]) -> set[str]:
|
|
47
|
+
"""States that no declared transition's to_state ever produces.
|
|
48
|
+
|
|
49
|
+
Every state in the returned set is either an intentional entry point
|
|
50
|
+
or an orphaned from_state -- this function makes no claim about which.
|
|
51
|
+
Useful for auditing a map you didn't author: it's the candidate list
|
|
52
|
+
to sort into "yes, callers arrive here directly" and "nobody meant
|
|
53
|
+
this," by hand, once, rather than rediscovering it via a break like
|
|
54
|
+
the one described in this module's docstring.
|
|
55
|
+
"""
|
|
56
|
+
transitions = list(transitions)
|
|
57
|
+
produced = {t.to_state for t in transitions}
|
|
58
|
+
consumed = {t.from_state for t in transitions}
|
|
59
|
+
return consumed - produced
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def forward_reachable(
|
|
63
|
+
transitions: Iterable[TransitionPrimitive],
|
|
64
|
+
entry_states: Iterable[str],
|
|
65
|
+
) -> set[str]:
|
|
66
|
+
"""States reachable from entry_states by following declared to_state edges.
|
|
67
|
+
|
|
68
|
+
Plain BFS over the (from_state -> to_state) graph -- algorithm 10.1's
|
|
69
|
+
graph formulation, forward direction. At domain-map scale (tens of
|
|
70
|
+
edges) this is microseconds; there is no reason to approximate it.
|
|
71
|
+
"""
|
|
72
|
+
transitions = list(transitions)
|
|
73
|
+
edges: dict[str, list[str]] = {}
|
|
74
|
+
for t in transitions:
|
|
75
|
+
edges.setdefault(t.from_state, []).append(t.to_state)
|
|
76
|
+
|
|
77
|
+
seen = set(entry_states)
|
|
78
|
+
frontier = list(seen)
|
|
79
|
+
while frontier:
|
|
80
|
+
s = frontier.pop()
|
|
81
|
+
for nxt in edges.get(s, []):
|
|
82
|
+
if nxt not in seen:
|
|
83
|
+
seen.add(nxt)
|
|
84
|
+
frontier.append(nxt)
|
|
85
|
+
return seen
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def unreachable_transitions(
|
|
89
|
+
sm: SemanticMap,
|
|
90
|
+
entry_states: Iterable[str],
|
|
91
|
+
) -> list[TransitionPrimitive]:
|
|
92
|
+
"""Transitions whose from_state is unreachable given the declared entry points.
|
|
93
|
+
|
|
94
|
+
entry_states must be supplied explicitly. Omitting a state that a
|
|
95
|
+
caller genuinely does set current_state to directly is not a bug in
|
|
96
|
+
this function -- it is the intended failure mode, surfacing every
|
|
97
|
+
undeclared entry point by name instead of silently accepting it. See
|
|
98
|
+
check_reachability in verify.py for both directions demonstrated: the
|
|
99
|
+
library's own build_destructive_action_map() example map is clean when
|
|
100
|
+
"reviewed" is declared as an entry point, and loudly not clean when it
|
|
101
|
+
isn't.
|
|
102
|
+
|
|
103
|
+
Returns transitions in registration order (dict-ordered, matching
|
|
104
|
+
SemanticMap.transitions), not sorted -- callers building error messages
|
|
105
|
+
generally want map-declaration order, not alphabetical.
|
|
106
|
+
"""
|
|
107
|
+
transitions = list(sm.transitions.values())
|
|
108
|
+
reachable = forward_reachable(transitions, entry_states)
|
|
109
|
+
return [t for t in transitions if t.from_state not in reachable]
|
|
@@ -15,12 +15,14 @@ import traceback
|
|
|
15
15
|
from fpf_thinking_map.examples import (
|
|
16
16
|
build_deploy_decision_map,
|
|
17
17
|
build_deploy_rules,
|
|
18
|
+
build_destructive_action_map,
|
|
18
19
|
run_logic_scenario,
|
|
19
20
|
run_scenario_full_traversal,
|
|
20
21
|
run_scenario_missing_evidence,
|
|
21
22
|
run_scenario_role_conflict,
|
|
22
23
|
run_truth_table_demo,
|
|
23
24
|
)
|
|
25
|
+
from fpf_thinking_map.reachability import unreachable_transitions
|
|
24
26
|
from fpf_thinking_map.authorization import (
|
|
25
27
|
AuthorizationReceipt,
|
|
26
28
|
compute_state_fingerprint,
|
|
@@ -1680,6 +1682,62 @@ def check_move_intent():
|
|
|
1680
1682
|
)
|
|
1681
1683
|
|
|
1682
1684
|
|
|
1685
|
+
def check_reachability():
|
|
1686
|
+
"""Discrete reachability over a map's declared transition graph (val.pdf ch.10).
|
|
1687
|
+
|
|
1688
|
+
Worked example, both directions, using the library's own canonical
|
|
1689
|
+
destructive-move map (build_destructive_action_map): delete_records and
|
|
1690
|
+
archive_records both declare from_state="reviewed", which is nobody's
|
|
1691
|
+
to_state in this map — callers arrive at "reviewed" from outside (a
|
|
1692
|
+
dry-run happened upstream), the same shape as the case that motivated
|
|
1693
|
+
this module (see reachability.py's docstring for the full story: an
|
|
1694
|
+
unreachable-looking from_state turned out to be an intentional external
|
|
1695
|
+
entry point, and the "obvious" fix of merging it onto a shared from_state
|
|
1696
|
+
broke four behavior tests by changing what missing_evidence aggregates
|
|
1697
|
+
across).
|
|
1698
|
+
|
|
1699
|
+
entry_states is required, not inferred, precisely so that mistake isn't
|
|
1700
|
+
repeatable: declaring "reviewed" as an entry point is a one-line,
|
|
1701
|
+
reviewable statement of intent instead of something a reachability walk
|
|
1702
|
+
has to guess at.
|
|
1703
|
+
"""
|
|
1704
|
+
sm = build_destructive_action_map()
|
|
1705
|
+
|
|
1706
|
+
# declared correctly: "reviewed" is a named entry point, nothing is
|
|
1707
|
+
# unreachable
|
|
1708
|
+
dead = unreachable_transitions(sm, entry_states={"reviewed"})
|
|
1709
|
+
assert not dead, f"unexpected unreachable transitions: {[t.transition_id for t in dead]}"
|
|
1710
|
+
|
|
1711
|
+
# omit the entry point on purpose: the check's whole job is to make an
|
|
1712
|
+
# undeclared entry point loud, not to silently let it through. All three
|
|
1713
|
+
# transitions rooted at "reviewed" must come back.
|
|
1714
|
+
undeclared = unreachable_transitions(sm, entry_states=set())
|
|
1715
|
+
undeclared_ids = {t.transition_id for t in undeclared}
|
|
1716
|
+
assert undeclared_ids == {"delete_records", "archive_records", "log_status"}, undeclared_ids
|
|
1717
|
+
|
|
1718
|
+
# a genuinely orphaned transition — from_state nobody produces and
|
|
1719
|
+
# nobody declared as an entry point — must still be caught even when a
|
|
1720
|
+
# real entry point is declared correctly
|
|
1721
|
+
sm2 = build_destructive_action_map()
|
|
1722
|
+
sm2.register_transition(TransitionPrimitive(
|
|
1723
|
+
transition_id="orphan_move",
|
|
1724
|
+
label="Unreachable by construction — nothing produces 'nowhere'",
|
|
1725
|
+
context_id="data_ops",
|
|
1726
|
+
from_state="nowhere",
|
|
1727
|
+
to_state="deleted",
|
|
1728
|
+
))
|
|
1729
|
+
orphaned = unreachable_transitions(sm2, entry_states={"reviewed"})
|
|
1730
|
+
assert [t.transition_id for t in orphaned] == ["orphan_move"], (
|
|
1731
|
+
[t.transition_id for t in orphaned]
|
|
1732
|
+
)
|
|
1733
|
+
assert orphaned[0].requires_human_authorization is False, (
|
|
1734
|
+
"orphan_move isn't gated — reachability catches dead transitions "
|
|
1735
|
+
"regardless of whether they happen to be destructive; it is not a "
|
|
1736
|
+
"substitute for requires_human_authorization, it is a separate check "
|
|
1737
|
+
"on a separate property (can the graph get here at all)"
|
|
1738
|
+
)
|
|
1739
|
+
|
|
1740
|
+
|
|
1683
1741
|
def main():
|
|
1684
1742
|
print("FPF Thinking Map — Self-verification (horizontal)")
|
|
1685
1743
|
print("=" * 55)
|
|
@@ -1711,6 +1769,7 @@ def main():
|
|
|
1711
1769
|
("authorization receipt (scoped, non-replayable, TOCTOU-safe)", check_authorization_receipt),
|
|
1712
1770
|
("pending input / AWAIT (distinct from IDLE)", check_pending_input_await),
|
|
1713
1771
|
("move intent / inspect_move (concrete move identity)", check_move_intent),
|
|
1772
|
+
("reachability (discrete graph analysis, val.pdf ch.10)", check_reachability),
|
|
1714
1773
|
]
|
|
1715
1774
|
|
|
1716
1775
|
passed = sum(check(name, fn) for name, fn in checks)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fpf-thinking-map
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
4
4
|
Summary: Deterministic Python runtime for lawful agent traversal with explicit state, evidence, authorization, waiting, and move identity.
|
|
5
5
|
Author-email: "igareosh.com (@igareosh)" <igareosh@igareosh.com>
|
|
6
6
|
Maintainer-email: "igareosh.com (@igareosh)" <igareosh@igareosh.com>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.3}/fpf_thinking_map.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|