gini-toolkit 6.0.1.dev0__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.
Files changed (278) hide show
  1. gini/__init__.py +12 -0
  2. gini/__main__.py +107 -0
  3. gini/_version.py +24 -0
  4. gini/agent/__init__.py +17 -0
  5. gini/agent/agent_gamemaster.py +140 -0
  6. gini/agent/api.py +291 -0
  7. gini/agent/ask.py +123 -0
  8. gini/agent/authoring.py +72 -0
  9. gini/agent/blackboard.py +114 -0
  10. gini/agent/contracts.py +142 -0
  11. gini/agent/domains.py +91 -0
  12. gini/agent/embed.py +123 -0
  13. gini/agent/gamemaster.py +256 -0
  14. gini/agent/kb.py +148 -0
  15. gini/agent/lesson_resolver.py +261 -0
  16. gini/agent/llm/__init__.py +5 -0
  17. gini/agent/llm/backend.py +43 -0
  18. gini/agent/llm/fake.py +25 -0
  19. gini/agent/llm/ollama.py +206 -0
  20. gini/agent/loop.py +258 -0
  21. gini/agent/mcp_server.py +86 -0
  22. gini/agent/meaning.py +225 -0
  23. gini/agent/mission.py +210 -0
  24. gini/agent/mission_controller.py +208 -0
  25. gini/agent/narration.py +116 -0
  26. gini/agent/notifier.py +86 -0
  27. gini/agent/personas.py +79 -0
  28. gini/agent/reasoning.py +172 -0
  29. gini/agent/recall.py +248 -0
  30. gini/agent/session.py +79 -0
  31. gini/agent/teaching_center.py +482 -0
  32. gini/agent/tools/__init__.py +3 -0
  33. gini/agent/tools/registry.py +193 -0
  34. gini/agent/twin/__init__.py +28 -0
  35. gini/agent/twin/authoring.py +71 -0
  36. gini/agent/twin/contracts.py +54 -0
  37. gini/agent/twin/dialectic.py +189 -0
  38. gini/agent/twin/harness.py +93 -0
  39. gini/agent/twin/justify.py +156 -0
  40. gini/agent/twin/learner.py +64 -0
  41. gini/agent/twin/mission.py +60 -0
  42. gini/agent/twin/os_coach.py +79 -0
  43. gini/agent/twin/salience.py +30 -0
  44. gini/agent/understand.py +250 -0
  45. gini/agent/verifiers.py +106 -0
  46. gini/agent/wizard.py +178 -0
  47. gini/agent/xv6_pack.py +74 -0
  48. gini/app/__init__.py +3 -0
  49. gini/app/context.py +368 -0
  50. gini/app/paths.py +121 -0
  51. gini/data/README.md +21 -0
  52. gini/domain/__init__.py +9 -0
  53. gini/domain/assembly.py +209 -0
  54. gini/domain/authoring.py +353 -0
  55. gini/domain/blueprints.py +5 -0
  56. gini/domain/capabilities.py +177 -0
  57. gini/domain/catalog.py +85 -0
  58. gini/domain/certify.py +201 -0
  59. gini/domain/compose.py +413 -0
  60. gini/domain/composition.py +88 -0
  61. gini/domain/concepts.py +383 -0
  62. gini/domain/connection_rules.py +269 -0
  63. gini/domain/constraints.py +153 -0
  64. gini/domain/content.py +59 -0
  65. gini/domain/cpu_journey.py +89 -0
  66. gini/domain/devices.py +747 -0
  67. gini/domain/diagnose.py +201 -0
  68. gini/domain/element_guide.py +327 -0
  69. gini/domain/explain.py +90 -0
  70. gini/domain/fingerprint.py +201 -0
  71. gini/domain/firewall.py +34 -0
  72. gini/domain/flowlog.py +61 -0
  73. gini/domain/flowtable.py +179 -0
  74. gini/domain/fragment_yaml.py +230 -0
  75. gini/domain/fragments.py +169 -0
  76. gini/domain/games/__init__.py +2 -0
  77. gini/domain/games/paging_games.py +119 -0
  78. gini/domain/games/policy_game.py +86 -0
  79. gini/domain/games/process_game.py +48 -0
  80. gini/domain/games/thrash_game.py +75 -0
  81. gini/domain/games/translate_game.py +60 -0
  82. gini/domain/games/trap_game.py +86 -0
  83. gini/domain/grader.py +155 -0
  84. gini/domain/grouping.py +67 -0
  85. gini/domain/legality.py +103 -0
  86. gini/domain/lesson.py +241 -0
  87. gini/domain/lexicon.py +150 -0
  88. gini/domain/machine_state.py +410 -0
  89. gini/domain/missions/networking/basic-lan.yaml +32 -0
  90. gini/domain/missions/networking/cache-in-front.yaml +23 -0
  91. gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
  92. gini/domain/missions/networking/drive-load.yaml +20 -0
  93. gini/domain/missions/networking/fix-the-address.yaml +75 -0
  94. gini/domain/missions/networking/fix-the-lan.yaml +43 -0
  95. gini/domain/missions/networking/inspect-flows.yaml +16 -0
  96. gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
  97. gini/domain/missions/networking/least-privilege.yaml +21 -0
  98. gini/domain/missions/networking/load-balanced-web.yaml +29 -0
  99. gini/domain/missions/networking/observe-it.yaml +24 -0
  100. gini/domain/missions/networking/put-in-vpc.yaml +30 -0
  101. gini/domain/missions/networking/reachability-boundary.yaml +56 -0
  102. gini/domain/missions/networking/sdn-reactive.yaml +35 -0
  103. gini/domain/missions/networking/send-request.yaml +19 -0
  104. gini/domain/missions/networking/serverless-api.yaml +25 -0
  105. gini/domain/missions/networking/service-chain.yaml +33 -0
  106. gini/domain/missions/os/lottery-fix.yaml +19 -0
  107. gini/domain/missions/os/priority-fix.yaml +24 -0
  108. gini/domain/missions.py +111 -0
  109. gini/domain/modulechain.py +36 -0
  110. gini/domain/objectives.py +488 -0
  111. gini/domain/os_zoo.py +79 -0
  112. gini/domain/paging_sim.py +141 -0
  113. gini/domain/pricing.py +199 -0
  114. gini/domain/probes.py +226 -0
  115. gini/domain/profile.py +142 -0
  116. gini/domain/recipes.py +738 -0
  117. gini/domain/riders.py +309 -0
  118. gini/domain/router_modules.py +224 -0
  119. gini/domain/routetable.py +67 -0
  120. gini/domain/scoring.py +76 -0
  121. gini/domain/staging.py +122 -0
  122. gini/domain/syscall_builder.py +144 -0
  123. gini/domain/topic_cloud.py +62 -0
  124. gini/domain/topology.py +213 -0
  125. gini/domain/vocabulary.py +51 -0
  126. gini/domain/xv6.py +808 -0
  127. gini/domain/xv6_fs.py +250 -0
  128. gini/domain/xv6_runner.py +113 -0
  129. gini/domain/xv6_vm.py +385 -0
  130. gini/gloader.py +17 -0
  131. gini/runtime/__init__.py +18 -0
  132. gini/runtime/cloudfabric_agent.py +370 -0
  133. gini/runtime/console.py +68 -0
  134. gini/runtime/control.py +70 -0
  135. gini/runtime/frame.py +138 -0
  136. gini/runtime/gbridge.py +638 -0
  137. gini/runtime/grouter.py +223 -0
  138. gini/runtime/hostsim.py +90 -0
  139. gini/runtime/shuttle.py +348 -0
  140. gini/runtime/switch.py +109 -0
  141. gini/runtime/transport.py +77 -0
  142. gini/runtime/xv6_bridge.py +312 -0
  143. gini/server/__init__.py +22 -0
  144. gini/server/__main__.py +74 -0
  145. gini/server/app.py +140 -0
  146. gini/server/auth.py +82 -0
  147. gini/server/policy.py +57 -0
  148. gini/server/session.py +23 -0
  149. gini/services/__init__.py +15 -0
  150. gini/services/boardflash.py +248 -0
  151. gini/services/boardsetup.py +374 -0
  152. gini/services/cloud_catalog.py +143 -0
  153. gini/services/compiler.py +1858 -0
  154. gini/services/discovery.py +324 -0
  155. gini/services/gloader.py +183 -0
  156. gini/services/orchestrator.py +1460 -0
  157. gini/services/persistence.py +28 -0
  158. gini/services/probe_runner.py +149 -0
  159. gini/services/project.py +217 -0
  160. gini/services/remote.py +93 -0
  161. gini/services/rider_runner.py +96 -0
  162. gini/services/rider_session.py +171 -0
  163. gini/services/shadow_store.py +52 -0
  164. gini/services/terminal.py +45 -0
  165. gini/setup/__init__.py +17 -0
  166. gini/setup/cli.py +109 -0
  167. gini/setup/images.py +33 -0
  168. gini/setup/marker.py +43 -0
  169. gini/setup/runtime.py +69 -0
  170. gini/ui/__init__.py +3 -0
  171. gini/ui/assets/app_icon.icns +0 -0
  172. gini/ui/assets/app_icon.ico +0 -0
  173. gini/ui/assets/app_icon.png +0 -0
  174. gini/ui/assets/app_icon_1024.png +0 -0
  175. gini/ui/assets/cue/_w.txt +1 -0
  176. gini/ui/assets/cue/ai.png +0 -0
  177. gini/ui/assets/cue/canvas.png +0 -0
  178. gini/ui/assets/cue/cloud.png +0 -0
  179. gini/ui/assets/cue/cost.png +0 -0
  180. gini/ui/assets/cue/dark/ai.png +0 -0
  181. gini/ui/assets/cue/dark/canvas.png +0 -0
  182. gini/ui/assets/cue/dark/cloud.png +0 -0
  183. gini/ui/assets/cue/dark/cost.png +0 -0
  184. gini/ui/assets/cue/dark/metrics.png +0 -0
  185. gini/ui/assets/cue/dark/router.png +0 -0
  186. gini/ui/assets/cue/dark/run.png +0 -0
  187. gini/ui/assets/cue/dark/serverless.png +0 -0
  188. gini/ui/assets/cue/dark/settings.png +0 -0
  189. gini/ui/assets/cue/dark/welcome.png +0 -0
  190. gini/ui/assets/cue/dark/wizard.png +0 -0
  191. gini/ui/assets/cue/ginibrand/ai.png +0 -0
  192. gini/ui/assets/cue/ginibrand/canvas.png +0 -0
  193. gini/ui/assets/cue/ginibrand/cloud.png +0 -0
  194. gini/ui/assets/cue/ginibrand/cost.png +0 -0
  195. gini/ui/assets/cue/ginibrand/metrics.png +0 -0
  196. gini/ui/assets/cue/ginibrand/router.png +0 -0
  197. gini/ui/assets/cue/ginibrand/run.png +0 -0
  198. gini/ui/assets/cue/ginibrand/serverless.png +0 -0
  199. gini/ui/assets/cue/ginibrand/settings.png +0 -0
  200. gini/ui/assets/cue/ginibrand/welcome.png +0 -0
  201. gini/ui/assets/cue/ginibrand/wizard.png +0 -0
  202. gini/ui/assets/cue/highcontrast/ai.png +0 -0
  203. gini/ui/assets/cue/highcontrast/canvas.png +0 -0
  204. gini/ui/assets/cue/highcontrast/cloud.png +0 -0
  205. gini/ui/assets/cue/highcontrast/cost.png +0 -0
  206. gini/ui/assets/cue/highcontrast/metrics.png +0 -0
  207. gini/ui/assets/cue/highcontrast/router.png +0 -0
  208. gini/ui/assets/cue/highcontrast/run.png +0 -0
  209. gini/ui/assets/cue/highcontrast/serverless.png +0 -0
  210. gini/ui/assets/cue/highcontrast/settings.png +0 -0
  211. gini/ui/assets/cue/highcontrast/welcome.png +0 -0
  212. gini/ui/assets/cue/highcontrast/wizard.png +0 -0
  213. gini/ui/assets/cue/light/ai.png +0 -0
  214. gini/ui/assets/cue/light/canvas.png +0 -0
  215. gini/ui/assets/cue/light/cloud.png +0 -0
  216. gini/ui/assets/cue/light/cost.png +0 -0
  217. gini/ui/assets/cue/light/metrics.png +0 -0
  218. gini/ui/assets/cue/light/router.png +0 -0
  219. gini/ui/assets/cue/light/run.png +0 -0
  220. gini/ui/assets/cue/light/serverless.png +0 -0
  221. gini/ui/assets/cue/light/settings.png +0 -0
  222. gini/ui/assets/cue/light/welcome.png +0 -0
  223. gini/ui/assets/cue/light/wizard.png +0 -0
  224. gini/ui/assets/cue/metrics.png +0 -0
  225. gini/ui/assets/cue/router.png +0 -0
  226. gini/ui/assets/cue/run.png +0 -0
  227. gini/ui/assets/cue/serverless.png +0 -0
  228. gini/ui/assets/cue/settings.png +0 -0
  229. gini/ui/assets/cue/welcome.png +0 -0
  230. gini/ui/assets/cue/wizard.png +0 -0
  231. gini/ui/assistant.py +2111 -0
  232. gini/ui/author_dialog.py +184 -0
  233. gini/ui/board_dialog.py +247 -0
  234. gini/ui/branding.py +21 -0
  235. gini/ui/canvas.py +2007 -0
  236. gini/ui/chat_panel.py +7 -0
  237. gini/ui/cpu_journey.py +212 -0
  238. gini/ui/cpu_lab.py +306 -0
  239. gini/ui/cue_cards.py +214 -0
  240. gini/ui/dashboard.py +222 -0
  241. gini/ui/diagnose_game.py +336 -0
  242. gini/ui/fingerprint_lab.py +219 -0
  243. gini/ui/flash_dialog.py +244 -0
  244. gini/ui/flow_layout.py +63 -0
  245. gini/ui/fragment_manager.py +1415 -0
  246. gini/ui/game_catalog.py +184 -0
  247. gini/ui/game_renderers.py +340 -0
  248. gini/ui/games_lab.py +90 -0
  249. gini/ui/inspector.py +1055 -0
  250. gini/ui/live_metrics.py +130 -0
  251. gini/ui/machine_lab.py +1412 -0
  252. gini/ui/main_window.py +3153 -0
  253. gini/ui/memory_lab.py +371 -0
  254. gini/ui/mission_panel.py +302 -0
  255. gini/ui/mode_indicator.py +227 -0
  256. gini/ui/palette.py +112 -0
  257. gini/ui/peripherals.py +218 -0
  258. gini/ui/process_tree.py +130 -0
  259. gini/ui/reset_dialog.py +179 -0
  260. gini/ui/router_lab.py +776 -0
  261. gini/ui/run_button.py +183 -0
  262. gini/ui/settings_dialog.py +234 -0
  263. gini/ui/signin_dialog.py +111 -0
  264. gini/ui/storage_lab.py +219 -0
  265. gini/ui/syscall_builder.py +235 -0
  266. gini/ui/syscall_lab.py +152 -0
  267. gini/ui/theme/__init__.py +5 -0
  268. gini/ui/theme/icons.py +145 -0
  269. gini/ui/theme/manager.py +291 -0
  270. gini/ui/theme/tokens.py +194 -0
  271. gini/ui/trap_lab.py +270 -0
  272. gini/ui/worker_host.py +102 -0
  273. gini/ui/zoo_lab.py +112 -0
  274. gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
  275. gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
  276. gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
  277. gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
  278. gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,177 @@
