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,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)
|
gini/domain/grouping.py
ADDED
|
@@ -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
|
gini/domain/legality.py
ADDED
|
@@ -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}
|