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,75 @@
1
+ """Thrashing diagnosis game — riding on the page-replacement simulator.
2
+
3
+ Each case is a real LRU run over a generated reference string; the student reads the run's signature
4
+ (fault rate, frames vs working set, locality, working-set growth) and names WHY it is faulting:
5
+
6
+ healthy — low fault rate; frames cover the working set
7
+ too few frames — good locality, stable working set, but frames < working set (more RAM helps)
8
+ working set too big — the distinct-page set grows past the frames (a phase change)
9
+ poor locality — random access; more frames barely helps until they cover the whole set
10
+
11
+ Ground truth is the generator's label (deterministic). classify_thrash() is the practice-mode hint.
12
+ Simulator-backed, so this game is offline (a live xv6 mechanism would add real cases later).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import random
17
+
18
+ from ..diagnose import Case, GameSpec
19
+ from ..paging_sim import run_features
20
+
21
+ THRASH_CLASSES = ["healthy", "too few frames", "working set too big", "poor locality"]
22
+ THRASH_ABBR = {"healthy": "ok", "too few frames": "frames",
23
+ "working set too big": "ws-big", "poor locality": "local"}
24
+ THRASH_SPEC = GameSpec("thrash-diagnose", "Diagnose the thrashing",
25
+ "Why is this workload faulting?", THRASH_CLASSES, THRASH_ABBR)
26
+
27
+
28
+ def _loop(pages, reps) -> list:
29
+ return [p for _ in range(reps) for p in range(pages)]
30
+
31
+
32
+ def gen_healthy(rng) -> tuple:
33
+ return _loop(3, 12), 4 # 3-page loop, 4 frames -> ~no faults after warmup
34
+
35
+
36
+ def gen_too_few(rng) -> tuple:
37
+ return _loop(8, 8), 3 # 8-page loop, 3 frames -> ~100% fault, good locality
38
+
39
+
40
+ def gen_ws_big(rng) -> tuple:
41
+ refs = _loop(4, 4) + _loop(10, 3) + _loop(20, 3) # growing distinct set (phase changes)
42
+ return refs, 4
43
+
44
+
45
+ def gen_poor_local(rng) -> tuple:
46
+ return [rng.randrange(40) for _ in range(140)], 5 # random over 40 pages -> weak locality
47
+
48
+
49
+ _GENERATORS = [("healthy", gen_healthy), ("too few frames", gen_too_few),
50
+ ("working set too big", gen_ws_big), ("poor locality", gen_poor_local)]
51
+
52
+
53
+ def classify_thrash(f: dict, thresholds: dict | None = None) -> str:
54
+ """Heuristic used for the hint. Low faults -> healthy; else the distinguishing signal wins:
55
+ a growing distinct set -> working set too big; poor locality -> poor locality; otherwise the
56
+ frames are simply too few."""
57
+ th = {"fault": 0.25, "growth": 0.5, "local": 0.4}
58
+ th.update(thresholds or {})
59
+ if f["fault_rate"] < th["fault"]:
60
+ return "healthy"
61
+ if f["ws_growth"] >= th["growth"]:
62
+ return "working set too big"
63
+ if f["locality"] < th["local"]:
64
+ return "poor locality"
65
+ return "too few frames"
66
+
67
+
68
+ def demo_cases(seed: int = 0) -> list:
69
+ rng = random.Random(seed)
70
+ out = []
71
+ for name, gen in _GENERATORS:
72
+ refs, frames = gen(rng)
73
+ f = run_features(refs, frames)
74
+ out.append(Case(f"thr-{name}", f, name, subtitle=name, hint=classify_thrash(f)))
75
+ return out
@@ -0,0 +1,60 @@
1
+ """Address-translation game — the one that makes live page tables playable.
2
+
3
+ Given a page table (leaf VA→PA mappings) and a target virtual address, the student computes the
4
+ physical address: PA = (PA base of the page holding VA) + (VA − VA base). Exact answer (tolerance 0),
5
+ graded against the real mapping. Live cases come straight from the kernel's `vmprint` leaves; the
6
+ demo deck synthesizes small tables.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import random
11
+
12
+ from ..diagnose import Case, GameSpec
13
+
14
+ PGSIZE = 4096
15
+
16
+ TRANSLATE_SPEC = GameSpec(
17
+ "addr-translate", "Translate the address",
18
+ "Compute the physical address (PA) for the target VA — enter hex (0x…) or decimal.",
19
+ classes=[], answer="estimate", tolerance=0, unit="PA",
20
+ )
21
+
22
+
23
+ def _case(rows, va, cid) -> Case | None:
24
+ """rows = [(va_base, pa_base, perms)]; find the page holding `va` and compute its PA."""
25
+ for vb, pb, _perm in rows:
26
+ if vb <= va < vb + PGSIZE:
27
+ pa = pb + (va - vb)
28
+ return Case(cid, {"rows": rows, "va": va}, pa, subtitle=hex(pa),
29
+ hint=f"page base VA {hex(vb)} → PA {hex(pb)}, offset {hex(va - vb)}")
30
+ return None
31
+
32
+
33
+ def demo_cases(seed: int = 0) -> list:
34
+ rng = random.Random(seed)
35
+ out = []
36
+ for k in range(5):
37
+ vpns = sorted(rng.sample(range(0, 12), 4))
38
+ rows = [(v * PGSIZE, rng.randrange(0x80, 0x140) * PGSIZE,
39
+ rng.choice(["r-x u", "rw- u", "r-- u"])) for v in vpns]
40
+ vb, _pb, _ = rng.choice(rows)
41
+ c = _case(rows, vb + rng.randrange(0, PGSIZE), f"tr-{k}")
42
+ if c is not None:
43
+ out.append(c)
44
+ return out
45
+
46
+
47
+ def live_cases(leaves, seed: int = 0, window: int = 6) -> list:
48
+ """Cases from real vmprint leaves ([Pte with .va/.pa/.perms]). Each targets a mapped page and
49
+ shows a small window of the table around it."""
50
+ rows = [(p.va, p.pa, getattr(p, "perms", "----")) for p in (leaves or [])]
51
+ if not rows:
52
+ return []
53
+ rng = random.Random(seed)
54
+ out = []
55
+ for i, (vb, pb, _perm) in enumerate(rows[:5]):
56
+ lo = max(0, i - window // 2)
57
+ c = _case(rows[lo:lo + window], vb + rng.randrange(0, PGSIZE), f"trl-{i}")
58
+ if c is not None:
59
+ out.append(c)
60
+ return out
@@ -0,0 +1,86 @@
1
+ """Case sources for two "decode the cause CSR" games — both grade against a deterministic scause
2
+ decode (the RISC-V privileged encoding), so ground truth is exact.
3
+
4
+ trap-cause — any trap's scause -> timer / external / software int, syscall, page fault, illegal.
5
+ fault-type — a page fault's scause -> instruction / load / store access.
6
+
7
+ The signature is the raw scause (+ stval/epc as context); the skill is knowing the encoding. Live
8
+ cases come from the trap-taxonomy ring; the demo deck synthesizes one of each.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from ..diagnose import Case, GameSpec
13
+
14
+ # ---- trap-cause game ------------------------------------------------------ #
15
+ TRAP_CLASSES = ["timer int", "external int", "software int", "syscall", "page fault", "illegal"]
16
+ TRAP_ABBR = {"timer int": "timer", "external int": "extern", "software int": "soft",
17
+ "syscall": "ecall", "page fault": "fault", "illegal": "illegal"}
18
+ TRAP_SPEC = GameSpec("trap-cause", "Decode the trap", "What kind of trap is this scause?",
19
+ TRAP_CLASSES, TRAP_ABBR)
20
+
21
+ _INT = {1: "software int", 5: "timer int", 9: "external int"}
22
+ _EXC = {8: "syscall", 12: "page fault", 13: "page fault", 15: "page fault", 2: "illegal"}
23
+
24
+
25
+ def trap_class(scause: int) -> str:
26
+ code = scause & 0xFF
27
+ if scause >> 63: # top bit set -> interrupt
28
+ return _INT.get(code, "external int")
29
+ return _EXC.get(code, "illegal")
30
+
31
+
32
+ # ---- fault-type game ------------------------------------------------------ #
33
+ FAULT_CLASSES = ["instruction", "load", "store"]
34
+ FAULT_ABBR = {"instruction": "insn", "load": "load", "store": "store"}
35
+ FAULT_SPEC = GameSpec("fault-type", "Decode the page fault", "Which access caused this page fault?",
36
+ FAULT_CLASSES, FAULT_ABBR)
37
+
38
+ _FAULT = {12: "instruction", 13: "load", 15: "store"}
39
+
40
+
41
+ def fault_class(scause: int) -> str | None:
42
+ return _FAULT.get(scause & 0xFF)
43
+
44
+
45
+ def _event(scause: int, stval: str = "0x0", epc: str = "0x0"):
46
+ return [("scause", hex(scause)), ("stval", stval), ("epc", epc)]
47
+
48
+
49
+ # demo scause values, one per class
50
+ _TRAP_DEMO = [(0x8000000000000005, "timer int"), (0x8000000000000009, "external int"),
51
+ (0x8000000000000001, "software int"), (0x8, "syscall"),
52
+ (0xD, "page fault"), (0x2, "illegal")]
53
+ _FAULT_DEMO = [(0xC, "instruction"), (0xD, "load"), (0xF, "store")]
54
+
55
+
56
+ def trap_demo_cases() -> list:
57
+ return [Case(f"trap-{i}", _event(sc, epc="0x1a2c"), truth, subtitle=truth)
58
+ for i, (sc, truth) in enumerate(_TRAP_DEMO)]
59
+
60
+
61
+ def fault_demo_cases() -> list:
62
+ vas = ["0x1330", "0x2f00", "0x3ff8"]
63
+ return [Case(f"flt-{i}", _event(sc, stval=vas[i % 3], epc="0x1a2c"), truth, subtitle=truth)
64
+ for i, (sc, truth) in enumerate(_FAULT_DEMO)]
65
+
66
+
67
+ def _live(trap_events, labeler) -> list:
68
+ """Build cases from trap-ring TrapEvents (pid, kind, cause, epc, tval) whose scause labels."""
69
+ out = []
70
+ for e in trap_events or []:
71
+ try:
72
+ sc = int(e.cause, 16)
73
+ except (TypeError, ValueError):
74
+ continue
75
+ truth = labeler(sc)
76
+ if truth:
77
+ out.append(Case(f"live-{e.epc}-{sc}", _event(sc, e.tval, e.epc), truth, subtitle=truth))
78
+ return out
79
+
80
+
81
+ def trap_live_cases(trap_events) -> list:
82
+ return _live(trap_events, trap_class)
83
+
84
+
85
+ def fault_live_cases(trap_events) -> list:
86
+ return _live(trap_events, fault_class)
gini/domain/grader.py ADDED
@@ -0,0 +1,155 @@
1
+ """Server-side re-grade + gradebook — the buildable slice of the hybrid grading authority.
2
+
3
+ The client grades a Mission instantly for feedback and submits the canvas **snapshot** + its
4
+ reported objective results. To make the OFFICIAL result trustworthy, the Teaching Center re-grades
5
+ deterministically:
6
+
7
+ • STRUCTURAL objectives — re-evaluated server-side from the submitted snapshot (pure graph
8
+ predicates, NO Docker). This is fully authoritative and catches tampering: if the client
9
+ claimed a structural objective met but the snapshot doesn't satisfy it, the server wins.
10
+ • BEHAVIORAL objectives — re-running them needs GINI's runtime (Docker); until that grader tier
11
+ exists (Phase 6.x, Mac/server-side), the server *trusts* the client's reported behavioral
12
+ results. So this module makes structural grading authoritative and leaves behavioral as a
13
+ documented trust boundary.
14
+
15
+ Plus a `Gradebook` that aggregates submissions into the teacher's roster×lesson view. Pure Python;
16
+ no Qt, no Docker, no network — fully unit-testable.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from dataclasses import dataclass
22
+ from types import SimpleNamespace
23
+
24
+ from . import objectives as _obj
25
+ from . import scoring as _scoring
26
+ from .profile import band_rank, better_band
27
+
28
+
29
+ # -- snapshot → world (no live Topology needed) ----------------------------- #
30
+ def snapshot_of(topology) -> dict:
31
+ """Serialize the live topology to a portable snapshot (what a submission carries)."""
32
+ return topology.to_dict()
33
+
34
+
35
+ def world_from_snapshot(snapshot) -> _obj.TopologyWorld:
36
+ """Build the objective World from a serialized snapshot (dict or JSON string), so structural
37
+ predicates can be re-evaluated server-side without reconstructing the full Topology model."""
38
+ if isinstance(snapshot, str):
39
+ snapshot = json.loads(snapshot)
40
+ devices = {}
41
+ for d in snapshot.get("devices", []):
42
+ devices[d["id"]] = SimpleNamespace(
43
+ id=d["id"], name=d.get("name", ""), type_key=d.get("type_key", ""),
44
+ parent_id=d.get("parent_id"), properties=d.get("properties", {}) or {})
45
+ links = {}
46
+ for i, l in enumerate(snapshot.get("links", [])):
47
+ lid = l.get("id", f"l{i}")
48
+ links[lid] = SimpleNamespace(source_id=l["source_id"], target_id=l["target_id"])
49
+ return _obj.TopologyWorld(SimpleNamespace(devices=devices, links=links))
50
+
51
+
52
+ # -- re-grade --------------------------------------------------------------- #
53
+ def regrade_structural(lesson, snapshot) -> list:
54
+ """Re-evaluate the lesson's STRUCTURAL objectives from the snapshot (behavioral → pending)."""
55
+ return _obj.evaluate_all(lesson.objectives, world_from_snapshot(snapshot), runner=None)
56
+
57
+
58
+ @dataclass
59
+ class OfficialResult:
60
+ band: str
61
+ server_results: list # per-objective (structural authoritative, behavioral from client)
62
+ regraded_down: bool # official band < the band the client claimed
63
+ note: str = ""
64
+
65
+
66
+ def official_result(lesson, submission) -> OfficialResult:
67
+ """Compute the official result from a submission {snapshot, objective_results, time_taken, band}.
68
+
69
+ Structural objectives come from the server re-grade (authoritative); behavioral objectives are
70
+ trusted from the client's reported results (until the Docker re-grader lands)."""
71
+ client = dict(submission.get("objective_results", [])) # id -> status
72
+ server_struct = {r.id: r for r in regrade_structural(lesson, submission.get("snapshot", {}))}
73
+
74
+ merged = []
75
+ for o in lesson.objectives:
76
+ if o.kind == "behavioral":
77
+ status = client.get(o.id, _obj.PENDING) # trust the client for behavioral
78
+ merged.append(_obj.ObjectiveResult(o.id, o.say, o.kind, status))
79
+ else:
80
+ merged.append(server_struct.get(
81
+ o.id, _obj.ObjectiveResult(o.id, o.say, o.kind, _obj.UNMET)))
82
+
83
+ limit = lesson.time_limit_s
84
+ on_time = limit is None or float(submission.get("time_taken", 0)) <= limit
85
+ sc = _scoring.score(merged, complete_when=lesson.complete_when, on_time=on_time)
86
+
87
+ claimed = submission.get("band", "")
88
+ down = band_rank(sc.band) < band_rank(claimed)
89
+ note = ("official band is lower than the client claimed — structural re-grade disagreed"
90
+ if down else "")
91
+ return OfficialResult(band=sc.band, server_results=merged, regraded_down=down, note=note)
92
+
93
+
94
+ # -- gradebook -------------------------------------------------------------- #
95
+ @dataclass
96
+ class Gradebook:
97
+ # student -> lesson_id -> best band
98
+ rows: dict
99
+
100
+ def band(self, student: str, lesson_id: str) -> str:
101
+ return self.rows.get(student, {}).get(lesson_id, "")
102
+
103
+ def students(self) -> list:
104
+ return sorted(self.rows)
105
+
106
+ def lessons(self) -> list:
107
+ out = set()
108
+ for lessons in self.rows.values():
109
+ out.update(lessons)
110
+ return sorted(out)
111
+
112
+ def completed_count(self, student: str) -> int:
113
+ return sum(1 for b in self.rows.get(student, {}).values()
114
+ if band_rank(b) >= band_rank("pass"))
115
+
116
+
117
+ def gradebook(submissions) -> Gradebook:
118
+ """Aggregate raw submissions into a roster×lesson best-band table (the teacher face)."""
119
+ rows: dict[str, dict[str, str]] = {}
120
+ for s in submissions:
121
+ student = s.get("student", "")
122
+ lesson_id = s.get("lesson_id", "")
123
+ if not student or not lesson_id:
124
+ continue
125
+ cur = rows.setdefault(student, {}).get(lesson_id, "")
126
+ rows[student][lesson_id] = better_band(cur, s.get("band", ""))
127
+ return Gradebook(rows=rows)
128
+
129
+
130
+ def official_from_composition(spec, submission) -> OfficialResult:
131
+ """Re-grade a submission against a COMPOSITION (assemble the referenced fragments locally, then
132
+ apply the structural authority). The Center's server side of the M4/M6 bridge."""
133
+ from . import composition as _comp
134
+ lesson = _comp.from_composition(spec)
135
+ return official_result(lesson, submission)
136
+
137
+
138
+ def gradebook_official(submissions, resolve_lesson) -> Gradebook:
139
+ """The authoritative gradebook: each submission is SERVER re-graded (structural authority) before
140
+ aggregation, so a tampered client band is downgraded. `resolve_lesson(lesson_id) -> Lesson`
141
+ (return None to fall back to the client-claimed band, e.g. an unknown lesson)."""
142
+ rows: dict[str, dict[str, str]] = {}
143
+ for s in submissions:
144
+ student = s.get("student", "")
145
+ lesson_id = s.get("lesson_id", "")
146
+ if not student or not lesson_id:
147
+ continue
148
+ les = resolve_lesson(lesson_id)
149
+ try:
150
+ band = official_result(les, s).band if les is not None else s.get("band", "")
151
+ except Exception:
152
+ band = s.get("band", "")
153
+ cur = rows.setdefault(student, {}).get(lesson_id, "")
154
+ rows[student][lesson_id] = better_band(cur, band)
155
+ return Gradebook(rows=rows)
@@ -0,0 +1,67 @@
1
+ """Spatial containment for canvas grouping boxes (VPC / Subnet / Region).
2
+
3
+ Membership is decided by geometry: an element belongs to the *innermost* box whose
4
+ rectangle contains the element's centre, mirroring how you'd read a cloud diagram —
5
+ a database drawn inside a Subnet drawn inside a VPC belongs to that Subnet (and through
6
+ it, that VPC). This module is pure (no Qt) so the rule is unit-testable; the canvas feeds
7
+ it scene rectangles and applies the resulting parent_id assignments.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ # The element types drawn as grouping boxes (not every is_container type — k8s_cluster and
12
+ # Pod are containers too but are real nodes that take part in the grammar / X-ray).
13
+ BOX_TYPES = ("vpc", "cloud_subnet", "region")
14
+
15
+ # A box on the canvas: (id, x, y, w, h) in scene coordinates.
16
+ Box = tuple[str, float, float, float, float]
17
+
18
+
19
+ def _contains(box: Box, cx: float, cy: float) -> bool:
20
+ _id, x, y, w, h = box
21
+ return x <= cx <= x + w and y <= cy <= y + h
22
+
23
+
24
+ def _area(box: Box) -> float:
25
+ return box[3] * box[4]
26
+
27
+
28
+ def innermost_box(cx: float, cy: float, boxes, exclude=(), min_area: float = 0.0) -> str | None:
29
+ """The id of the smallest-area box containing (cx, cy), or None. `exclude` lists ids
30
+ to ignore (never self, nor a descendant). `min_area` filters to boxes strictly larger
31
+ than this — so a container only nests inside a *bigger* container, never a smaller box
32
+ it happens to overlap (a VPC is never captured by a Subnet drawn over its centre)."""
33
+ hits = [b for b in boxes
34
+ if b[0] not in exclude and _area(b) > min_area and _contains(b, cx, cy)]
35
+ if not hits:
36
+ return None
37
+ return min(hits, key=_area)[0]
38
+
39
+
40
+ def _descendants(box_id: str, parent_of: dict) -> set:
41
+ """Every id whose parent chain currently passes through box_id (so an inner box can't
42
+ accidentally 'capture' its own ancestor when boxes overlap)."""
43
+ kids = {box_id}
44
+ while True:
45
+ nxt = {i for i, p in parent_of.items() if p in kids and i not in kids}
46
+ if not nxt:
47
+ return kids - {box_id}
48
+ kids |= nxt
49
+
50
+
51
+ def recompute(centers: dict, boxes, parent_of: dict) -> dict:
52
+ """Compute fresh parent_id for every placeable item.
53
+
54
+ centers : {id: (cx, cy)} for every node AND box on the canvas.
55
+ boxes : iterable of Box (the grouping rectangles).
56
+ parent_of : current {id: parent_id|None}, used only to skip self/descendants.
57
+
58
+ Returns {id: new_parent_id_or_None}.
59
+ """
60
+ boxes = list(boxes)
61
+ area_of = {b[0]: _area(b) for b in boxes} # a box's own area (0 for plain nodes)
62
+ out: dict = {}
63
+ for iid, (cx, cy) in centers.items():
64
+ exclude = {iid} | _descendants(iid, parent_of)
65
+ out[iid] = innermost_box(cx, cy, boxes, exclude=exclude,
66
+ min_area=area_of.get(iid, 0.0))
67
+ return out
@@ -0,0 +1,103 @@
1
+ """Move legality & relevance for Missions — the deterministic layer behind the red "that's wrong"
2
+ flag. Fast (no model), so the flag is INSTANT and reliable; the game master supplies the spoken
3
+ reasoning separately (async).
4
+
5
+ Three kinds of flag, all non-destructive (nothing is ever deleted — the flag clears when the
6
+ student fixes it):
7
+
8
+ • off-task element — a placed element whose TYPE is outside the mission's relevant family. The
9
+ family is generous (the taught concept's elements + the types the objectives mention + all
10
+ their grammar-legal neighbours), so a SUPERSET of the requirement is welcome — more hosts,
11
+ switches, routers are fine on a LAN mission. Only genuinely off-domain drops (a K8s cluster in
12
+ a switching lesson) fall out here. That superset-vs-off-domain distinction is the reasoning.
13
+ • illegal link — a connection the grammar says can't exist.
14
+ • forbid violation — a lesson `forbid:` rule (a predicate that must stay false) that got tripped
15
+ (e.g. `link(database, cloud)` — the DB wired to the Internet).
16
+
17
+ Pure logic; evaluated against the live topology / an objectives World.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from . import concepts as _concepts
22
+ from . import connection_rules as _cr
23
+ from .objectives import PredicateError, element_types_in_check, evaluate_check
24
+
25
+ # grouping boxes are scaffolding, never "off-task"
26
+ _CONTAINERS = {"vpc", "cloud_subnet", "region"}
27
+
28
+
29
+ def relevant_types(lesson) -> set[str]:
30
+ """The element types that belong in this mission (generous — supersets welcome)."""
31
+ types: set[str] = set(_CONTAINERS)
32
+ concept = getattr(getattr(lesson, "intent", None), "concept", "") or ""
33
+ c = _concepts.by_key(concept)
34
+ if c is not None:
35
+ types.update(c.elements)
36
+ for o in getattr(lesson, "objectives", []):
37
+ if getattr(o, "check", ""):
38
+ types.update(element_types_in_check(o.check))
39
+ for f in getattr(lesson, "forbid", []):
40
+ chk = getattr(f, "check", "") or ""
41
+ if chk:
42
+ types.update(element_types_in_check(chk))
43
+ # a legit grammar-neighbour of an in-family element is in-domain too (e.g. an Internet for a
44
+ # router-based lesson) — this is what makes the family reason about the domain, not a fixed list
45
+ for tk in list(types):
46
+ types.update(_cr.partner_types(tk))
47
+ return types
48
+
49
+
50
+ def off_task_devices(lesson, topology) -> list[str]:
51
+ """Device ids whose type is outside the relevant family (candidate off-task flags)."""
52
+ fam = relevant_types(lesson)
53
+ return [d.id for d in topology.devices.values()
54
+ if getattr(d, "type_key", "") and d.type_key not in fam]
55
+
56
+
57
+ def illegal_links(topology) -> list[str]:
58
+ """Link ids whose endpoints the connection grammar says shouldn't connect."""
59
+ out: list[str] = []
60
+ for l in topology.links.values():
61
+ s = topology.devices.get(l.source_id)
62
+ t = topology.devices.get(l.target_id)
63
+ if s is None or t is None:
64
+ continue
65
+ if t.type_key not in _cr.partner_types(s.type_key):
66
+ out.append(l.id)
67
+ return out
68
+
69
+
70
+ def forbid_violations(lesson, world) -> list:
71
+ """Forbid rules currently TRUE — a rule that must stay false was tripped."""
72
+ out = []
73
+ for f in getattr(lesson, "forbid", []):
74
+ chk = getattr(f, "check", "") or ""
75
+ try:
76
+ if chk and evaluate_check(chk, world):
77
+ out.append(f)
78
+ except PredicateError:
79
+ pass
80
+ return out
81
+
82
+
83
+ def flags(lesson, topology, world=None) -> dict:
84
+ """The full flag set for the current board: {device_id -> reason} for off-task devices +
85
+ forbid-violation endpoints, plus illegal link ids. Drives the red badge/glow; recomputed on
86
+ every canvas change (cheap, deterministic)."""
87
+ from .objectives import TopologyWorld
88
+ world = world or TopologyWorld(topology)
89
+ dev_flags: dict[str, str] = {}
90
+ for did in off_task_devices(lesson, topology):
91
+ d = topology.devices.get(did)
92
+ label = getattr(d, "type", None)
93
+ name = getattr(label, "label", None) or getattr(d, "type_key", "element")
94
+ dev_flags[did] = f"A {name} isn't part of this mission — remove it or you're off-task."
95
+ bad_links = illegal_links(topology)
96
+ for lid in bad_links: # flag both endpoints of a bad connection
97
+ l = topology.links.get(lid)
98
+ if l is None:
99
+ continue
100
+ for did in (l.source_id, l.target_id):
101
+ dev_flags.setdefault(did, "This connection isn't allowed here — rewire it.")
102
+ forbids = forbid_violations(lesson, world)
103
+ return {"devices": dev_flags, "links": bad_links, "forbid": forbids}