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/lesson.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Lesson Pack parsing + validation.
|
|
2
|
+
|
|
3
|
+
A Lesson is the authored (or AI-resolved-then-ratified) unit a Mission plays. In Phase 1 a
|
|
4
|
+
Lesson is read from a dict/YAML with concrete objectives; the `intent` block is carried for the
|
|
5
|
+
game master to reason over (Phase 3). Validation catches authoring mistakes early — a structural
|
|
6
|
+
predicate that doesn't parse, or an element type that doesn't exist — so a broken lesson never
|
|
7
|
+
reaches a student.
|
|
8
|
+
|
|
9
|
+
Pure data + parsing; no Qt. YAML is optional (falls back to a dict) so tests need no file I/O.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
from . import catalog as _catalog
|
|
16
|
+
from .objectives import Objective, check_ok, unknown_element_types
|
|
17
|
+
|
|
18
|
+
_HELP = ("none", "warmer_colder", "full_tutor_logged")
|
|
19
|
+
_PERSONA = ("coach", "challenger")
|
|
20
|
+
_KINDS = ("structural", "behavioral")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Intent:
|
|
25
|
+
"""Teacher intent — the game master reasons over this; it is NOT a checklist.
|
|
26
|
+
|
|
27
|
+
`notes` is the teacher's free-text nuance ("stress that private doesn't mean unreachable —
|
|
28
|
+
the web tier still gets in"). It travels WITH the mission and is interpreted by the student's
|
|
29
|
+
game master at play time, so the course server never needs an LLM: the teacher writes plain
|
|
30
|
+
language, and the model that reads it is the one already running on the student's machine."""
|
|
31
|
+
concept: str = ""
|
|
32
|
+
goal: str = ""
|
|
33
|
+
spirit: str = ""
|
|
34
|
+
misconceptions: list[str] = field(default_factory=list)
|
|
35
|
+
notes: str = "" # the teacher's nuance, reasoned over by the game master
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Forbid:
|
|
40
|
+
"""A rule the student must NOT trip — a structural predicate that should stay FALSE. When it
|
|
41
|
+
becomes true (e.g. `link(database, cloud)` — the DB wired to the Internet), the offending move
|
|
42
|
+
is flagged (red badge) and the game master explains. `say` is the student-facing reason."""
|
|
43
|
+
say: str
|
|
44
|
+
check: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Step:
|
|
49
|
+
"""One beat of a GUIDED mission: an instruction, and how the student advances past it.
|
|
50
|
+
`advance` is one of: 'reply'/'ack' (any student message — a read/reflect beat), a structural
|
|
51
|
+
predicate (drop/connect/configure — advances when the canvas satisfies it), or a behavioral
|
|
52
|
+
probe (run/observe — advances on a Run). The game master presents each beat and reasons about
|
|
53
|
+
what the student did before moving on."""
|
|
54
|
+
say: str
|
|
55
|
+
advance: str = "reply"
|
|
56
|
+
hint: str = ""
|
|
57
|
+
|
|
58
|
+
def kind(self) -> str:
|
|
59
|
+
return step_kind(self.advance)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def step_kind(advance: str) -> str:
|
|
63
|
+
a = (advance or "reply").strip().lower()
|
|
64
|
+
if a in ("reply", "ack", "say", "read", "observe", ""):
|
|
65
|
+
return "reply"
|
|
66
|
+
from . import probes as _probes
|
|
67
|
+
if _probes.probe_ok(advance):
|
|
68
|
+
return "behavioral"
|
|
69
|
+
return "structural"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Lesson:
|
|
74
|
+
id: str
|
|
75
|
+
title: str = ""
|
|
76
|
+
brief: str = ""
|
|
77
|
+
objectives: list[Objective] = field(default_factory=list)
|
|
78
|
+
steps: list[Step] = field(default_factory=list) # optional GUIDED beats (empty = free-form)
|
|
79
|
+
forbid: list = field(default_factory=list) # Forbid rules: predicates that must stay FALSE
|
|
80
|
+
complete_when: str = "all" # all | any | at_least(n)
|
|
81
|
+
time_limit_s: int | None = None # None = untimed
|
|
82
|
+
attempts: int = 3
|
|
83
|
+
help: str = "warmer_colder"
|
|
84
|
+
persona: str = "coach"
|
|
85
|
+
stage: str = "" # optional saved-canvas file to pre-build
|
|
86
|
+
intent: Intent = field(default_factory=Intent)
|
|
87
|
+
archetype: str = "" # the Game-Catalog archetype it was resolved from (if any)
|
|
88
|
+
params: dict = field(default_factory=dict)
|
|
89
|
+
genre: str = "" # experience | expedition | challenge (defaults from assembly)
|
|
90
|
+
level: int | None = None # quest level 0..3 (defaults from assembly; may be pinned)
|
|
91
|
+
fragments: list = field(default_factory=list) # fragment ids this lesson was assembled from
|
|
92
|
+
# optional difficulty FORKS (the golden-path model): each is
|
|
93
|
+
# {id, label, difficulty, kind, objectives: [Objective]}. Completing a fork lifts the band.
|
|
94
|
+
forks: list = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
def behavioral_ids(self) -> list[str]:
|
|
97
|
+
ids = [o.id for o in self.objectives if o.is_behavioral()]
|
|
98
|
+
for fk in self.forks:
|
|
99
|
+
ids += [o.id for o in fk.get("objectives", []) if o.is_behavioral()]
|
|
100
|
+
return ids
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def guided(self) -> bool:
|
|
104
|
+
"""True when the lesson leads the student through beats turn-by-turn (vs. free-form)."""
|
|
105
|
+
return bool(self.steps)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class LessonError(ValueError):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# -- time parsing ----------------------------------------------------------- #
|
|
113
|
+
def parse_duration(v) -> int | None:
|
|
114
|
+
"""'25m' / '90s' / '1h' / 1500 (seconds) → seconds. None/'' → untimed."""
|
|
115
|
+
if v is None or v == "":
|
|
116
|
+
return None
|
|
117
|
+
if isinstance(v, (int, float)):
|
|
118
|
+
return int(v)
|
|
119
|
+
s = str(v).strip().lower()
|
|
120
|
+
units = {"s": 1, "m": 60, "h": 3600}
|
|
121
|
+
if s and s[-1] in units:
|
|
122
|
+
try:
|
|
123
|
+
return int(float(s[:-1]) * units[s[-1]])
|
|
124
|
+
except ValueError as e:
|
|
125
|
+
raise LessonError(f"bad duration {v!r}") from e
|
|
126
|
+
try:
|
|
127
|
+
return int(s)
|
|
128
|
+
except ValueError as e:
|
|
129
|
+
raise LessonError(f"bad duration {v!r}") from e
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _objective_from(d: dict) -> Objective:
|
|
133
|
+
if "id" not in d:
|
|
134
|
+
raise LessonError("objective missing 'id'")
|
|
135
|
+
kind = d.get("kind", "structural")
|
|
136
|
+
if kind not in _KINDS:
|
|
137
|
+
raise LessonError(f"objective {d['id']}: bad kind {kind!r}")
|
|
138
|
+
return Objective(id=d["id"], say=d.get("say", d["id"]), kind=kind,
|
|
139
|
+
check=d.get("check", ""), probe=d.get("probe", ""), level=d.get("level"))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def from_dict(d: dict) -> Lesson:
|
|
143
|
+
"""Build a Lesson from a plain dict (already-parsed YAML)."""
|
|
144
|
+
if "id" not in d:
|
|
145
|
+
raise LessonError("lesson missing 'id'")
|
|
146
|
+
it = d.get("intent", {}) or {}
|
|
147
|
+
intent = Intent(concept=it.get("concept", ""), goal=it.get("goal", ""),
|
|
148
|
+
spirit=it.get("spirit", ""),
|
|
149
|
+
misconceptions=list(it.get("misconceptions", []) or []),
|
|
150
|
+
notes=it.get("notes", "") or d.get("notes", ""))
|
|
151
|
+
objs = [_objective_from(o) for o in (d.get("objectives", []) or [])]
|
|
152
|
+
steps = [Step(say=s.get("say", ""), advance=s.get("advance", "reply"), hint=s.get("hint", ""))
|
|
153
|
+
for s in (d.get("steps", []) or [])]
|
|
154
|
+
forbid = [Forbid(say=f.get("say", ""), check=f.get("check", ""))
|
|
155
|
+
for f in (d.get("forbid", []) or [])]
|
|
156
|
+
return Lesson(
|
|
157
|
+
id=d["id"], title=d.get("title", ""), brief=d.get("brief", ""),
|
|
158
|
+
objectives=objs, steps=steps, forbid=forbid, complete_when=d.get("complete_when", "all"),
|
|
159
|
+
time_limit_s=parse_duration(d.get("time_limit")),
|
|
160
|
+
attempts=int(d.get("attempts", 3)), help=d.get("help", "warmer_colder"),
|
|
161
|
+
persona=d.get("persona", "coach"), stage=d.get("stage", ""), intent=intent,
|
|
162
|
+
archetype=d.get("archetype", ""), params=dict(d.get("params", {}) or {}),
|
|
163
|
+
genre=d.get("genre", ""), level=d.get("level"),
|
|
164
|
+
fragments=list(d.get("fragments", []) or []),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def from_yaml(text: str) -> Lesson:
|
|
169
|
+
import yaml # PyYAML ships with the app; imported lazily
|
|
170
|
+
return from_dict(yaml.safe_load(text))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def from_archetype(archetype_id: str, params: dict, *, id: str, title: str = "",
|
|
174
|
+
brief: str = "", **over) -> Lesson:
|
|
175
|
+
"""Build a concrete Lesson by instantiating a Game-Catalog archetype (the shortcut the
|
|
176
|
+
resolver will use; handy for explicit authoring + tests)."""
|
|
177
|
+
arch = _catalog.get(archetype_id)
|
|
178
|
+
if arch is None:
|
|
179
|
+
raise LessonError(f"unknown archetype {archetype_id!r}")
|
|
180
|
+
missing = _catalog.unbound_refs(arch, params)
|
|
181
|
+
if missing:
|
|
182
|
+
raise LessonError(f"archetype {archetype_id!r} missing params: {missing}")
|
|
183
|
+
objs = _catalog.instantiate(arch, params)
|
|
184
|
+
intent = Intent(concept=arch.teaches, goal=arch.summary, spirit=arch.spirit,
|
|
185
|
+
misconceptions=list(arch.misconceptions))
|
|
186
|
+
return Lesson(id=id, title=title or arch.summary, brief=brief or arch.summary,
|
|
187
|
+
objectives=objs, complete_when=over.get("complete_when", arch.complete_when),
|
|
188
|
+
time_limit_s=parse_duration(over.get("time_limit")),
|
|
189
|
+
attempts=int(over.get("attempts", 3)), help=over.get("help", "warmer_colder"),
|
|
190
|
+
persona=over.get("persona", "coach"),
|
|
191
|
+
stage=over.get("stage") or getattr(arch, "stage", "") or "",
|
|
192
|
+
intent=intent, archetype=archetype_id, params=dict(params))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# -- validation ------------------------------------------------------------- #
|
|
196
|
+
def validate(lesson: Lesson) -> list[str]:
|
|
197
|
+
"""Return a list of problems (empty = valid). Catches bad predicates, unknown element
|
|
198
|
+
types, empty objectives, and bad enum values — the authoring safety net."""
|
|
199
|
+
problems: list[str] = []
|
|
200
|
+
if not lesson.objectives:
|
|
201
|
+
problems.append("lesson has no objectives")
|
|
202
|
+
if lesson.help not in _HELP:
|
|
203
|
+
problems.append(f"bad help level {lesson.help!r}")
|
|
204
|
+
if lesson.persona not in _PERSONA:
|
|
205
|
+
problems.append(f"bad persona {lesson.persona!r}")
|
|
206
|
+
cw = lesson.complete_when
|
|
207
|
+
if cw not in ("all", "any") and not cw.startswith("at_least("):
|
|
208
|
+
problems.append(f"bad complete_when {cw!r}")
|
|
209
|
+
if lesson.attempts < 1:
|
|
210
|
+
problems.append("attempts must be >= 1")
|
|
211
|
+
seen = set()
|
|
212
|
+
for o in lesson.objectives:
|
|
213
|
+
if o.id in seen:
|
|
214
|
+
problems.append(f"duplicate objective id {o.id!r}")
|
|
215
|
+
seen.add(o.id)
|
|
216
|
+
if o.kind == "structural":
|
|
217
|
+
if not o.check:
|
|
218
|
+
problems.append(f"structural objective {o.id!r} has no check")
|
|
219
|
+
elif not check_ok(o.check):
|
|
220
|
+
problems.append(f"objective {o.id!r}: check does not parse: {o.check!r}")
|
|
221
|
+
else:
|
|
222
|
+
bad = unknown_element_types(o.check)
|
|
223
|
+
if bad:
|
|
224
|
+
problems.append(f"objective {o.id!r}: unknown element types {bad}")
|
|
225
|
+
elif o.kind == "behavioral" and not o.probe:
|
|
226
|
+
problems.append(f"behavioral objective {o.id!r} has no probe")
|
|
227
|
+
for i, s in enumerate(lesson.steps):
|
|
228
|
+
if not s.say:
|
|
229
|
+
problems.append(f"step {i + 1} has no instruction ('say')")
|
|
230
|
+
k = s.kind()
|
|
231
|
+
if k == "structural" and not check_ok(s.advance):
|
|
232
|
+
problems.append(f"step {i + 1}: advance predicate does not parse: {s.advance!r}")
|
|
233
|
+
elif k == "structural":
|
|
234
|
+
bad = unknown_element_types(s.advance)
|
|
235
|
+
if bad:
|
|
236
|
+
problems.append(f"step {i + 1}: unknown element types {bad}")
|
|
237
|
+
return problems
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def is_valid(lesson: Lesson) -> bool:
|
|
241
|
+
return not validate(lesson)
|
gini/domain/lexicon.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Lexical normalization for GINI retrieval — the dependency-free layer that closes most of
|
|
2
|
+
the recall gap before we ever reach embeddings.
|
|
3
|
+
|
|
4
|
+
Three cheap transforms turn a student's phrasing into the GINI vocabulary the (lexical)
|
|
5
|
+
retriever indexes on:
|
|
6
|
+
|
|
7
|
+
1. tokenize — split to lowercase word tokens, drop query-noise ("explain", "what", …);
|
|
8
|
+
2. stem — a tiny suffix stemmer so "routing"/"routes" collapse to one form;
|
|
9
|
+
3. synonyms — a small, HAND-OWNED map from student words / real-world product names to
|
|
10
|
+
GINI's canonical terms (e.g. "vm" -> instance, "software defined" -> sdn).
|
|
11
|
+
|
|
12
|
+
Pure data + functions, no dependencies. The synonym map is deliberately tiny and lives here
|
|
13
|
+
beside the concepts it feeds; a test asserts every mapped target is a real concept keyword,
|
|
14
|
+
so the map can't rot silently.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
# words that carry no topic signal in a question — stripped from the QUERY side so a short
|
|
21
|
+
# query like "explain SDN" reduces to its content term {sdn}.
|
|
22
|
+
_QUERY_STOP = frozenset((
|
|
23
|
+
"a", "an", "the", "is", "are", "am", "of", "to", "in", "on", "for", "and", "or", "with",
|
|
24
|
+
"how", "what", "whats", "why", "when", "where", "which", "who", "do", "does", "did", "can",
|
|
25
|
+
"could", "would", "should", "i", "we", "you", "my", "me", "us", "explain", "tell", "about",
|
|
26
|
+
"show", "give", "describe", "please", "help", "want", "need", "use", "using", "work",
|
|
27
|
+
"works", "working", "it", "this", "that", "these", "those", "be", "get", "make", "set",
|
|
28
|
+
"up", "into", "from", "at", "as", "if", "so", "then", "than", "vs", "versus", "between",
|
|
29
|
+
))
|
|
30
|
+
|
|
31
|
+
# single-token synonyms → one or more canonical GINI terms (already in the KB's vocabulary).
|
|
32
|
+
_SYNONYMS: dict[str, tuple[str, ...]] = {
|
|
33
|
+
"vm": ("instance",), "vms": ("instance",), "virtualmachine": ("instance",),
|
|
34
|
+
"lambda": ("function", "serverless"), "faas": ("function", "serverless"),
|
|
35
|
+
"serverlessfunction": ("function",),
|
|
36
|
+
"bucket": ("object", "storage"), "buckets": ("object", "storage"), "s3": ("object", "storage"),
|
|
37
|
+
"blob": ("object", "storage"),
|
|
38
|
+
"bus": ("queue", "messaging"), "broker": ("queue", "messaging"),
|
|
39
|
+
"kafka": ("stream",), "rabbitmq": ("queue",), "nats": ("messaging",),
|
|
40
|
+
"waf": ("firewall",), "iptables": ("firewall",),
|
|
41
|
+
"segmentation": ("security", "group", "isolation"),
|
|
42
|
+
"microsegmentation": ("security", "group", "isolation"),
|
|
43
|
+
"k8s": ("kubernetes",), "k3s": ("kubernetes",), "hpa": ("autoscaling", "pod"),
|
|
44
|
+
"db": ("database",), "rdbms": ("database",), "postgres": ("database",), "postgresql": ("database",),
|
|
45
|
+
"mongo": ("nosql",), "mongodb": ("nosql",), "redis": ("cache",), "memcached": ("cache",),
|
|
46
|
+
"lb": ("load", "balancer"), "nginx": ("load", "balancer"), "haproxy": ("load", "balancer"),
|
|
47
|
+
"traefik": ("proxy", "gateway"), "envoy": ("proxy",),
|
|
48
|
+
"prometheus": ("metrics",), "grafana": ("dashboard",), "jaeger": ("tracing",),
|
|
49
|
+
"microvm": ("kata", "isolation"), "hypervisor": ("kata", "isolation"),
|
|
50
|
+
"os": ("kernel", "process"), "syscall": ("system", "call"),
|
|
51
|
+
"vpn": ("gateway", "network"), "nat": ("internet", "gateway"),
|
|
52
|
+
"middlebox": ("vnf", "nfv"), "appliance": ("vnf", "nfv"),
|
|
53
|
+
"l2": ("switch", "layer"), "l3": ("router", "layer"),
|
|
54
|
+
"wifi": ("wap", "wireless"), "wlan": ("wap", "wireless"),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# multi-word phrases → canonical terms, matched on the raw (lowercased) query before tokenizing.
|
|
58
|
+
# This is where "software defined" -> sdn lives (the exact case that exposed lexical brittleness).
|
|
59
|
+
_PHRASES: dict[str, tuple[str, ...]] = {
|
|
60
|
+
"software defined": ("sdn", "openflow"),
|
|
61
|
+
"software-defined": ("sdn", "openflow"),
|
|
62
|
+
"control plane": ("sdn", "control"),
|
|
63
|
+
"data plane": ("sdn", "data"),
|
|
64
|
+
"message bus": ("queue", "messaging"),
|
|
65
|
+
"message queue": ("queue",),
|
|
66
|
+
"event log": ("stream",),
|
|
67
|
+
"object storage": ("object", "storage"),
|
|
68
|
+
"block storage": ("block", "volume"),
|
|
69
|
+
"virtual machine": ("instance", "vm"),
|
|
70
|
+
"load balancer": ("load", "balancer"),
|
|
71
|
+
"load balancing": ("load", "balancer"),
|
|
72
|
+
"reverse proxy": ("proxy",),
|
|
73
|
+
"service mesh": ("proxy", "sidecar"),
|
|
74
|
+
"security group": ("security", "group"),
|
|
75
|
+
"network function": ("vnf", "nfv"),
|
|
76
|
+
"service chain": ("sfc", "chain"),
|
|
77
|
+
"service function chain": ("sfc", "chain"),
|
|
78
|
+
"access point": ("wap", "wireless"),
|
|
79
|
+
"context switch": ("scheduler", "context"),
|
|
80
|
+
"page table": ("paging", "memory"),
|
|
81
|
+
"virtual memory": ("paging", "memory"),
|
|
82
|
+
"file system": ("filesystem", "inode"),
|
|
83
|
+
"operating system": ("kernel", "process"),
|
|
84
|
+
"system call": ("syscall", "kernel"),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_WORD = re.compile(r"[a-z0-9][a-z0-9+]*")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _stem(word: str) -> str:
|
|
91
|
+
"""A deliberately tiny suffix stemmer — enough to fold common inflections without a
|
|
92
|
+
dictionary. Order matters (longest suffix first); never stem below 3 chars."""
|
|
93
|
+
w = word
|
|
94
|
+
for suf in ("ization", "isation", "ing", "ies", "ied", "ion", "ers", "er", "ed", "es", "s"):
|
|
95
|
+
if w.endswith(suf) and len(w) - len(suf) >= 3:
|
|
96
|
+
base = w[: -len(suf)]
|
|
97
|
+
if suf == "ies":
|
|
98
|
+
base += "y"
|
|
99
|
+
return base
|
|
100
|
+
return w
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def tokenize(text: str, *, drop_stop: bool = False) -> list[str]:
|
|
104
|
+
"""Lowercase word tokens. With `drop_stop`, remove query-noise words (used on the QUERY
|
|
105
|
+
side; documents keep all their words)."""
|
|
106
|
+
toks = _WORD.findall((text or "").lower())
|
|
107
|
+
if drop_stop:
|
|
108
|
+
toks = [t for t in toks if t not in _QUERY_STOP]
|
|
109
|
+
return toks
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _expand_phrases(low: str) -> list[str]:
|
|
113
|
+
extra: list[str] = []
|
|
114
|
+
for phrase, terms in _PHRASES.items():
|
|
115
|
+
if phrase in low:
|
|
116
|
+
extra.extend(terms)
|
|
117
|
+
return extra
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def normalize(text: str, *, query: bool = False) -> list[str]:
|
|
121
|
+
"""Normalized, de-duplicated token list for scoring: phrase expansion + tokenization +
|
|
122
|
+
stemming + single-token synonym expansion. On the query side, noise words are dropped."""
|
|
123
|
+
low = (text or "").lower()
|
|
124
|
+
raw = tokenize(low, drop_stop=query)
|
|
125
|
+
out: list[str] = []
|
|
126
|
+
seen: set[str] = set()
|
|
127
|
+
|
|
128
|
+
def add(tok: str) -> None:
|
|
129
|
+
s = _stem(tok)
|
|
130
|
+
if s and s not in seen:
|
|
131
|
+
seen.add(s)
|
|
132
|
+
out.append(s)
|
|
133
|
+
|
|
134
|
+
for tok in _expand_phrases(low): # multi-word phrase hits first
|
|
135
|
+
add(tok)
|
|
136
|
+
for tok in raw:
|
|
137
|
+
add(tok)
|
|
138
|
+
for syn in _SYNONYMS.get(tok, ()): # single-token synonyms
|
|
139
|
+
add(syn)
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def synonym_targets() -> set[str]:
|
|
144
|
+
"""Every canonical term the synonym/phrase maps point at (for the sync test)."""
|
|
145
|
+
out: set[str] = set()
|
|
146
|
+
for terms in _SYNONYMS.values():
|
|
147
|
+
out.update(terms)
|
|
148
|
+
for terms in _PHRASES.values():
|
|
149
|
+
out.update(terms)
|
|
150
|
+
return out
|