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
gini/domain/diagnose.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Diagnose game engine — the reusable core behind "diagnose from the signature" games.
|
|
2
|
+
|
|
3
|
+
The Process-Fingerprint classify game generalizes: show a real telemetry signature, the student
|
|
4
|
+
names the hidden cause, the ORACLE grades against deterministic ground truth, a confusion matrix
|
|
5
|
+
scores the run. This module owns that loop once; each subsystem supplies data + a renderer.
|
|
6
|
+
|
|
7
|
+
Everything here is pure and deterministic (seeded case selection), so it is fully unit-tested and the
|
|
8
|
+
UI stays a thin renderer. The label on every Case is derived from real kernel state — never guessed,
|
|
9
|
+
never from an LLM.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import random
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
PRACTICE, GRADED = "practice", "graded"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Case:
|
|
21
|
+
"""One labeled instance: a real signature + its deterministic ground truth."""
|
|
22
|
+
id: str
|
|
23
|
+
signature: object # opaque payload the game's renderer understands (fp / gantt / event)
|
|
24
|
+
truth: object # class/spot: a label; estimate: a number; rank: an ordered list
|
|
25
|
+
subtitle: str = "" # revealed after guessing ("writer", "lazy alloc at 0x1330")
|
|
26
|
+
hint: str | None = None # rule-classifier baseline, shown in practice mode only
|
|
27
|
+
options: list | None = None # per-case candidates for spot/rank (buttons/chips vary per case)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class GameSpec:
|
|
32
|
+
"""Static definition of a game: its answer kind + presentation.
|
|
33
|
+
|
|
34
|
+
answer="class" → pick one of `classes` (predict-outcome is just a 2/3-class game); scored by a
|
|
35
|
+
confusion matrix.
|
|
36
|
+
answer="estimate" → type a number; scored by closeness. `tolerance` is absolute unless
|
|
37
|
+
`relative` (a fraction of the truth); tolerance 0 means an EXACT answer.
|
|
38
|
+
"""
|
|
39
|
+
id: str
|
|
40
|
+
title: str
|
|
41
|
+
prompt: str
|
|
42
|
+
classes: list
|
|
43
|
+
abbrev: dict = field(default_factory=dict) # short labels for the confusion-matrix axes
|
|
44
|
+
answer: str = "class" # "class" | "estimate" | "spot" | "rank"
|
|
45
|
+
tolerance: float = 0.0
|
|
46
|
+
relative: bool = False
|
|
47
|
+
unit: str = "" # shown next to the numeric input (e.g. "faults")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def confusion_matrix(pairs, classes) -> dict:
|
|
51
|
+
"""[(true, predicted), …] -> {(true, pred): count} over the class set. Off-diagonal = confusion."""
|
|
52
|
+
m = {(t, p): 0 for t in classes for p in classes}
|
|
53
|
+
for true, pred in pairs:
|
|
54
|
+
if (true, pred) in m:
|
|
55
|
+
m[(true, pred)] += 1
|
|
56
|
+
return m
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def accuracy(pairs) -> float:
|
|
60
|
+
"""Fraction of (true, pred) pairs on the diagonal."""
|
|
61
|
+
pairs = list(pairs)
|
|
62
|
+
if not pairs:
|
|
63
|
+
return 0.0
|
|
64
|
+
return sum(1 for t, p in pairs if t == p) / len(pairs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def order_score(answer, truth) -> float:
|
|
68
|
+
"""Rank games: fraction of ordered pairs in `truth` that appear in the same order in `answer`
|
|
69
|
+
(a normalized Kendall agreement, 0..1; 1.0 = a perfect ordering). Tolerant of items the student
|
|
70
|
+
didn't place."""
|
|
71
|
+
truth = list(truth or [])
|
|
72
|
+
idx = {x: i for i, x in enumerate(answer or [])}
|
|
73
|
+
good = pairs = 0
|
|
74
|
+
for i in range(len(truth)):
|
|
75
|
+
for j in range(i + 1, len(truth)):
|
|
76
|
+
a, b = truth[i], truth[j]
|
|
77
|
+
if a in idx and b in idx:
|
|
78
|
+
pairs += 1
|
|
79
|
+
if idx[a] < idx[b]:
|
|
80
|
+
good += 1
|
|
81
|
+
return good / pairs if pairs else 0.0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def per_class(pairs, classes) -> dict:
|
|
85
|
+
"""{class: {'recall': r, 'precision': p, 'n': support}} from the pairs — for a richer scoreboard."""
|
|
86
|
+
out = {}
|
|
87
|
+
for c in classes:
|
|
88
|
+
tp = sum(1 for t, g in pairs if t == c and g == c)
|
|
89
|
+
support = sum(1 for t, _ in pairs if t == c)
|
|
90
|
+
called = sum(1 for _, g in pairs if g == c)
|
|
91
|
+
out[c] = {"recall": tp / support if support else 0.0,
|
|
92
|
+
"precision": tp / called if called else 0.0, "n": support}
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DiagnoseSession:
|
|
97
|
+
"""The pure game state machine. Feed it Cases; it serves mysteries, records guesses, and scores.
|
|
98
|
+
|
|
99
|
+
Modes: PRACTICE serves cases forever with an immediate reveal + hint; GRADED serves a fixed
|
|
100
|
+
`deck` of cases with the hint suppressed, then finishes. Case selection is seeded for
|
|
101
|
+
reproducibility (a mission can replay the same deck)."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, spec: GameSpec, cases=(), mode: str = PRACTICE, deck: int = 10,
|
|
104
|
+
seed: int = 0) -> None:
|
|
105
|
+
self.spec = spec
|
|
106
|
+
self.mode = mode
|
|
107
|
+
self.deck = deck
|
|
108
|
+
self._rng = random.Random(seed)
|
|
109
|
+
self._cases = list(cases)
|
|
110
|
+
self.pairs: list = []
|
|
111
|
+
self.current: Case | None = None
|
|
112
|
+
self.finished = False
|
|
113
|
+
self._served = 0
|
|
114
|
+
|
|
115
|
+
# -- case pool --------------------------------------------------------- #
|
|
116
|
+
def set_cases(self, cases) -> None:
|
|
117
|
+
"""Refresh the available cases (e.g. live telemetry changed) without losing the score."""
|
|
118
|
+
self._cases = list(cases)
|
|
119
|
+
|
|
120
|
+
def has_cases(self) -> bool:
|
|
121
|
+
return bool(self._cases)
|
|
122
|
+
|
|
123
|
+
# -- flow -------------------------------------------------------------- #
|
|
124
|
+
def next(self) -> Case | None:
|
|
125
|
+
"""Serve the next mystery, or None when a graded run is complete / no cases exist."""
|
|
126
|
+
if self.mode == GRADED and self._served >= self.deck:
|
|
127
|
+
self.finished = True
|
|
128
|
+
self.current = None
|
|
129
|
+
return None
|
|
130
|
+
if not self._cases:
|
|
131
|
+
self.current = None
|
|
132
|
+
return None
|
|
133
|
+
self.current = self._rng.choice(self._cases)
|
|
134
|
+
self._served += 1
|
|
135
|
+
return self.current
|
|
136
|
+
|
|
137
|
+
def guess(self, label) -> dict:
|
|
138
|
+
"""Record a guess against the current mystery; return the reveal (correct?/truth/subtitle/
|
|
139
|
+
hint). Correctness is per-kind (class equality, or numeric closeness for estimate). Hint is
|
|
140
|
+
only surfaced in practice mode."""
|
|
141
|
+
if self.current is None:
|
|
142
|
+
return {}
|
|
143
|
+
truth = self.current.truth
|
|
144
|
+
self.pairs.append((truth, label))
|
|
145
|
+
part = order_score(label, truth) if self.spec.answer == "rank" else None
|
|
146
|
+
return {"correct": self._hit(truth, label), "truth": truth, "label": label,
|
|
147
|
+
"subtitle": self.current.subtitle, "partial": part,
|
|
148
|
+
"hint": self.current.hint if self.mode == PRACTICE else None,
|
|
149
|
+
"complete": self.mode == GRADED and self._served >= self.deck}
|
|
150
|
+
|
|
151
|
+
def _hit(self, truth, answer) -> bool:
|
|
152
|
+
"""Correct? Class/spot = exact match; estimate = within tolerance; rank = perfect order."""
|
|
153
|
+
kind = self.spec.answer
|
|
154
|
+
if kind == "estimate":
|
|
155
|
+
try:
|
|
156
|
+
t, a = float(truth), float(answer)
|
|
157
|
+
except (TypeError, ValueError):
|
|
158
|
+
return False
|
|
159
|
+
tol = abs(t) * self.spec.tolerance if self.spec.relative else self.spec.tolerance
|
|
160
|
+
return abs(a - t) <= tol
|
|
161
|
+
if kind == "rank":
|
|
162
|
+
return order_score(answer, truth) == 1.0
|
|
163
|
+
return answer == truth
|
|
164
|
+
|
|
165
|
+
def reset(self) -> None:
|
|
166
|
+
self.pairs = []
|
|
167
|
+
self.current = None
|
|
168
|
+
self.finished = False
|
|
169
|
+
self._served = 0
|
|
170
|
+
|
|
171
|
+
# -- scoreboard -------------------------------------------------------- #
|
|
172
|
+
def matrix(self) -> dict:
|
|
173
|
+
return confusion_matrix(self.pairs, self.spec.classes)
|
|
174
|
+
|
|
175
|
+
def accuracy(self) -> float:
|
|
176
|
+
if not self.pairs:
|
|
177
|
+
return 0.0
|
|
178
|
+
return sum(1 for t, a in self.pairs if self._hit(t, a)) / len(self.pairs)
|
|
179
|
+
|
|
180
|
+
def score(self) -> tuple[int, int]:
|
|
181
|
+
return sum(1 for t, a in self.pairs if self._hit(t, a)), len(self.pairs)
|
|
182
|
+
|
|
183
|
+
def mean_abs_error(self) -> float:
|
|
184
|
+
"""Estimate games: average |guess − truth| over the run (0 for a perfect run)."""
|
|
185
|
+
errs = []
|
|
186
|
+
for t, a in self.pairs:
|
|
187
|
+
try:
|
|
188
|
+
errs.append(abs(float(a) - float(t)))
|
|
189
|
+
except (TypeError, ValueError):
|
|
190
|
+
pass
|
|
191
|
+
return sum(errs) / len(errs) if errs else 0.0
|
|
192
|
+
|
|
193
|
+
def mean_order_score(self) -> float:
|
|
194
|
+
"""Rank games: average pairwise-order agreement over the run (partial credit)."""
|
|
195
|
+
if not self.pairs:
|
|
196
|
+
return 0.0
|
|
197
|
+
return sum(order_score(a, t) for t, a in self.pairs) / len(self.pairs)
|
|
198
|
+
|
|
199
|
+
def remaining(self) -> int | None:
|
|
200
|
+
"""Cases left in a graded run (None in practice)."""
|
|
201
|
+
return None if self.mode == PRACTICE else max(0, self.deck - self._served)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Teaching guide for every palette element — what it is and WHEN to use it.
|
|
2
|
+
|
|
3
|
+
This is the knowledge the tutor grounds on when a student asks about an element from
|
|
4
|
+
the palette (rather than a placed instance). Each entry is plain, student-facing, and
|
|
5
|
+
focuses on the decision: when would I reach for this vs. a similar element?
|
|
6
|
+
|
|
7
|
+
The deterministic core returns this text; an LLM (when connected) rephrases/expands it
|
|
8
|
+
into a fuller explanation, but stays anchored to these facts.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
GUIDE: dict[str, str] = {
|
|
13
|
+
# --- core networking -------------------------------------------------- #
|
|
14
|
+
"router": (
|
|
15
|
+
"A Router forwards packets between DIFFERENT IP networks (subnets), choosing the "
|
|
16
|
+
"next hop from its routing table and decrementing TTL. Use one whenever two "
|
|
17
|
+
"subnets must talk — it's the gateway out of a LAN. In gBuilder the router is a "
|
|
18
|
+
"real, programmable C data plane you can open in the Router Lab. Reach for a "
|
|
19
|
+
"router (not a switch) the moment hosts are on different 10.0.x.0/24 networks."),
|
|
20
|
+
"switch": (
|
|
21
|
+
"A Switch is a Layer-2 device: it forwards Ethernet FRAMES within a single network "
|
|
22
|
+
"by learning which MAC address lives on which port. Use it to connect several hosts "
|
|
23
|
+
"on the SAME subnet into one LAN. It does not route between subnets and assigns no "
|
|
24
|
+
"IPs. Choose a switch over a hub for any realistic LAN — it sends traffic only to "
|
|
25
|
+
"the right port instead of flooding everyone."),
|
|
26
|
+
"hub": (
|
|
27
|
+
"A Hub is a Layer-1 repeater: every frame that arrives is blindly copied to ALL "
|
|
28
|
+
"other ports, so all hosts share one collision domain. It's mostly a teaching tool "
|
|
29
|
+
"— use it to demonstrate collisions, flooding, and why switches replaced hubs. "
|
|
30
|
+
"Avoid it in real designs; a switch does the same job without the noise."),
|
|
31
|
+
"firewall": (
|
|
32
|
+
"A Firewall filters traffic against rules (allow/deny by address, port, protocol). "
|
|
33
|
+
"Place one between zones of different trust — e.g. between your LAN and the Internet "
|
|
34
|
+
"— to control exactly what is permitted. In gBuilder it behaves like a router with "
|
|
35
|
+
"an ACL stage, so use it when the lesson is about policy, not just connectivity."),
|
|
36
|
+
"vnf": (
|
|
37
|
+
"A VNF (Virtualized Network Function) is a CONTAINER running a network function — a "
|
|
38
|
+
"firewall, IDS, cache, or shaper — inserted INLINE in the forwarding path. Wire it "
|
|
39
|
+
"between two elements and traffic flows through it; pick the function in 'Kind' and "
|
|
40
|
+
"give its config in 'Rules'. Chain several in series (firewall → IDS → NAT) to build "
|
|
41
|
+
"a Service Function Chain (SFC). Use it to teach NFV — functions as software in the "
|
|
42
|
+
"path — versus a fixed hardware appliance."),
|
|
43
|
+
"wap": (
|
|
44
|
+
"An Access Point bridges WIRELESS clients onto the wired LAN — it's essentially a "
|
|
45
|
+
"switch with a radio. Use it when the topology needs Wi-Fi devices joining an "
|
|
46
|
+
"existing wired network."),
|
|
47
|
+
"cloud": (
|
|
48
|
+
"The Internet element represents the outside world / upstream network. Connect it "
|
|
49
|
+
"to a router or gateway to model traffic leaving your topology toward the public "
|
|
50
|
+
"Internet."),
|
|
51
|
+
"gini32": (
|
|
52
|
+
"A GINI32 Board is a REAL ESP32 on your desk running the gBridge firmware — the "
|
|
53
|
+
"only element that is not emulated. It raises its own Wi-Fi network; phones, "
|
|
54
|
+
"Raspberry Pis and sensors that join it become hosts inside the drawn topology, "
|
|
55
|
+
"their traffic carried as Ethernet-in-UDP to the emulated core. Wire it to a "
|
|
56
|
+
"router or switch and set BoardID to the id on the board's LABEL (written by "
|
|
57
|
+
"`gini32 provision --id`); everything else — address, hotspot name, subnet — is "
|
|
58
|
+
"handed to the board from the canvas when it checks in. Mode 'routed' (the "
|
|
59
|
+
"default) gives the physical subnet its own route so traffic flows BOTH ways; "
|
|
60
|
+
"'nat' hides the real devices behind the board's single address, which is the "
|
|
61
|
+
"asymmetry the book asks you to discover. Channel is REPORTED by the board, not "
|
|
62
|
+
"set — one radio serves both faces, so the hotspot follows the uplink's channel. "
|
|
63
|
+
"Devices that join the hotspot appear on the canvas by themselves."),
|
|
64
|
+
|
|
65
|
+
# --- software-defined networking -------------------------------------- #
|
|
66
|
+
"ovs": (
|
|
67
|
+
"Open vSwitch is a programmable software switch that speaks OpenFlow. Use it instead "
|
|
68
|
+
"of a plain switch when the lesson is SDN — you want an external controller to install "
|
|
69
|
+
"the forwarding rules rather than relying on MAC learning."),
|
|
70
|
+
"controller": (
|
|
71
|
+
"An OpenFlow Controller is the SDN 'brain': it connects to OVS/switches and programs "
|
|
72
|
+
"their flow tables, deciding centrally how packets are handled. Use it with Open "
|
|
73
|
+
"vSwitch to show centralized control separated from the data plane."),
|
|
74
|
+
|
|
75
|
+
# --- compute ---------------------------------------------------------- #
|
|
76
|
+
"host": (
|
|
77
|
+
"A Machine is an end host — a PC or server that runs programs and originates and "
|
|
78
|
+
"receives traffic. It's the source and sink of every experiment. In gBuilder it "
|
|
79
|
+
"runs a Debian container with the GINI networking toolkit preinstalled — ping, "
|
|
80
|
+
"traceroute, mtr, tcpdump, tshark, nmap, nc, dig, iperf3, curl, hping3, iptables, "
|
|
81
|
+
"and more — so the book's experiments work out of the box (apt is there for "
|
|
82
|
+
"anything else). Give it one link to a switch or router; attach it to two for two "
|
|
83
|
+
"networks at once (multi-homed)."),
|
|
84
|
+
"instance": (
|
|
85
|
+
"A cloud Instance is a virtual machine in a cloud provider. Use it as a host inside "
|
|
86
|
+
"cloud scenarios (VPCs, security groups) rather than a plain LAN."),
|
|
87
|
+
"xv6": (
|
|
88
|
+
"An xv6 Machine runs the real xv6 teaching kernel (MIT 6.1810) on QEMU-RISC-V — not a "
|
|
89
|
+
"container, an actual operating system booting from a tiny kernel. It is the OS-course "
|
|
90
|
+
"workbench: double-click it to open the Machine Lab and watch the kernel run — the "
|
|
91
|
+
"scheduler moving the CPU between processes, the process table, CPU registers, memory "
|
|
92
|
+
"and the kernel stack — then slow the time-slice to watch context switches one at a "
|
|
93
|
+
"time. It is standalone by default (no fabric wiring); you drive it from its own serial "
|
|
94
|
+
"console. Use it to teach processes, scheduling, virtual memory, traps and file "
|
|
95
|
+
"systems on a kernel small enough to read end to end."),
|
|
96
|
+
"desktop": (
|
|
97
|
+
"A Desktop is a headful Machine — a real Linux host on the fabric (it has an IP, is "
|
|
98
|
+
"pingable and routable, and carries the usual networking tools) that also runs a light "
|
|
99
|
+
"graphical desktop: a fluxbox window manager, a file manager, a terminal, and the Dillo "
|
|
100
|
+
"browser. Double-click it to open its screen in an embedded window over noVNC. Reach for it "
|
|
101
|
+
"when you want a GUI in the topology — for example, to point a browser at a web server "
|
|
102
|
+
"another machine is serving. It carries an X stack, so it's heavier than a plain Machine; "
|
|
103
|
+
"use a plain Machine when you only need a shell."),
|
|
104
|
+
"terminal": (
|
|
105
|
+
"A Terminal is an xv6 Machine's console — a screen and keyboard in one, like a real tty "
|
|
106
|
+
"(xv6's console is a single bidirectional UART, so output and input share one stream). "
|
|
107
|
+
"Connect it to an xv6 Machine and double-click to open it: type xv6 commands (ls, cat, "
|
|
108
|
+
"echo, spin 10 &, …) and their output appears inline. Up-arrow recalls history and `help` "
|
|
109
|
+
"lists what you can run; this is the authentic way to launch programs with arguments."),
|
|
110
|
+
"storage_volume": (
|
|
111
|
+
"A Storage Volume is the xv6 Machine's disk. Connect it and double-click to open the "
|
|
112
|
+
"Storage view — the on-disk layout (boot/super/log/inodes/bitmap/data), the inodes and "
|
|
113
|
+
"directory tree, the buffer cache, and the write-ahead log. xv6 has a single custom file "
|
|
114
|
+
"system with no VFS; supporting alternate file systems is an advanced student project."),
|
|
115
|
+
"kinstance": (
|
|
116
|
+
"A Kata Instance runs your workload inside a lightweight microVM with its OWN guest "
|
|
117
|
+
"kernel (Kata Containers), instead of sharing the host kernel like a normal container. "
|
|
118
|
+
"Use it to compare VM-vs-container trade-offs: stronger isolation, but slower boot and "
|
|
119
|
+
"more memory/IO overhead. It needs a Kata-enabled Linux backend and is kept to flat "
|
|
120
|
+
"experiment topologies (no VPCs, no Kubernetes)."),
|
|
121
|
+
"instance_group": (
|
|
122
|
+
"A Pod Autoscaler is a Kubernetes Horizontal Pod Autoscaler (HPA): it watches one "
|
|
123
|
+
"Deployment's CPU and adds or removes pod replicas between Min and Max to hold a "
|
|
124
|
+
"target CPU%. Connect it to a Pod — the HPA scales that one workload, not the whole "
|
|
125
|
+
"cluster, because each workload scales on its own policy. Note the contrast with two "
|
|
126
|
+
"other K8s autoscalers: the Cluster Autoscaler adds/removes Nodes (machines) when "
|
|
127
|
+
"pods don't fit, and the Vertical Pod Autoscaler (VPA) resizes each pod's CPU/memory. "
|
|
128
|
+
"The HPA is also the container-world cousin of a cloud Auto Scaling Group, which "
|
|
129
|
+
"instead scales VM instances behind a load balancer."),
|
|
130
|
+
"region": (
|
|
131
|
+
"A Region / Zone groups cloud resources by physical location. Use it to discuss "
|
|
132
|
+
"latency, availability zones, and multi-region designs."),
|
|
133
|
+
|
|
134
|
+
# --- containers & kubernetes ------------------------------------------ #
|
|
135
|
+
"container": (
|
|
136
|
+
"A Container packages one application with its dependencies, sharing the host kernel. "
|
|
137
|
+
"Use it for lightweight, app-per-box scenarios; many containers run on one node."),
|
|
138
|
+
"pod": (
|
|
139
|
+
"A Pod is Kubernetes' smallest unit — one or more containers that share a network "
|
|
140
|
+
"namespace and IP. Use it when teaching Kubernetes networking specifically."),
|
|
141
|
+
"k8s_node": (
|
|
142
|
+
"A K8s Node is a worker machine that runs pods. Use it to show how a cluster places "
|
|
143
|
+
"workloads across several nodes."),
|
|
144
|
+
"k8s_cluster": (
|
|
145
|
+
"A K8s Cluster groups nodes under one control plane. Use it as the boundary for a "
|
|
146
|
+
"Kubernetes deployment."),
|
|
147
|
+
"registry": (
|
|
148
|
+
"A Container Registry stores and serves container images. Use it to model where "
|
|
149
|
+
"nodes pull images from."),
|
|
150
|
+
|
|
151
|
+
# --- cloud networking -------------------------------------------------- #
|
|
152
|
+
"vpc": (
|
|
153
|
+
"A VPC is your private, isolated network in a cloud — the cloud analog of a site LAN, "
|
|
154
|
+
"carved into subnets. Use it as the top-level container for cloud instances and "
|
|
155
|
+
"subnets."),
|
|
156
|
+
"cloud_subnet": (
|
|
157
|
+
"A Cloud Subnet is one IP range inside a VPC, often split public vs. private. Use it "
|
|
158
|
+
"to separate internet-facing tiers from internal ones."),
|
|
159
|
+
"security_group": (
|
|
160
|
+
"A Security Group is a per-instance stateful firewall (allow rules by port/source). "
|
|
161
|
+
"Use it to control instance traffic in a cloud — conceptually an ACL attached to the "
|
|
162
|
+
"instance rather than a separate box."),
|
|
163
|
+
"gateway": (
|
|
164
|
+
"A Gateway connects a VPC/subnet to something outside it (the Internet, or another "
|
|
165
|
+
"VPC). Use it as the controlled exit/entry point for cloud traffic."),
|
|
166
|
+
"load_balancer": (
|
|
167
|
+
"A Load Balancer spreads incoming connections across several backends (instances or "
|
|
168
|
+
"an autoscaling group). Use it to model high availability and horizontal scaling — "
|
|
169
|
+
"it's a forwarder that picks a healthy backend per request."),
|
|
170
|
+
|
|
171
|
+
# --- storage & data ---------------------------------------------------- #
|
|
172
|
+
"object_store": (
|
|
173
|
+
"Object Storage holds files/blobs addressed by key over HTTP (think S3). Use it for "
|
|
174
|
+
"static assets, backups, and data lakes — not for low-latency block access."),
|
|
175
|
+
"block_volume": (
|
|
176
|
+
"A Block Volume is a virtual disk attached to one instance. Use it when an instance "
|
|
177
|
+
"needs persistent, filesystem-style storage."),
|
|
178
|
+
"database": (
|
|
179
|
+
"A Managed Database is a provider-run SQL/NoSQL store. Use it so apps have a "
|
|
180
|
+
"queryable, durable data tier without you running the DB host yourself."),
|
|
181
|
+
|
|
182
|
+
# --- serverless -------------------------------------------------------- #
|
|
183
|
+
"function": (
|
|
184
|
+
"A Function is serverless code that runs on demand and scales to zero. Use it for "
|
|
185
|
+
"event-driven glue logic where you don't manage a server."),
|
|
186
|
+
"api_gateway": (
|
|
187
|
+
"An API Gateway is the managed front door for APIs — routing, auth, rate limiting — "
|
|
188
|
+
"usually in front of functions or services. Use it to expose backends to clients."),
|
|
189
|
+
"queue": (
|
|
190
|
+
"A Message Queue buffers messages between producers and consumers so they can work "
|
|
191
|
+
"asynchronously and absorb bursts. Use it to decouple components."),
|
|
192
|
+
|
|
193
|
+
# --- edge & traffic ---------------------------------------------------- #
|
|
194
|
+
"proxy": (
|
|
195
|
+
"A Reverse Proxy sits in front of your services and forwards client requests to "
|
|
196
|
+
"them, adding TLS, routing by host/path, and a single entry point. Use it (vs a "
|
|
197
|
+
"raw Load Balancer) when you want application-aware routing and a dashboard — in "
|
|
198
|
+
"gBuilder it runs real Traefik."),
|
|
199
|
+
"web_app": (
|
|
200
|
+
"A Web App is a small demo backend that returns its own hostname. Use several of "
|
|
201
|
+
"them behind a Load Balancer or Reverse Proxy to SEE requests spread across "
|
|
202
|
+
"backends — the simplest way to teach horizontal scaling and load balancing."),
|
|
203
|
+
|
|
204
|
+
# --- streaming & messaging --------------------------------------------- #
|
|
205
|
+
"stream": (
|
|
206
|
+
"An Event Stream is an append-only log of events that many consumers read at their "
|
|
207
|
+
"own pace (Kafka-style). Use it for event-driven pipelines, metrics, and replay — "
|
|
208
|
+
"choose it over a Message Queue when you need durable, replayable history rather "
|
|
209
|
+
"than once-delivered messages."),
|
|
210
|
+
"messaging": (
|
|
211
|
+
"Pub/Sub (NATS) delivers messages to whoever is subscribed to a subject, right now. "
|
|
212
|
+
"Use it for fast, lightweight fan-out between services when you don't need the "
|
|
213
|
+
"durable, replayable log that an Event Stream gives you."),
|
|
214
|
+
|
|
215
|
+
# --- cache & NoSQL ----------------------------------------------------- #
|
|
216
|
+
"cache": (
|
|
217
|
+
"A Cache (Redis) keeps hot data in memory for microsecond reads. Put one in front "
|
|
218
|
+
"of a database to cut load and latency — use it for sessions, counters, and "
|
|
219
|
+
"results you can afford to recompute if lost."),
|
|
220
|
+
"nosql": (
|
|
221
|
+
"A NoSQL Database (MongoDB) stores schema-flexible documents instead of SQL tables. "
|
|
222
|
+
"Reach for it when your data is hierarchical/varied and you value flexible schemas "
|
|
223
|
+
"over relational joins and constraints."),
|
|
224
|
+
|
|
225
|
+
# --- observability ----------------------------------------------------- #
|
|
226
|
+
"metrics": (
|
|
227
|
+
"Metrics (Prometheus) scrapes numeric time-series from your services and lets you "
|
|
228
|
+
"query them with PromQL. Use it to measure rate, errors, and latency — the data "
|
|
229
|
+
"behind every dashboard and alert."),
|
|
230
|
+
"dashboard": (
|
|
231
|
+
"Dashboards (Grafana) visualise metrics and logs as graphs. Point it at a Metrics "
|
|
232
|
+
"(Prometheus) source so students can watch a system's behaviour while they load it."),
|
|
233
|
+
"tracing": (
|
|
234
|
+
"Tracing (Jaeger) records the path of a single request as it hops across services, "
|
|
235
|
+
"with timing at each step. Use it to teach where latency comes from in a "
|
|
236
|
+
"distributed system — the 'why is this slow?' tool."),
|
|
237
|
+
|
|
238
|
+
# --- workload & testing ------------------------------------------------ #
|
|
239
|
+
"load_generator": (
|
|
240
|
+
"A Load Generator (Fortio) fires controlled traffic at a target and reports QPS and "
|
|
241
|
+
"latency histograms. Use it to run experiments — push a service until it slows or "
|
|
242
|
+
"an autoscaler reacts, and watch the metrics/dashboards respond."),
|
|
243
|
+
# --- Sources / Sinks (riders: run inside a donor, no container of their own) --------- #
|
|
244
|
+
"ping_probe": (
|
|
245
|
+
"A Ping Probe is a Source: attach it to a Machine/Router and it runs `ping` INSIDE that "
|
|
246
|
+
"donor, streaming live RTT and loss. Double-click to start/stop. Count 0 pings until you "
|
|
247
|
+
"stop it; Count N sends N. It has no container — it rides its donor over a dotted edge."),
|
|
248
|
+
"http_probe": (
|
|
249
|
+
"An HTTP Probe is a Source: it runs `curl` inside its donor at a Target/Path and reports "
|
|
250
|
+
"2xx success rate and latency. Continuous (Count 0) or a fixed number (Count N). Lighter "
|
|
251
|
+
"than the Load Generator — use it to prove a service answers, not to stress it."),
|
|
252
|
+
"packet_view": (
|
|
253
|
+
"A Packet View is a Sink: it runs `tcpdump` inside its donor and streams the packets it "
|
|
254
|
+
"sees. Attach it to the RECEIVER and run a Source on the sender to watch traffic arrive. "
|
|
255
|
+
"Count 0 captures until stopped; Count N stops after N packets."),
|
|
256
|
+
"dns_probe": (
|
|
257
|
+
"A DNS Probe is a Source: it resolves the hostname in Target from inside its donor and "
|
|
258
|
+
"reports whether (and to what) it resolved. It uses the system resolver over the drawn "
|
|
259
|
+
"gini0 network (GINI writes peer names into /etc/hosts), so it returns the topology address "
|
|
260
|
+
"— not Docker's. (Target is the name to look up; Name is just this element's label.)"),
|
|
261
|
+
"traceroute_probe": (
|
|
262
|
+
"A Traceroute is a Source: it runs `traceroute` inside its donor to a Target and reports "
|
|
263
|
+
"the hop path. Pair it with a Packet View to watch each hop, or use it to see routing."),
|
|
264
|
+
"iperf_client": (
|
|
265
|
+
"An iPerf Client is a Source: it drives `iperf3 -c` at a Target running an iPerf Server "
|
|
266
|
+
"and reports measured throughput (Mbit/s). Use it to teach bandwidth and congestion."),
|
|
267
|
+
"iperf_server": (
|
|
268
|
+
"An iPerf Server is a Sink: it runs `iperf3 -s` inside its donor and reports the "
|
|
269
|
+
"throughput it receives from an iPerf Client. Pair the two across a link to measure it."),
|
|
270
|
+
"iface_stats": (
|
|
271
|
+
"Interface Stats is a Sink: it reads /proc/net/dev inside its donor and streams rx/tx "
|
|
272
|
+
"packet and byte counts, so you can watch traffic volume rise and fall on an interface."),
|
|
273
|
+
"xv6_shell": (
|
|
274
|
+
"A Shell Probe is a Source for the xv6 Machine: type a Command (e.g. `ls`, `cat README`) "
|
|
275
|
+
"and it types it into the kernel's console and streams the output back — the OS-course "
|
|
276
|
+
"counterpart of the HTTP Probe."),
|
|
277
|
+
"xv6_workload": (
|
|
278
|
+
"A Workload is a Source for the xv6 Machine: it spawns a Program (`spin`, `forktest`, "
|
|
279
|
+
"`usertests`) to drive the scheduler. Run it in the background so several compete, and "
|
|
280
|
+
"watch the effect in the Machine Lab — the OS-course load generator."),
|
|
281
|
+
"freedos": (
|
|
282
|
+
"FreeDOS is an OS Zoo element: a real, still-maintained MS-DOS-compatible operating "
|
|
283
|
+
"system running under QEMU in a container, its screen embedded in gBuilder over noVNC. "
|
|
284
|
+
"Double-click it to open the Zoo Lab and use it live — the single-tasking, real-mode "
|
|
285
|
+
"command-line PC of the DOS era. It boots out of the box; boots are ephemeral unless you "
|
|
286
|
+
"turn on Persist."),
|
|
287
|
+
"kolibri": (
|
|
288
|
+
"KolibriOS is an OS Zoo element: a tiny GUI operating system written entirely in assembly, "
|
|
289
|
+
"running under QEMU and embedded over noVNC. The whole system boots from a single 1.44 MB "
|
|
290
|
+
"floppy to a graphical desktop in seconds — even under software emulation — so it's the "
|
|
291
|
+
"fast OS Zoo guest to reach for. Double-click to open the Zoo Lab; ephemeral unless "
|
|
292
|
+
"Persist is on."),
|
|
293
|
+
"menuet": (
|
|
294
|
+
"MenuetOS is an OS Zoo element: the assembly GUI OS that KolibriOS forked from, running "
|
|
295
|
+
"under QEMU and embedded over noVNC. Like KolibriOS, the whole graphical desktop lives on "
|
|
296
|
+
"a single 1.44 MB floppy and boots in seconds under emulation (GINI ships the open-source "
|
|
297
|
+
"32-bit build). Double-click to open the Zoo Lab; ephemeral unless Persist is on."),
|
|
298
|
+
"msdos": (
|
|
299
|
+
"MS-DOS 6.22 is an OS Zoo preset: the real Microsoft MS-DOS, booted from a disk image under "
|
|
300
|
+
"QEMU and embedded over noVNC — so it's the genuine article (`VER` reports MS-DOS, not a "
|
|
301
|
+
"clone). Drag it on and Run; GINI downloads a public pre-installed MS-DOS 6.22 disk on first "
|
|
302
|
+
"boot (it ships nothing proprietary) and boots to the C:\\> prompt. Pair it with FreeDOS to "
|
|
303
|
+
"compare the original MS-DOS with the open re-implementation. Ephemeral unless Persist."),
|
|
304
|
+
"mac7": (
|
|
305
|
+
"Mac System 7 is an OS Zoo preset: classic Macintosh System 7.5.3 on an emulated 68k Mac "
|
|
306
|
+
"(Basilisk II), embedded over noVNC. Drag it on and Run — GINI downloads a Quadra ROM and a "
|
|
307
|
+
"bootable System 7 disk from a public archive on first boot (it ships nothing proprietary), "
|
|
308
|
+
"caches them, and boots to the Mac desktop. It's the 'Classic OS (your image)' element with "
|
|
309
|
+
"the Image/Rom URLs pre-filled; edit them to use your own files. Ephemeral unless Persist."),
|
|
310
|
+
"win31": (
|
|
311
|
+
"Windows 3.11 is an OS Zoo preset: Windows for Workgroups 3.11 under DOSBox (the fast "
|
|
312
|
+
"vintage-Windows path), embedded over noVNC. Drag it on and Run — GINI downloads a public "
|
|
313
|
+
"pre-installed Windows 3.11 on first boot (it ships nothing proprietary), mounts it as C:, "
|
|
314
|
+
"and starts Windows. It's the 'Classic OS (your image)' element with the Image URL "
|
|
315
|
+
"pre-filled; edit it to use your own folder or zip. Ephemeral unless Persist."),
|
|
316
|
+
"oszoo_byo": (
|
|
317
|
+
"Classic OS (your image) is the bring-your-own OS Zoo element for proprietary systems "
|
|
318
|
+
"GINI can't ship (Windows 95, Mac System 7, …). GINI provides the emulator and points to "
|
|
319
|
+
"where the image legally lives; you set Image to a disk image you own (and, for a 68k "
|
|
320
|
+
"Mac, choose the Basilisk emulator and supply a Mac ROM). GINI hosts nothing "
|
|
321
|
+
"copyrighted — you source the image, GINI runs it and embeds the screen over noVNC."),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def guide_for(key: str) -> str | None:
|
|
326
|
+
"""Return the teaching guide for an element type key, or None if not catalogued."""
|
|
327
|
+
return GUIDE.get(key)
|
gini/domain/explain.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Predicate explainer — the deterministic "why is this objective red?" fact.
|
|
2
|
+
|
|
3
|
+
The oracle doesn't just say met/unmet; it can report the *counterexample*: which atom of a structural
|
|
4
|
+
predicate is failing, in board terms ("no switch is wired to a router", "no path from a web app to a
|
|
5
|
+
database", "a cloud can still reach the database"). That fact is generated by walking the topology —
|
|
6
|
+
never by the model. The Reasoning persona then turns the fact into a warm, specific diagnosis; this is
|
|
7
|
+
what makes the multi-agent answers grounded in the student's actual board instead of canned.
|
|
8
|
+
|
|
9
|
+
Handles the structural predicates missions use (`exists`, `count`, `link`, `path`, `through`,
|
|
10
|
+
`contains_type`, and their `not`), including `and`/`or` compounds — reporting the atoms that are the
|
|
11
|
+
problem. Behavioral probes explain as "run it to check".
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
|
|
17
|
+
from .devices import REGISTRY
|
|
18
|
+
from .objectives import TopologyWorld, _call, parse_check
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _label(type_key: str) -> str:
|
|
22
|
+
spec = REGISTRY.get(type_key)
|
|
23
|
+
return (getattr(spec, "label", None) or type_key).lower() if spec else type_key
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _atom_true(node: ast.Call, world) -> bool:
|
|
27
|
+
args = [a.id if isinstance(a, ast.Name) else getattr(a, "value", None) for a in node.args]
|
|
28
|
+
try:
|
|
29
|
+
return bool(_call(node.func.id, args, world))
|
|
30
|
+
except Exception:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _describe(node: ast.Call, negated: bool) -> str:
|
|
35
|
+
"""A board-terms reason why this atom is the problem (given it violates the objective)."""
|
|
36
|
+
fn = node.func.id
|
|
37
|
+
a = [x.id if isinstance(x, ast.Name) else getattr(x, "value", "") for x in node.args]
|
|
38
|
+
if fn == "exists":
|
|
39
|
+
return (f"there's a {_label(a[0])} but there shouldn't be" if negated
|
|
40
|
+
else f"there's no {_label(a[0])} on the board yet")
|
|
41
|
+
if fn == "count":
|
|
42
|
+
return f"you don't have enough {_label(a[0])}s yet"
|
|
43
|
+
if fn == "link":
|
|
44
|
+
return (f"a {_label(a[0])} is wired directly to a {_label(a[1])} and shouldn't be" if negated
|
|
45
|
+
else f"no {_label(a[0])} is wired to a {_label(a[1])} yet")
|
|
46
|
+
if fn == "path":
|
|
47
|
+
return (f"a {_label(a[0])} can still reach a {_label(a[1])} (it should be isolated)" if negated
|
|
48
|
+
else f"a {_label(a[0])} can't reach a {_label(a[1])} — they're not connected through")
|
|
49
|
+
if fn == "through":
|
|
50
|
+
return (f"traffic from a {_label(a[1])} to a {_label(a[2])} doesn't pass through the "
|
|
51
|
+
f"{_label(a[0])} (or there's a path around it)")
|
|
52
|
+
if fn == "contains_type":
|
|
53
|
+
return (f"a {_label(a[1])} sits inside the {_label(a[0])} and shouldn't" if negated
|
|
54
|
+
else f"no {_label(a[1])} is inside the {_label(a[0])} yet")
|
|
55
|
+
return f"the condition {fn} isn't satisfied"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _walk(node, world, negated: bool, out: list) -> None:
|
|
59
|
+
if isinstance(node, ast.BoolOp):
|
|
60
|
+
for v in node.values: # for and/or, report each failing branch
|
|
61
|
+
_walk(v, world, negated, out)
|
|
62
|
+
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
63
|
+
_walk(node.operand, world, not negated, out)
|
|
64
|
+
elif isinstance(node, ast.Call):
|
|
65
|
+
true = _atom_true(node, world)
|
|
66
|
+
violates = true if negated else (not true) # atom is the problem if it's the wrong way round
|
|
67
|
+
if violates:
|
|
68
|
+
r = _describe(node, negated)
|
|
69
|
+
if r not in out:
|
|
70
|
+
out.append(r)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def why_unmet(check: str, world) -> str:
|
|
74
|
+
"""A concrete, board-grounded reason an (unmet) structural objective is failing — or '' if it
|
|
75
|
+
actually holds / can't be explained. Deterministic: the oracle reporting, not reasoning."""
|
|
76
|
+
try:
|
|
77
|
+
node = parse_check(check)
|
|
78
|
+
except Exception:
|
|
79
|
+
return ""
|
|
80
|
+
out: list[str] = []
|
|
81
|
+
_walk(node, world, False, out)
|
|
82
|
+
return "; ".join(out)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def diagnose(objective, world) -> str:
|
|
86
|
+
"""The reason an objective is red (structural) or a prompt to run it (behavioral)."""
|
|
87
|
+
if getattr(objective, "kind", "structural") == "behavioral":
|
|
88
|
+
return "run it to check (this one needs the live system)"
|
|
89
|
+
check = getattr(objective, "check", "")
|
|
90
|
+
return why_unmet(check, world) if check else ""
|