1
+ """The capability vocabulary — the typed interface that lets mission fragments COMPOSE.
2
+
3
+ Every fragment declares what it `provides` and `requires` in these role names; assembly then joins
4
+ fragments by matching one's provides to another's requires. Roles form a small **is-a hierarchy**
5
+ (a role may have several super-roles), so a fragment can require a *broad* role (loose — many
6
+ providers satisfy it) or a *specific* leaf (tight). `satisfies(provided, required)` is true when the
7
+ required role is the provided role or any of its ancestors — i.e. providing a `web-endpoint`
8
+ satisfies a requirement for a `traffic-sink`.
9
+
10
+ Pure data + two small helpers; no Qt, no LLM. This is the vocabulary drafted in
11
+ GINI_MISSIONS_COMPOSABLE_DESIGN.md §4 — prune here as we learn.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ # role -> its super-roles (is-a). Roots map to (). Multiple parents are allowed (e.g. a web tier is
16
+ # both a compute node and a traffic sink).
17
+ PARENTS: dict[str, tuple[str, ...]] = {
18
+ # F1 · network fabric
19
+ # `network` is the recursion hinge: a slot that requires a `network` is satisfied by a LAN
20
+ # (l2-fabric) OR by another routed network (routed-network) — so routers nest to any depth.
21
+ "network": (),
22
+ "l2-fabric": ("network",),
23
+ "switched-segment": ("l2-fabric",),
24
+ "sdn-fabric": ("l2-fabric",),
25
+ "routed-network": ("network",),
26
+ "l3-gateway": (),
27
+ "router-gateway": ("l3-gateway", "routed-network"), # a router exposes the routed network it fronts
28
+ "nat-gateway": ("l3-gateway",),
29
+ "network-boundary": (),
30
+ "vpc-boundary": ("network-boundary",),
31
+ "subnet-boundary": ("network-boundary",),
32
+ # F2 · edge / egress
33
+ "internet-egress": (),
34
+ "public-entrypoint": (),
35
+ # F3 · compute / workload
36
+ "compute-node": (),
37
+ "host-node": ("compute-node",),
38
+ "pod-workload": ("compute-node",),
39
+ "serverless-fn": ("compute-node",),
40
+ "orchestrated-compute": (),
41
+ "web-tier": ("compute-node", "traffic-sink"),
42
+ # F4 · traffic
43
+ "traffic-source": (),
44
+ "load-generator": ("traffic-source",),
45
+ "request-client": ("traffic-source",),
46
+ "traffic-sink": (),
47
+ "web-endpoint": ("traffic-sink",),
48
+ "api-endpoint": ("traffic-sink",),
49
+ "service-endpoint": ("traffic-sink",),
50
+ # F5 · steering / distribution
51
+ "load-distributor": (),
52
+ "inline-nf": (),
53
+ "flow-control": (),
54
+ # F6 · security / policy
55
+ "access-policy": (),
56
+ "perimeter-filter": ("access-policy",),
57
+ "micro-segmentation": ("access-policy",),
58
+ # F7 · data / state
59
+ "datastore": (),
60
+ "relational-store": ("datastore",),
61
+ "cache-tier": ("datastore",),
62
+ "object-store": ("datastore",),
63
+ "message-broker": (),
64
+ # F8 · observability
65
+ "metrics-source": (),
66
+ "metrics-collector": (),
67
+ "visualizer": (),
68
+ "dashboard-view": ("visualizer",),
69
+ "flow-inspector": ("visualizer",),
70
+ "packet-visualizer": ("visualizer",),
71
+ # F9 · fault / perturbation
72
+ "fault-injector": (),
73
+ "link-break": ("fault-injector",),
74
+ "misconfig": ("fault-injector",),
75
+ "overload": ("fault-injector",),
76
+ # F10 · operating systems (xv6 track) — standalone machine, no fabric
77
+ "kernel-host": (),
78
+ "workload-source": ("traffic-source",), # a process/syscall stimulus (a "source" for the OS)
79
+ "kernel-observer": ("visualizer",),
80
+ }
81
+
82
+ # Which domain each role belongs to (for presentation + so the composer knows the track). ONE
83
+ # namespace, tagged three ways — a role like compute-node is shared, which is what lets fragments
84
+ # compose ACROSS domains (a cloud app on a network, an OS reached over a link).
85
+ NETWORKING, CLOUD, OS, SHARED = "networking", "cloud", "os", "shared"
86
+ DOMAIN: dict[str, str] = {
87
+ **{r: NETWORKING for r in (
88
+ "network", "routed-network",
89
+ "l2-fabric", "switched-segment", "sdn-fabric", "l3-gateway", "router-gateway", "nat-gateway",
90
+ "network-boundary", "vpc-boundary", "subnet-boundary", "internet-egress", "public-entrypoint",
91
+ "traffic-source", "load-generator", "request-client", "traffic-sink", "load-distributor",
92
+ "inline-nf", "flow-control", "access-policy", "perimeter-filter", "micro-segmentation",
93
+ "packet-visualizer", "flow-inspector", "fault-injector", "link-break", "misconfig", "overload")},
94
+ **{r: CLOUD for r in (
95
+ "compute-node", "host-node", "pod-workload", "serverless-fn", "orchestrated-compute",
96
+ "web-tier", "web-endpoint", "api-endpoint", "service-endpoint", "datastore",
97
+ "relational-store", "cache-tier", "object-store", "message-broker")},
98
+ **{r: SHARED for r in ("metrics-source", "metrics-collector", "visualizer", "dashboard-view")},
99
+ **{r: OS for r in ("kernel-host", "workload-source", "kernel-observer")},
100
+ }
101
+
102
+ # Device type → the capability roles it can PLAY. Multi-role is deliberate: it gives the composer
103
+ # more ways to satisfy a requirement (a host is both a compute-node and, when it pings, a source).
104
+ DEVICE_ROLES: dict[str, tuple[str, ...]] = {
105
+ # networking fabric / edge
106
+ "router": ("router-gateway",), "switch": ("switched-segment",), "hub": ("switched-segment",),
107
+ "firewall": ("perimeter-filter",), "wap": ("switched-segment",), "vnf": ("inline-nf",),
108
+ "cloud": ("internet-egress",), "ovs": ("sdn-fabric",), "controller": ("flow-control",),
109
+ # compute
110
+ "host": ("host-node",), "container": ("compute-node",), "instance": ("compute-node",),
111
+ "kinstance": ("compute-node",), "web_app": ("web-endpoint", "compute-node"),
112
+ "pod": ("pod-workload",), "k8s_cluster": ("orchestrated-compute",), "k8s_node": ("compute-node",),
113
+ "instance_group": ("orchestrated-compute",),
114
+ # cloud networking
115
+ "vpc": ("vpc-boundary",), "cloud_subnet": ("subnet-boundary",),
116
+ "security_group": ("micro-segmentation",), "gateway": ("nat-gateway",),
117
+ "load_balancer": ("load-distributor",), "proxy": ("load-distributor",),
118
+ # data / serverless / streaming
119
+ "database": ("relational-store",), "nosql": ("datastore",), "cache": ("cache-tier",),
120
+ "object_store": ("object-store",), "function": ("serverless-fn",),
121
+ "api_gateway": ("api-endpoint", "public-entrypoint"),
122
+ "queue": ("message-broker",), "stream": ("message-broker",), "messaging": ("message-broker",),
123
+ # observability (sinks)
124
+ "metrics": ("metrics-collector",), "dashboard": ("dashboard-view",), "tracing": ("visualizer",),
125
+ # sources / sinks (riders)
126
+ "ping_probe": ("request-client",), "http_probe": ("request-client",),
127
+ "dns_probe": ("request-client",), "traceroute_probe": ("request-client",),
128
+ "iperf_client": ("load-generator",), "load_generator": ("load-generator",),
129
+ "iperf_server": ("traffic-sink",), "iface_stats": ("metrics-source",),
130
+ "packet_view": ("packet-visualizer",),
131
+ # operating systems (xv6)
132
+ "xv6": ("kernel-host",), "terminal": ("kernel-observer",), "storage_volume": ("kernel-observer",),
133
+ "xv6_shell": ("workload-source",), "xv6_workload": ("workload-source",),
134
+ }
135
+
136
+
137
+ def roles_for(device_key: str) -> tuple[str, ...]:
138
+ """The capability roles a device type can play (may be several)."""
139
+ return DEVICE_ROLES.get(device_key, ())
140
+
141
+
142
+ def domain_of(role: str) -> str:
143
+ return DOMAIN.get(role, SHARED)
144
+
145
+
146
+ def is_role(name: str) -> bool:
147
+ return name in PARENTS
148
+
149
+
150
+ def ancestors(role: str) -> set[str]:
151
+ """All super-roles of `role`, inclusive of itself (walks the is-a DAG)."""
152
+ seen: set[str] = set()
153
+ stack = [role]
154
+ while stack:
155
+ r = stack.pop()
156
+ if r in seen:
157
+ continue
158
+ seen.add(r)
159
+ stack.extend(PARENTS.get(r, ()))
160
+ return seen
161
+
162
+
163
+ def satisfies(provided: str, required: str) -> bool:
164
+ """True when a fragment providing `provided` meets a requirement for `required` — i.e. the
165
+ required role is the provided role or one of its ancestors (a specific provider satisfies a
166
+ broad requirement)."""
167
+ return required in ancestors(provided)
168
+
169
+
170
+ def any_satisfies(provided_roles, required: str) -> bool:
171
+ """True when any of `provided_roles` satisfies the single `required` role."""
172
+ return any(satisfies(p, required) for p in provided_roles)
173
+
174
+
175
+ def unknown_roles(roles) -> list[str]:
176
+ """Roles not in the vocabulary (authoring safety net)."""
177
+ return sorted({r for r in roles if r not in PARENTS})
gini/domain/catalog.py ADDED
@@ -0,0 +1,85 @@
1
+ """The Game Catalog — an ADAPTER over the foundational fragments (now YAML packs under `missions/`).
2
+
3
+ Historically this module held the archetypes as Python literals. Those foundational templates now live
4
+ as data (YAML) and are loaded by `fragments`; this module presents the pickable/standalone fragments as
5
+ `Archetype`s so the older call sites (lesson.from_archetype, lesson_resolver, the mission picker) keep
6
+ working unchanged. Content moved to data; the API is stable.
7
+
8
+ `ObjectiveTemplate` is re-exported from `fragments` for back-compat. Objectives use the same type-based,
9
+ name-agnostic predicates (`exists`, `count`, `link`, `path`, `through`, `contains_type`).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from dataclasses import dataclass, field
15
+
16
+ from . import fragments as _frag
17
+ from .fragments import ObjectiveTemplate # noqa: F401 (re-export for back-compat)
18
+ from .objectives import Objective
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Archetype:
23
+ id: str
24
+ teaches: str
25
+ spirit: str
26
+ summary: str
27
+ params: tuple[str, ...] = ()
28
+ objectives: tuple[ObjectiveTemplate, ...] = ()
29
+ misconceptions: tuple[str, ...] = ()
30
+ complete_when: str = "all"
31
+ difficulty: dict = field(default_factory=dict)
32
+ stage: dict = field(default_factory=dict)
33
+
34
+
35
+ # Type-based objectives need no name bindings, so archetypes launch with no params.
36
+ DEMO_PARAMS: dict[str, dict] = {}
37
+
38
+
39
+ def _arch_of(frag) -> Archetype:
40
+ return Archetype(id=frag.id, teaches=frag.teaches, spirit=frag.spirit, summary=frag.summary,
41
+ params=(), objectives=tuple(frag.objectives),
42
+ misconceptions=tuple(frag.misconceptions), complete_when=frag.complete_when,
43
+ stage=dict(frag.stage))
44
+
45
+
46
+ def get(archetype_id: str) -> Archetype | None:
47
+ frag = _frag.get(archetype_id)
48
+ return _arch_of(frag) if (frag is not None and frag.catalog) else None
49
+
50
+
51
+ def all_archetypes() -> list[Archetype]:
52
+ """The standalone, pickable missions (fragments flagged `catalog`), preserving fragment order."""
53
+ return [_arch_of(f) for f in _frag.all_fragments() if f.catalog]
54
+
55
+
56
+ def demo_params(archetype_id: str) -> dict:
57
+ return dict(DEMO_PARAMS.get(archetype_id, {}))
58
+
59
+
60
+ # -- instantiation ---------------------------------------------------------- #
61
+ _PLACEHOLDER = re.compile(r"\{(\w+)\}")
62
+
63
+
64
+ def _bind(text: str, params: dict) -> str:
65
+ """Replace {ref} placeholders with concrete names (a no-op for type-based checks)."""
66
+ return _PLACEHOLDER.sub(lambda m: str(params.get(m.group(1), m.group(0))), text)
67
+
68
+
69
+ def instantiate(archetype: Archetype, params: dict) -> list[Objective]:
70
+ from .objectives import by_level
71
+ out: list[Objective] = []
72
+ for t in archetype.objectives:
73
+ out.append(Objective(id=t.id, say=t.say, kind=t.kind,
74
+ check=_bind(t.check, params) if t.check else "",
75
+ probe=_bind(t.probe, params) if t.probe else "",
76
+ level=t.level))
77
+ return by_level(out) # progressive ladder: place → connect → group → prove live
78
+
79
+
80
+ def unbound_refs(archetype: Archetype, params: dict) -> list[str]:
81
+ refs: set[str] = set()
82
+ for t in archetype.objectives:
83
+ refs.update(_PLACEHOLDER.findall(t.check))
84
+ refs.update(_PLACEHOLDER.findall(t.probe))
85
+ return sorted(r for r in refs if r not in params)
gini/domain/certify.py ADDED
@@ -0,0 +1,201 @@
1
+ """Fragment certification — the client-side quality gate before a block goes to the Teaching Center.
2
+
3
+ Certification has two organs, matching the generate⇄verify spine:
4
+
5
+ * **the compiler (deterministic, HARD gate)** — proves ground truth: the fragment validates, and
6
+ (once a live runner is wired) that a reference solution grades to complete. The AI never decides
7
+ correctness — a hallucinated "looks fine" on a broken block would poison every mission composed
8
+ from it.
9
+ * **the AI certifier (advisory, SOFT gate)** — reviews *composability*: the issues the composer
10
+ will hit when it recombines this block. Its strongest form is a **dry-run of the composition
11
+ engine** — actually attempt to compose the fragment against the library and report what fit,
12
+ what's orphaned, and what it can feed. The reasoning here is deterministic today (contract
13
+ matching via `assembly`); an LLM advisor can layer richer judgement on top later.
14
+
15
+ `certify()` returns a report of tiered issues. `blocked` (any BLOCK issue) means "not certified".
16
+ WARN issues are soft — the teacher may upload over them, but they travel with the fragment so the
17
+ composer knows the risk.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from dataclasses import dataclass, field
23
+
24
+ from . import capabilities as _caps
25
+ from . import fragment_yaml as _fy
26
+ from . import fragments as _frag
27
+
28
+ BLOCK, WARN, INFO = "block", "warn", "info"
29
+
30
+
31
+ @dataclass
32
+ class Issue:
33
+ level: str # BLOCK | WARN | INFO
34
+ code: str
35
+ message: str
36
+
37
+
38
+ @dataclass
39
+ class CertReport:
40
+ fragment_id: str
41
+ issues: list[Issue] = field(default_factory=list)
42
+ composes_into: list[str] = field(default_factory=list) # fragments this one can feed (forward)
43
+ unmet_requires: list[str] = field(default_factory=list) # requires nothing in the library gives
44
+
45
+ def add(self, level: str, code: str, message: str) -> None:
46
+ self.issues.append(Issue(level, code, message))
47
+
48
+ @property
49
+ def blocked(self) -> bool:
50
+ return any(i.level == BLOCK for i in self.issues)
51
+
52
+ @property
53
+ def certified(self) -> bool:
54
+ return not self.blocked
55
+
56
+ def of(self, level: str) -> list[Issue]:
57
+ return [i for i in self.issues if i.level == level]
58
+
59
+
60
+ @dataclass
61
+ class RuntimeGrade:
62
+ """The outcome of grading the fragment's AUTHORING BOARD (the reference solution) on a live
63
+ stack. The caller runs `objectives.evaluate_all` with the fragment's sources auto-started, then
64
+ hands the summary here. `available=False` means the stack wasn't running, so nothing was proven.
65
+ """
66
+ available: bool = False
67
+ unmet: list = field(default_factory=list) # objective ids that graded UNMET (a real failure)
68
+ pending: list = field(default_factory=list) # behavioral ids the runner couldn't witness
69
+
70
+ @property
71
+ def all_met(self) -> bool:
72
+ return self.available and not self.unmet and not self.pending
73
+
74
+
75
+ _COUNT_RE = re.compile(r"^\s*count\(\s*(\w+)\s*\)\s*>=\s*(\d+)")
76
+ _PROBE_ENDS_RE = re.compile(r"(?:reach|ping|http)\(\s*(\w+)\s*->\s*(\w+)")
77
+ _MEASURE_RE = re.compile(r"measure\(\s*(\w+)\s*,")
78
+
79
+
80
+ def _multi_types(objectives) -> set[str]:
81
+ """Types the fragment asks for MORE THAN ONE of — the ones a type-based probe can be ambiguous
82
+ about in a merged board."""
83
+ out = set()
84
+ for o in objectives:
85
+ m = _COUNT_RE.match(o.get("check") or "")
86
+ if m and int(m.group(2)) >= 2:
87
+ out.add(m.group(1))
88
+ return out
89
+
90
+
91
+ def _probe_types(objectives) -> set[str]:
92
+ out = set()
93
+ for o in objectives:
94
+ p = o.get("probe") or ""
95
+ m = _PROBE_ENDS_RE.search(p)
96
+ if m:
97
+ out.update((m.group(1), m.group(2)))
98
+ m = _MEASURE_RE.search(p)
99
+ if m:
100
+ out.add(m.group(1))
101
+ return out
102
+
103
+
104
+ def _dry_run_compose(frag, rep: CertReport, library) -> None:
105
+ """A deterministic dry-run of the composition engine: can the fragment's requirements be met by
106
+ the library (backward), and what can consume what it provides (forward)? This is exactly the
107
+ contract-matching the assembler does when it chains fragments."""
108
+ lib = library if library is not None else _frag.all_fragments()
109
+ others = [f for f in lib if f.id != frag.id]
110
+
111
+ for req in frag.requires: # backward: who can satisfy what I need?
112
+ if any(_caps.any_satisfies(f.provides, req) for f in others):
113
+ continue
114
+ rep.unmet_requires.append(req)
115
+ rep.add(WARN, "orphan-requires",
116
+ f"Nothing in the library provides '{req}', so I can only ever use this block to "
117
+ f"START a chain, never in the middle.")
118
+
119
+ for f in others: # forward: who can consume what I provide?
120
+ if any(_caps.any_satisfies(frag.provides, r) for r in f.requires):
121
+ rep.composes_into.append(f.id)
122
+ if frag.provides and rep.composes_into:
123
+ rep.add(INFO, "composes-into",
124
+ f"Composes into: {', '.join(sorted(set(rep.composes_into)))}.")
125
+
126
+
127
+ def certify(frag_dict: dict, *, library=None, runtime: "RuntimeGrade | None" = None) -> CertReport:
128
+ """Certify an authored fragment dict. `runtime` is the result of grading the fragment's authoring
129
+ board on a live stack (None = not graded). A fragment with live/output checks is only certified
130
+ once that grade is clean — the HARD compiler gate; pure-structural fragments need no runtime."""
131
+ rep = CertReport(fragment_id=frag_dict.get("id", ""))
132
+
133
+ # -- compiler: gradable (BLOCK) ---------------------------------------- #
134
+ problems = _fy.validate(_fy.fragment_from_dict(frag_dict))
135
+ for p in problems:
136
+ rep.add(BLOCK, "invalid", p)
137
+ if rep.blocked:
138
+ return rep # a broken block: don't reason further
139
+
140
+ frag = _fy.fragment_from_dict(frag_dict)
141
+ objectives = frag_dict.get("objectives", [])
142
+ has_output = any((o.get("probe") or "").startswith("measure(") for o in objectives)
143
+ has_live = any((o.get("probe") or "").startswith(("reach", "ping", "http")) for o in objectives)
144
+ has_behavioral = has_output or has_live
145
+
146
+ # -- compiler: winnable / runtime-sound (HARD gate) -------------------- #
147
+ # The authoring board IS the reference solution: grade it live and it must pass. Behavioral /
148
+ # output checks need a running stack (with the Sources driving traffic); pure-structural doesn't.
149
+ if not has_behavioral:
150
+ rep.add(INFO, "structural-only",
151
+ "Structural fragment — no live checks, so no runtime playtest is needed.")
152
+ elif runtime is None or not runtime.available:
153
+ rep.add(BLOCK, "runtime-required",
154
+ "Run the topology and Certify to prove the live/output checks — required before "
155
+ "upload (its Sources drive traffic while the board is graded).")
156
+ elif runtime.unmet:
157
+ rep.add(BLOCK, "not-winnable",
158
+ f"The reference board doesn't satisfy: {', '.join(runtime.unmet)} — is it winnable?")
159
+ elif runtime.pending:
160
+ rep.add(BLOCK, "runtime-incomplete",
161
+ f"Couldn't witness: {', '.join(runtime.pending)} — is the whole stack running?")
162
+ else:
163
+ rep.add(INFO, "runtime-ok",
164
+ "Playtested live: the reference solution grades to complete.")
165
+
166
+ # -- AI certifier: composability review (WARN, soft gate) -------------- #
167
+ if not (frag_dict.get("spirit") or "").strip():
168
+ rep.add(WARN, "no-spirit",
169
+ "No spirit/intent — the composer can't generate variants or negotiate level without "
170
+ "a sentence on what success means.")
171
+ if not has_output and not has_live:
172
+ rep.add(WARN, "no-output",
173
+ "No live/output check — this grades only the picture, not that the system works. "
174
+ "Attach a Sink and add an output check.")
175
+ if not frag.forks:
176
+ rep.add(WARN, "no-difficulty",
177
+ "No difficulty knob — the composer can't offer an easier/harder variant. Add input "
178
+ "presets or a fork.")
179
+ multi, probed = _multi_types(objectives), _probe_types(objectives)
180
+ for tk in sorted(multi & probed):
181
+ # not actually ambiguous — type-based grading reads this as "SOME <tk>" by default (add
182
+ # ', all' if you mean EVERY one). Just a hint so the author picks the intended quantifier.
183
+ rep.add(INFO, "quantifier",
184
+ f"You have ≥2 '{tk}' and a check on '{tk}' — it grades as \"some {tk}\"; add ', all' "
185
+ f"to the probe if you mean every one.")
186
+ if len(objectives) > 8:
187
+ rep.add(WARN, "too-big",
188
+ f"{len(objectives)} objectives — big blocks recombine poorly; consider splitting "
189
+ f"into single-idea fragments.")
190
+
191
+ # -- AI certifier: dry-run the composition engine (WARN/INFO) ---------- #
192
+ _dry_run_compose(frag, rep, library)
193
+ return rep
194
+
195
+
196
+ def runtime_from_results(results, *, available: bool = True) -> RuntimeGrade:
197
+ """Summarise an `objectives.evaluate_all` result list into a RuntimeGrade for `certify()`."""
198
+ from . import objectives as _obj
199
+ unmet = [r.id for r in results if r.status == _obj.UNMET]
200
+ pending = [r.id for r in results if r.status == _obj.PENDING]
201
+ return RuntimeGrade(available=available, unmet=unmet, pending=pending)