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,269 @@
1
+ """Connection grammar — what can wire to what, and *why*.
2
+
3
+ A single declarative table of the meaningful connections between element types, each
4
+ with a one-line, student-facing reason and whether the link is *required* for one side
5
+ to do its job (e.g. a Pod needs a Cluster to run in). This is pure data with no Qt or
6
+ compiler dependency so every layer can share one source of truth:
7
+
8
+ • **X-ray** (long-press a node) highlights its compatible partners + shows the *why*.
9
+ • **Explain** can answer "what can I connect this to, and what's missing?".
10
+ • **Wizard / lint** can validate a topology and suggest the next element.
11
+
12
+ The grammar is intentionally *advisory*, not a hard constraint: gBuilder still lets a
13
+ student wire anything (mistakes are teachable). It just makes the good paths visible.
14
+
15
+ Edges are undirected for matching purposes; the `why` is phrased to read sensibly from
16
+ either end. `requires` names the element type(s) that genuinely *need* such an edge.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass
21
+
22
+ from . import devices as _dev
23
+
24
+ # Reusable groups (expanded into concrete edges at import). Keep these aligned with the
25
+ # device registry keys.
26
+ GROUPS: dict[str, tuple[str, ...]] = {
27
+ "WORKLOAD": ("web_app", "instance", "container"), # VM-style cloud app runtimes
28
+ "BACKEND": ("web_app", "instance", "container", "pod"), # things traffic targets
29
+ "DATASTORE": ("database", "nosql", "cache"),
30
+ # anything that runs app code — a Pod is a real workload too, so it can use data
31
+ # stores, emit metrics/traces, and talk to streams/queues just like a Web App.
32
+ "APPISH": ("web_app", "instance", "container", "function", "pod"),
33
+ }
34
+
35
+ # Each row: (a, b, why, requires). `a`/`b` may be a GROUP name (uppercase). `requires` is
36
+ # a tuple of type_keys (or group names) for which this edge is *required* to function.
37
+ _SPEC: tuple[tuple[str, str, str, tuple[str, ...]], ...] = (
38
+ # ---- classic networking ---------------------------------------------- #
39
+ ("host", "switch", "Join a LAN — a switch links machines on the same subnet.", ()),
40
+ ("host", "hub", "Share one collision domain (teaching: watch flooding & collisions).", ()),
41
+ ("host", "router", "Attach a machine straight to a router as its gateway off the LAN.", ()),
42
+ ("host", "wap", "Join the network over Wi-Fi.", ()),
43
+ # a graphical Desktop is a real networked host — same LAN connectivity as `host`.
44
+ ("desktop", "switch", "Join a LAN — the graphical host sits on a subnet like any machine.", ()),
45
+ ("desktop", "hub", "Share one collision domain (watch flooding & collisions).", ()),
46
+ ("desktop", "router", "Attach the graphical host straight to a router as its gateway.", ()),
47
+ ("desktop", "wap", "Join the network over Wi-Fi.", ()),
48
+ ("switch", "router", "Uplink the LAN to a router so it can reach other subnets.", ()),
49
+ ("switch", "switch", "Extend the LAN onto a second switch.", ()),
50
+ ("hub", "switch", "Bridge a shared-media segment into a switched LAN.", ()),
51
+ ("wap", "switch", "Uplink the access point into the wired LAN.", ()),
52
+ ("router", "router", "Link routers for multi-hop routing between subnets.", ()),
53
+ ("router", "cloud", "Reach the public Internet through this router.", ()),
54
+ ("router", "firewall", "Filter traffic between trust zones with a firewall.", ()),
55
+ ("firewall", "cloud", "Guard the boundary between the LAN and the Internet.", ()),
56
+
57
+ # ---- real hardware in the loop (GINI32) ------------------------------- #
58
+ # The board is a gateway for the physical devices behind its radio, so it wires
59
+ # into the topology exactly where a router or a LAN does.
60
+ ("gini32", "router", "Bring real devices behind the board into this router's subnet.", ()),
61
+ ("gini32", "switch", "Put the board's real devices on this LAN.", ()),
62
+
63
+ # ---- NFV / service function chaining --------------------------------- #
64
+ # A VNF is an inline network function: it sits BETWEEN two elements in the path, so it
65
+ # wires to hosts, switches, routers, the OVS, the Internet, and other VNFs (a chain).
66
+ ("vnf", "host", "Put this network function inline in front of a machine.", ()),
67
+ ("vnf", "switch", "Insert this network function on the path through a switch.", ()),
68
+ ("vnf", "router", "Chain this network function to a router in the path.", ()),
69
+ ("vnf", "ovs", "Attach this VNF to the SDN switch (steer flows through it).", ()),
70
+ ("vnf", "firewall", "Chain this VNF next to a firewall in the path.", ()),
71
+ ("vnf", "cloud", "Put this network function at the edge, before the Internet.", ()),
72
+ ("vnf", "vnf", "Chain VNFs in series — a Service Function Chain (e.g. firewall → IDS).", ()),
73
+
74
+ # ---- software-defined networking ------------------------------------- #
75
+ ("ovs", "controller", "An OpenFlow switch needs a controller to program its flow rules.",
76
+ ("ovs",)),
77
+ ("ovs", "host", "Attach end machines to the SDN switch.", ()),
78
+ ("ovs", "ovs", "Build a multi-switch SDN fabric.", ()),
79
+
80
+ # ---- containers & Kubernetes ----------------------------------------- #
81
+ ("pod", "k8s_cluster", "A Pod needs a Cluster to run in — this is where it deploys.",
82
+ ("pod",)),
83
+ ("pod", "instance_group", "Add a Pod Autoscaler (HPA) to scale this Pod's replicas on CPU.",
84
+ ("instance_group",)),
85
+ ("pod", "registry", "Pull the Pod's image from a private registry.", ()),
86
+ ("k8s_cluster", "registry", "Serve images to the cluster's workloads from a registry.", ()),
87
+
88
+ # ---- traffic into backends (LB / proxy / load-gen) ------------------- #
89
+ ("load_balancer", "BACKEND", "Distribute incoming traffic across this backend.", ()),
90
+ ("proxy", "BACKEND", "Front this service with a reverse proxy (routing / TLS).", ()),
91
+ ("load_generator", "BACKEND", "Fire HTTP load at this app to test it under traffic.", ()),
92
+ ("load_balancer", "proxy", "Chain a load balancer in front of a reverse proxy.", ()),
93
+
94
+ # ---- apps + their data (a Pod is an app too, so it's a BACKEND here) -- #
95
+ ("BACKEND", "DATASTORE", "Back this app with a database / cache for its state.", ()),
96
+ ("BACKEND", "object_store", "Store & serve files / objects for this app.", ()),
97
+ ("instance", "block_volume", "Attach a persistent disk to this instance.",
98
+ ("block_volume",)),
99
+
100
+ # ---- cloud networking ------------------------------------------------ #
101
+ ("vpc", "cloud_subnet", "A VPC is divided into subnets — place this subnet inside it.",
102
+ ("cloud_subnet",)),
103
+ ("cloud_subnet", "WORKLOAD", "Place this workload inside the subnet.", ()),
104
+ ("security_group", "WORKLOAD", "Attach a stateful firewall to this workload.", ()),
105
+ ("security_group", "DATASTORE", "Lock down this datastore — allow only the tiers you list.", ()),
106
+ ("gateway", "vpc", "Give the VPC outbound Internet through this gateway.", ()),
107
+ ("gateway", "cloud", "The gateway faces the public Internet.", ()),
108
+
109
+ # ---- serverless ------------------------------------------------------ #
110
+ ("api_gateway", "function", "Route a URL path to this function (the front door).", ()),
111
+ ("function", "DATASTORE", "Read & write state from the function (it's stateless itself).", ()),
112
+ ("function", "object_store", "Read & write objects from the function.", ()),
113
+ ("function", "queue", "Trigger the function from queue messages (event-driven).", ()),
114
+ ("function", "stream", "Trigger the function from an event stream.", ()),
115
+ ("function", "messaging", "Trigger the function from pub/sub messages.", ()),
116
+ ("api_gateway", "web_app", "Front a web service with a managed API.", ()),
117
+ ("load_generator", "api_gateway", "Fire HTTP load at the gateway to watch functions scale.", ()),
118
+ ("load_generator", "function", "Invoke the function under load to test it.", ()),
119
+ ("metrics", "function", "Scrape invocation metrics from the function runtime.", ()),
120
+ ("metrics", "api_gateway", "Scrape request metrics from the API gateway.", ()),
121
+ ("queue", "WORKLOAD", "Pass messages asynchronously between services.", ()),
122
+
123
+ # ---- VM-vs-container experiment (Kata Instance) ---------------------- #
124
+ # A deliberately RESTRICTED set: a Kata Instance is a VM-isolated workload used only
125
+ # for the isolation experiment, so it wires to a load source, a backend, and metrics —
126
+ # never k8s, the networking plane, or VPCs (it can't run those / stays flat).
127
+ ("load_generator", "kinstance", "Fire HTTP load at the VM workload to measure it.", ()),
128
+ ("load_balancer", "kinstance", "Fan traffic across several VM workloads.", ()),
129
+ ("kinstance", "DATASTORE", "Back the VM workload with a database / cache.", ()),
130
+ ("kinstance", "object_store", "Read & write objects from the VM workload.", ()),
131
+ ("metrics", "kinstance", "Scrape metrics from the VM workload.", ()),
132
+ ("kinstance", "kinstance", "Build a multi-VM topology to compare against containers.", ()),
133
+
134
+ # ---- streaming & messaging ------------------------------------------- #
135
+ ("stream", "APPISH", "Produce / consume an event log with this service.", ()),
136
+ ("messaging", "APPISH", "Publish / subscribe messages with this service.", ()),
137
+
138
+ # ---- observability --------------------------------------------------- #
139
+ ("metrics", "BACKEND", "Scrape metrics from this target.", ()),
140
+ ("metrics", "proxy", "Scrape request metrics from the proxy.", ()),
141
+ ("metrics", "load_balancer", "Scrape metrics from the load balancer.", ()),
142
+ ("dashboard", "metrics", "A dashboard needs a metrics source to visualize.", ("dashboard",)),
143
+ ("tracing", "APPISH", "Collect distributed traces from this service.", ()),
144
+
145
+ # ---- xv6 peripherals (xv6 has NO networking; you attach software devices) --- #
146
+ ("xv6", "terminal", "Attach a Terminal to type commands and watch xv6's console.", ()),
147
+ ("xv6", "storage_volume", "Attach the xv6 disk to inspect its file system.", ()),
148
+ )
149
+
150
+
151
+ # xv6 has no network stack, so unlike the advisory grammar above these ARE hard constraints:
152
+ # an xv6 Machine wires ONLY to its peripherals, and a peripheral wires ONLY to an xv6 Machine.
153
+ _XV6_PERIPHERALS = frozenset({"terminal", "storage_volume"})
154
+
155
+
156
+ # --------------------------------------------------------------------------- #
157
+ # Attach grammar: riders (Sources / Sinks) mount onto a donor, they don't wire.
158
+ # --------------------------------------------------------------------------- #
159
+ def is_rider(type_key: str) -> bool:
160
+ dt = _dev.REGISTRY.get(type_key)
161
+ return bool(dt and getattr(dt, "rider", False))
162
+
163
+
164
+ def attach_targets(rider_type: str) -> tuple[str, ...]:
165
+ """The donor type_keys a rider may ride (empty if it isn't a rider)."""
166
+ dt = _dev.REGISTRY.get(rider_type)
167
+ return tuple(getattr(dt, "attaches_to", ()) or ()) if dt else ()
168
+
169
+
170
+ def riders_for(donor_type: str) -> list[str]:
171
+ """Every rider type that can mount on this donor (drives X-ray from the donor's side)."""
172
+ return sorted(k for k, dt in _dev.REGISTRY.items()
173
+ if getattr(dt, "rider", False)
174
+ and donor_type in (getattr(dt, "attaches_to", ()) or ()))
175
+
176
+
177
+ def attach_blocked(rider_type: str, donor_type: str) -> str | None:
178
+ """A reason to REJECT mounting `rider_type` on `donor_type`, or None to allow it."""
179
+ if not is_rider(rider_type):
180
+ return "Only a Source or Sink can be attached — wire the rest as network links."
181
+ if is_rider(donor_type):
182
+ return "A donor can't be another Source/Sink — attach the rider to a Machine, Router, …"
183
+ if donor_type not in attach_targets(rider_type):
184
+ tgts = ", ".join(attach_targets(rider_type)) or "a compatible element"
185
+ return f"A {rider_type} attaches to {tgts} — not {donor_type}."
186
+ return None
187
+
188
+
189
+ def link_blocked(a: str, b: str) -> str | None:
190
+ """A reason to REJECT an a–b link (a HARD constraint), or None to allow it."""
191
+ # A Source/Sink rides its donor with a dotted ATTACH edge, never a network cable.
192
+ for x, other in ((a, b), (b, a)):
193
+ if is_rider(x):
194
+ return (f"A {x} is a Source/Sink — attach it to its donor (dotted), don't wire it as "
195
+ f"a network link.")
196
+ if a == "xv6" or b == "xv6":
197
+ other = b if a == "xv6" else a
198
+ if other != "xv6" and other not in _XV6_PERIPHERALS:
199
+ return ("xv6 has no networking — attach a Screen, Keyboard, or Storage Volume "
200
+ "instead of a network connection.")
201
+ if a in _XV6_PERIPHERALS or b in _XV6_PERIPHERALS:
202
+ other = b if a in _XV6_PERIPHERALS else a
203
+ if other != "xv6":
204
+ return "An xv6 peripheral attaches only to an xv6 Machine."
205
+ return None
206
+
207
+
208
+ @dataclass(frozen=True)
209
+ class Partner:
210
+ """A valid connection partner for some element, with its teaching reason."""
211
+ type_key: str
212
+ why: str
213
+ required: bool # True if the *queried* element needs at least one such link
214
+
215
+
216
+ def _expand(name: str) -> tuple[str, ...]:
217
+ return GROUPS.get(name, (name,))
218
+
219
+
220
+ # adjacency: type_key -> {partner_type: (why, requires_set)}; built once at import.
221
+ _ADJ: dict[str, dict[str, tuple[str, frozenset]]] = {}
222
+
223
+
224
+ def _add(a: str, b: str, why: str, requires: frozenset) -> None:
225
+ cur = _ADJ.setdefault(a, {}).get(b)
226
+ # keep the first reason but union requirements (a pair may appear via several rows)
227
+ if cur is None:
228
+ _ADJ[a][b] = (why, requires)
229
+ else:
230
+ _ADJ[a][b] = (cur[0], cur[1] | requires)
231
+
232
+
233
+ for _a, _b, _why, _req in _SPEC:
234
+ _reqset = frozenset(t for r in _req for t in _expand(r))
235
+ for ea in _expand(_a):
236
+ for eb in _expand(_b):
237
+ _add(ea, eb, _why, _reqset)
238
+ if ea != eb:
239
+ _add(eb, ea, _why, _reqset)
240
+
241
+
242
+ def partners_for(type_key: str) -> list[Partner]:
243
+ """Every element type `type_key` can meaningfully connect to, required ones first."""
244
+ out = [Partner(p, why, type_key in req)
245
+ for p, (why, req) in _ADJ.get(type_key, {}).items()]
246
+ out.sort(key=lambda p: (not p.required, p.type_key))
247
+ return out
248
+
249
+
250
+ def partner_types(type_key: str) -> set[str]:
251
+ """Just the set of compatible partner type keys (for fast highlight tests)."""
252
+ return set(_ADJ.get(type_key, {}).keys())
253
+
254
+
255
+ def can_connect(a: str, b: str) -> str | None:
256
+ """The teaching reason for an a–b link, or None if it isn't a recommended connection."""
257
+ edge = _ADJ.get(a, {}).get(b)
258
+ return edge[0] if edge else None
259
+
260
+
261
+ def required_partners(type_key: str) -> list[Partner]:
262
+ """Partner types this element *needs* at least one of to function."""
263
+ return [p for p in partners_for(type_key) if p.required]
264
+
265
+
266
+ def missing_required(type_key: str, neighbor_types) -> list[Partner]:
267
+ """Required partners that are absent from `neighbor_types` (drives lint / hints)."""
268
+ have = set(neighbor_types)
269
+ return [p for p in required_partners(type_key) if p.type_key not in have]
@@ -0,0 +1,153 @@
1
+ """DOs / DON'Ts for a described mission — the student's explicit *negative* intent.
2
+
3
+ The composer must honour "…but no metrics or dashboards": otherwise the auto-filled enrichment layers
4
+ put things on the board the student explicitly refused. We extract an `Excludes` set (roles / element
5
+ types / layers to leave OUT) two ways and merge them:
6
+
7
+ • a deterministic **negation scan** of the intent text (reliable, model-free) — catches "no X",
8
+ "without X", "don't add X", "skip X", across an "and" list;
9
+ • the reasoning model's own `exclude:` list (belt-and-suspenders, mapped through the same table).
10
+
11
+ Only *optional* things are excludable (the enrichment layers + their elements) — core network pieces
12
+ (firewall, router, switch, host, internet, LAN) are deliberately NOT here, so "hide it from the
13
+ Internet" can never be misread as "remove the Internet". Assembly then suppresses excluded companions,
14
+ and `compose` reports an **infeasibility** when a thing the student asked FOR inherently needs a thing
15
+ they asked to leave out. Pure data; prune/extend the table as we learn.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ from dataclasses import dataclass, field
21
+
22
+ from .capabilities import ancestors
23
+ from .objectives import element_types_in_check
24
+
25
+ # keyword (substring, lower) -> (roles, element type_keys, assembly layers) it refers to
26
+ _EXCLUDE_TABLE: dict[str, tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...]]] = {
27
+ "metric": (("metrics-source", "metrics-collector"), ("metrics",), ("observe",)),
28
+ "dashboard": (("dashboard-view", "visualizer"), ("dashboard",), ("observe",)),
29
+ "grafana": (("dashboard-view", "visualizer"), ("dashboard",), ("observe",)),
30
+ "monitor": (("visualizer", "metrics-source"), ("metrics", "dashboard"), ("observe",)),
31
+ "observab": (("visualizer", "metrics-source"), ("metrics", "dashboard"), ("observe",)),
32
+ "visuali": (("visualizer",), (), ("observe",)),
33
+ "telemetry": (("metrics-source", "metrics-collector"), ("metrics",), ("observe",)),
34
+ "load generat": (("load-generator",), (), ("exercise",)),
35
+ "load-generat": (("load-generator",), (), ("exercise",)),
36
+ "load gen": (("load-generator",), (), ("exercise",)),
37
+ "load test": (("load-generator",), (), ("exercise",)),
38
+ "load-test": (("load-generator",), (), ("exercise",)),
39
+ "traffic gen": (("traffic-source",), (), ("exercise",)),
40
+ # "nothing else / no extras / just the network" — leave out ALL enrichment
41
+ "nothing else": ((), (), ("exercise", "observe")),
42
+ "no extras": ((), (), ("exercise", "observe")),
43
+ "no extra": ((), (), ("exercise", "observe")),
44
+ }
45
+
46
+ # negation cues; the excluded span runs from the cue to the next clause boundary (so "no metrics and
47
+ # dashboards" captures both, but "no metrics but add a router" stops the exclusion at "but").
48
+ _NEG = re.compile(r"\b(no|without|not|don'?t|do not|avoid|skip|exclude|omit|leave out|minus|"
49
+ r"except|sans)\b", re.I)
50
+ _STOP = re.compile(r"[.;,]|\b(but|however|while|yet|although|though|and then)\b", re.I)
51
+
52
+
53
+ @dataclass
54
+ class Excludes:
55
+ roles: set[str] = field(default_factory=set)
56
+ types: set[str] = field(default_factory=set)
57
+ layers: set[str] = field(default_factory=set)
58
+
59
+ def __bool__(self) -> bool:
60
+ return bool(self.roles or self.types or self.layers)
61
+
62
+ def label(self) -> str:
63
+ """A short human phrase for what was excluded (for the infeasibility / suppression note)."""
64
+ bits = sorted(self.types) or sorted(self.layers) or sorted(self.roles)
65
+ return ", ".join(bits)
66
+
67
+
68
+ def _add_keyword(ex: Excludes, kw: str) -> None:
69
+ roles, types, layers = _EXCLUDE_TABLE[kw]
70
+ ex.roles.update(roles)
71
+ ex.types.update(types)
72
+ ex.layers.update(layers)
73
+
74
+
75
+ def _negated_text(text: str) -> str:
76
+ """The concatenation of every span under a negation cue."""
77
+ spans = []
78
+ for m in _NEG.finditer(text or ""):
79
+ rest = text[m.end():]
80
+ stop = _STOP.search(rest)
81
+ spans.append(rest[:stop.start()] if stop else rest)
82
+ return " ".join(spans).lower()
83
+
84
+
85
+ def positive_text(text: str) -> str:
86
+ """The intent with every negated span removed — so a coverage/keyword match can't be triggered by
87
+ a thing the student said they DON'T want ("no dashboards" must not pull in a dashboard)."""
88
+ out, idx = [], 0
89
+ for m in _NEG.finditer(text or ""):
90
+ out.append(text[idx:m.start()])
91
+ rest = text[m.end():]
92
+ stop = _STOP.search(rest)
93
+ idx = m.end() + (stop.start() if stop else len(rest))
94
+ out.append(text[idx:])
95
+ return " ".join(" ".join(out).split())
96
+
97
+
98
+ def from_text(text: str) -> Excludes:
99
+ """Extract DON'Ts from free-form intent by scanning only the NEGATED spans (model-free)."""
100
+ ex = Excludes()
101
+ neg = _negated_text(text)
102
+ for kw in _EXCLUDE_TABLE:
103
+ if kw in neg:
104
+ _add_keyword(ex, kw)
105
+ return ex
106
+
107
+
108
+ def from_terms(terms) -> Excludes:
109
+ """Map an explicit exclude list (e.g. from the model) through the same table — here the terms are
110
+ already known to be exclusions, so no negation scan is needed."""
111
+ ex = Excludes()
112
+ for t in terms or []:
113
+ low = str(t).lower()
114
+ for kw in _EXCLUDE_TABLE:
115
+ if kw in low:
116
+ _add_keyword(ex, kw)
117
+ return ex
118
+
119
+
120
+ def merge(*exes: Excludes) -> Excludes:
121
+ out = Excludes()
122
+ for e in exes:
123
+ out.roles |= e.roles
124
+ out.types |= e.types
125
+ out.layers |= e.layers
126
+ return out
127
+
128
+
129
+ def fragment_excluded(fragment, ex: Excludes) -> bool:
130
+ """Should this fragment be kept OUT under `ex`? True if its layer is excluded, it provides an
131
+ excluded capability (matched up the is-a hierarchy), or its objectives place an excluded element."""
132
+ if not ex:
133
+ return False
134
+ if fragment.layer in ex.layers:
135
+ return True
136
+ for p in fragment.provides:
137
+ if ancestors(p) & ex.roles:
138
+ return True
139
+ for t in fragment.objectives:
140
+ if any(et in ex.types for et in element_types_in_check(getattr(t, "check", "") or "")):
141
+ return True
142
+ return False
143
+
144
+
145
+ def objective_conflicts(objectives, ex: Excludes) -> list[str]:
146
+ """Excluded element types that STILL appear in the assembled objectives — i.e. a requested core
147
+ inherently needs something the student asked to leave out (an infeasibility)."""
148
+ bad: list[str] = []
149
+ for o in objectives:
150
+ for et in element_types_in_check(getattr(o, "check", "") or ""):
151
+ if et in ex.types and et not in bad:
152
+ bad.append(et)
153
+ return bad
gini/domain/content.py ADDED
@@ -0,0 +1,59 @@
1
+ """Where fragment content lives on disk — the system layer and the user layer.
2
+
3
+ Two content roots, so gBuilder can be a packaged app (no source tree) AND have somewhere for
4
+ teacher-authored / OTA-pulled fragments to land:
5
+
6
+ * SYSTEM layer — read-only, ships with the app. The built-in fragments. From source this is the
7
+ package dir; packaged it's bundled package data (resolved via importlib.resources, which works
8
+ from source, a wheel, or a bundle). Moves only with a software update.
9
+ * USER layer — writable, ``~/.gini/content/fragments``. Teacher-authored fragments and content
10
+ pulled over the air from the Teaching Center. Overlays the system layer: a user fragment with the
11
+ same id as a built-in wins (so a course can override), but usually it just adds new ones.
12
+
13
+ `ENGINE_VERSION` is the vocabulary version a fragment is authored/validated against; a fragment YAML
14
+ stamps it so the Teaching Center and each student client can refuse-with-reason on a version gap
15
+ rather than mis-compose. See GINI_AUTHORING_DESIGN.md.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ from pathlib import Path
21
+
22
+ # The engine's vocabulary version. Bump when the set of primitives (elements / predicates / probes /
23
+ # capabilities) changes in a way that content must be re-validated against. Authored fragments stamp
24
+ # the version they were blessed on.
25
+ ENGINE_VERSION = "6.0"
26
+ FRAGMENT_SCHEMA = 1 # the fragment YAML shape; bump on breaking schema changes
27
+
28
+
29
+ def _home() -> Path:
30
+ # Same rule as app.paths.gini_home, replicated so `domain` stays free of an `app` import.
31
+ return Path(os.environ.get("GINI_HOME_DIR") or (Path.home() / ".gini")).expanduser()
32
+
33
+
34
+ def system_content_dir() -> Path:
35
+ """The bundled built-in fragments — packaging-safe."""
36
+ try:
37
+ from importlib.resources import files
38
+ p = Path(str(files("gini.domain"))) / "missions" / "networking"
39
+ if p.exists():
40
+ return p
41
+ except Exception: # noqa: BLE001 — fall back to __file__
42
+ pass
43
+ return Path(__file__).parent / "missions" / "networking"
44
+
45
+
46
+ def user_content_dir() -> Path:
47
+ """Where authored / OTA fragments live. Created on demand (never at import)."""
48
+ return _home() / "content" / "fragments"
49
+
50
+
51
+ def ensure_user_content_dir() -> Path:
52
+ d = user_content_dir()
53
+ d.mkdir(parents=True, exist_ok=True)
54
+ return d
55
+
56
+
57
+ def content_dirs() -> list[Path]:
58
+ """System first, user second — user overlays system when ids collide."""
59
+ return [system_content_dir(), user_content_dir()]
@@ -0,0 +1,89 @@
1
+ """The two ways the CPU changes what it's doing in xv6 — as ordered, steppable stages.
2
+
3
+ Students constantly conflate a *system call* (a TRAP: same process, user<->kernel, saves the
4
+ TRAPFRAME) with a *context switch* (swtch: a different process, kernel<->kernel, saves the
5
+ CONTEXT). And a *preemption* is both, nested. This module is the pure data behind the step-driven
6
+ "CPU journey" view: each stage says which privilege band and which process lane the CPU is in,
7
+ and which save-area is being written/read — so the difference becomes visible one step at a time.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+
14
+ @dataclass
15
+ class Stage:
16
+ title: str
17
+ band: str # "user" | "kernel"
18
+ lane: str # "A" (running proc) | "sched" | "B" (next proc)
19
+ save: str # "trapframe" | "context" | "" (which save-area this stage touches)
20
+ caption: str
21
+
22
+
23
+ # A system call: one process, dips into the kernel and back. Save unit = trapframe.
24
+ SYSCALL = [
25
+ Stage("ecall", "user", "A", "",
26
+ "The user program puts the syscall number in a7 and args in a0–a5, then executes "
27
+ "`ecall` — a deliberate trap into the kernel."),
28
+ Stage("uservec", "kernel", "A", "trapframe",
29
+ "Hardware jumps to uservec (trampoline). It saves ALL user registers into this "
30
+ "process's trapframe and switches to the kernel page table."),
31
+ Stage("usertrap", "kernel", "A", "",
32
+ "usertrap() sees scause = 8 (a syscall), advances the saved epc past the ecall, "
33
+ "enables interrupts, and calls syscall()."),
34
+ Stage("syscall()", "kernel", "A", "",
35
+ "syscall() reads a7, dispatches syscalls[a7] (e.g. sys_fork), and stores the result "
36
+ "back into the trapframe's a0."),
37
+ Stage("userret", "kernel", "A", "trapframe",
38
+ "userret restores the user registers FROM the trapframe, switches back to the user "
39
+ "page table, and executes sret."),
40
+ Stage("resume", "user", "A", "",
41
+ "The SAME process resumes right after its ecall, with the return value in a0. No other "
42
+ "process ran. Privilege went U → S → U."),
43
+ ]
44
+
45
+ # A context switch: kernel thread of A hands the CPU to B via the scheduler. Save unit = context.
46
+ CONTEXT = [
47
+ Stage("sched()", "kernel", "A", "",
48
+ "Process A's kernel thread gives up the CPU (yield or sleep) and calls sched(), which "
49
+ "calls swtch()."),
50
+ Stage("swtch → sched", "kernel", "A", "context",
51
+ "swtch saves A's 14 callee-saved registers into A's context and loads the scheduler's "
52
+ "context. The CPU is now running scheduler()."),
53
+ Stage("scheduler()", "kernel", "sched", "",
54
+ "The per-CPU scheduler loop scans proc[] and picks the next RUNNABLE process, B, and "
55
+ "marks it RUNNING."),
56
+ Stage("swtch → B", "kernel", "B", "context",
57
+ "swtch saves the scheduler's context and loads B's context. The CPU is now running B's "
58
+ "kernel thread, exactly where B last called swtch."),
59
+ Stage("B resumes", "kernel", "B", "",
60
+ "B returns up through its own kernel path. A DIFFERENT process now has the CPU. "
61
+ "Privilege stayed in S the whole time — no user/kernel crossing."),
62
+ ]
63
+
64
+ # Preemption: a timer TRAP that triggers a context SWITCH — both mechanisms, nested.
65
+ PREEMPT = [
66
+ Stage("timer trap", "kernel", "A", "trapframe",
67
+ "A timer interrupt traps process A into the kernel — uservec saves A's trapframe. This "
68
+ "is a TRAP, just like a syscall."),
69
+ Stage("yield()", "kernel", "A", "",
70
+ "usertrap sees a timer (which_dev == 2) and, once the time-slice is up, calls yield() "
71
+ "→ sched()."),
72
+ Stage("swtch → sched", "kernel", "A", "context",
73
+ "swtch saves A's CONTEXT and enters the scheduler — a context switch now happens INSIDE "
74
+ "the trap. Two different save-areas, one event."),
75
+ Stage("swtch → B", "kernel", "B", "context",
76
+ "The scheduler picks B and swtch loads B's context."),
77
+ Stage("userret (B)", "kernel", "B", "trapframe",
78
+ "B eventually returns to user via userret, restoring B's TRAPFRAME."),
79
+ Stage("resume B", "user", "B", "",
80
+ "A DIFFERENT process resumes in user mode. Preemption = a trap (trapframe) wrapping a "
81
+ "context switch (context)."),
82
+ ]
83
+
84
+ JOURNEYS = {"syscall": SYSCALL, "context": CONTEXT, "preempt": PREEMPT}
85
+ JOURNEY_TITLES = {
86
+ "syscall": "System call (trap · same process)",
87
+ "context": "Context switch (swtch · different process)",
88
+ "preempt": "Preemption (trap + context switch)",
89
+ }