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/staging.py ADDED
@@ -0,0 +1,122 @@
1
+ """Board staging — a lesson can open with part of the canvas already built (M3).
2
+
3
+ Enables scaffolded labs ("here's a half-built network, finish it") and fault-injection ("I broke the
4
+ routing — fix it"). A lesson's `stage:` is an authorable, self-contained spec of devices + links; on
5
+ mission start the engine builds them onto the canvas. Pure logic (the caller supplies `add_device` /
6
+ `add_link` so it works against the live AppContext in the app and against fakes in tests).
7
+
8
+ Two kinds of fault can be injected, and the difference matters pedagogically:
9
+
10
+ * **Missing element** — something isn't there (`fix-the-lan`: two subnets, no router). The student
11
+ diagnoses an absence.
12
+ * **Mis-configuration** — everything is there and *looks* right, but a setting is wrong (a host
13
+ addressed into the wrong subnet). Nothing is visibly absent, so the picture can't reveal the
14
+ bug: only *running it* can. This is the harder and more realistic skill, and it's why a
15
+ mis-config mission must be graded by a live probe, never by structure.
16
+
17
+ Stage schema (in the lesson YAML):
18
+
19
+ stage:
20
+ reset: true # default: clear the canvas first, so the board is exactly this
21
+ manual_addressing: true # turn OFF auto-IP, so the addresses below actually stick
22
+ devices:
23
+ - {ref: r1, type: router}
24
+ - {ref: s1, type: switch}
25
+ - {ref: h1, type: host, x: 100, y: 200,
26
+ ips: {s1: 10.0.0.10}, # IP on the interface facing s1 (mis-config knob)
27
+ properties: {Note: "was working yesterday"}}
28
+ links:
29
+ - [h1, s1] # by ref
30
+ - [s1, r1]
31
+
32
+ `ref` is a local handle used only to wire links and address interfaces; it never has to match the
33
+ student's device names. `ips` is keyed by the *peer's ref* — the author says "the leg facing s1" and
34
+ staging resolves it to the real link id once the link exists, because link ids don't exist until the
35
+ board is built.
36
+ """
37
+ from __future__ import annotations
38
+
39
+
40
+ def normalize(stage) -> dict:
41
+ """Coerce a stage spec into a canonical dict (tolerant of shapes)."""
42
+ empty = {"devices": [], "links": [], "reset": True, "manual_addressing": False}
43
+ if not stage or not isinstance(stage, dict):
44
+ return empty
45
+ links = []
46
+ for l in stage.get("links", []) or []:
47
+ if isinstance(l, (list, tuple)) and len(l) >= 2:
48
+ links.append([l[0], l[1]])
49
+ elif isinstance(l, dict) and "source" in l and "target" in l:
50
+ links.append([l["source"], l["target"]])
51
+ return {
52
+ "devices": list(stage.get("devices", []) or []),
53
+ "links": links,
54
+ # a staged board is a *designed* board: by default it replaces whatever was on the canvas,
55
+ # or the mission would grade against elements the author never put there.
56
+ "reset": bool(stage.get("reset", True)),
57
+ "manual_addressing": bool(stage.get("manual_addressing", False)),
58
+ }
59
+
60
+
61
+ def is_staged(lesson) -> bool:
62
+ spec = normalize(getattr(lesson, "stage", None))
63
+ return bool(spec["devices"])
64
+
65
+
66
+ def wants_reset(lesson) -> bool:
67
+ spec = normalize(getattr(lesson, "stage", None))
68
+ return bool(spec["devices"]) and spec["reset"]
69
+
70
+
71
+ def apply(stage, *, add_device, add_link, topology=None) -> dict:
72
+ """Build a stage. `add_device(type_key, x, y) -> instance` (with an `.id`); `add_link(src_id,
73
+ tgt_id)` -> link (with an `.id`; a caller that returns None just forfeits IP injection).
74
+ Returns {ref -> instance}. Links to an unknown ref are skipped (authoring safety). Never raises
75
+ on a bad single row — best-effort, because a typo in one stage row must not stop the lesson.
76
+
77
+ If `topology` is given and the stage asks for it, manual addressing is switched on — without
78
+ that the compiler auto-assigns IPs and would silently *repair* the very fault we injected.
79
+ """
80
+ spec = normalize(stage)
81
+ if topology is not None and spec["manual_addressing"]:
82
+ topology.manual_addressing = True
83
+
84
+ placed: dict[str, object] = {}
85
+ wanted: dict[str, dict] = {} # ref -> {peer_ref: ip}
86
+ for i, d in enumerate(spec["devices"]):
87
+ tk = d.get("type")
88
+ if not tk:
89
+ continue
90
+ ref = d.get("ref") or d.get("name") or f"{tk}{i}"
91
+ try:
92
+ inst = add_device(tk, float(d.get("x", 0) or 0), float(d.get("y", 0) or 0))
93
+ except Exception:
94
+ continue
95
+ placed[ref] = inst
96
+ props = d.get("properties") or {}
97
+ if props and hasattr(inst, "properties"):
98
+ inst.properties.update({str(k): str(v) for k, v in props.items()})
99
+ ips = d.get("ips") or {}
100
+ if ips:
101
+ wanted[ref] = {str(k): str(v) for k, v in ips.items()}
102
+
103
+ for a, b in spec["links"]:
104
+ da, db = placed.get(a), placed.get(b)
105
+ if da is None or db is None:
106
+ continue
107
+ try:
108
+ link = add_link(da.id, db.id)
109
+ except Exception:
110
+ continue
111
+ lid = getattr(link, "id", None)
112
+ if lid is None:
113
+ continue
114
+ # now that the link exists we can pin each authored IP to the right leg: the author writes
115
+ # "on h1, the interface facing s1", i.e. wanted[near_ref][far_ref].
116
+ for near_ref, far_ref in ((a, b), (b, a)):
117
+ ip = (wanted.get(near_ref) or {}).get(far_ref)
118
+ near = placed[near_ref]
119
+ if ip and hasattr(near, "static_ips"):
120
+ near.static_ips[lid] = ip
121
+
122
+ return placed
@@ -0,0 +1,144 @@
1
+ """Syscall builder — turn a student's syscall spec into the real xv6 (RISC-V) edits.
2
+
3
+ Adding a system call to xv6 touches five places; this module generates all of them from a
4
+ small spec (name, return type, args, C body). It is PURE text generation, so the codegen and
5
+ validation are unit-tested without a compiler — the actual write-patches-and-recompile step is
6
+ Mac-side (xv6 tree + `make`), exactly the same split as the rest of the Machine Lab.
7
+
8
+ The five edit sites (xv6-riscv, MIT 6.1810):
9
+ 1. kernel/syscall.h — `#define SYS_<name> <n>`
10
+ 2. kernel/syscall.c — `extern uint64 sys_<name>(void);` + `[SYS_<name>] sys_<name>,`
11
+ 3. kernel/sysproc.c — the `uint64 sys_<name>(void){…}` implementation (with arg fetch)
12
+ 4. user/user.h — the user-facing prototype
13
+ 5. user/usys.pl — `entry("<name>");` (generates the usys.S trampoline)
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from dataclasses import dataclass, field
19
+
20
+ # The stock xv6-riscv system calls (numbers 1..21), used to reject name collisions and to
21
+ # compute the next free syscall number.
22
+ # current xv6-riscv: fork=1 .. sync=22 (note `pause`, not `sleep`, and `sync` at 22)
23
+ STOCK_SYSCALLS = (
24
+ "fork", "exit", "wait", "pipe", "read", "kill", "exec", "fstat", "chdir", "dup",
25
+ "getpid", "sbrk", "pause", "uptime", "open", "write", "mknod", "unlink", "link",
26
+ "mkdir", "close", "sync",
27
+ )
28
+ NEXT_FREE_NUMBER = len(STOCK_SYSCALLS) + 1 # 23
29
+
30
+ ARG_TYPES = ("int", "addr", "str") # how the arg is fetched in the kernel
31
+ RET_TYPES = ("int", "uint64", "void")
32
+
33
+ _IDENT = re.compile(r"^[a-z_][a-z0-9_]*$")
34
+ # user.h C type shown to the student, per kernel arg type
35
+ _USER_CTYPE = {"int": "int", "addr": "void*", "str": "const char*"}
36
+
37
+
38
+ @dataclass
39
+ class SyscallArg:
40
+ name: str
41
+ type: str = "int" # one of ARG_TYPES
42
+
43
+
44
+ @dataclass
45
+ class SyscallSpec:
46
+ name: str
47
+ ret: str = "uint64"
48
+ args: list = field(default_factory=list) # [SyscallArg]
49
+ body: str = " return 0;"
50
+
51
+
52
+ def validate(spec: SyscallSpec, existing=STOCK_SYSCALLS) -> list[str]:
53
+ """Return a list of human error strings ([] == valid)."""
54
+ errs: list[str] = []
55
+ if not spec.name or not _IDENT.match(spec.name):
56
+ errs.append("Name must be a valid C identifier (lower-case letters, digits, '_').")
57
+ elif spec.name in existing:
58
+ errs.append(f"'{spec.name}' is already an xv6 system call — pick another name.")
59
+ if spec.ret not in RET_TYPES:
60
+ errs.append(f"Return type must be one of {', '.join(RET_TYPES)}.")
61
+ if len(spec.args) > 6:
62
+ errs.append("A system call can take at most 6 arguments (registers a0–a5).")
63
+ seen = set()
64
+ for a in spec.args:
65
+ if not a.name or not _IDENT.match(a.name):
66
+ errs.append(f"Argument name '{a.name}' is not a valid C identifier.")
67
+ elif a.name in seen:
68
+ errs.append(f"Duplicate argument name '{a.name}'.")
69
+ seen.add(a.name)
70
+ if a.type not in ARG_TYPES:
71
+ errs.append(f"Argument '{a.name}' has an unknown type '{a.type}'.")
72
+ if not (spec.body or "").strip():
73
+ errs.append("The C body is empty — add what the system call should do.")
74
+ elif spec.ret != "void" and "return" not in spec.body:
75
+ errs.append("The body has no `return` but the return type isn't void.")
76
+ return errs
77
+
78
+
79
+ def _fetch(arg: SyscallArg, i: int) -> str:
80
+ if arg.type == "int":
81
+ return f" int {arg.name};\n argint({i}, &{arg.name});"
82
+ if arg.type == "addr":
83
+ return f" uint64 {arg.name};\n argaddr({i}, &{arg.name});"
84
+ return (f" char {arg.name}[128];\n"
85
+ f" argstr({i}, {arg.name}, sizeof({arg.name}));")
86
+
87
+
88
+ @dataclass
89
+ class Codegen:
90
+ number: int
91
+ syscall_h: str # #define line
92
+ extern: str # extern decl for syscall.c
93
+ dispatch: str # dispatch-table row for syscall.c
94
+ impl: str # the sys_<name> function for sysproc.c
95
+ user_h: str # user-space prototype
96
+ usys_entry: str # entry("<name>");
97
+
98
+ def files(self) -> list[tuple[str, str, str]]:
99
+ """(path, where, snippet) for each of the five edit sites."""
100
+ return [
101
+ ("kernel/syscall.h", "add the #define", self.syscall_h),
102
+ ("kernel/syscall.c", "add extern + dispatch row",
103
+ self.extern + "\n" + self.dispatch),
104
+ ("kernel/sysproc.c", "append the implementation", self.impl),
105
+ ("user/user.h", "add the prototype", self.user_h),
106
+ ("user/usys.pl", "add the entry", self.usys_entry),
107
+ ]
108
+
109
+ def preview(self) -> str:
110
+ return "\n\n".join(f"/* {path} — {where} */\n{snippet}"
111
+ for path, where, snippet in self.files())
112
+
113
+
114
+ def generate(spec: SyscallSpec, number: int = NEXT_FREE_NUMBER) -> Codegen:
115
+ """Render the five edits for `spec`. Assumes `spec` already validated."""
116
+ name = spec.name
117
+ fetch = "\n".join(_fetch(a, i) for i, a in enumerate(spec.args))
118
+ body = spec.body if spec.body.strip() else " return 0;"
119
+ impl_lines = [f"uint64", f"sys_{name}(void)", "{"]
120
+ if fetch:
121
+ impl_lines.append(fetch)
122
+ impl_lines += [" // ---- your code ----", body.rstrip(), " // -------------------", "}"]
123
+ impl = "\n".join(impl_lines)
124
+
125
+ user_args = ", ".join(f"{_USER_CTYPE[a.type]} {a.name}" for a in spec.args) or "void"
126
+ user_ret = "int" if spec.ret in ("int", "uint64") else "int" # xv6 user.h convention
127
+
128
+ return Codegen(
129
+ number=number,
130
+ syscall_h=f"#define SYS_{name} {number}",
131
+ extern=f"extern uint64 sys_{name}(void);",
132
+ dispatch=f"[SYS_{name}] sys_{name},",
133
+ impl=impl,
134
+ user_h=f"{user_ret} {name}({user_args});",
135
+ usys_entry=f'entry("{name}");',
136
+ )
137
+
138
+
139
+ def starter_body(spec_name: str = "") -> str:
140
+ """A friendly starter the builder pre-fills the body editor with."""
141
+ return (" // read your args above, do the work, and return a value.\n"
142
+ " // e.g. print from the kernel:\n"
143
+ ' printf("hello from sys_' + (spec_name or "mycall") + '\\n");\n'
144
+ " return 0;")
@@ -0,0 +1,62 @@
1
+ """The Ask GINI topic cloud — an inviting, clickable set of things a student can explore or
2
+ build, generated from the live GINI knowledge base (concept notes, recipes, a few marquee
3
+ elements). Tapping a term SENDS the matching question/build to Ask GINI, so the cloud both
4
+ sparks ideas AND nudges the student toward vocabulary the (lexical) retriever lands on.
5
+
6
+ Framed as an invitation, not a cage — "GINI's great at these, or ask anything." Pure/data so
7
+ the cloud content is unit-tested; the UI renders these `CloudItem`s as a flow of pills.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from . import concepts as _concepts
14
+ from . import recipes as _recipes
15
+ from .devices import REGISTRY
16
+
17
+ # a colour rotation so the cloud looks lively (theme accent keys)
18
+ _ACCENTS = ["blue", "green", "purple", "teal", "indigo", "amber", "pink", "cyan", "orange"]
19
+ # recognisable elements worth a small pill (concepts + recipes carry the rest)
20
+ _MARQUEE = ["router", "switch", "firewall", "load_balancer", "function", "database",
21
+ "vnf", "ovs", "container", "pod"]
22
+
23
+
24
+ @dataclass
25
+ class CloudItem:
26
+ label: str # what's shown on the pill
27
+ kind: str # concept | recipe | element
28
+ query: str # sent to Ask GINI when the pill is tapped
29
+ weight: int # 1..3 → font-size tier (concepts biggest)
30
+ accent: str # theme accent key for colour
31
+
32
+
33
+ def _short(c) -> str:
34
+ """A punchy label for a concept — its canonical keyword, cased (acronyms upper)."""
35
+ w = (c.keywords[0] if c.keywords else c.title)
36
+ return w.upper() if len(w) <= 3 else w[:1].upper() + w[1:]
37
+
38
+
39
+ def _pill(text: str, cap: int = 22) -> str:
40
+ """A tidy pill label: drop parentheticals and clip, so the cloud stays a cloud of
41
+ short punchy words (the full name still goes in the query for retrieval)."""
42
+ import re
43
+ text = re.sub(r"\s*\([^)]*\)", "", text).strip()
44
+ return text if len(text) <= cap else text[: cap - 1].rstrip() + "…"
45
+
46
+
47
+ def topic_cloud() -> list[CloudItem]:
48
+ """Every explorable/buildable topic, as pills. Concepts → 'explain X' (big), recipes →
49
+ 'show me an X' build (medium), marquee elements → 'what is a X' (small)."""
50
+ items: list[CloudItem] = []
51
+ for i, c in enumerate(_concepts.CONCEPTS):
52
+ lbl = _short(c)
53
+ items.append(CloudItem(lbl, "concept", f"explain {lbl}", 3, _ACCENTS[i % len(_ACCENTS)]))
54
+ for r in _recipes.RECIPES:
55
+ items.append(CloudItem(_pill(r.name), "recipe",
56
+ f"show me a {r.name} example", 2, "green"))
57
+ for key in _MARQUEE:
58
+ dt = REGISTRY.get(key)
59
+ if dt is not None:
60
+ items.append(CloudItem(_pill(dt.label), "element", f"what is a {dt.label}?", 1,
61
+ getattr(dt.accent, "value", "slate")))
62
+ return items
@@ -0,0 +1,213 @@
1
+ """In-memory topology model (pure Python, no Qt).
2
+
3
+ This is the single source of truth for what's on the canvas. The UI renders it,
4
+ the compiler/persistence layers read it, and the AI agent layer mutates it.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import itertools
9
+ from dataclasses import dataclass, field, asdict
10
+
11
+ from . import devices
12
+ from .devices import DeviceType
13
+
14
+
15
+ @dataclass
16
+ class DeviceInstance:
17
+ id: str
18
+ type_key: str
19
+ name: str
20
+ x: float = 0.0
21
+ y: float = 0.0
22
+ parent_id: str | None = None
23
+ properties: dict[str, str] = field(default_factory=dict)
24
+ # manual addressing: link_id -> static IPv4 (bare dotted-quad). Only honored when
25
+ # the topology is in manual_addressing mode; empty/missing entries auto-fill.
26
+ static_ips: dict[str, str] = field(default_factory=dict)
27
+ # instance "size" tier (1=S … 4=XL) for resizable elements — bigger = more vCPU/mem
28
+ # and proportionally more GINI $/hr. See domain/pricing.py SIZE_TIERS.
29
+ size: int = 1
30
+ # for container elements (VPC/Subnet/Region): the box's drawn size on the canvas.
31
+ # 0 = use the type's default; non-container elements ignore these.
32
+ w: float = 0.0
33
+ h: float = 0.0
34
+ # composition slot this device belongs to (a scaffold/bound dependency, e.g. "A"). Empty = the
35
+ # fragment's own delta. Predicates reference it as `type@slot`.
36
+ slot: str = ""
37
+ # WHICH fragment was materialized into that slot (e.g. "cap-lan"). Provenance only — no predicate
38
+ # reads it; it exists so the canvas can label a slot group with what actually fills it
39
+ # ("nets · cap-lan ×4") instead of just a count, and so a composed board is self-describing.
40
+ slot_source: str = ""
41
+
42
+ @property
43
+ def type(self) -> DeviceType:
44
+ return devices.get(self.type_key)
45
+
46
+
47
+ @dataclass
48
+ class Link:
49
+ id: str
50
+ source_id: str
51
+ target_id: str
52
+ label: str = ""
53
+ # "link" = a network cable (carries traffic, compiled to real wiring).
54
+ # "attach" = a rider→donor mount: a Source/Sink runs ON the donor. Carries no traffic and is
55
+ # NOT compiled as a cable — a "runs on" relationship, drawn dotted. source_id is the rider.
56
+ kind: str = "link"
57
+
58
+
59
+ class Topology:
60
+ """A graph of device instances and the links between them."""
61
+
62
+ def __init__(self, name: str = "untitled") -> None:
63
+ self.name = name
64
+ self.devices: dict[str, DeviceInstance] = {}
65
+ self.links: dict[str, Link] = {}
66
+ # when True the compiler stops auto-assigning IPs and honors each device's
67
+ # static_ips, auto-filling any interface left blank.
68
+ self.manual_addressing: bool = False
69
+ self._ids = itertools.count(1)
70
+ self._name_counters: dict[str, int] = {}
71
+ # per-type auto-name prefix overrides (type_key -> prefix), set from Settings;
72
+ # empty means use the curated DEFAULT_PREFIXES (R1, S1, M1, …).
73
+ self.prefix_overrides: dict[str, str] = {}
74
+
75
+ # -- creation ----------------------------------------------------------- #
76
+ def _new_id(self, prefix: str) -> str:
77
+ return f"{prefix}{next(self._ids)}"
78
+
79
+ def _auto_name(self, dt: DeviceType) -> str:
80
+ # prefix: a user override (e.g. "Mach_"), else the curated default (M, R, S, …)
81
+ base = self.prefix_overrides.get(dt.key) or devices.default_prefix(dt.key)
82
+ n = self._name_counters.get(base, 0) + 1
83
+ self._name_counters[base] = n
84
+ return f"{base}{n}"
85
+
86
+ def add_device(
87
+ self,
88
+ type_key: str,
89
+ name: str | None = None,
90
+ x: float = 0.0,
91
+ y: float = 0.0,
92
+ parent_id: str | None = None,
93
+ properties: dict[str, str] | None = None,
94
+ ) -> DeviceInstance:
95
+ dt = devices.get(type_key)
96
+ name = name or self._auto_name(dt)
97
+ props = dict(dt.default_properties)
98
+ if properties:
99
+ props.update(properties)
100
+ props["Name"] = name
101
+ inst = DeviceInstance(
102
+ id=self._new_id(dt.key + "-"),
103
+ type_key=type_key,
104
+ name=name,
105
+ x=x,
106
+ y=y,
107
+ parent_id=parent_id,
108
+ properties=props,
109
+ )
110
+ self.devices[inst.id] = inst
111
+ return inst
112
+
113
+ def add_link(self, source_id: str, target_id: str, label: str = "") -> Link:
114
+ if source_id not in self.devices or target_id not in self.devices:
115
+ raise KeyError("link endpoints must be existing devices")
116
+ link = Link(self._new_id("link-"), source_id, target_id, label)
117
+ self.links[link.id] = link
118
+ return link
119
+
120
+ def add_attach(self, rider_id: str, donor_id: str, label: str = "") -> Link:
121
+ """Mount a rider (Source/Sink) onto its donor. Distinct from a network link: it carries no
122
+ traffic and is not compiled — the rider merely RUNS ON the donor. `rider_id` is source_id."""
123
+ if rider_id not in self.devices or donor_id not in self.devices:
124
+ raise KeyError("attach endpoints must be existing devices")
125
+ link = Link(self._new_id("attach-"), rider_id, donor_id, label, kind="attach")
126
+ self.links[link.id] = link
127
+ return link
128
+
129
+ # -- mutation ----------------------------------------------------------- #
130
+ def remove_device(self, device_id: str) -> None:
131
+ self.devices.pop(device_id, None)
132
+ for lid in [l.id for l in self.links.values()
133
+ if l.source_id == device_id or l.target_id == device_id]:
134
+ self.links.pop(lid, None)
135
+
136
+ def rename(self, device_id: str, name: str) -> None:
137
+ d = self.devices[device_id]
138
+ d.name = name
139
+ d.properties["Name"] = name
140
+
141
+ # -- queries ------------------------------------------------------------ #
142
+ def find_by_name(self, name: str) -> DeviceInstance | None:
143
+ for d in self.devices.values():
144
+ if d.name == name:
145
+ return d
146
+ return None
147
+
148
+ def neighbors(self, device_id: str) -> list[DeviceInstance]:
149
+ out = []
150
+ for l in self.links.values():
151
+ if l.source_id == device_id:
152
+ out.append(self.devices[l.target_id])
153
+ elif l.target_id == device_id:
154
+ out.append(self.devices[l.source_id])
155
+ return out
156
+
157
+ def degree(self, device_id: str) -> int:
158
+ return sum(1 for l in self.links.values()
159
+ if device_id in (l.source_id, l.target_id))
160
+
161
+ # -- riders / attach edges ---------------------------------------------- #
162
+ def net_links(self) -> list["Link"]:
163
+ """Only the network cables — what the compiler wires. Attach edges are excluded."""
164
+ return [l for l in self.links.values() if l.kind != "attach"]
165
+
166
+ def donor_of(self, rider_id: str) -> "DeviceInstance | None":
167
+ """The donor a rider is mounted on (the far end of its attach edge), or None."""
168
+ for l in self.links.values():
169
+ if l.kind == "attach" and l.source_id == rider_id:
170
+ return self.devices.get(l.target_id)
171
+ return None
172
+
173
+ def riders_on(self, donor_id: str) -> list["DeviceInstance"]:
174
+ """Every Source/Sink rider mounted on this donor."""
175
+ return [self.devices[l.source_id] for l in self.links.values()
176
+ if l.kind == "attach" and l.target_id == donor_id
177
+ and l.source_id in self.devices]
178
+
179
+ def counts_by_category(self) -> dict[str, int]:
180
+ out: dict[str, int] = {}
181
+ for d in self.devices.values():
182
+ cat = d.type.category.value
183
+ out[cat] = out.get(cat, 0) + 1
184
+ return out
185
+
186
+ # -- serialization (used by persistence + agent layer) ------------------ #
187
+ def to_dict(self) -> dict:
188
+ return {
189
+ "name": self.name,
190
+ "manual_addressing": self.manual_addressing,
191
+ "devices": [asdict(d) for d in self.devices.values()],
192
+ "links": [asdict(l) for l in self.links.values()],
193
+ }
194
+
195
+ @classmethod
196
+ def from_dict(cls, data: dict) -> "Topology":
197
+ t = cls(data.get("name", "untitled"))
198
+ t.manual_addressing = bool(data.get("manual_addressing", False))
199
+ max_n = 0
200
+ for d in data.get("devices", []):
201
+ inst = DeviceInstance(**d)
202
+ t.devices[inst.id] = inst
203
+ for token in inst.id.replace("-", " ").split():
204
+ if token.isdigit():
205
+ max_n = max(max_n, int(token))
206
+ for l in data.get("links", []):
207
+ link = Link(**l)
208
+ t.links[link.id] = link
209
+ t._ids = itertools.count(max_n + 1)
210
+ return t
211
+
212
+ def __repr__(self) -> str:
213
+ return f"<Topology {self.name!r}: {len(self.devices)} devices, {len(self.links)} links>"
@@ -0,0 +1,51 @@
1
+ """The engine's vocabulary — the discovery protocol (GINI_AUTHORING_DESIGN.md, "Discover").
2
+
3
+ A single, versioned manifest of everything a fragment can be built FROM on this engine: elements (+
4
+ their grammar), predicates, probes, and capability roles. gBuilder exports it; the Teaching Center
5
+ consumes it so it composes against a *declared version* rather than importing the live engine. This
6
+ is the forward-extensibility seam: when the engine gains a primitive, the exported vocabulary grows,
7
+ and authoring/composition see it automatically — no author-tool code change.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from . import capabilities as _caps
12
+ from . import devices as _devices
13
+ from .content import ENGINE_VERSION, FRAGMENT_SCHEMA
14
+ from .objectives import _TYPE_ARG_FUNCS
15
+
16
+
17
+ def export() -> dict:
18
+ """The full vocabulary manifest for this engine version."""
19
+ elements = []
20
+ for d in _devices.all_devices():
21
+ cat = getattr(d, "category", None)
22
+ elements.append({"key": d.key, "label": d.label,
23
+ "category": str(getattr(cat, "value", cat) or "")})
24
+ return {
25
+ "engine_version": ENGINE_VERSION,
26
+ "schema_version": FRAGMENT_SCHEMA,
27
+ "elements": sorted(elements, key=lambda e: e["key"]),
28
+ # structural predicates that take element type_keys as args
29
+ "predicates": sorted(_TYPE_ARG_FUNCS),
30
+ # behavioral probes the runtime can witness (measure = a Source/Sink measurement assertion)
31
+ "probes": ["reach", "ping", "http", "balances", "flow_installed", "measure"],
32
+ # capability roles (the provides/requires vocabulary), as an is-a map
33
+ "capabilities": dict(sorted(_caps.PARENTS.items())),
34
+ }
35
+
36
+
37
+ def is_compatible(fragment_engine: str) -> tuple[bool, str]:
38
+ """Coarse version gate: can content authored on `fragment_engine` be trusted here? Empty = a
39
+ built-in (always fine). The FINE gate is validation against the real vocabulary (an unknown
40
+ primitive fails); this is the friendly up-front check + message."""
41
+ if not fragment_engine or fragment_engine == ENGINE_VERSION:
42
+ return True, ""
43
+ try:
44
+ theirs = tuple(int(x) for x in fragment_engine.split("."))
45
+ ours = tuple(int(x) for x in ENGINE_VERSION.split("."))
46
+ except ValueError:
47
+ return True, "" # unparseable → let validation be the judge
48
+ if theirs > ours:
49
+ return False, (f"authored on gBuilder {fragment_engine}, but this is {ENGINE_VERSION} — "
50
+ f"update the engine to use it")
51
+ return True, "" # older content is fine (this engine is a superset)