fpf-thinking-map 1.9.2__tar.gz → 1.9.4__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.4}/PKG-INFO +2 -2
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/README.md +1 -1
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/__init__.py +8 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/primitives.py +29 -0
- fpf_thinking_map-1.9.4/fpf_thinking_map/reachability.py +109 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/traversal.py +27 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/verify.py +152 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4/fpf_thinking_map.egg-info}/PKG-INFO +2 -2
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map.egg-info/SOURCES.txt +1 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/pyproject.toml +1 -1
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/LICENSE +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/NOTICE +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/authorization.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/examples.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/guards.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/logic.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/move_intent.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/pending_input.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/py.typed +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map/state.py +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map.egg-info/dependency_links.txt +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/fpf_thinking_map.egg-info/top_level.txt +0 -0
- {fpf_thinking_map-1.9.2 → fpf_thinking_map-1.9.4}/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.4
|
|
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>
|
|
@@ -49,7 +49,7 @@ context. The model can inspect the map and choose; the runtime decides whether
|
|
|
49
49
|
the move is valid.
|
|
50
50
|
|
|
51
51
|
[](https://pypi.org/project/fpf-thinking-map/)
|
|
52
|
-
[](https://img.shields.io/badge/downloads%20%28honest%29-4k-1f6feb?style=flat-square)](https://pypistats.org/packages/fpf-thinking-map)
|
|
53
53
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/pyproject.toml)
|
|
54
54
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/LICENSE)
|
|
55
55
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/pyproject.toml)
|
|
@@ -13,7 +13,7 @@ context. The model can inspect the map and choose; the runtime decides whether
|
|
|
13
13
|
the move is valid.
|
|
14
14
|
|
|
15
15
|
[](https://pypi.org/project/fpf-thinking-map/)
|
|
16
|
-
[](https://img.shields.io/badge/downloads%20%28honest%29-4k-1f6feb?style=flat-square)](https://pypistats.org/packages/fpf-thinking-map)
|
|
17
17
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/pyproject.toml)
|
|
18
18
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/LICENSE)
|
|
19
19
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/pyproject.toml)
|
|
@@ -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
|
+
)
|
|
@@ -438,6 +438,35 @@ class TransitionPrimitive:
|
|
|
438
438
|
does — the engine only makes the
|
|
439
439
|
option visible, it never picks one. Whether an alternative actually
|
|
440
440
|
satisfies the goal is a domain judgment outside this library's scope.
|
|
441
|
+
|
|
442
|
+
guard_expression: names a DecisionRule (fpf_thinking_map.logic) that
|
|
443
|
+
must currently recommend *this* transition_id for the fire to
|
|
444
|
+
proceed unopposed — the routing-policy counterpart to
|
|
445
|
+
required_gate_id/required_evidence. Checked in
|
|
446
|
+
ThinkingMapTraversal.attempt_transition(), not by the guard engine
|
|
447
|
+
despite the name (guards.py stays pure ActiveState predicates; a
|
|
448
|
+
LogicLayer reference only ThinkingMapTraversal can resolve doesn't
|
|
449
|
+
belong there).
|
|
450
|
+
|
|
451
|
+
Deliberately not an ESCALATE: a routing-policy mismatch is not
|
|
452
|
+
"needs a human," it's "the map disagrees with the proposed move and
|
|
453
|
+
knows what it would prefer instead" — the same shape as
|
|
454
|
+
COLLECT_EVIDENCE, not requires_human_authorization. Fires
|
|
455
|
+
REVISE_PLAN (declared in OutcomeKind since the traversal engine's
|
|
456
|
+
outcome space was first drawn up, never previously returned by any
|
|
457
|
+
code path) with the rule's actual current recommendation in
|
|
458
|
+
Outcome.alternatives, so an agentic caller can retry the recommended
|
|
459
|
+
transition_id in the same turn — solution-seeking stays inside the
|
|
460
|
+
map's own vocabulary, no external tool, no waiting on a person.
|
|
461
|
+
|
|
462
|
+
Silent no-op, matching required_gate_id's existing convention for a
|
|
463
|
+
dangling reference, when: no logic_layer is bound on the traversal,
|
|
464
|
+
the named rule doesn't exist in it, or the rule currently has no
|
|
465
|
+
active recommendation at all (condition false with no
|
|
466
|
+
action_if_false, or a HINT/WARN rule's vacuous-implication
|
|
467
|
+
suppression) — "the policy has no opinion right now" defaults to
|
|
468
|
+
allow, same as not setting guard_expression at all. Only an active
|
|
469
|
+
recommendation for a *different* transition_id blocks.
|
|
441
470
|
"""
|
|
442
471
|
transition_id: str
|
|
443
472
|
label: str
|
|
@@ -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]
|
|
@@ -451,6 +451,33 @@ class ThinkingMapTraversal:
|
|
|
451
451
|
missing_evidence=gate.missing_evidence(state.available_evidence_ids),
|
|
452
452
|
)
|
|
453
453
|
|
|
454
|
+
if t.guard_expression and self.logic_layer:
|
|
455
|
+
rule = next(
|
|
456
|
+
(r for r in self.logic_layer.rules if r.name == t.guard_expression),
|
|
457
|
+
None,
|
|
458
|
+
)
|
|
459
|
+
# dangling rule name: same silent no-op as an unresolved
|
|
460
|
+
# required_gate_id above — a missing reference doesn't block.
|
|
461
|
+
if rule:
|
|
462
|
+
_, recommended = rule.evaluate(state)
|
|
463
|
+
# no active recommendation right now ("" from a false
|
|
464
|
+
# condition with no action_if_false, or vacuous HINT/WARN
|
|
465
|
+
# suppression) means the policy has no opinion — allow,
|
|
466
|
+
# same as guard_expression being unset. Only a *different*
|
|
467
|
+
# active recommendation blocks.
|
|
468
|
+
if recommended and recommended != transition_id:
|
|
469
|
+
reason = (
|
|
470
|
+
f"Transition '{transition_id}' does not match routing policy "
|
|
471
|
+
f"'{rule.name}' — policy currently recommends '{recommended}'"
|
|
472
|
+
)
|
|
473
|
+
if rule.explanation:
|
|
474
|
+
reason += f": {rule.explanation}"
|
|
475
|
+
return Outcome(
|
|
476
|
+
kind=OutcomeKind.REVISE_PLAN,
|
|
477
|
+
reason=reason,
|
|
478
|
+
alternatives=[recommended],
|
|
479
|
+
)
|
|
480
|
+
|
|
454
481
|
allowed, guard_results = self.guard_engine.is_action_allowed(state, transition_id)
|
|
455
482
|
if not allowed:
|
|
456
483
|
denials = [r.reason for r in guard_results if r.verdict == GuardVerdict.DENY]
|
|
@@ -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,154 @@ 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
|
+
|
|
1741
|
+
def check_route_gated_transition():
|
|
1742
|
+
"""guard_expression -> DecisionRule: routing-policy legality, agentic
|
|
1743
|
+
|
|
1744
|
+
not human-facing. A transition naming a DecisionRule via
|
|
1745
|
+
guard_expression only fires when that rule's current recommendation
|
|
1746
|
+
matches the attempted transition_id — mismatches return REVISE_PLAN
|
|
1747
|
+
(never ESCALATE: no human is asked, nothing is added to
|
|
1748
|
+
pending_authorizations) with the rule's actual recommendation in
|
|
1749
|
+
alternatives, so an agentic caller retries in the same turn, inside
|
|
1750
|
+
the map's own vocabulary."""
|
|
1751
|
+
sm = SemanticMap()
|
|
1752
|
+
sm.register_context(ContextPrimitive("cis", "Compound Intelligence Stack"))
|
|
1753
|
+
sm.register_transition(TransitionPrimitive(
|
|
1754
|
+
"route_local", "route to local/open tier", "cis",
|
|
1755
|
+
"control_plane", "local_open", guard_expression="escalation_policy",
|
|
1756
|
+
))
|
|
1757
|
+
sm.register_transition(TransitionPrimitive(
|
|
1758
|
+
"route_frontier", "escalate to frontier oracle", "cis",
|
|
1759
|
+
"control_plane", "frontier", guard_expression="escalation_policy",
|
|
1760
|
+
))
|
|
1761
|
+
|
|
1762
|
+
logic = LogicLayer()
|
|
1763
|
+
logic.add_rule(DecisionRule(
|
|
1764
|
+
name="escalation_policy", condition=RiskAbove("high"),
|
|
1765
|
+
action_if_true="route_frontier", action_if_false="route_local",
|
|
1766
|
+
explanation="routine load stays local; frontier is the rare escalation",
|
|
1767
|
+
kind=RuleKind.ROUTE,
|
|
1768
|
+
))
|
|
1769
|
+
engine = ThinkingMapTraversal(sm, logic_layer=logic)
|
|
1770
|
+
|
|
1771
|
+
# normal risk: policy recommends route_local. Attempting the
|
|
1772
|
+
# mismatched transition does not fire it, and — the whole point —
|
|
1773
|
+
# does not touch pending_authorizations the way an ESCALATE would.
|
|
1774
|
+
s_normal = engine.build_active_state(
|
|
1775
|
+
RuntimeBinding(active_context_id="cis", risk_level="normal"),
|
|
1776
|
+
current_state="control_plane",
|
|
1777
|
+
)
|
|
1778
|
+
o_mismatch = engine.attempt_transition(s_normal, "route_frontier")
|
|
1779
|
+
assert o_mismatch.kind == OutcomeKind.REVISE_PLAN, o_mismatch.kind
|
|
1780
|
+
assert o_mismatch.kind != OutcomeKind.ESCALATE, (
|
|
1781
|
+
"a routing-policy mismatch must never read as 'needs a human'"
|
|
1782
|
+
)
|
|
1783
|
+
assert o_mismatch.alternatives == ["route_local"], o_mismatch.alternatives
|
|
1784
|
+
assert "escalation_policy" in o_mismatch.reason
|
|
1785
|
+
assert s_normal.current_state == "control_plane", "mismatch must not move state"
|
|
1786
|
+
assert s_normal.pending_authorizations == set(), (
|
|
1787
|
+
"REVISE_PLAN is not an authorization ask — nothing pending for a human"
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
# the matching transition at the same state fires normally
|
|
1791
|
+
o_match = engine.attempt_transition(s_normal, "route_local")
|
|
1792
|
+
assert o_match.kind == OutcomeKind.CONTINUE, o_match.kind
|
|
1793
|
+
assert s_normal.current_state == "local_open"
|
|
1794
|
+
|
|
1795
|
+
# high risk: policy now recommends route_frontier instead. Attempting
|
|
1796
|
+
# the now-wrong default gets REVISE_PLAN naming the correct one —
|
|
1797
|
+
# and the caller can retry it immediately, autonomously, same turn.
|
|
1798
|
+
s_high = engine.build_active_state(
|
|
1799
|
+
RuntimeBinding(active_context_id="cis", risk_level="high"),
|
|
1800
|
+
current_state="control_plane",
|
|
1801
|
+
)
|
|
1802
|
+
o_wrong_tier = engine.attempt_transition(s_high, "route_local")
|
|
1803
|
+
assert o_wrong_tier.kind == OutcomeKind.REVISE_PLAN, o_wrong_tier.kind
|
|
1804
|
+
assert o_wrong_tier.alternatives == ["route_frontier"], o_wrong_tier.alternatives
|
|
1805
|
+
assert s_high.current_state == "control_plane"
|
|
1806
|
+
|
|
1807
|
+
o_retry = engine.attempt_transition(s_high, o_wrong_tier.alternatives[0])
|
|
1808
|
+
assert o_retry.kind == OutcomeKind.CONTINUE, (
|
|
1809
|
+
"the model's own retry, using exactly what REVISE_PLAN handed back, "
|
|
1810
|
+
f"must succeed — got {o_retry.kind}"
|
|
1811
|
+
)
|
|
1812
|
+
assert s_high.current_state == "frontier"
|
|
1813
|
+
|
|
1814
|
+
# dangling reference / no logic_layer bound: same silent no-op as an
|
|
1815
|
+
# unresolved required_gate_id — backward compatible with maps that
|
|
1816
|
+
# never opted into routing policy at all
|
|
1817
|
+
sm2 = SemanticMap()
|
|
1818
|
+
sm2.register_context(ContextPrimitive("cis2", "Test"))
|
|
1819
|
+
sm2.register_transition(TransitionPrimitive(
|
|
1820
|
+
"t_a", "A", "cis2", "start", "mid", guard_expression="no_such_rule",
|
|
1821
|
+
))
|
|
1822
|
+
engine_no_logic = ThinkingMapTraversal(sm2) # no logic_layer at all
|
|
1823
|
+
s2 = engine_no_logic.build_active_state(
|
|
1824
|
+
RuntimeBinding(active_context_id="cis2"), current_state="start",
|
|
1825
|
+
)
|
|
1826
|
+
o2 = engine_no_logic.attempt_transition(s2, "t_a")
|
|
1827
|
+
assert o2.kind == OutcomeKind.CONTINUE, (
|
|
1828
|
+
f"a dangling guard_expression with no bound logic_layer must not "
|
|
1829
|
+
f"block — got {o2.kind}"
|
|
1830
|
+
)
|
|
1831
|
+
|
|
1832
|
+
|
|
1683
1833
|
def main():
|
|
1684
1834
|
print("FPF Thinking Map — Self-verification (horizontal)")
|
|
1685
1835
|
print("=" * 55)
|
|
@@ -1711,6 +1861,8 @@ def main():
|
|
|
1711
1861
|
("authorization receipt (scoped, non-replayable, TOCTOU-safe)", check_authorization_receipt),
|
|
1712
1862
|
("pending input / AWAIT (distinct from IDLE)", check_pending_input_await),
|
|
1713
1863
|
("move intent / inspect_move (concrete move identity)", check_move_intent),
|
|
1864
|
+
("reachability (discrete graph analysis, val.pdf ch.10)", check_reachability),
|
|
1865
|
+
("route-gated transition (guard_expression -> DecisionRule, REVISE_PLAN not ESCALATE)", check_route_gated_transition),
|
|
1714
1866
|
]
|
|
1715
1867
|
|
|
1716
1868
|
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.4
|
|
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>
|
|
@@ -49,7 +49,7 @@ context. The model can inspect the map and choose; the runtime decides whether
|
|
|
49
49
|
the move is valid.
|
|
50
50
|
|
|
51
51
|
[](https://pypi.org/project/fpf-thinking-map/)
|
|
52
|
-
[](https://img.shields.io/badge/downloads%20%28honest%29-4k-1f6feb?style=flat-square)](https://pypistats.org/packages/fpf-thinking-map)
|
|
53
53
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/pyproject.toml)
|
|
54
54
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/LICENSE)
|
|
55
55
|
[](https://github.com/igareosh/fpf-agentic-thinking-map/blob/main/pyproject.toml)
|
|
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.4}/fpf_thinking_map.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|