topkit 0.2.0a3__tar.gz → 0.2.0a4__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.
Files changed (30) hide show
  1. {topkit-0.2.0a3/topkit.egg-info → topkit-0.2.0a4}/PKG-INFO +33 -21
  2. topkit-0.2.0a4/README.md +127 -0
  3. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/access.py +166 -23
  4. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/contracts.py +68 -7
  5. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/declarations.py +108 -10
  6. topkit-0.2.0a4/TopKit/fields.py +386 -0
  7. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/geometry.py +15 -9
  8. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/lifecycle.py +66 -29
  9. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/overlay.py +277 -22
  10. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/queries.py +4 -4
  11. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/state.py +53 -10
  12. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/tags.py +48 -2
  13. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/transactions.py +88 -17
  14. {topkit-0.2.0a3 → topkit-0.2.0a4}/pyproject.toml +2 -4
  15. topkit-0.2.0a4/tests/test_differential_fuzz.py +52 -0
  16. topkit-0.2.0a4/tests/test_documents.py +71 -0
  17. topkit-0.2.0a4/tests/test_oracle.py +34 -0
  18. topkit-0.2.0a4/tests/test_performance.py +263 -0
  19. {topkit-0.2.0a3 → topkit-0.2.0a4}/tests/test_topkit.py +2179 -5
  20. {topkit-0.2.0a3 → topkit-0.2.0a4/topkit.egg-info}/PKG-INFO +33 -21
  21. {topkit-0.2.0a3 → topkit-0.2.0a4}/topkit.egg-info/SOURCES.txt +4 -0
  22. topkit-0.2.0a3/README.md +0 -113
  23. topkit-0.2.0a3/TopKit/fields.py +0 -165
  24. {topkit-0.2.0a3 → topkit-0.2.0a4}/LICENSE +0 -0
  25. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/__init__.py +0 -0
  26. {topkit-0.2.0a3 → topkit-0.2.0a4}/TopKit/errors.py +0 -0
  27. {topkit-0.2.0a3 → topkit-0.2.0a4}/setup.cfg +0 -0
  28. {topkit-0.2.0a3 → topkit-0.2.0a4}/tests/test_examples.py +0 -0
  29. {topkit-0.2.0a3 → topkit-0.2.0a4}/topkit.egg-info/dependency_links.txt +0 -0
  30. {topkit-0.2.0a3 → topkit-0.2.0a4}/topkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topkit
3
- Version: 0.2.0a3
3
+ Version: 0.2.0a4
4
4
  Summary: TopKit: the Python reference implementation of Tag-Oriented Programming.
5
5
  Author: Julio Toboso
6
6
  License-Expression: Apache-2.0
@@ -11,12 +11,10 @@ Keywords: tag-oriented-programming,top,topkit,tags
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
14
  Classifier: Programming Language :: Python :: 3.12
17
15
  Classifier: Programming Language :: Python :: 3.13
18
16
  Classifier: Programming Language :: Python :: 3.14
19
- Requires-Python: >=3.10
17
+ Requires-Python: >=3.12
20
18
  Description-Content-Type: text/markdown
21
19
  License-File: LICENSE
22
20
  Dynamic: license-file
@@ -76,38 +74,52 @@ assert ari.Attack() == "Ari casts Magic Missile while holding a shield"
76
74
  assert ari.Wizard.Attack() == "Ari casts Magic Missile" # the view after Wizard
77
75
  ```
78
76
 
79
- Read [`TopKit/GUIDE.md`](TopKit/GUIDE.md) for the patterns, and run
77
+ Read [`TopKit/GUIDE.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TopKit/GUIDE.md) for the patterns, and run
80
78
  `examples/dnd_character.py` and `examples/biome.py` for the long form;
81
79
  `examples/fleet_patching.py` and `examples/crew_access.py` for the design
82
80
  patterns Pins and published members give you.
83
81
 
82
+ ## Installing
83
+
84
+ ```
85
+ pip install topkit
86
+ ```
87
+
88
+ TopKit is in alpha. While no final release exists, a plain
89
+ `pip install topkit` installs the newest alpha. Once a final release is
90
+ out, it installs that, and `pip install --pre topkit` asks for the newest
91
+ alpha.
92
+
84
93
  ## This repository
