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
gini/domain/recipes.py ADDED
@@ -0,0 +1,738 @@
1
+ """Recipes — curated, guaranteed-to-work blueprints the agent lays out on the canvas.
2
+
3
+ The safety model: the LLM only *selects and explains* recipes (matching the student's
4
+ intent against `intent` tags + the summary); the actual building is this deterministic
5
+ data + `GiniAPI.apply_recipe`. So even a small local model can't produce a broken topology
6
+ — it just picks from known-good blueprints. With no model at all the recipes are still
7
+ browsable by tag.
8
+
9
+ Each recipe is a set of typed elements (a local `ref` for linking, a grid position for
10
+ layout, an optional one-line `why`, and an optional `parent` ref for box containment —
11
+ VPC / Subnet / Region) plus grammar-valid links. The set covers every palette element
12
+ except the hidden `k8s_node` (see `covered_elements()` and the test).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class RecipeElement:
21
+ ref: str # local key, used by `links` / `parent`
22
+ type_key: str # palette element type
23
+ props: dict = field(default_factory=dict)
24
+ col: int = 0 # layout grid column
25
+ row: int = 0 # layout grid row
26
+ why: str = "" # one-line teaching reason (narration)
27
+ parent: str = "" # ref of the box (vpc/cloud_subnet/region) this sits in
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Recipe:
32
+ id: str
33
+ name: str
34
+ summary: str # one line the LLM/UX shows
35
+ intent: tuple[str, ...] # keywords the LLM/offline matcher scores against
36
+ teaches: str
37
+ elements: tuple[RecipeElement, ...]
38
+ links: tuple[tuple[str, str], ...] = ()
39
+ concept: str = "" # related concepts.Concept.key
40
+
41
+
42
+ def _e(ref, type_key, why="", *, col=0, row=0, props=None, parent=""):
43
+ return RecipeElement(ref, type_key, props=props or {}, col=col, row=row,
44
+ why=why, parent=parent)
45
+
46
+
47
+ RECIPES: tuple[Recipe, ...] = (
48
+ # ---- networking plane ------------------------------------------------- #
49
+ Recipe(
50
+ id="lan", name="A basic switched LAN",
51
+ summary="Two machines on one subnet through a switch, with a router as their gateway.",
52
+ intent=("lan", "switch", "router", "subnet", "network", "basic", "ethernet"),
53
+ teaches="Layer-2 switching within a subnet and a Layer-3 router as the gateway",
54
+ concept="networking-basics",
55
+ elements=(
56
+ _e("m1", "host", "An end machine on the LAN.", col=0, row=0),
57
+ _e("m2", "host", "A second machine on the same subnet.", col=0, row=1),
58
+ _e("s1", "switch", "Switch linking the machines in one subnet.", col=1, row=0),
59
+ _e("r1", "router", "Gateway off the LAN to other subnets.", col=2, row=0),
60
+ ),
61
+ links=(("m1", "s1"), ("m2", "s1"), ("s1", "r1")),
62
+ ),
63
+ Recipe(
64
+ id="gui_desktop", name="A graphical Linux host",
65
+ summary="A headful Desktop machine on the LAN — open its screen and browse a web server "
66
+ "another machine is serving.",
67
+ intent=("desktop", "gui", "graphical", "headful", "browser", "x11", "vnc", "linux desktop",
68
+ "point and click"),
69
+ teaches="a graphical host on the network — point-and-click Linux inside a topology",
70
+ concept="networking-basics",
71
+ elements=(
72
+ _e("gui", "desktop", "The graphical host — double-click to open its desktop over noVNC.",
73
+ col=0, row=0),
74
+ _e("s1", "switch", "The LAN switch.", col=1, row=0),
75
+ _e("m1", "host", "A machine that can serve a page for the desktop's browser to fetch.",
76
+ col=2, row=0),
77
+ ),
78
+ links=(("gui", "s1"), ("m1", "s1")),
79
+ ),
80
+ Recipe(
81
+ id="wifi_lan", name="A Wi-Fi LAN",
82
+ summary="A wireless client joining the wired LAN through an access point.",
83
+ intent=("wifi", "wireless", "access point", "wap", "mobile", "802.11"),
84
+ teaches="bridging a wireless segment into a wired LAN",
85
+ concept="networking-basics",
86
+ elements=(
87
+ _e("m1", "host", "A wireless client.", col=0, row=0),
88
+ _e("ap", "wap", "Access point bridging Wi-Fi into the wired LAN.", col=1, row=0),
89
+ _e("s1", "switch", "Wired switch behind the AP.", col=2, row=0),
90
+ _e("r1", "router", "Gateway off the LAN.", col=3, row=0),
91
+ ),
92
+ links=(("m1", "ap"), ("ap", "s1"), ("s1", "r1")),
93
+ ),
94
+ Recipe(
95
+ id="hub_demo", name="Hub vs switch (collision domain)",
96
+ summary="A Layer-1 hub shared segment (watch flooding) bridged into a switched LAN.",
97
+ intent=("hub", "collision", "broadcast", "flooding", "repeater", "layer 1", "shared"),
98
+ teaches="why a hub floods and a switch doesn't",
99
+ concept="networking-basics",
100
+ elements=(
101
+ _e("m1", "host", "One machine sharing the medium.", col=0, row=0),
102
+ _e("m2", "host", "Another machine in the same collision domain.", col=0, row=1),
103
+ _e("h1", "hub", "Layer-1 hub — floods every frame to all ports.", col=1, row=0),
104
+ _e("s1", "switch", "Switch the shared segment bridges into.", col=2, row=0),
105
+ ),
106
+ links=(("m1", "h1"), ("m2", "h1"), ("h1", "s1")),
107
+ ),
108
+ Recipe(
109
+ id="internet_access", name="LAN to the Internet (NAT + firewall)",
110
+ summary="A LAN reaching the Internet through a router, a firewall, and NAT.",
111
+ intent=("internet", "nat", "egress", "firewall", "outbound", "gateway", "wan"),
112
+ teaches="how egress flows through the drawn path and a firewall guards the edge",
113
+ concept="internet-nat",
114
+ elements=(
115
+ _e("m1", "host", "A machine that needs the Internet.", col=0, row=0),
116
+ _e("s1", "switch", "LAN switch.", col=1, row=0),
117
+ _e("r1", "router", "Routes the LAN toward the edge.", col=2, row=0),
118
+ _e("fw", "firewall", "Filters traffic at the trust boundary.", col=3, row=0),
119
+ _e("net", "cloud", "The Internet / NAT gateway (faithful egress).", col=4, row=0),
120
+ ),
121
+ links=(("m1", "s1"), ("s1", "r1"), ("r1", "fw"), ("fw", "net")),
122
+ ),
123
+ Recipe(
124
+ id="sdn", name="OpenFlow SDN",
125
+ summary="Two hosts on an OpenFlow switch programmed by a controller.",
126
+ intent=("sdn", "openflow", "controller", "flow", "software defined", "pox", "ovs"),
127
+ teaches="the control/data-plane split and reactive flow installation",
128
+ concept="sdn",
129
+ elements=(
130
+ _e("m1", "host", "End host on the SDN fabric.", col=0, row=0),
131
+ _e("m2", "host", "Second host across the fabric.", col=0, row=1),
132
+ _e("ovs", "ovs", "OpenFlow switch — no logic of its own.", col=1, row=0),
133
+ _e("c1", "controller", "Programs the switch's flow rules reactively.", col=2, row=0),
134
+ ),
135
+ links=(("m1", "ovs"), ("m2", "ovs"), ("ovs", "c1")),
136
+ ),
137
+ Recipe(
138
+ id="nfv_chain", name="Service function chain (firewall → NAT)",
139
+ summary="A host whose traffic is steered through a firewall function and then a NAT "
140
+ "gateway on the way out — network functions chained in the forwarding path.",
141
+ intent=("nfv", "sfc", "service chain", "service function chain", "chaining", "chain",
142
+ "firewall", "nat", "middlebox", "network function", "vnf", "steering"),
143
+ teaches="inserting network functions (firewall, NAT) in series in the path (NFV/SFC)",
144
+ concept="sfc",
145
+ elements=(
146
+ _e("m1", "host", "The client whose traffic is steered through the chain.",
147
+ col=0, row=0),
148
+ _e("r1", "router", "Routes the client toward the chain and the edge.",
149
+ col=1, row=0),
150
+ _e("fw", "firewall", "Function #1 — filters traffic in the path.", col=2, row=0),
151
+ _e("net", "cloud", "Function #2 — NAT gateway to the Internet (the chain's egress).",
152
+ col=3, row=0),
153
+ ),
154
+ links=(("m1", "r1"), ("r1", "fw"), ("fw", "net")),
155
+ ),
156
+ Recipe(
157
+ id="sfc_container", name="Container VNF service chain",
158
+ summary="A host whose traffic passes through a firewall VNF then an IDS VNF "
159
+ "(containers inserted in the path) before routing out to the Internet.",
160
+ intent=("vnf", "sfc", "service function chain", "service chain", "nfv", "chain",
161
+ "firewall", "ids", "middlebox", "container vnf", "network function"),
162
+ teaches="chaining container VNFs (firewall -> IDS) inline in the forwarding path",
163
+ concept="sfc",
164
+ elements=(
165
+ _e("m1", "host", "The client whose traffic is steered through the chain.",
166
+ col=0, row=0),
167
+ _e("fw", "vnf", "VNF #1 — a firewall container filtering the traffic.",
168
+ props={"Kind": "firewall", "Rules": "deny 10.0.9.0/24"}, col=1, row=0),
169
+ _e("ids", "vnf", "VNF #2 — an IDS container inspecting the traffic.",
170
+ props={"Kind": "ids"}, col=2, row=0),
171
+ _e("r1", "router", "Routes the chain's egress toward the Internet.", col=3, row=0),
172
+ _e("net", "cloud", "The Internet (the chain's egress).", col=4, row=0),
173
+ ),
174
+ links=(("m1", "fw"), ("fw", "ids"), ("ids", "r1"), ("r1", "net")),
175
+ ),
176
+ # ---- serverless & event-driven --------------------------------------- #
177
+ Recipe(
178
+ id="serverless", name="Serverless API (gateway + function + storage)",
179
+ summary="An API gateway invoking a function that stores data and can fire on queue events.",
180
+ intent=("serverless", "faas", "lambda", "function", "api gateway", "cloud function",
181
+ "gateway"),
182
+ teaches="stateless functions behind an API gateway, with storage and event triggers",
183
+ concept="serverless",
184
+ elements=(
185
+ _e("gw", "api_gateway", "The front door — routes a URL path to the function.",
186
+ col=0, row=0),
187
+ _e("f1", "function", "Stateless code that runs per request and scales to zero.",
188
+ col=1, row=0),
189
+ _e("obj", "object_store", "Durable storage for the function's data/objects.",
190
+ col=2, row=0),
191
+ _e("q1", "queue", "Also triggers the function from messages (event-driven).",
192
+ col=1, row=1),
193
+ ),
194
+ links=(("gw", "f1"), ("f1", "obj"), ("q1", "f1")),
195
+ ),
196
+ Recipe(
197
+ id="message_queue", name="Message queue (producer -> consumer)",
198
+ summary="A producer enqueuing work that a function consumes asynchronously.",
199
+ intent=("message queue", "queue", "rabbitmq", "async", "producer", "consumer", "job",
200
+ "decouple"),
201
+ teaches="asynchronous decoupling — a work queue between a producer and a consumer",
202
+ concept="messaging-queue",
203
+ elements=(
204
+ _e("w1", "web_app", "Producer — enqueues work without waiting.", col=0, row=0),
205
+ _e("q1", "queue", "Buffers messages; each goes to one consumer.", col=1, row=0),
206
+ _e("f1", "function", "Consumer — triggered to drain the queue.", col=2, row=0),
207
+ ),
208
+ links=(("w1", "q1"), ("q1", "f1")),
209
+ ),
210
+ Recipe(
211
+ id="streaming", name="Event stream",
212
+ summary="An app producing to an event stream that a function consumes.",
213
+ intent=("stream", "streaming", "kafka", "event log", "event sourcing", "redpanda"),
214
+ teaches="an ordered, replayable event log with independent consumers",
215
+ concept="messaging-queue",
216
+ elements=(
217
+ _e("w1", "web_app", "Produces events onto the log.", col=0, row=0),
218
+ _e("st", "stream", "Ordered, replayable event log.", col=1, row=0),
219
+ _e("f1", "function", "Consumes the stream independently.", col=2, row=0),
220
+ ),
221
+ links=(("w1", "st"), ("st", "f1")),
222
+ ),
223
+ Recipe(
224
+ id="pubsub", name="Publish / subscribe",
225
+ summary="A publisher fanning messages out to a subscribing function.",
226
+ intent=("pubsub", "pub/sub", "publish", "subscribe", "nats", "fan out", "messaging"),
227
+ teaches="pub/sub fan-out to many subscribers",
228
+ concept="messaging-queue",
229
+ elements=(
230
+ _e("w1", "web_app", "Publisher.", col=0, row=0),
231
+ _e("msg", "messaging", "Fans each message out to all subscribers.", col=1, row=0),
232
+ _e("f1", "function", "A subscriber, triggered on publish.", col=2, row=0),
233
+ ),
234
+ links=(("w1", "msg"), ("msg", "f1")),
235
+ ),
236
+ # ---- web apps, data, LB, proxy --------------------------------------- #
237
+ Recipe(
238
+ id="web_3tier", name="Load-balanced web app + data",
239
+ summary="A load balancer fronting a web app backed by a database and a cache.",
240
+ intent=("web app", "load balancer", "three tier", "3-tier", "database", "cache",
241
+ "scale"),
242
+ teaches="load balancing across an app tier backed by a database and a cache",
243
+ concept="load-balancing",
244
+ elements=(
245
+ _e("lb", "load_balancer", "Spreads traffic across the app.", col=0, row=0),
246
+ _e("w1", "web_app", "The application tier.", col=1, row=0),
247
+ _e("db", "database", "Relational store for app state.", col=2, row=0),
248
+ _e("ca", "cache", "In-memory cache in front of the database.", col=2, row=1),
249
+ ),
250
+ links=(("lb", "w1"), ("w1", "db"), ("w1", "ca")),
251
+ ),
252
+ Recipe(
253
+ id="reverse_proxy", name="Reverse proxy (routing / TLS)",
254
+ summary="A reverse proxy fronting a web service.",
255
+ intent=("reverse proxy", "proxy", "traefik", "tls", "ingress", "routing"),
256
+ teaches="path routing and TLS termination in front of a service",
257
+ concept="load-balancing",
258
+ elements=(
259
+ _e("px", "proxy", "Reverse proxy for path routing and TLS.", col=0, row=0),
260
+ _e("w1", "web_app", "The service behind the proxy.", col=1, row=0),
261
+ ),
262
+ links=(("px", "w1"),),
263
+ ),
264
+ Recipe(
265
+ id="load_test", name="Load-test rig",
266
+ summary="A load generator firing HTTP traffic at a web app while metrics are scraped.",
267
+ intent=("load", "stress", "benchmark", "performance", "throughput", "latency",
268
+ "test", "experiment", "traffic", "qps"),
269
+ teaches="generating controlled load and reading QPS / latency results",
270
+ concept="load-balancing",
271
+ elements=(
272
+ _e("gen", "load_generator", "Fires HTTP load at the target.", col=0, row=0),
273
+ _e("app", "web_app", "The service under test.", col=1, row=0),
274
+ _e("met", "metrics", "Scrapes throughput/latency while it runs.", col=1, row=1),
275
+ ),
276
+ links=(("gen", "app"), ("met", "app")),
277
+ ),
278
+ Recipe(
279
+ id="observability", name="Live observability stack",
280
+ summary="Metrics + dashboard + tracing watching a web app.",
281
+ intent=("observe", "monitor", "visualize", "visualization", "metrics", "dashboard",
282
+ "grafana", "prometheus", "tracing", "jaeger", "telemetry"),
283
+ teaches="scrape metrics -> dashboard, distributed tracing, driven under load",
284
+ concept="observability",
285
+ elements=(
286
+ _e("gen", "load_generator", "Drives traffic so there's something to observe.",
287
+ col=0, row=0),
288
+ _e("app", "web_app", "The service being observed.", col=0, row=1),
289
+ _e("prom", "metrics", "Scrapes numeric metrics from the service.", col=1, row=1),
290
+ _e("dash", "dashboard", "Visualises the metrics.", col=2, row=0),
291
+ _e("tr", "tracing", "Collects distributed traces.", col=1, row=2),
292
+ ),
293
+ links=(("gen", "app"), ("prom", "app"), ("dash", "prom"), ("tr", "app")),
294
+ ),
295
+ Recipe(
296
+ id="nosql_app", name="NoSQL-backed app",
297
+ summary="A web app backed by a NoSQL document store.",
298
+ intent=("nosql", "mongo", "document", "flexible schema", "no-sql"),
299
+ teaches="when a document store fits better than a relational one",
300
+ concept="datastores",
301
+ elements=(
302
+ _e("w1", "web_app", "App with a document data model.", col=0, row=0),
303
+ _e("ndb", "nosql", "Document store (Mongo-style).", col=1, row=0),
304
+ ),
305
+ links=(("w1", "ndb"),),
306
+ ),
307
+ Recipe(
308
+ id="container_app", name="Containerised service + DB",
309
+ summary="A container backed by a database.",
310
+ intent=("container", "docker", "microservice", "image"),
311
+ teaches="running a single containerised process with its own store",
312
+ concept="cloud-compute",
313
+ elements=(
314
+ _e("ct", "container", "A single containerised process.", col=0, row=0),
315
+ _e("db", "database", "Its relational store.", col=1, row=0),
316
+ ),
317
+ links=(("ct", "db"),),
318
+ ),
319
+ Recipe(
320
+ id="instance_disk", name="VM instance with a persistent disk",
321
+ summary="An instance with a persistent block volume attached.",
322
+ intent=("instance", "vm", "block volume", "disk", "ebs", "persistent", "virtual machine"),
323
+ teaches="attaching durable block storage to a VM-style instance",
324
+ concept="cloud-compute",
325
+ elements=(
326
+ _e("i1", "instance", "A VM-style workload.", col=0, row=0),
327
+ _e("bv", "block_volume", "A persistent disk attached to it.", col=1, row=0),
328
+ ),
329
+ links=(("i1", "bv"),),
330
+ ),
331
+ # ---- Kubernetes ------------------------------------------------------- #
332
+ Recipe(
333
+ id="kubernetes", name="Kubernetes with autoscaling",
334
+ summary="A cluster running an autoscaled Pod that pulls from a private registry.",
335
+ intent=("kubernetes", "k8s", "pod", "cluster", "hpa", "autoscale", "registry",
336
+ "replicas", "orchestration"),
337
+ teaches="Pods in a cluster, a Pod Autoscaler (HPA), and an image registry",
338
+ concept="kubernetes",
339
+ elements=(
340
+ _e("k1", "k8s_cluster", "The Kubernetes cluster (k3s).", col=0, row=0),
341
+ _e("p1", "pod", "A Pod (Deployment) running your image.", col=1, row=0),
342
+ _e("hpa", "instance_group", "Pod Autoscaler — scales replicas on CPU.", col=2, row=0),
343
+ _e("reg", "registry", "Private image registry for the cluster.", col=0, row=1),
344
+ ),
345
+ links=(("p1", "k1"), ("p1", "hpa"), ("k1", "reg")),
346
+ ),
347
+ # ---- cloud networking ------------------------------------------------- #
348
+ Recipe(
349
+ id="vpc_public_private", name="VPC with public & private subnets",
350
+ summary="A VPC where a public web tier reaches a private database the Internet can't.",
351
+ intent=("vpc", "subnet", "public", "private", "isolation", "region", "cloud network",
352
+ "segmentation"),
353
+ teaches="VPC isolation and public vs private subnets",
354
+ concept="vpc-networking",
355
+ elements=(
356
+ _e("us", "region", "A region label wrapping the VPC.", col=0, row=0),
357
+ _e("vpc", "vpc", "An isolated network with its own CIDR.", col=0, row=0, parent="us"),
358
+ _e("pub", "cloud_subnet", "Public subnet — its members get Internet.",
359
+ props={"Tier": "public"}, col=0, row=1, parent="vpc"),
360
+ _e("priv", "cloud_subnet", "Private subnet — no Internet, VPC-internal only.",
361
+ props={"Tier": "private"}, col=1, row=1, parent="vpc"),
362
+ _e("w1", "web_app", "Public web tier.", col=0, row=2, parent="pub"),
363
+ _e("db", "database", "Private database — reachable by the web tier, not outside.",
364
+ col=1, row=2, parent="priv"),
365
+ ),
366
+ links=(("w1", "db"),),
367
+ ),
368
+ Recipe(
369
+ id="vpc_gateway", name="VPC Internet gateway",
370
+ summary="A gateway giving a VPC outbound access to the Internet.",
371
+ intent=("gateway", "internet gateway", "igw", "vpc egress", "outbound"),
372
+ teaches="giving a private VPC controlled outbound Internet",
373
+ concept="vpc-networking",
374
+ elements=(
375
+ _e("gw", "gateway", "Gives the VPC outbound Internet.", col=1, row=0),
376
+ _e("vpc", "vpc", "The private network being connected.", col=0, row=0),
377
+ _e("net", "cloud", "The public Internet.", col=2, row=0),
378
+ ),
379
+ links=(("gw", "vpc"), ("gw", "net")),
380
+ ),
381
+ Recipe(
382
+ id="security_groups", name="Least-privilege security groups",
383
+ summary="A web app open to the world and a database reachable only from that web app.",
384
+ intent=("security group", "firewall", "least privilege", "ingress", "default deny",
385
+ "port", "lock down", "segmentation"),
386
+ teaches="default-deny security groups and referencing one group from another",
387
+ concept="security-groups",
388
+ elements=(
389
+ _e("w1", "web_app", "Public web tier.", col=0, row=0),
390
+ _e("db", "database", "Private database.", col=2, row=0),
391
+ _e("wsg", "security_group", "Opens the web tier to the world on 80.",
392
+ props={"Ingress": "80 from anywhere"}, col=0, row=1),
393
+ _e("dsg", "security_group", "Opens the DB port ONLY to the web tier.",
394
+ props={"Ingress": "5432 from web-sg"}, col=2, row=1),
395
+ ),
396
+ links=(("wsg", "w1"), ("dsg", "db"), ("w1", "db")),
397
+ ),
398
+ # ---- VM-vs-container experiment -------------------------------------- #
399
+ Recipe(
400
+ id="kata", name="VM isolation experiment (Kata)",
401
+ summary="A Kata VM workload under load, backed by a DB, measured against container overhead.",
402
+ intent=("kata", "microvm", "vm vs container", "isolation", "secure workload",
403
+ "hypervisor"),
404
+ teaches="the isolation-vs-startup trade-off of a VM-isolated workload",
405
+ concept="kata-isolation",
406
+ elements=(
407
+ _e("kv", "kinstance", "A VM-isolated workload (Kata microVM).", col=1, row=0),
408
+ _e("lg", "load_generator", "Drives load to measure it.", col=0, row=0),
409
+ _e("db", "database", "Backs the workload.", col=2, row=0),
410
+ _e("met", "metrics", "Scrapes its startup/throughput to compare.", col=1, row=1),
411
+ ),
412
+ links=(("lg", "kv"), ("kv", "db"), ("met", "kv")),
413
+ ),
414
+ # ---- OS course: a real kernel to watch ------------------------------- #
415
+ Recipe(
416
+ id="xv6_scheduler", name="Watch the CPU scheduler (xv6)",
417
+ summary="A standalone xv6 kernel you open in the Machine Lab to watch context switches.",
418
+ intent=("xv6", "os", "operating system", "kernel", "scheduler", "scheduling",
419
+ "context switch", "process", "time slice", "preemption", "machine lab"),
420
+ teaches="how a real kernel schedules processes — the process table, CPU registers and "
421
+ "kernel stack changing on each context switch",
422
+ concept="os-scheduling",
423
+ elements=(
424
+ _e("k", "xv6", "A real teaching kernel (xv6 on QEMU-RISC-V). Double-click it to "
425
+ "open the Machine Lab, slow the time-slice, and watch the scheduler run.",
426
+ col=0, row=0, props={"Timeslice": "1"}),
427
+ ),
428
+ links=(),
429
+ ),
430
+ Recipe(
431
+ id="xv6_starvation", name="Provoke starvation (xv6)",
432
+ summary="An xv6 kernel with a wide time-slice — spawn CPU-bound procs and watch one "
433
+ "hog the CPU while others wait.",
434
+ intent=("xv6", "starvation", "fairness", "unfair", "hog", "monopoly", "time slice",
435
+ "quantum", "priority", "scheduling"),
436
+ teaches="how a large time-slice (or an unfair policy) lets one process monopolize the "
437
+ "CPU while others starve",
438
+ concept="os-scheduling",
439
+ elements=(
440
+ _e("k", "xv6", "Open the Machine Lab, keep the wide time-slice, run a few `spin` "
441
+ "processes, and watch the Gantt strip and the RUNNABLE queue.",
442
+ col=0, row=0, props={"Timeslice": "100"}),
443
+ ),
444
+ links=(),
445
+ ),
446
+ Recipe(
447
+ id="xv6_terminal", name="An xv6 machine with a terminal + disk",
448
+ summary="An xv6 Machine wired to a Terminal (its shell console) and a Storage Volume — "
449
+ "its software peripherals (xv6 has no networking).",
450
+ intent=("xv6", "terminal", "shell", "console", "peripheral", "device", "tty",
451
+ "storage volume", "disk", "io"),
452
+ teaches="how an OS talks to devices — the console (a Terminal) and the disk are "
453
+ "peripherals, reached through drivers",
454
+ concept="os-processes",
455
+ elements=(
456
+ _e("k", "xv6", "The teaching kernel.", col=1, row=1),
457
+ _e("term", "terminal", "Type commands and watch xv6's console.", col=2, row=0),
458
+ _e("vol", "storage_volume", "The xv6 disk — open its file system.", col=1, row=2),
459
+ ),
460
+ links=(("k", "term"), ("k", "vol")),
461
+ ),
462
+ Recipe(
463
+ id="xv6_syscall", name="Add your own system call (xv6)",
464
+ summary="An xv6 kernel to extend — use the Syscall Builder to add a real system call.",
465
+ intent=("xv6", "system call", "syscall", "add a syscall", "new syscall", "kernel",
466
+ "user program", "trap", "sysproc"),
467
+ teaches="how a system call is wired through xv6 — the number, the dispatch table, the "
468
+ "kernel handler, and the user stub",
469
+ concept="os-processes",
470
+ elements=(
471
+ _e("k", "xv6", "Open the Machine Lab, click Syscall Builder, declare a call and "
472
+ "drop in a C body; GINI generates the five real xv6 edits.",
473
+ col=0, row=0, props={"Timeslice": "1"}),
474
+ ),
475
+ links=(),
476
+ ),
477
+ Recipe(
478
+ id="xv6_paging", name="Watch demand paging (xv6)",
479
+ summary="An xv6 kernel to open in the Memory face — watch page tables and a fault grow "
480
+ "the stack.",
481
+ intent=("xv6", "memory", "virtual memory", "paging", "page table", "page fault",
482
+ "demand paging", "lazy allocation", "satp", "address space"),
483
+ teaches="how virtual memory maps VA→PA and how a page fault triggers demand allocation",
484
+ concept="os-memory",
485
+ elements=(
486
+ _e("k", "xv6", "Open the Machine Lab → Memory: read the page table and the allocator, "
487
+ "then Simulate a fault to watch the stack grow.", col=0, row=0),
488
+ ),
489
+ links=(),
490
+ ),
491
+ Recipe(
492
+ id="xv6_journal", name="Watch a journal transaction (xv6)",
493
+ summary="An xv6 kernel to open in the Storage face — watch the write-ahead log fill, "
494
+ "commit, and install.",
495
+ intent=("xv6", "file system", "filesystem", "journal", "journaling", "log", "crash",
496
+ "transaction", "inode", "buffer cache", "commit"),
497
+ teaches="how the write-ahead log makes file-system writes crash-safe (all-or-nothing)",
498
+ concept="os-filesystem",
499
+ elements=(
500
+ _e("k", "xv6", "Open the Machine Lab → Storage: inspect the layout and inodes, then "
501
+ "Simulate a write to watch a transaction commit.", col=0, row=0),
502
+ ),
503
+ links=(),
504
+ ),
505
+ Recipe(
506
+ id="xv6_step_switch", name="Step one context switch (xv6)",
507
+ summary="An xv6 kernel to single-step through a context switch and watch the registers, "
508
+ "process table and kernel stack change.",
509
+ intent=("xv6", "context switch", "swtch", "step", "single step", "trap", "registers",
510
+ "kernel stack", "how does a context switch work"),
511
+ teaches="what actually changes across a single context switch — the saved registers, "
512
+ "the running process, and the kernel stack",
513
+ concept="os-scheduling",
514
+ elements=(
515
+ _e("k", "xv6", "Open the Machine Lab and use Step-switch to advance one context "
516
+ "switch at a time; read the four panels between steps.",
517
+ col=0, row=0, props={"Timeslice": "1"}),
518
+ ),
519
+ links=(),
520
+ ),
521
+ # ---- Sources / Sinks (riders: instruments that run inside a donor) ---- #
522
+ Recipe(
523
+ id="ping_capture", name="Ping and capture it",
524
+ summary="Ping one machine from another and watch the ICMP packets arrive on the receiver.",
525
+ intent=("ping", "icmp", "capture", "tcpdump", "packet", "rtt", "latency", "reachability",
526
+ "sniff", "packet view", "loss"),
527
+ teaches="how to inject ICMP traffic from a Source and observe it arrive with a Sink capture",
528
+ elements=(
529
+ _e("m1", "host", "The sender.", col=0, row=0),
530
+ _e("s1", "switch", "LAN switch.", col=1, row=0),
531
+ _e("m2", "host", "The receiver.", col=2, row=0),
532
+ _e("ping", "ping_probe", "Rides the sender — pings the receiver.", col=0, row=1),
533
+ _e("pcap", "packet_view", "Rides the receiver — watch the pings arrive.", col=2, row=1),
534
+ ),
535
+ links=(("m1", "s1"), ("m2", "s1"), ("m1", "ping"), ("m2", "pcap")),
536
+ ),
537
+ Recipe(
538
+ id="http_check", name="Probe a web service",
539
+ summary="Fire HTTP requests at a web app from a machine and read the success rate + latency.",
540
+ intent=("http", "curl", "web", "request", "probe", "2xx", "latency", "service"),
541
+ teaches="how an HTTP Source proves a service answers, reporting success rate and latency",
542
+ elements=(
543
+ _e("m1", "host", "The client machine.", col=0, row=0),
544
+ _e("web", "web_app", "The web service, reached by name (set it as the probe's Target).",
545
+ col=1, row=0),
546
+ _e("http", "http_probe", "Rides the client — requests the web app by name.",
547
+ col=0, row=1),
548
+ ),
549
+ links=(("m1", "http"),),
550
+ ),
551
+ Recipe(
552
+ id="throughput_test", name="Measure throughput (iPerf)",
553
+ summary="Drive iPerf traffic between two machines across a switch and read the bandwidth.",
554
+ intent=("iperf", "throughput", "bandwidth", "mbps", "congestion", "speed", "capacity"),
555
+ teaches="how to measure link throughput with an iPerf Client Source and Server Sink",
556
+ elements=(
557
+ _e("m1", "host", "The client.", col=0, row=0),
558
+ _e("s1", "switch", "Links the two machines.", col=1, row=0),
559
+ _e("m2", "host", "The server.", col=2, row=0),
560
+ _e("cli", "iperf_client", "Rides the client — drives traffic at the server.", col=0, row=1),
561
+ _e("srv", "iperf_server", "Rides the server — reports received throughput.", col=2, row=1),
562
+ ),
563
+ links=(("m1", "s1"), ("m2", "s1"), ("m1", "cli"), ("m2", "srv")),
564
+ ),
565
+ Recipe(
566
+ id="net_diagnostics", name="Diagnose a path (DNS · traceroute · counters)",
567
+ summary="Resolve a name, trace the path to the edge, and watch interface counters — the "
568
+ "classic diagnostic Sources and Sinks on one machine.",
569
+ intent=("dns", "dig", "resolve", "traceroute", "path", "hops", "interface", "counters",
570
+ "diagnostics", "iface"),
571
+ teaches="how DNS, traceroute and interface counters reveal what a machine sees on the network",
572
+ elements=(
573
+ _e("m1", "host", "The machine you diagnose from.", col=0, row=0),
574
+ _e("r1", "router", "The gateway to the edge.", col=1, row=0),
575
+ _e("net", "cloud", "The Internet.", col=2, row=0),
576
+ _e("dns", "dns_probe", "Rides the machine — resolves a name.", col=0, row=1),
577
+ _e("trace", "traceroute_probe", "Rides the machine — traces the path.", col=0, row=2),
578
+ _e("ifs", "iface_stats", "Rides the machine — streams rx/tx counters.", col=0, row=3),
579
+ ),
580
+ links=(("m1", "r1"), ("r1", "net"), ("m1", "dns"), ("m1", "trace"), ("m1", "ifs")),
581
+ ),
582
+ Recipe(
583
+ id="xv6_drive", name="Drive an xv6 kernel (shell + workload)",
584
+ summary="Run a custom command and spawn a scheduler workload on an xv6 Machine, over its "
585
+ "console.",
586
+ intent=("xv6", "shell", "command", "workload", "spin", "forktest", "scheduler", "process",
587
+ "os", "syscall"),
588
+ teaches="how to drive an xv6 kernel with a Shell Probe (a command) and a Workload (a process)",
589
+ concept="os-scheduling",
590
+ elements=(
591
+ _e("k", "xv6", "The xv6 Machine — open the Machine Lab to watch it react.", col=0, row=0),
592
+ _e("sh", "xv6_shell", "Rides the kernel — types a command into the console.", col=0, row=1),
593
+ _e("wl", "xv6_workload", "Rides the kernel — spawns a process to drive the scheduler.",
594
+ col=0, row=2),
595
+ ),
596
+ links=(("k", "sh"), ("k", "wl")),
597
+ ),
598
+ # ---- real hardware in the loop ---------------------------------------- #
599
+ Recipe(
600
+ id="gini32_phone", name="A real phone inside the emulated network",
601
+ summary="A GINI32 board carries a real phone (or Pi) into the topology, so it can "
602
+ "ping an emulated machine.",
603
+ intent=("gini32", "esp32", "board", "hardware", "physical", "phone", "real device",
604
+ "cyber-physical", "hardware in the loop", "wireless", "gbridge"),
605
+ teaches="hardware-in-the-loop: a real radio bridging physical devices into an "
606
+ "emulated topology over Ethernet-in-UDP",
607
+ concept="networking-basics",
608
+ elements=(
609
+ _e("gb", "gini32",
610
+ "The real board — set BoardID to the id on its label (`gini32 provision`).",
611
+ col=0, row=0, props={"Mode": "routed"}),
612
+ _e("r1", "router", "The board's devices arrive on this router's subnet.",
613
+ col=1, row=0),
614
+ _e("s1", "switch", "The LAN the emulated machine sits on.", col=2, row=0),
615
+ _e("m1", "host", "An emulated machine for the real phone to ping.",
616
+ col=3, row=0),
617
+ ),
618
+ links=(("gb", "r1"), ("r1", "s1"), ("s1", "m1")),
619
+ ),
620
+ # ---- OS Zoo — boot a real historical OS ------------------------------- #
621
+ Recipe(
622
+ id="os_zoo_freedos", name="Boot FreeDOS (OS Zoo)",
623
+ summary="A real MS-DOS-compatible OS you boot and drive from an embedded screen — the "
624
+ "command-line PC of the DOS era.",
625
+ intent=("freedos", "dos", "os zoo", "historical", "vintage", "retro", "command line",
626
+ "boot", "emulator", "old os"),
627
+ teaches="what a single-tasking, real-mode DOS PC looks like, live under emulation",
628
+ concept="os-zoo",
629
+ elements=(
630
+ _e("os", "freedos", "Double-click to open the Zoo Lab and use FreeDOS live.",
631
+ col=0, row=0),
632
+ ),
633
+ ),
634
+ Recipe(
635
+ id="os_zoo_kolibri", name="Boot KolibriOS (OS Zoo)",
636
+ summary="A tiny GUI OS written in assembly that boots from a single floppy to a graphical "
637
+ "desktop in seconds — the fast OS Zoo guest.",
638
+ intent=("kolibri", "kolibrios", "assembly", "tiny", "fast", "gui", "os zoo", "floppy",
639
+ "desktop", "boot", "emulator"),
640
+ teaches="how small and fast an OS can be — a full GUI desktop in 1.44 MB of assembly",
641
+ concept="os-zoo",
642
+ elements=(
643
+ _e("os", "kolibri", "Double-click to open the Zoo Lab and use the KolibriOS desktop.",
644
+ col=0, row=0),
645
+ ),
646
+ ),
647
+ Recipe(
648
+ id="os_zoo_menuet", name="Boot MenuetOS (OS Zoo)",
649
+ summary="The assembly GUI OS KolibriOS forked from — a whole desktop on one floppy, "
650
+ "booting in seconds.",
651
+ intent=("menuet", "menuetos", "assembly", "tiny", "fast", "gui", "os zoo", "floppy",
652
+ "desktop", "boot", "emulator"),
653
+ teaches="another take on a tiny, fast, all-assembly GUI OS (the root of KolibriOS)",
654
+ concept="os-zoo",
655
+ elements=(
656
+ _e("os", "menuet", "Double-click to open the Zoo Lab and use the MenuetOS desktop.",
657
+ col=0, row=0),
658
+ ),
659
+ ),
660
+ Recipe(
661
+ id="os_zoo_msdos", name="Boot MS-DOS 6.22 (OS Zoo)",
662
+ summary="The real Microsoft MS-DOS 6.22, booted from a disk image under QEMU — drag on and "
663
+ "Run; the disk downloads on first boot.",
664
+ intent=("ms-dos", "msdos", "dos", "microsoft dos", "6.22", "real dos", "os zoo", "boot",
665
+ "emulator", "command prompt"),
666
+ teaches="the original Microsoft MS-DOS — compare it side by side with FreeDOS",
667
+ concept="os-zoo",
668
+ elements=(
669
+ _e("os", "msdos", "Double-click to open the Zoo Lab and use the MS-DOS C:\\> prompt.",
670
+ col=0, row=0),
671
+ ),
672
+ ),
673
+ Recipe(
674
+ id="os_zoo_mac7", name="Boot Mac System 7 (OS Zoo)",
675
+ summary="Classic Macintosh System 7 on an emulated 68k Mac — drag on and Run; the ROM and "
676
+ "disk download on first boot.",
677
+ intent=("mac", "macintosh", "system 7", "mac os", "classic mac", "basilisk", "68k",
678
+ "apple", "os zoo", "boot", "emulator"),
679
+ teaches="what classic Mac OS (System 7) looked and felt like, on emulated 68k hardware",
680
+ concept="os-zoo",
681
+ elements=(
682
+ _e("os", "mac7", "Double-click to open the Zoo Lab and use the System 7 desktop.",
683
+ col=0, row=0),
684
+ ),
685
+ ),
686
+ Recipe(
687
+ id="os_zoo_win31", name="Boot Windows 3.11 (OS Zoo)",
688
+ summary="Windows for Workgroups 3.11 under DOSBox — drag on and Run; a pre-installed image "
689
+ "downloads on first boot.",
690
+ intent=("windows", "windows 3.1", "windows 3.11", "win31", "dosbox", "program manager",
691
+ "wfw", "os zoo", "boot", "emulator", "vintage windows"),
692
+ teaches="what early graphical Windows (3.x) was like, running fast under DOSBox",
693
+ concept="os-zoo",
694
+ elements=(
695
+ _e("os", "win31", "Double-click to open the Zoo Lab and use Windows 3.11.",
696
+ col=0, row=0),
697
+ ),
698
+ ),
699
+ Recipe(
700
+ id="os_zoo_byo", name="Bring your own classic OS (OS Zoo)",
701
+ summary="Run a proprietary classic OS (Windows 95, Mac System 7, …) GINI can't ship: "
702
+ "supply a disk image you own and GINI provides the emulator.",
703
+ intent=("windows 95", "win95", "mac", "system 7", "classic mac", "bring your own",
704
+ "byo", "own image", "proprietary", "os zoo", "emulator", "rom", "disk image"),
705
+ teaches="how GINI runs a proprietary OS legally — you supply the image, GINI the emulator",
706
+ concept="os-zoo",
707
+ elements=(
708
+ _e("os", "oszoo_byo",
709
+ "Set Image to a disk image you legally own (and Arch/Emulator to match — 68k Mac "
710
+ "needs Basilisk and a ROM), then open the Zoo Lab.", col=0, row=0),
711
+ ),
712
+ ),
713
+ )
714
+
715
+ _BY_ID = {r.id: r for r in RECIPES}
716
+
717
+
718
+ def get_recipe(recipe_id: str) -> Recipe | None:
719
+ return _BY_ID.get(recipe_id)
720
+
721
+
722
+ def suggest_recipes(query: str) -> list[Recipe]:
723
+ """Deterministic intent match (the offline / fallback ranker the LLM mirrors):
724
+ score recipes by how many intent tags or name words appear in the query."""
725
+ q = (query or "").lower()
726
+ scored: list[tuple[int, Recipe]] = []
727
+ for r in RECIPES:
728
+ score = sum(1 for tag in r.intent if tag in q)
729
+ score += sum(1 for w in r.name.lower().split() if len(w) > 3 and w in q)
730
+ if score:
731
+ scored.append((score, r))
732
+ scored.sort(key=lambda x: -x[0])
733
+ return [r for _, r in scored]
734
+
735
+
736
+ def covered_elements() -> set[str]:
737
+ """Every element type that appears in at least one recipe."""
738
+ return {el.type_key for r in RECIPES for el in r.elements}