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.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- 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
|
gini/domain/fragments.py
ADDED
|
@@ -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,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
|