85
94
 
86
95
  | Path | What | License |
87
96
  | --- | --- | --- |
88
- | [`spec/SPECIFICATION.md`](spec/SPECIFICATION.md) | **The Specification**, written in rings from the kernel outward. The source of truth. | CC-BY-4.0 |
89
- | [`TopKit/GUIDE.md`](TopKit/GUIDE.md) | **The Guide**: TOP for people, pattern by pattern. Start here. | Apache-2.0 |
90
- | [`TopKit/`](TopKit/) | **TopKit**, the Python reference implementation (the T is the Tag in T.O.P.). | Apache-2.0 |
91
- | [`TopKit/CONTRACTS.md`](TopKit/CONTRACTS.md) | **The Contracts Guide**: gates, promises, membership and error control, aboard a starship. | Apache-2.0 |
92
- | [`tests/`](tests/) | The conformance suite, organized by ring. | Apache-2.0 |
93
- | [`examples/`](examples/) | A D&D character sheet, a mix-and-match biome, a drone fleet patched through Pins, a starship crew under published members. Each is a set of design patterns. | Apache-2.0 |
94
- | [`benchmarks/`](benchmarks/) | The runtime budget: reads, calls, tagging, memory. | Apache-2.0 |
95
- | [`steps/`](steps/) | **STEP**s, Standard TOP Enhancement Proposals. | CC-BY-4.0 |
96
- | [`RELEASING.md`](RELEASING.md) | How TopKit reaches PyPI, and what to check first. | Apache-2.0 |
97
+ | [`spec/SPECIFICATION.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/spec/SPECIFICATION.md) | **The Specification**, written in rings from the kernel outward. The source of truth. | CC-BY-4.0 |
98
+ | [`TopKit/GUIDE.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TopKit/GUIDE.md) | **The Guide**: TOP for people, pattern by pattern. Start here. | Apache-2.0 |
99
+ | [`TopKit/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/TopKit) | **TopKit**, the Python reference implementation (the T is the Tag in T.O.P.). | Apache-2.0 |
100
+ | [`TopKit/CONTRACTS.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TopKit/CONTRACTS.md) | **The Contracts Guide**: gates, promises, membership and error control, aboard a starship. | Apache-2.0 |
101
+ | [`TopKit/FIELDS.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TopKit/FIELDS.md) | **The Fields Guide**: populations, partitions and the algebra between them. | Apache-2.0 |
102
+ | [`tests/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/tests) | The conformance suite, the examples and every document block, the oracle (an independent model of the paradigm checked against the kit on a random walk), and the differential fuzzer (the same random programs on two versions of the kit, transcripts compared). | Apache-2.0 |
103
+ | [`examples/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/examples) | A D&D character sheet, a mix-and-match biome, a drone fleet patched through Pins, a starship crew under published members. Each is a set of design patterns. | Apache-2.0 |
104
+ | [`benchmarks/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/benchmarks) | The runtime budget: reads, calls, tagging, memory. | Apache-2.0 |
105
+ | [`steps/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/steps) | **STEP**s, Standard TOP Enhancement Proposals. | CC-BY-4.0 |
106
+ | [`RELEASING.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/RELEASING.md) | How TopKit reaches PyPI, and what to check first. | Apache-2.0 |
97
107
 
98
108
  The **Specification is the source of truth.** TopKit demonstrates it and
99
109
  must perform as the Specification describes; any gap in TopKit is TopKit's
100
110
  to fix, not a change to TOP.
101
111
 
102
- ## Using TopKit
112
+ ## Using TopKit from a checkout
113
+
114
+ From a checkout of this repository:
103
115
 
104
116
  ```
105
117
  pip install . # or: PYTHONPATH=. python3 ...
106
- PYTHONPATH=. python3 -m unittest tests.test_topkit tests.test_examples
118
+ PYTHONPATH=. python3 -m unittest discover -s tests -t .
107
119
  PYTHONPATH=. python3 benchmarks/bench.py
108
120
  ```
109
121
 
110
- Python 3.10 or later, no dependencies. TopKit is built so that an Agent's
122
+ Python 3.12 or later, no dependencies. TopKit is built so that an Agent's
111
123
  attribute reads and Action calls cost what they cost on a plain object;
