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,230 @@
1
+ """Fragment YAML — read/write mission fragments as data (GINI_MISSIONS_COMPOSABLE_DESIGN.md §7).
2
+
3
+ This is the "content is data, behaviour is code" line made concrete: a *foundational fragment* is a
4
+ YAML file the local install ships (bound to the local oracle's predicates); the engine composes them.
5
+ The Teaching Center distributes compositions that REFERENCE these — never new foundational fragments
6
+ (a new primitive would need a predicate the local oracle can't run). See the loader's `validate`,
7
+ which refuses any fragment whose predicates don't parse, whose roles are unknown, or whose element
8
+ types aren't real — so a broken pack never reaches a student.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import glob
13
+ import os
14
+
15
+ import yaml
16
+
17
+ from . import capabilities as _caps
18
+ from .objectives import check_ok, unknown_element_types
19
+
20
+ # `fragments` is imported lazily inside the functions below — importing this module must NOT trigger
21
+ # the fragments load (fragments imports us back to load its YAML packs; a top-level import would cycle).
22
+ _LAYERS = {"core", "exercise", "observe"}
23
+
24
+
25
+ def to_dict(frag: Fragment) -> dict:
26
+ """A fragment → a clean dict for YAML (empty fields pruned)."""
27
+ d: dict = {"id": frag.id, "layer": frag.layer}
28
+ for k in ("teaches", "spirit", "summary", "parent", "step"):
29
+ v = getattr(frag, k)
30
+ if v:
31
+ d[k] = v
32
+ if frag.provides:
33
+ d["provides"] = list(frag.provides)
34
+ if frag.requires:
35
+ d["requires"] = list(frag.requires)
36
+ if frag.complete_when and frag.complete_when != "all":
37
+ d["complete_when"] = frag.complete_when
38
+ if frag.objectives:
39
+ d["objectives"] = [_obj_to_dict(o) for o in frag.objectives]
40
+ if frag.misconceptions:
41
+ d["misconceptions"] = list(frag.misconceptions)
42
+ if frag.peers:
43
+ d["peers"] = list(frag.peers)
44
+ if not frag.catalog: # standalone-mission by default; only note the exceptions
45
+ d["catalog"] = False
46
+ if frag.stage:
47
+ d["stage"] = frag.stage
48
+ if getattr(frag, "slots", ()):
49
+ d["slots"] = [_slot_to_dict(s) for s in frag.slots]
50
+ if getattr(frag, "peerings", ()):
51
+ d["peerings"] = [_peering_to_dict(p) for p in frag.peerings]
52
+ if frag.forks:
53
+ d["forks"] = [_fork_to_dict(f) for f in frag.forks]
54
+ for k in ("engine_version", "author"): # provenance — only present on authored fragments
55
+ v = getattr(frag, k)
56
+ if v:
57
+ d[k] = v
58
+ if getattr(frag, "certified", False): # the runtime-playtest stamp travels to the TC
59
+ d["certified"] = True
60
+ if frag.schema_version and frag.schema_version != 1:
61
+ d["schema_version"] = frag.schema_version
62
+ return d
63
+
64
+
65
+ def _slot_to_dict(s) -> dict:
66
+ d = {"name": s.name, "role": s.role}
67
+ if s.min != 1:
68
+ d["min"] = s.min
69
+ if s.max != 1:
70
+ d["max"] = s.max
71
+ if not s.distinct:
72
+ d["distinct"] = False
73
+ return d
74
+
75
+
76
+ def _slots_from(rows) -> tuple:
77
+ from .fragments import Slot
78
+ return tuple(Slot(name=s["name"], role=s["role"], min=int(s.get("min", 1)),
79
+ max=int(s.get("max", 1)), distinct=bool(s.get("distinct", True)))
80
+ for s in (rows or []))
81
+
82
+
83
+ def _peering_to_dict(p) -> dict:
84
+ d = {"name": p.name, "role": p.role, "topology": p.topology}
85
+ if p.min != 2:
86
+ d["min"] = p.min
87
+ if p.max != 0:
88
+ d["max"] = p.max
89
+ return d
90
+
91
+
92
+ def _peerings_from(rows) -> tuple:
93
+ from .fragments import Peering
94
+ return tuple(Peering(name=p["name"], role=p["role"], min=int(p.get("min", 2)),
95
+ max=int(p.get("max", 0)), topology=p.get("topology", "mesh"))
96
+ for p in (rows or []))
97
+
98
+
99
+ def _fork_to_dict(f) -> dict:
100
+ d = {"id": f.id}
101
+ if f.label:
102
+ d["label"] = f.label
103
+ d["difficulty"] = f.difficulty
104
+ d["kind"] = f.kind
105
+ d["objectives"] = [_obj_to_dict(o) for o in f.objectives]
106
+ return d
107
+
108
+
109
+ def _obj_to_dict(o: ObjectiveTemplate) -> dict:
110
+ d = {"id": o.id, "say": o.say, "check": o.check}
111
+ if o.kind and o.kind != "structural":
112
+ d["kind"] = o.kind
113
+ if o.probe:
114
+ d["probe"] = o.probe
115
+ if o.level:
116
+ d["level"] = o.level
117
+ if getattr(o, "stars", 0):
118
+ d["stars"] = o.stars
119
+ if not o.check:
120
+ d.pop("check")
121
+ return d
122
+
123
+
124
+ def _objs_from(rows) -> tuple:
125
+ from .fragments import ObjectiveTemplate
126
+ return tuple(ObjectiveTemplate(id=o["id"], say=o.get("say", o["id"]),
127
+ kind=o.get("kind", "structural"),
128
+ check=o.get("check", ""), probe=o.get("probe", ""),
129
+ level=o.get("level"), stars=int(o.get("stars", 0) or 0))
130
+ for o in (rows or []))
131
+
132
+
133
+ def fragment_from_dict(d: dict):
134
+ from .fragments import CONVERGE, Fork, Fragment
135
+ forks = tuple(Fork(id=f["id"], label=f.get("label", ""),
136
+ difficulty=int(f.get("difficulty", 1)), kind=f.get("kind", CONVERGE),
137
+ objectives=_objs_from(f.get("objectives")))
138
+ for f in (d.get("forks", []) or []))
139
+ return Fragment(
140
+ id=d["id"], layer=d.get("layer", "core"), teaches=d.get("teaches", ""),
141
+ spirit=d.get("spirit", ""), summary=d.get("summary", ""),
142
+ provides=tuple(d.get("provides", []) or ()), requires=tuple(d.get("requires", []) or ()),
143
+ objectives=_objs_from(d.get("objectives")),
144
+ misconceptions=tuple(d.get("misconceptions", []) or ()),
145
+ complete_when=d.get("complete_when", "all"), parent=d.get("parent", ""),
146
+ peers=tuple(d.get("peers", []) or ()), step=d.get("step", ""),
147
+ catalog=bool(d.get("catalog", True)), stage=dict(d.get("stage", {}) or {}),
148
+ slots=_slots_from(d.get("slots")), peerings=_peerings_from(d.get("peerings")),
149
+ forks=forks, engine_version=str(d.get("engine_version", "")),
150
+ schema_version=int(d.get("schema_version", 1)), author=str(d.get("author", "")),
151
+ certified=bool(d.get("certified", False)))
152
+
153
+
154
+ def to_yaml(frag: Fragment) -> str:
155
+ return yaml.safe_dump(to_dict(frag), sort_keys=False, allow_unicode=True, width=100)
156
+
157
+
158
+ def from_yaml(text: str) -> Fragment:
159
+ return fragment_from_dict(yaml.safe_load(text))
160
+
161
+
162
+ def validate(frag: Fragment) -> list[str]:
163
+ """Problems that make a fragment unloadable (empty = good)."""
164
+ problems: list[str] = []
165
+ if not frag.id:
166
+ problems.append("fragment missing id")
167
+ if frag.layer not in _LAYERS:
168
+ problems.append(f"bad layer {frag.layer!r}")
169
+ bad_roles = _caps.unknown_roles(list(frag.provides) + list(frag.requires))
170
+ if bad_roles:
171
+ problems.append(f"unknown capability roles {bad_roles}")
172
+ from .fragments import CONVERGE, DIVERGE
173
+ seen: set[str] = set()
174
+ _validate_objectives(frag.objectives, seen, problems, "objective")
175
+ for fk in frag.forks: # forks are validated exactly like the core ladder
176
+ if fk.kind not in (CONVERGE, DIVERGE):
177
+ problems.append(f"fork {fk.id!r}: bad kind {fk.kind!r}")
178
+ if not fk.objectives:
179
+ problems.append(f"fork {fk.id!r} has no objectives")
180
+ _validate_objectives(fk.objectives, seen, problems, f"fork {fk.id!r} objective")
181
+ return problems
182
+
183
+
184
+ def _validate_objectives(objs, seen: set, problems: list, label: str) -> None:
185
+ from .probes import probe_ok
186
+ for o in objs:
187
+ if o.id in seen:
188
+ problems.append(f"duplicate objective id {o.id!r}")
189
+ seen.add(o.id)
190
+ if o.kind == "structural" and o.check:
191
+ if not check_ok(o.check):
192
+ problems.append(f"{label} {o.id!r}: check does not parse: {o.check!r}")
193
+ else:
194
+ bad = unknown_element_types(o.check)
195
+ if bad:
196
+ problems.append(f"{label} {o.id!r}: unknown element types {bad}")
197
+ elif o.kind == "behavioral":
198
+ if not o.probe:
199
+ problems.append(f"behavioral {label} {o.id!r} has no probe")
200
+ elif not probe_ok(o.probe):
201
+ problems.append(f"{label} {o.id!r}: probe does not parse: {o.probe!r}")
202
+
203
+
204
+ def load_dir(path: str, *, strict: bool = True):
205
+ """Load + validate every *.yaml fragment in a directory.
206
+
207
+ Returns (fragments, warnings). `strict=True` (the system layer) raises on the first invalid pack —
208
+ a broken built-in is a bug. `strict=False` (the user / OTA layer) DECLINES a bad pack and keeps
209
+ going, collecting a warning — a broken authored/pulled fragment must never brick the client.
210
+ """
211
+ out: dict[str, Fragment] = {}
212
+ warnings: list[str] = []
213
+ if not os.path.isdir(path):
214
+ return out, warnings
215
+ for fp in sorted(glob.glob(os.path.join(path, "*.yaml"))):
216
+ name = os.path.basename(fp)
217
+ try:
218
+ frag = fragment_from_dict(yaml.safe_load(open(fp, encoding="utf-8").read()))
219
+ problems = validate(frag)
220
+ except Exception as e: # noqa: BLE001 — malformed YAML is just "declined"
221
+ problems = [f"could not read: {e}"]
222
+ frag = None
223
+ if problems:
224
+ msg = f"{name}: {'; '.join(problems)}"
225
+ if strict:
226
+ raise ValueError(msg)
227
+ warnings.append(msg) # refuse this one, keep the rest
228
+ continue
229
+ out[frag.id] = frag
230
+ return out, warnings
@@ -0,0 +1,169 @@
1
+ """Mission fragments — the composable building blocks.
2
+
3
+ A *fragment* is smaller than a whole mission: a labelled piece (core / exercise / observe) that
4
+ declares a capability contract (`provides` / `requires`) so the assembler can join fragments into a
5
+ complete mission by graph closure. The 12 seed Game-Catalog archetypes are re-exposed here as
6
+ `core` fragments (with capability metadata attached — the catalog stays the source of their
7
+ objectives, so nothing regresses), plus a handful of *enrichment* fragments (the exercise/observe
8
+ layers) that make a bare skeleton educational.
9
+
10
+ See GINI_MISSIONS_COMPOSABLE_DESIGN.md §3–4. Pure data; no Qt, no LLM.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+
16
+ from .content import content_dirs
17
+ from .objectives import Objective
18
+
19
+ CORE, EXERCISE, OBSERVE = "core", "exercise", "observe"
20
+
21
+ # fork kinds — see GINI_AUTHORING_DESIGN.md "FORKS — the difficulty knob"
22
+ CONVERGE, DIVERGE = "converge", "diverge"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ObjectiveTemplate:
27
+ id: str
28
+ say: str
29
+ kind: str = "structural" # structural | behavioral
30
+ check: str = "" # structural predicate (may contain {ref} placeholders)
31
+ probe: str = "" # behavioral probe (may contain {ref} placeholders)
32
+ level: int | None = None # explicit ladder tier (1 place · 2 connect · 3 group · 4 live)
33
+ stars: int = 0 # difficulty PASS: 0 = base, 1+ = harder progressive passes
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class Slot:
38
+ """A named, typed dependency socket — the composition non-terminal (see the graph-grammar model).
39
+ `role` is the capability it needs (is-a matched, so a `network` slot accepts a LAN or a routed
40
+ network). Cardinality `min..max` (max 0 = unbounded) makes it a fixed leg or a variable group
41
+ (a router's ≥2 legs, a load balancer's N backends). `distinct` = must bind to a different
42
+ provider than the fragment's other slots. Objectives reference a slot as `type@name`."""
43
+ name: str
44
+ role: str
45
+ min: int = 1
46
+ max: int = 1
47
+ distinct: bool = True
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class Peering:
52
+ """A LATERAL composition group — N sibling members of one role that interconnect as PEERS (a
53
+ graph, possibly with cycles), unlike a Slot which links the delta to each member. `topology`
54
+ shapes the graph: `mesh` (every pair), `ring`, `line`, or `star` (member 0 is the hub). This is
55
+ the axis slots can't express — meshes/graphs of routers. Objectives reference it as `type@name`
56
+ (all-pairs), and the assembler wires the members' connection points per topology."""
57
+ name: str
58
+ role: str
59
+ min: int = 2
60
+ max: int = 0 # 0 = unbounded
61
+ topology: str = "mesh" # mesh | ring | line | star
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class Fork:
66
+ """A difficulty branch off the core. `converge` = a harder *way to the same goal* (rejoins the
67
+ main line); `diverge` = a *different path* (may not rejoin). Completing a fork lifts the band
68
+ toward gold — difficulty is how deep into the forks you go."""
69
+ id: str
70
+ label: str = ""
71
+ difficulty: int = 1 # 1 = the golden path; higher = harder
72
+ kind: str = CONVERGE # converge | diverge
73
+ objectives: tuple[ObjectiveTemplate, ...] = ()
74
+
75
+ def instantiate(self) -> list[Objective]:
76
+ return [Objective(id=t.id, say=t.say, kind=t.kind, check=t.check, probe=t.probe,
77
+ level=t.level, stars=getattr(t, "stars", 0)) for t in self.objectives]
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class Fragment:
82
+ id: str
83
+ layer: str # core | exercise | observe
84
+ teaches: str = "" # concepts.Concept.key
85
+ spirit: str = "" # mechanism-free success description (game master reasons on it)
86
+ summary: str = ""
87
+ provides: tuple[str, ...] = ()
88
+ requires: tuple[str, ...] = ()
89
+ objectives: tuple[ObjectiveTemplate, ...] = ()
90
+ misconceptions: tuple[str, ...] = ()
91
+ complete_when: str = "all"
92
+ parent: str = "" # domain-taxonomy node (browsing / coarse selection)
93
+ peers: tuple[str, ...] = () # horizontal "goes-with" hints
94
+ step: str = "" # optional guided beat this fragment contributes
95
+ catalog: bool = True # a standalone, pickable mission? (False = pure layer)
96
+ stage: dict = field(default_factory=dict) # optional pre-built board (M3 staging)
97
+ slots: tuple[Slot, ...] = () # named dependency sockets (the composition non-terminals)
98
+ peerings: tuple[Peering, ...] = () # lateral peer groups (meshes/graphs — the Phase 5 axis)
99
+ forks: tuple[Fork, ...] = () # optional difficulty branches (the golden-path model)
100
+ # provenance / compatibility — stamped when a fragment is authored & blessed. Empty on built-ins
101
+ # (they ARE the engine). Lets the TC / a student client refuse-with-reason on a version gap.
102
+ engine_version: str = ""
103
+ schema_version: int = 1
104
+ author: str = ""
105
+ certified: bool = False # runtime-playtested at the client (winnable + live)
106
+
107
+ def instantiate(self) -> list[Objective]:
108
+ return [Objective(id=t.id, say=t.say, kind=t.kind, check=t.check, probe=t.probe,
109
+ level=t.level, stars=getattr(t, "stars", 0)) for t in self.objectives]
110
+
111
+
112
+ # -- registry: fragments loaded from the SYSTEM layer + the USER layer -------- #
113
+ # Content is data (YAML), behaviour is code. The system layer (bundled built-ins) is authoritative and
114
+ # must be valid — a broken built-in is a bug and raises. The user layer (~/.gini/content/fragments:
115
+ # authored + OTA-pulled) is best-effort — a broken pack there is DECLINED, not fatal, so a bad OTA
116
+ # fragment can never brick a student's client. User fragments overlay system ones by id.
117
+ LOAD_WARNINGS: list[str] = []
118
+
119
+
120
+ def _load() -> dict[str, Fragment]:
121
+ from . import content as _content
122
+ from . import fragment_yaml as _fy # deferred: fragment_yaml imports names from here
123
+ LOAD_WARNINGS.clear()
124
+ roots = _content.content_dirs()
125
+ out: dict[str, Fragment] = {}
126
+ for i, root in enumerate(roots):
127
+ strict = (i == 0) # system layer strict; user layer best-effort
128
+ loaded, warnings = _fy.load_dir(str(root), strict=strict)
129
+ out.update(loaded) # later roots (user) overlay earlier (system)
130
+ LOAD_WARNINGS.extend(warnings)
131
+ return out
132
+
133
+
134
+ FRAGMENTS: dict[str, Fragment] = _load()
135
+
136
+
137
+ def reload() -> None:
138
+ """Re-read the YAML packs from disk (for authoring / hot-edit / after an OTA pull)."""
139
+ global FRAGMENTS
140
+ FRAGMENTS = _load()
141
+
142
+
143
+ def get(fragment_id: str) -> Fragment | None:
144
+ return FRAGMENTS.get(fragment_id)
145
+
146
+
147
+ def all_fragments() -> list[Fragment]:
148
+ return list(FRAGMENTS.values())
149
+
150
+
151
+ def cores() -> list[Fragment]:
152
+ return [f for f in FRAGMENTS.values() if f.layer == CORE]
153
+
154
+
155
+ def by_layer(layer: str) -> list[Fragment]:
156
+ return [f for f in FRAGMENTS.values() if f.layer == layer]
157
+
158
+
159
+ def provides_role(fragment: Fragment, required: str) -> bool:
160
+ from . import capabilities as _caps
161
+ return _caps.any_satisfies(fragment.provides, required)
162
+
163
+
164
+ def find_providers(required: str, *, layer: str | None = None) -> list[Fragment]:
165
+ """Fragments that provide (satisfy) the `required` role, optionally restricted to a layer."""
166
+ out = [f for f in FRAGMENTS.values() if provides_role(f, required)]
167
+ if layer is not None:
168
+ out = [f for f in out if f.layer == layer]
169
+ return out
@@ -0,0 +1,2 @@
1
+ """Diagnose-game case sources — one module per game, each turning real telemetry into labeled
2
+ Cases for the shared engine in domain/diagnose.py."""
@@ -0,0 +1,119 @@
1
+ """Three more paging games on the page-replacement simulator — all with EXACT ground truth computed
2
+ by the simulator (no guessing on our side):
3
+
4
+ fault-count (estimate) — refs + frames + policy → how many faults?
5
+ belady (predict-outcome) — will adding ONE frame reduce FIFO's faults? (sometimes NO — the
6
+ anomaly)
7
+ showdown (classify) — on this string, does FIFO or LRU fault fewer (or tie)?
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from ..diagnose import Case, GameSpec
12
+ from ..paging_sim import resident_state, simulate
13
+
14
+ # a small bank of reference strings (kept short enough to reason about by hand)
15
+ _STRINGS = [
16
+ ([1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5], 3), # the classic Belady string
17
+ ([7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2], 3),
18
+ ([0, 1, 2, 0, 1, 2, 0, 1, 2, 0], 2),
19
+ ([1, 2, 3, 1, 4, 1, 5, 6, 2, 1], 3),
20
+ ([4, 3, 2, 1, 4, 3, 5, 4, 3, 2, 1, 5], 4),
21
+ ([1, 2, 1, 3, 1, 4, 1, 5, 1, 6], 3),
22
+ ]
23
+
24
+ # ---- fault-count (estimate) ----------------------------------------------- #
25
+ FAULTCOUNT_SPEC = GameSpec("faults-estimate", "Count the page faults",
26
+ "How many page faults will this run take?", classes=[],
27
+ answer="estimate", tolerance=2, unit="faults")
28
+
29
+
30
+ def faultcount_cases() -> list:
31
+ out = []
32
+ for i, (refs, frames) in enumerate(_STRINGS):
33
+ faults = simulate(refs, frames, "lru").faults
34
+ out.append(Case(f"fc-{i}", {"refs": refs, "frames": frames, "policy": "LRU"},
35
+ faults, subtitle=f"{faults} faults"))
36
+ return out
37
+
38
+
39
+ # ---- Belady spotter (predict-outcome) ------------------------------------- #
40
+ BELADY_CLASSES = ["fewer faults", "same or more"]
41
+ BELADY_SPEC = GameSpec("belady-spot", "Belady spotter",
42
+ "Will adding ONE more frame reduce FIFO's faults?",
43
+ BELADY_CLASSES, {"fewer faults": "fewer", "same or more": "same+"})
44
+
45
+
46
+ def belady_cases() -> list:
47
+ out = []
48
+ for i, (refs, frames) in enumerate(_STRINGS):
49
+ f0 = simulate(refs, frames, "fifo").faults
50
+ f1 = simulate(refs, frames + 1, "fifo").faults
51
+ truth = "fewer faults" if f1 < f0 else "same or more"
52
+ out.append(Case(f"bel-{i}",
53
+ {"refs": refs, "frames": frames, "policy": "FIFO",
54
+ "note": f"currently {f0} faults with {frames} frames — add one?"},
55
+ truth, subtitle=f"{frames}f: {f0} → {frames + 1}f: {f1}"))
56
+ return out
57
+
58
+
59
+ # ---- policy showdown (classify) ------------------------------------------- #
60
+ SHOWDOWN_CLASSES = ["FIFO", "LRU", "tie"]
61
+ SHOWDOWN_SPEC = GameSpec("policy-showdown", "Policy showdown",
62
+ "Which faults fewer on this string — FIFO or LRU?",
63
+ SHOWDOWN_CLASSES, {"FIFO": "FIFO", "LRU": "LRU", "tie": "tie"})
64
+
65
+
66
+ def showdown_cases() -> list:
67
+ out = []
68
+ for i, (refs, frames) in enumerate(_STRINGS):
69
+ fifo = simulate(refs, frames, "fifo").faults
70
+ lru = simulate(refs, frames, "lru").faults
71
+ truth = "FIFO" if fifo < lru else "LRU" if lru < fifo else "tie"
72
+ out.append(Case(f"sd-{i}", {"refs": refs, "frames": frames},
73
+ truth, subtitle=f"FIFO {fifo} · LRU {lru}"))
74
+ return out
75
+
76
+
77
+ # ---- next eviction (spot the culprit) ------------------------------------- #
78
+ NEXTEVICT_SPEC = GameSpec("next-evict", "Spot the next eviction",
79
+ "Which resident page is evicted on the next fault?",
80
+ classes=[], answer="spot")
81
+
82
+
83
+ def nextevict_cases() -> list:
84
+ out = []
85
+ for i, (refs, frames) in enumerate(_STRINGS):
86
+ for policy in ("fifo", "lru"):
87
+ resident, order = resident_state(refs, frames, policy)
88
+ if len(resident) < frames or not order:
89
+ continue
90
+ miss = [p for p in sorted(set(refs)) if p not in resident]
91
+ if not miss:
92
+ continue
93
+ victim = order[0]
94
+ out.append(Case(
95
+ f"ne-{i}-{policy}",
96
+ {"refs": refs, "frames": frames, "policy": policy.upper(),
97
+ "note": f"next access: page {miss[0]} (faults) — who is evicted?"},
98
+ victim, subtitle=f"evict page {victim}", options=sorted(resident)))
99
+ return out
100
+
101
+
102
+ # ---- policy rank (order them) --------------------------------------------- #
103
+ POLICYRANK_SPEC = GameSpec("policy-rank", "Rank the policies",
104
+ "Order FIFO, LRU, OPT by faults — fewest first.",
105
+ classes=[], answer="rank")
106
+
107
+
108
+ def policyrank_cases() -> list:
109
+ out = []
110
+ for i, (refs, frames) in enumerate(_STRINGS):
111
+ counts = {pol: simulate(refs, frames, pol).faults for pol in ("fifo", "lru", "opt")}
112
+ if len(set(counts.values())) < 3: # only strictly-ordered strings (no ties)
113
+ continue
114
+ order = sorted(counts, key=lambda p: counts[p])
115
+ out.append(Case(f"pr-{i}", {"refs": refs, "frames": frames},
116
+ [p.upper() for p in order],
117
+ subtitle=" < ".join(f"{p.upper()} {counts[p]}" for p in order),
118
+ options=["FIFO", "LRU", "OPT"]))
119
+ return out
@@ -0,0 +1,86 @@
1
+ """Case source for the guess-the-scheduler-policy game.
2
+
3
+ The signature is a short running-pid timeline (a Gantt window); the truth is the scheduler that
4
+ produced it. Live cases come from the real `SchedTimeline` + the kernel's actual `sched_policy`; the
5
+ demo deck synthesizes the three characteristic patterns:
6
+
7
+ round-robin — a strict cyclic rotation through the runnable set
8
+ priority — one process dominates (highest priority runs, others starve)
9
+ lottery — no pattern (a random draw)
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import random
14
+ from collections import Counter
15
+
16
+ from ..diagnose import Case, GameSpec
17
+
18
+ POLICY_CLASSES = ["round-robin", "priority", "lottery"]
19
+ POLICY_ABBR = {"round-robin": "RR", "priority": "prio", "lottery": "lotto"}
20
+
21
+ POLICY_SPEC = GameSpec(
22
+ id="guess-policy",
23
+ title="Guess the scheduler",
24
+ prompt="Which scheduler produced this timeline?",
25
+ classes=POLICY_CLASSES,
26
+ abbrev=POLICY_ABBR,
27
+ )
28
+
29
+
30
+ def _rr(pids, n) -> list:
31
+ return [pids[i % len(pids)] for i in range(n)]
32
+
33
+
34
+ def _priority(pids, n, rng) -> list:
35
+ """One pid dominates; every so often another sneaks a slice (aging), then back."""
36
+ top = pids[0]
37
+ out = []
38
+ for _ in range(n):
39
+ out.append(rng.choice(pids[1:]) if pids[1:] and rng.random() < 0.12 else top)
40
+ return out
41
+
42
+
43
+ def _lottery(pids, n, rng) -> list:
44
+ return [rng.choice(pids) for _ in range(n)]
45
+
46
+
47
+ def _is_periodic(pids, period) -> bool:
48
+ if period <= 0 or len(pids) <= period:
49
+ return False
50
+ return all(pids[i] == pids[i - period] for i in range(period, len(pids)))
51
+
52
+
53
+ def classify_timeline(pids) -> str:
54
+ """A rough heuristic used for the practice-mode HINT (not the grader). Dominant → priority;
55
+ strictly periodic over the distinct set → round-robin; otherwise lottery."""
56
+ if not pids:
57
+ return "lottery"
58
+ c = Counter(pids)
59
+ if c.most_common(1)[0][1] / len(pids) > 0.55:
60
+ return "priority"
61
+ if _is_periodic(pids, len(set(pids))):
62
+ return "round-robin"
63
+ return "lottery"
64
+
65
+
66
+ def demo_cases(seed: int = 0, n: int = 24, instances: int = 2) -> list:
67
+ """The offline deck: `instances` timelines per policy."""
68
+ rng = random.Random(seed)
69
+ pids = [3, 4, 5]
70
+ out = []
71
+ for k in range(instances):
72
+ rr = _rr(pids, n)
73
+ out.append(Case(f"rr{k}", rr, "round-robin", "round-robin", classify_timeline(rr)))
74
+ pr = _priority(pids, n, rng)
75
+ out.append(Case(f"prio{k}", pr, "priority", "priority", classify_timeline(pr)))
76
+ lo = _lottery(pids, n, rng)
77
+ out.append(Case(f"lot{k}", lo, "lottery", "lottery", classify_timeline(lo)))
78
+ return out
79
+
80
+
81
+ def live_cases(timeline_pids, policy_name) -> list:
82
+ """One case from the live Gantt window + the kernel's real policy (empty if we lack either)."""
83
+ pids = [p for p in (timeline_pids or []) if p is not None]
84
+ if len(pids) < 6 or not policy_name:
85
+ return []
86
+ return [Case("live", pids[-24:], policy_name, policy_name, classify_timeline(pids[-24:]))]
@@ -0,0 +1,48 @@
1
+ """Case source for the process-classify game (instance #1 of the Diagnose engine).
2
+
3
+ Reuses the fingerprint domain: a Case's signature is the 5-axis fingerprint, its truth is the
4
+ oracle's GROUND_TRUTH class for the launched program, and its hint is the rule-classifier's own guess
5
+ (shown in practice mode)."""
6
+ from __future__ import annotations
7
+
8
+ from ..diagnose import GameSpec
9
+ from ..diagnose import Case
10
+ from ..fingerprint import CLASSES, classify, demo_features, fingerprint, true_class
11
+
12
+ PROCESS_ABBR = {"cpu-bound": "cpu", "io-bound": "io", "memory": "mem",
13
+ "fork-heavy": "fork", "mixed": "mix"}
14
+
15
+ PROCESS_SPEC = GameSpec(
16
+ id="process-classify",
17
+ title="Classify the process",
18
+ prompt="What is this process?",
19
+ classes=CLASSES,
20
+ abbrev=PROCESS_ABBR,
21
+ )
22
+
23
+
24
+ def _case(name: str, fp: dict, cid: str) -> Case | None:
25
+ t = true_class(name)
26
+ if not t:
27
+ return None
28
+ return Case(id=cid, signature=fp, truth=t, subtitle=name, hint=classify(fp))
29
+
30
+
31
+ def demo_cases() -> list:
32
+ """Canned deck from the shipped programs — always available (offline fallback)."""
33
+ out = []
34
+ for f in demo_features():
35
+ c = _case(f.name, fingerprint(f), f"proc-{f.name}")
36
+ if c is not None:
37
+ out.append(c)
38
+ return out
39
+
40
+
41
+ def live_cases(fingerprints_by_pid: dict, names: dict) -> list:
42
+ """Cases from live fingerprints ({pid: fp}) whose program has a known ground-truth class."""
43
+ out = []
44
+ for pid, fp in fingerprints_by_pid.items():
45
+ c = _case(names.get(pid, ""), fp, f"proc-{pid}")
46
+ if c is not None:
47
+ out.append(c)
48
+ return out