112
124
  tagging is the slower, rarer act.
113
125
 
@@ -115,22 +127,22 @@ tagging is the slower, rarer act.
115
127
 
116
128
  An implementation in any language is welcome. "TOP-conformant" means it
117
129
  preserves the observable semantics in the Specification, ring by ring, and
118
- passes the conformance suite. See [`CONFORMANCE.md`](CONFORMANCE.md). The
130
+ passes the conformance suite. See [`CONFORMANCE.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/CONFORMANCE.md). The
119
131
  **TOP Verified** mark is granted by the steward, so the standard stays
120
132
  meaningful.
121
133
 
122
134
  ## Governance & contributing
123
135
 
124
136
  TOP is led by a **Director**, with a path to shared governance; see
125
- [`GOVERNANCE.md`](GOVERNANCE.md). Propose changes through the **STEP**
126
- process ([`CONTRIBUTING.md`](CONTRIBUTING.md)); every decision is recorded,
137
+ [`GOVERNANCE.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/GOVERNANCE.md). Propose changes through the **STEP**
138
+ process ([`CONTRIBUTING.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/CONTRIBUTING.md)); every decision is recorded,
127
139
  in the open, with its reason.
128
140
 
129
141
  ## License & trademark
130
142
 
131
143
  Code is **Apache-2.0**; the Specification and STEPs are **CC-BY-4.0**.
132
144
  "Tag-Oriented Programming", "TOP", and "TopKit" are trademarks; see
133
- [`TRADEMARK.md`](TRADEMARK.md). You may implement the paradigm freely; the
145
+ [`TRADEMARK.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TRADEMARK.md). You may implement the paradigm freely; the
134
146
  marks identify the official spec and conformant implementations.
135
147
 
136
148
  © 2026 Julio Toboso.
@@ -0,0 +1,127 @@
1
+ # Tag-Oriented Programming (TOP)™
2
+
3
+ > Compose semantic layers on one stable object identity.
4
+
5
+ **TOP** is a programming paradigm: a Target keeps its identity while Tags
6
+ add meaning, species, roles, backgrounds, capabilities, that cut across
7
+ ordinary class hierarchies. Where traditional code asks *"what is this
8
+ object?"*, TOP asks *"what is this object for, here?"*, and lets the answer
9
+ grow. The inspiration is tabletop character creation: species, class,
10
+ background and feats are independent choices, and the character sheet is
11
+ what they compose.
12
+
13
+ ```python
14
+ from TopKit import Tag, Record, Action, Underlay, Pre
15
+
16
+ class Character:
17
+ def __init__(self, name, level):
18
+ self.name, self.level = name, level
19
+
20
+ class Elf(Tag):
21
+ @Record
22
+ def spells(agent, stored): # piles up with other Tags
23
+ return (stored or []) + ["Light"]
24
+
25
+ class Wizard(Tag):
26
+ @Pre
27
+ def Can_Study(agent):
28
+ return agent.level >= 1
29
+
30
+ @Record
31
+ def spells(agent, stored):
32
+ return (stored or []) + ["Magic Missile"]
33
+
34
+ def Attack(agent):
35
+ return f"{agent.name} casts {agent.spells[-1]}"
36
+
37
+ class War_Caster(Tag):
38
+ @Pre
39
+ def Is_A_Caster(agent): # synergy: needs Wizard
40
+ return agent in Wizard
41
+
42
+ @Action
43
+ @Underlay
44
+ def Attack(agent, underlay):
45
+ return underlay() + " while holding a shield"
46
+
47
+ ari = Character("Ari", level=3)
48
+ Elf(ari); Wizard(ari); War_Caster(ari)
49
+
50
+ assert ari in Wizard # active membership
51
+ assert ari.spells == ["Light", "Magic Missile"]
52
+ assert ari.Attack() == "Ari casts Magic Missile while holding a shield"
53
+ assert ari.Wizard.Attack() == "Ari casts Magic Missile" # the view after Wizard
54
+ ```
55
+
56
+ Read [`TopKit/GUIDE.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TopKit/GUIDE.md) for the patterns, and run
57
+ `examples/dnd_character.py` and `examples/biome.py` for the long form;
58
+ `examples/fleet_patching.py` and `examples/crew_access.py` for the design
59
+ patterns Pins and published members give you.
60
+
61
+ ## Installing
62
+
63
+ ```
64
+ pip install topkit
65
+ ```
66
+
67
+ TopKit is in alpha. While no final release exists, a plain
68
+ `pip install topkit` installs the newest alpha. Once a final release is
69
+ out, it installs that, and `pip install --pre topkit` asks for the newest
70
+ alpha.
71
+
72
+ ## This repository
73
+
74
+ | Path | What | License |
75
+ | --- | --- | --- |
76
+ | [`spec/SPECIFICATION.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/spec/SPECIFICATION.md) | **The Specification**, written in rings from the kernel outward. The source of truth. | CC-BY-4.0 |
77
+ | [`TopKit/GUIDE.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TopKit/GUIDE.md) | **The Guide**: TOP for people, pattern by pattern. Start here. | Apache-2.0 |
78
+ | [`TopKit/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/TopKit) | **TopKit**, the Python reference implementation (the T is the Tag in T.O.P.). | Apache-2.0 |
79
+ | [`TopKit/CONTRACTS.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TopKit/CONTRACTS.md) | **The Contracts Guide**: gates, promises, membership and error control, aboard a starship. | Apache-2.0 |
80
+ | [`TopKit/FIELDS.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TopKit/FIELDS.md) | **The Fields Guide**: populations, partitions and the algebra between them. | Apache-2.0 |
81
+ | [`tests/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/tests) | The conformance suite, the examples and every document block, the oracle (an independent model of the paradigm checked against the kit on a random walk), and the differential fuzzer (the same random programs on two versions of the kit, transcripts compared). | Apache-2.0 |
82
+ | [`examples/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/examples) | A D&D character sheet, a mix-and-match biome, a drone fleet patched through Pins, a starship crew under published members. Each is a set of design patterns. | Apache-2.0 |
83
+ | [`benchmarks/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/benchmarks) | The runtime budget: reads, calls, tagging, memory. | Apache-2.0 |
84
+ | [`steps/`](https://github.com/JulTob/Tag_Oriented_Programming/tree/main/steps) | **STEP**s, Standard TOP Enhancement Proposals. | CC-BY-4.0 |
85
+ | [`RELEASING.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/RELEASING.md) | How TopKit reaches PyPI, and what to check first. | Apache-2.0 |
86
+
87
+ The **Specification is the source of truth.** TopKit demonstrates it and
88
+ must perform as the Specification describes; any gap in TopKit is TopKit's
89
+ to fix, not a change to TOP.
90
+
91
+ ## Using TopKit from a checkout
92
+
93
+ From a checkout of this repository:
94
+
95
+ ```
96
+ pip install . # or: PYTHONPATH=. python3 ...
97
+ PYTHONPATH=. python3 -m unittest discover -s tests -t .
98
+ PYTHONPATH=. python3 benchmarks/bench.py
99
+ ```
100
+
101
+ Python 3.12 or later, no dependencies. TopKit is built so that an Agent's
102
+ attribute reads and Action calls cost what they cost on a plain object;
103
+ tagging is the slower, rarer act.
104
+
105
+ ## Conformance
106
+
107
+ An implementation in any language is welcome. "TOP-conformant" means it
108
+ preserves the observable semantics in the Specification, ring by ring, and
109
+ passes the conformance suite. See [`CONFORMANCE.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/CONFORMANCE.md). The
110
+ **TOP Verified** mark is granted by the steward, so the standard stays
111
+ meaningful.
112
+
113
+ ## Governance & contributing
114
+
115
+ TOP is led by a **Director**, with a path to shared governance; see
116
+ [`GOVERNANCE.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/GOVERNANCE.md). Propose changes through the **STEP**
117
+ process ([`CONTRIBUTING.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/CONTRIBUTING.md)); every decision is recorded,
118
+ in the open, with its reason.
119
+
120
+ ## License & trademark
121
+
122
+ Code is **Apache-2.0**; the Specification and STEPs are **CC-BY-4.0**.
123
+ "Tag-Oriented Programming", "TOP", and "TopKit" are trademarks; see
124
+ [`TRADEMARK.md`](https://github.com/JulTob/Tag_Oriented_Programming/blob/main/TRADEMARK.md). You may implement the paradigm freely; the
125
+ marks identify the official spec and conformant implementations.
126
+
127
+ © 2026 Julio Toboso.
@@ -11,10 +11,19 @@ from __future__ import annotations
11
11
 
12
12
  from functools import partial
13
13
  from typing import Any
14
+ from typing import Callable
15
+ import sys
14
16
 
17
+ from . import declarations
18
+ from .contracts import _condition_member
19
+ from .contracts import _holds
15
20
  from .errors import TagCompositionError
16
21
  from .errors import TagResolutionError
22
+ from .declarations import STATE
17
23
  from .declarations import _MISSING
24
+ from .declarations import _is_flag
25
+ from .declarations import _words_of
26
+ from .lifecycle import _teardown_all
18
27
  from .state import _Bound
19
28
  from .state import _Pinned_Operation
20
29
  from .state import _Snapshot
@@ -46,6 +55,43 @@ def _host_member(
46
55
  return None
47
56
 
48
57
 
58
+ IN_SEAT = (
59
+ "__contains__",
60
+ "__iter__",
61
+ )
62
+
63
+
64
+ def _host_in_seat(
65
+ host_type: type,
66
+ ) -> tuple[str, type] | None:
67
+ """How the host answers `in`: the special method and the class that
68
+ defines it, or None when the seat is free (STEP-SPEC-7).
69
+
70
+ Resolved as Python resolves `in`: ``__contains__``, then ``__iter__``.
71
+ For each, the first class in the MRO that defines the name decides. A
72
+ method owns the seat; the value None says `in` is unavailable, Python
73
+ tries nothing further, and the seat is free. ``__getitem__`` alone is
74
+ not a seat a Flag respects: on a keyed host its `in` fails or never
75
+ ends; on an index-style host it worked, and stops meaning membership
76
+ once a Flag lands (the Director's call, STEP-SPEC-7)."""
77
+
78
+ for name in IN_SEAT:
79
+ for klass in host_type.__mro__:
80
+ if klass is object:
81
+ break
82
+
83
+ if name in klass.__dict__:
84
+ if klass.__dict__[name] is None:
85
+ return None
86
+
87
+ return (
88
+ name,
89
+ klass,
90
+ )
91
+
92
+ return None
93
+
94
+
49
95
  def _hooks_for(
50
96
  host_type: type,
51
97
  has_posts: bool,
@@ -56,7 +102,6 @@ def _hooks_for(
56
102
  "__del__": _agent_del,
57
103
  "_TOPKIT_HOST_TYPE": host_type,
58
104
  "_TOPKIT_HOST_GETATTR": _host_member(host_type, "__getattr__"),
59
- "_TOPKIT_HOST_DEL": _host_member(host_type, "__del__"),
60
105
  }
61
106
 
62
107
  if has_posts and _host_member(host_type, "__bool__") is None:
@@ -65,7 +110,7 @@ def _hooks_for(
65
110
  if _host_member(host_type, "__format__") is None:
66
111
  hooks["__format__"] = _agent_format
67
112
 
68
- if has_flags and _host_member(host_type, "__contains__") is None:
113
+ if has_flags and _host_in_seat(host_type) is None:
69
114
  hooks["__contains__"] = _agent_contains
70
115
 
71
116
  if _host_member(host_type, "__copy__") is None:
@@ -81,7 +126,8 @@ def _agent_getattr(
81
126
  agent: object,
82
127
  name: str,
83
128
  ) -> Any:
84
- """Miss path only: Tag views by name, then the host's own __getattr__."""
129
+ """Miss path only: Tag views by name, then a condition by name as a
130
+ plain bool (STEP-SPEC-14), then the host's own __getattr__."""
85
131
 
86
132
  state = _state_of(agent)
87
133
 
@@ -101,6 +147,16 @@ def _agent_getattr(
101
147
  state,
102
148
  )
103
149
 
150
+ if state.postconditions or state.preconditions:
151
+ verdict = _condition_member(
152
+ agent,
153
+ state,
154
+ name,
155
+ )
156
+
157
+ if verdict is not None:
158
+ return verdict
159
+
104
160
  host_getattr = type(agent).__dict__.get("_TOPKIT_HOST_GETATTR")
105
161
 
106
162
  if host_getattr is not None:
@@ -185,7 +241,7 @@ def _agent_contains(
185
241
  probe: object,
186
242
  ) -> bool:
187
243
  """``"Undead" in ghoul`` and ``Undead in ghoul``: an active Flag, by
188
- name or by class."""
244
+ name, alias or class."""
189
245
 
190
246
  return _keyword(
191
247
  agent,
@@ -197,18 +253,33 @@ def _keyword(
197
253
  agent: object,
198
254
  probe: object,
199
255
  ) -> bool:
200
- from .declarations import _is_flag
201
-
202
256
  state = _state_of(agent)
203
257
 
204
258
  if state is None:
205
259
  return False
206
260
 
207
261
  if isinstance(probe, str):
208
- return any(
209
- _is_flag(tag) and tag.__name__ == probe
210
- for tag in state.active
211
- )
262
+ if type(probe) is not str:
263
+ probe = str.__str__(probe) # its text: exact, and hashable
264
+
265
+ words = state.words
266
+
267
+ if words is None or words.flags_declared != declarations._flags_declared:
268
+ # Read the count before gathering: a @Flag declared meanwhile then
269
+ # marks these words stale, instead of letting them pass for new.
270
+ words = state.words = _words_of(
271
+ state.active,
272
+ declarations._flags_declared,
273
+ )
274
+
275
+ if probe in words.aliases:
276
+ return True
277
+
278
+ for tag in words.flags:
279
+ if tag.__name__ == probe:
280
+ return True
281
+
282
+ return False
212
283
 
213
284
  return (
214
285
  probe in state.active
@@ -219,28 +290,100 @@ def _keyword(
219
290
  def _agent_bool(
220
291
  agent: object,
221
292
  ) -> bool:
222
- from .contracts import _holds
223
-
224
293
  return _holds(agent)
225
294
 
226
295
 
227
- def _agent_del(
296
+ def _host_finalizer(
228
297
  agent: object,
298
+ kind: type = type,
299
+ missing: type = AttributeError,
229
300
  ) -> None:
230
- # Best effort: run remaining teardowns, then the host's finalizer.
231
- # Python does not promise finalizers at shutdown or inside cycles;
232
- # Scope() is the guaranteed path.
301
+ """The host's own ``__del__``, found as Python finds it: through the
302
+ object's type, past the kit's runtime types, the first class that
303
+ defines one decides, ``None`` there means there is none, and a
304
+ descriptor is bound first.
305
+
306
+ It also runs late in interpreter exit, when this module's globals and
307
+ even the builtins may be gone, so it needs neither: ``kind`` and
308
+ ``missing`` are bound here."""
309
+
310
+ for klass in kind(agent).__mro__:
311
+ if "_TOPKIT_HOST_TYPE" in klass.__dict__:
312
+ continue # a runtime type: its __del__ is the kit's finalizer
313
+
314
+ if "__del__" in klass.__dict__:
315
+ finalizer = klass.__dict__["__del__"]
316
+ break
317
+ else:
318
+ return
319
+
320
+ if finalizer is None:
321
+ return
322
+
233
323
  try:
234
- from .lifecycle import _teardown_all
324
+ bind = kind(finalizer).__get__
325
+ except missing:
326
+ finalizer() # not a descriptor: Python calls it as it is
327
+ return
235
328
 
236
- _teardown_all(agent)
329
+ bind(
330
+ finalizer,
331
+ agent,
332
+ kind(agent),
333
+ )()
237
334
 
238
- host_del = type(agent).__dict__.get("_TOPKIT_HOST_DEL")
239
335
 
240
- if host_del is not None:
241
- host_del(agent)
242
- except Exception:
243
- pass
336
+ def _agent_del(
337
+ agent: object,
338
+ finalizing: Callable[[], bool] = sys.is_finalizing,
339
+ state_key: str = STATE,
340
+ host_finalizer: Callable[[object], None] = _host_finalizer,
341
+ read: Callable[[object, str], Any] = object.__getattribute__,
342
+ ) -> None:
343
+ """Deletion (§3.2, STEP-SPEC-18). The teardowns still due run, best
344
+ effort; then the Agent's ``__del__`` runs as its Overlay shows it: the
345
+ top Layer, which reaches the host's own through ``@Underlay``, or the
346
+ host's own when no Tag declares one. At interpreter exit only the
347
+ ``__del__`` Layers run: teardowns there are At_Exit's, and opt-in. A
348
+ ``__del__`` Layer's own error is reported as Python reports any
349
+ finalizer's.
350
+
351
+ At exit this module's globals, and even the builtins, may already be
352
+ gone. So the exit path uses neither: what it needs is bound here as a
353
+ default, or found on the Agent. The Agent is read as Python reads it,
354
+ never through the host's own ``__getattribute__``."""
355
+
356
+ state = read(agent, "__dict__").get(state_key)
357
+
358
+ if state is None:
359
+ host_finalizer(agent) # built from an Agent's runtime type, never tagged: a plain host
360
+ return
361
+
362
+ interrupted = None
363
+
364
+ if not finalizing():
365
+ try:
366
+ _teardown_all(agent)
367
+ except Exception:
368
+ pass # best effort (§3.2)
369
+ except BaseException as error:
370
+ interrupted = error # Ctrl-C in a teardown: the __del__ Layers still run
371
+
372
+ try:
373
+ layer = state.actions.get("__del__")
374
+
375
+ if layer is not None:
376
+ state.composing += 1 # a Tag's Layer runs inside the composition door (§1.5)
377
+
378
+ try:
379
+ layer(agent)
380
+ finally:
381
+ state.composing -= 1
382
+ elif "__del__" not in state.deleted:
383
+ host_finalizer(agent)
384
+ finally:
385
+ if interrupted is not None:
386
+ raise interrupted # reported after the Layers, whatever they raised
244
387
 
245
388
 
246
389
  def _agent_copy(
@@ -70,6 +70,18 @@ def _bind_condition(
70
70
 
71
71
  skip = 2 if uses_underlay else 1
72
72
 
73
+ if not uses_underlay and not with_inputs:
74
+ def Plain_Check(
75
+ agent: object,
76
+ inputs: dict[str, Any],
77
+ ) -> Any:
78
+ return function(agent) # holds no prior binding: re-applying cannot pile up
79
+
80
+ return Plain_Check
81
+
82
+ if not uses_underlay:
83
+ prior = None # never called: do not keep the earlier binding alive
84
+
73
85
  def Check(
74
86
  agent: object,
75
87
  inputs: dict[str, Any],
@@ -182,13 +194,62 @@ def _holds(
182
194
  ) -> bool:
183
195
  """True exactly when every visible Postcondition holds."""
184
196
 
185
- return _guarded(
186
- agent,
187
- "postconditions",
188
- False,
189
- TagPostconditionError,
190
- "Postcondition",
191
- )
197
+ state = _state_of(agent)
198
+
199
+ if state is None or state.checking or not state.postconditions:
200
+ return True # nothing promised, or asked from inside a check
201
+
202
+ state.checking = True
203
+ state.composing += 1
204
+
205
+ try:
206
+ for name, check in state.postconditions.items():
207
+ try:
208
+ if not _verdict(check(agent, {}), name):
209
+ return False
210
+ except Exception:
211
+ return False
212
+
213
+ return True
214
+ finally:
215
+ state.composing -= 1
216
+ state.checking = False
217
+
218
+
219
+ def _condition_member(
220
+ agent: object,
221
+ state: _State,
222
+ name: str,
223
+ ) -> bool | None:
224
+ """The condition called ``name`` on the Agent, read as a plain bool
225
+ (STEP-SPEC-14): a Postcondition first, then a Precondition. None when
226
+ the Agent has no condition of that name."""
227
+
228
+ check = state.postconditions.get(name)
229
+
230
+ if check is None:
231
+ check = state.preconditions.get(name)
232
+
233
+ if check is None:
234
+ return None
235
+
236
+ reentrant = state.checking
237
+ state.checking = True
238
+ state.composing += 1
239
+
240
+ try:
241
+ try:
242
+ return _verdict(
243
+ check(agent, {}),
244
+ name,
245
+ )
246
+ except TagContractError:
247
+ raise
248
+ except Exception:
249
+ return False
250
+ finally:
251
+ state.composing -= 1
252
+ state.checking = reentrant
192
253
 
193
254
 
194
255
  def _status_of(