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/staging.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Board staging — a lesson can open with part of the canvas already built (M3).
|
|
2
|
+
|
|
3
|
+
Enables scaffolded labs ("here's a half-built network, finish it") and fault-injection ("I broke the
|
|
4
|
+
routing — fix it"). A lesson's `stage:` is an authorable, self-contained spec of devices + links; on
|
|
5
|
+
mission start the engine builds them onto the canvas. Pure logic (the caller supplies `add_device` /
|
|
6
|
+
`add_link` so it works against the live AppContext in the app and against fakes in tests).
|
|
7
|
+
|
|
8
|
+
Two kinds of fault can be injected, and the difference matters pedagogically:
|
|
9
|
+
|
|
10
|
+
* **Missing element** — something isn't there (`fix-the-lan`: two subnets, no router). The student
|
|
11
|
+
diagnoses an absence.
|
|
12
|
+
* **Mis-configuration** — everything is there and *looks* right, but a setting is wrong (a host
|
|
13
|
+
addressed into the wrong subnet). Nothing is visibly absent, so the picture can't reveal the
|
|
14
|
+
bug: only *running it* can. This is the harder and more realistic skill, and it's why a
|
|
15
|
+
mis-config mission must be graded by a live probe, never by structure.
|
|
16
|
+
|
|
17
|
+
Stage schema (in the lesson YAML):
|
|
18
|
+
|
|
19
|
+
stage:
|
|
20
|
+
reset: true # default: clear the canvas first, so the board is exactly this
|
|
21
|
+
manual_addressing: true # turn OFF auto-IP, so the addresses below actually stick
|
|
22
|
+
devices:
|
|
23
|
+
- {ref: r1, type: router}
|
|
24
|
+
- {ref: s1, type: switch}
|
|
25
|
+
- {ref: h1, type: host, x: 100, y: 200,
|
|
26
|
+
ips: {s1: 10.0.0.10}, # IP on the interface facing s1 (mis-config knob)
|
|
27
|
+
properties: {Note: "was working yesterday"}}
|
|
28
|
+
links:
|
|
29
|
+
- [h1, s1] # by ref
|
|
30
|
+
- [s1, r1]
|
|
31
|
+
|
|
32
|
+
`ref` is a local handle used only to wire links and address interfaces; it never has to match the
|
|
33
|
+
student's device names. `ips` is keyed by the *peer's ref* — the author says "the leg facing s1" and
|
|
34
|
+
staging resolves it to the real link id once the link exists, because link ids don't exist until the
|
|
35
|
+
board is built.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def normalize(stage) -> dict:
|
|
41
|
+
"""Coerce a stage spec into a canonical dict (tolerant of shapes)."""
|
|
42
|
+
empty = {"devices": [], "links": [], "reset": True, "manual_addressing": False}
|
|
43
|
+
if not stage or not isinstance(stage, dict):
|
|
44
|
+
return empty
|
|
45
|
+
links = []
|
|
46
|
+
for l in stage.get("links", []) or []:
|
|
47
|
+
if isinstance(l, (list, tuple)) and len(l) >= 2:
|
|
48
|
+
links.append([l[0], l[1]])
|
|
49
|
+
elif isinstance(l, dict) and "source" in l and "target" in l:
|
|
50
|
+
links.append([l["source"], l["target"]])
|
|
51
|
+
return {
|
|
52
|
+
"devices": list(stage.get("devices", []) or []),
|
|
53
|
+
"links": links,
|
|
54
|
+
# a staged board is a *designed* board: by default it replaces whatever was on the canvas,
|
|
55
|
+
# or the mission would grade against elements the author never put there.
|
|
56
|
+
"reset": bool(stage.get("reset", True)),
|
|
57
|
+
"manual_addressing": bool(stage.get("manual_addressing", False)),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_staged(lesson) -> bool:
|
|
62
|
+
spec = normalize(getattr(lesson, "stage", None))
|
|
63
|
+
return bool(spec["devices"])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def wants_reset(lesson) -> bool:
|
|
67
|
+
spec = normalize(getattr(lesson, "stage", None))
|
|
68
|
+
return bool(spec["devices"]) and spec["reset"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def apply(stage, *, add_device, add_link, topology=None) -> dict:
|
|
72
|
+
"""Build a stage. `add_device(type_key, x, y) -> instance` (with an `.id`); `add_link(src_id,
|
|
73
|
+
tgt_id)` -> link (with an `.id`; a caller that returns None just forfeits IP injection).
|
|
74
|
+
Returns {ref -> instance}. Links to an unknown ref are skipped (authoring safety). Never raises
|
|
75
|
+
on a bad single row — best-effort, because a typo in one stage row must not stop the lesson.
|
|
76
|
+
|
|
77
|
+
If `topology` is given and the stage asks for it, manual addressing is switched on — without
|
|
78
|
+
that the compiler auto-assigns IPs and would silently *repair* the very fault we injected.
|
|
79
|
+
"""
|
|
80
|
+
spec = normalize(stage)
|
|
81
|
+
if topology is not None and spec["manual_addressing"]:
|
|
82
|
+
topology.manual_addressing = True
|
|
83
|
+
|
|
84
|
+
placed: dict[str, object] = {}
|
|
85
|
+
wanted: dict[str, dict] = {} # ref -> {peer_ref: ip}
|
|
86
|
+
for i, d in enumerate(spec["devices"]):
|
|
87
|
+
tk = d.get("type")
|
|
88
|
+
if not tk:
|
|
89
|
+
continue
|
|
90
|
+
ref = d.get("ref") or d.get("name") or f"{tk}{i}"
|
|
91
|
+
try:
|
|
92
|
+
inst = add_device(tk, float(d.get("x", 0) or 0), float(d.get("y", 0) or 0))
|
|
93
|
+
except Exception:
|
|
94
|
+
continue
|
|
95
|
+
placed[ref] = inst
|
|
96
|
+
props = d.get("properties") or {}
|
|
97
|
+
if props and hasattr(inst, "properties"):
|
|
98
|
+
inst.properties.update({str(k): str(v) for k, v in props.items()})
|
|
99
|
+
ips = d.get("ips") or {}
|
|
100
|
+
if ips:
|
|
101
|
+
wanted[ref] = {str(k): str(v) for k, v in ips.items()}
|
|
102
|
+
|
|
103
|
+
for a, b in spec["links"]:
|
|
104
|
+
da, db = placed.get(a), placed.get(b)
|
|
105
|
+
if da is None or db is None:
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
link = add_link(da.id, db.id)
|
|
109
|
+
except Exception:
|
|
110
|
+
continue
|
|
111
|
+
lid = getattr(link, "id", None)
|
|
112
|
+
if lid is None:
|
|
113
|
+
continue
|
|
114
|
+
# now that the link exists we can pin each authored IP to the right leg: the author writes
|
|
115
|
+
# "on h1, the interface facing s1", i.e. wanted[near_ref][far_ref].
|
|
116
|
+
for near_ref, far_ref in ((a, b), (b, a)):
|
|
117
|
+
ip = (wanted.get(near_ref) or {}).get(far_ref)
|
|
118
|
+
near = placed[near_ref]
|
|
119
|
+
if ip and hasattr(near, "static_ips"):
|
|
120
|
+
near.static_ips[lid] = ip
|
|
121
|
+
|
|
122
|
+
return placed
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Syscall builder — turn a student's syscall spec into the real xv6 (RISC-V) edits.
|
|
2
|
+
|
|
3
|
+
Adding a system call to xv6 touches five places; this module generates all of them from a
|
|
4
|
+
small spec (name, return type, args, C body). It is PURE text generation, so the codegen and
|
|
5
|
+
validation are unit-tested without a compiler — the actual write-patches-and-recompile step is
|
|
6
|
+
Mac-side (xv6 tree + `make`), exactly the same split as the rest of the Machine Lab.
|
|
7
|
+
|
|
8
|
+
The five edit sites (xv6-riscv, MIT 6.1810):
|
|
9
|
+
1. kernel/syscall.h — `#define SYS_<name> <n>`
|
|
10
|
+
2. kernel/syscall.c — `extern uint64 sys_<name>(void);` + `[SYS_<name>] sys_<name>,`
|
|
11
|
+
3. kernel/sysproc.c — the `uint64 sys_<name>(void){…}` implementation (with arg fetch)
|
|
12
|
+
4. user/user.h — the user-facing prototype
|
|
13
|
+
5. user/usys.pl — `entry("<name>");` (generates the usys.S trampoline)
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
# The stock xv6-riscv system calls (numbers 1..21), used to reject name collisions and to
|
|
21
|
+
# compute the next free syscall number.
|
|
22
|
+
# current xv6-riscv: fork=1 .. sync=22 (note `pause`, not `sleep`, and `sync` at 22)
|
|
23
|
+
STOCK_SYSCALLS = (
|
|
24
|
+
"fork", "exit", "wait", "pipe", "read", "kill", "exec", "fstat", "chdir", "dup",
|
|
25
|
+
"getpid", "sbrk", "pause", "uptime", "open", "write", "mknod", "unlink", "link",
|
|
26
|
+
"mkdir", "close", "sync",
|
|
27
|
+
)
|
|
28
|
+
NEXT_FREE_NUMBER = len(STOCK_SYSCALLS) + 1 # 23
|
|
29
|
+
|
|
30
|
+
ARG_TYPES = ("int", "addr", "str") # how the arg is fetched in the kernel
|
|
31
|
+
RET_TYPES = ("int", "uint64", "void")
|
|
32
|
+
|
|
33
|
+
_IDENT = re.compile(r"^[a-z_][a-z0-9_]*$")
|
|
34
|
+
# user.h C type shown to the student, per kernel arg type
|
|
35
|
+
_USER_CTYPE = {"int": "int", "addr": "void*", "str": "const char*"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SyscallArg:
|
|
40
|
+
name: str
|
|
41
|
+
type: str = "int" # one of ARG_TYPES
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SyscallSpec:
|
|
46
|
+
name: str
|
|
47
|
+
ret: str = "uint64"
|
|
48
|
+
args: list = field(default_factory=list) # [SyscallArg]
|
|
49
|
+
body: str = " return 0;"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate(spec: SyscallSpec, existing=STOCK_SYSCALLS) -> list[str]:
|
|
53
|
+
"""Return a list of human error strings ([] == valid)."""
|
|
54
|
+
errs: list[str] = []
|
|
55
|
+
if not spec.name or not _IDENT.match(spec.name):
|
|
56
|
+
errs.append("Name must be a valid C identifier (lower-case letters, digits, '_').")
|
|
57
|
+
elif spec.name in existing:
|
|
58
|
+
errs.append(f"'{spec.name}' is already an xv6 system call — pick another name.")
|
|
59
|
+
if spec.ret not in RET_TYPES:
|
|
60
|
+
errs.append(f"Return type must be one of {', '.join(RET_TYPES)}.")
|
|
61
|
+
if len(spec.args) > 6:
|
|
62
|
+
errs.append("A system call can take at most 6 arguments (registers a0–a5).")
|
|
63
|
+
seen = set()
|
|
64
|
+
for a in spec.args:
|
|
65
|
+
if not a.name or not _IDENT.match(a.name):
|
|
66
|
+
errs.append(f"Argument name '{a.name}' is not a valid C identifier.")
|
|
67
|
+
elif a.name in seen:
|
|
68
|
+
errs.append(f"Duplicate argument name '{a.name}'.")
|
|
69
|
+
seen.add(a.name)
|
|
70
|
+
if a.type not in ARG_TYPES:
|
|
71
|
+
errs.append(f"Argument '{a.name}' has an unknown type '{a.type}'.")
|
|
72
|
+
if not (spec.body or "").strip():
|
|
73
|
+
errs.append("The C body is empty — add what the system call should do.")
|
|
74
|
+
elif spec.ret != "void" and "return" not in spec.body:
|
|
75
|
+
errs.append("The body has no `return` but the return type isn't void.")
|
|
76
|
+
return errs
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _fetch(arg: SyscallArg, i: int) -> str:
|
|
80
|
+
if arg.type == "int":
|
|
81
|
+
return f" int {arg.name};\n argint({i}, &{arg.name});"
|
|
82
|
+
if arg.type == "addr":
|
|
83
|
+
return f" uint64 {arg.name};\n argaddr({i}, &{arg.name});"
|
|
84
|
+
return (f" char {arg.name}[128];\n"
|
|
85
|
+
f" argstr({i}, {arg.name}, sizeof({arg.name}));")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class Codegen:
|
|
90
|
+
number: int
|
|
91
|
+
syscall_h: str # #define line
|
|
92
|
+
extern: str # extern decl for syscall.c
|
|
93
|
+
dispatch: str # dispatch-table row for syscall.c
|
|
94
|
+
impl: str # the sys_<name> function for sysproc.c
|
|
95
|
+
user_h: str # user-space prototype
|
|
96
|
+
usys_entry: str # entry("<name>");
|
|
97
|
+
|
|
98
|
+
def files(self) -> list[tuple[str, str, str]]:
|
|
99
|
+
"""(path, where, snippet) for each of the five edit sites."""
|
|
100
|
+
return [
|
|
101
|
+
("kernel/syscall.h", "add the #define", self.syscall_h),
|
|
102
|
+
("kernel/syscall.c", "add extern + dispatch row",
|
|
103
|
+
self.extern + "\n" + self.dispatch),
|
|
104
|
+
("kernel/sysproc.c", "append the implementation", self.impl),
|
|
105
|
+
("user/user.h", "add the prototype", self.user_h),
|
|
106
|
+
("user/usys.pl", "add the entry", self.usys_entry),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
def preview(self) -> str:
|
|
110
|
+
return "\n\n".join(f"/* {path} — {where} */\n{snippet}"
|
|
111
|
+
for path, where, snippet in self.files())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def generate(spec: SyscallSpec, number: int = NEXT_FREE_NUMBER) -> Codegen:
|
|
115
|
+
"""Render the five edits for `spec`. Assumes `spec` already validated."""
|
|
116
|
+
name = spec.name
|
|
117
|
+
fetch = "\n".join(_fetch(a, i) for i, a in enumerate(spec.args))
|
|
118
|
+
body = spec.body if spec.body.strip() else " return 0;"
|
|
119
|
+
impl_lines = [f"uint64", f"sys_{name}(void)", "{"]
|
|
120
|
+
if fetch:
|
|
121
|
+
impl_lines.append(fetch)
|
|
122
|
+
impl_lines += [" // ---- your code ----", body.rstrip(), " // -------------------", "}"]
|
|
123
|
+
impl = "\n".join(impl_lines)
|
|
124
|
+
|
|
125
|
+
user_args = ", ".join(f"{_USER_CTYPE[a.type]} {a.name}" for a in spec.args) or "void"
|
|
126
|
+
user_ret = "int" if spec.ret in ("int", "uint64") else "int" # xv6 user.h convention
|
|
127
|
+
|
|
128
|
+
return Codegen(
|
|
129
|
+
number=number,
|
|
130
|
+
syscall_h=f"#define SYS_{name} {number}",
|
|
131
|
+
extern=f"extern uint64 sys_{name}(void);",
|
|
132
|
+
dispatch=f"[SYS_{name}] sys_{name},",
|
|
133
|
+
impl=impl,
|
|
134
|
+
user_h=f"{user_ret} {name}({user_args});",
|
|
135
|
+
usys_entry=f'entry("{name}");',
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def starter_body(spec_name: str = "") -> str:
|
|
140
|
+
"""A friendly starter the builder pre-fills the body editor with."""
|
|
141
|
+
return (" // read your args above, do the work, and return a value.\n"
|
|
142
|
+
" // e.g. print from the kernel:\n"
|
|
143
|
+
' printf("hello from sys_' + (spec_name or "mycall") + '\\n");\n'
|
|
144
|
+
" return 0;")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The Ask GINI topic cloud — an inviting, clickable set of things a student can explore or
|
|
2
|
+
build, generated from the live GINI knowledge base (concept notes, recipes, a few marquee
|
|
3
|
+
elements). Tapping a term SENDS the matching question/build to Ask GINI, so the cloud both
|
|
4
|
+
sparks ideas AND nudges the student toward vocabulary the (lexical) retriever lands on.
|
|
5
|
+
|
|
6
|
+
Framed as an invitation, not a cage — "GINI's great at these, or ask anything." Pure/data so
|
|
7
|
+
the cloud content is unit-tested; the UI renders these `CloudItem`s as a flow of pills.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from . import concepts as _concepts
|
|
14
|
+
from . import recipes as _recipes
|
|
15
|
+
from .devices import REGISTRY
|
|
16
|
+
|
|
17
|
+
# a colour rotation so the cloud looks lively (theme accent keys)
|
|
18
|
+
_ACCENTS = ["blue", "green", "purple", "teal", "indigo", "amber", "pink", "cyan", "orange"]
|
|
19
|
+
# recognisable elements worth a small pill (concepts + recipes carry the rest)
|
|
20
|
+
_MARQUEE = ["router", "switch", "firewall", "load_balancer", "function", "database",
|
|
21
|
+
"vnf", "ovs", "container", "pod"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CloudItem:
|
|
26
|
+
label: str # what's shown on the pill
|
|
27
|
+
kind: str # concept | recipe | element
|
|
28
|
+
query: str # sent to Ask GINI when the pill is tapped
|
|
29
|
+
weight: int # 1..3 → font-size tier (concepts biggest)
|
|
30
|
+
accent: str # theme accent key for colour
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _short(c) -> str:
|
|
34
|
+
"""A punchy label for a concept — its canonical keyword, cased (acronyms upper)."""
|
|
35
|
+
w = (c.keywords[0] if c.keywords else c.title)
|
|
36
|
+
return w.upper() if len(w) <= 3 else w[:1].upper() + w[1:]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _pill(text: str, cap: int = 22) -> str:
|
|
40
|
+
"""A tidy pill label: drop parentheticals and clip, so the cloud stays a cloud of
|
|
41
|
+
short punchy words (the full name still goes in the query for retrieval)."""
|
|
42
|
+
import re
|
|
43
|
+
text = re.sub(r"\s*\([^)]*\)", "", text).strip()
|
|
44
|
+
return text if len(text) <= cap else text[: cap - 1].rstrip() + "…"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def topic_cloud() -> list[CloudItem]:
|
|
48
|
+
"""Every explorable/buildable topic, as pills. Concepts → 'explain X' (big), recipes →
|
|
49
|
+
'show me an X' build (medium), marquee elements → 'what is a X' (small)."""
|
|
50
|
+
items: list[CloudItem] = []
|
|
51
|
+
for i, c in enumerate(_concepts.CONCEPTS):
|
|
52
|
+
lbl = _short(c)
|
|
53
|
+
items.append(CloudItem(lbl, "concept", f"explain {lbl}", 3, _ACCENTS[i % len(_ACCENTS)]))
|
|
54
|
+
for r in _recipes.RECIPES:
|
|
55
|
+
items.append(CloudItem(_pill(r.name), "recipe",
|
|
56
|
+
f"show me a {r.name} example", 2, "green"))
|
|
57
|
+
for key in _MARQUEE:
|
|
58
|
+
dt = REGISTRY.get(key)
|
|
59
|
+
if dt is not None:
|
|
60
|
+
items.append(CloudItem(_pill(dt.label), "element", f"what is a {dt.label}?", 1,
|
|
61
|
+
getattr(dt.accent, "value", "slate")))
|
|
62
|
+
return items
|
gini/domain/topology.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""In-memory topology model (pure Python, no Qt).
|
|
2
|
+
|
|
3
|
+
This is the single source of truth for what's on the canvas. The UI renders it,
|
|
4
|
+
the compiler/persistence layers read it, and the AI agent layer mutates it.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import itertools
|
|
9
|
+
from dataclasses import dataclass, field, asdict
|
|
10
|
+
|
|
11
|
+
from . import devices
|
|
12
|
+
from .devices import DeviceType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DeviceInstance:
|
|
17
|
+
id: str
|
|
18
|
+
type_key: str
|
|
19
|
+
name: str
|
|
20
|
+
x: float = 0.0
|
|
21
|
+
y: float = 0.0
|
|
22
|
+
parent_id: str | None = None
|
|
23
|
+
properties: dict[str, str] = field(default_factory=dict)
|
|
24
|
+
# manual addressing: link_id -> static IPv4 (bare dotted-quad). Only honored when
|
|
25
|
+
# the topology is in manual_addressing mode; empty/missing entries auto-fill.
|
|
26
|
+
static_ips: dict[str, str] = field(default_factory=dict)
|
|
27
|
+
# instance "size" tier (1=S … 4=XL) for resizable elements — bigger = more vCPU/mem
|
|
28
|
+
# and proportionally more GINI $/hr. See domain/pricing.py SIZE_TIERS.
|
|
29
|
+
size: int = 1
|
|
30
|
+
# for container elements (VPC/Subnet/Region): the box's drawn size on the canvas.
|
|
31
|
+
# 0 = use the type's default; non-container elements ignore these.
|
|
32
|
+
w: float = 0.0
|
|
33
|
+
h: float = 0.0
|
|
34
|
+
# composition slot this device belongs to (a scaffold/bound dependency, e.g. "A"). Empty = the
|
|
35
|
+
# fragment's own delta. Predicates reference it as `type@slot`.
|
|
36
|
+
slot: str = ""
|
|
37
|
+
# WHICH fragment was materialized into that slot (e.g. "cap-lan"). Provenance only — no predicate
|
|
38
|
+
# reads it; it exists so the canvas can label a slot group with what actually fills it
|
|
39
|
+
# ("nets · cap-lan ×4") instead of just a count, and so a composed board is self-describing.
|
|
40
|
+
slot_source: str = ""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def type(self) -> DeviceType:
|
|
44
|
+
return devices.get(self.type_key)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Link:
|
|
49
|
+
id: str
|
|
50
|
+
source_id: str
|
|
51
|
+
target_id: str
|
|
52
|
+
label: str = ""
|
|
53
|
+
# "link" = a network cable (carries traffic, compiled to real wiring).
|
|
54
|
+
# "attach" = a rider→donor mount: a Source/Sink runs ON the donor. Carries no traffic and is
|
|
55
|
+
# NOT compiled as a cable — a "runs on" relationship, drawn dotted. source_id is the rider.
|
|
56
|
+
kind: str = "link"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Topology:
|
|
60
|
+
"""A graph of device instances and the links between them."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, name: str = "untitled") -> None:
|
|
63
|
+
self.name = name
|
|
64
|
+
self.devices: dict[str, DeviceInstance] = {}
|
|
65
|
+
self.links: dict[str, Link] = {}
|
|
66
|
+
# when True the compiler stops auto-assigning IPs and honors each device's
|
|
67
|
+
# static_ips, auto-filling any interface left blank.
|
|
68
|
+
self.manual_addressing: bool = False
|
|
69
|
+
self._ids = itertools.count(1)
|
|
70
|
+
self._name_counters: dict[str, int] = {}
|
|
71
|
+
# per-type auto-name prefix overrides (type_key -> prefix), set from Settings;
|
|
72
|
+
# empty means use the curated DEFAULT_PREFIXES (R1, S1, M1, …).
|
|
73
|
+
self.prefix_overrides: dict[str, str] = {}
|
|
74
|
+
|
|
75
|
+
# -- creation ----------------------------------------------------------- #
|
|
76
|
+
def _new_id(self, prefix: str) -> str:
|
|
77
|
+
return f"{prefix}{next(self._ids)}"
|
|
78
|
+
|
|
79
|
+
def _auto_name(self, dt: DeviceType) -> str:
|
|
80
|
+
# prefix: a user override (e.g. "Mach_"), else the curated default (M, R, S, …)
|
|
81
|
+
base = self.prefix_overrides.get(dt.key) or devices.default_prefix(dt.key)
|
|
82
|
+
n = self._name_counters.get(base, 0) + 1
|
|
83
|
+
self._name_counters[base] = n
|
|
84
|
+
return f"{base}{n}"
|
|
85
|
+
|
|
86
|
+
def add_device(
|
|
87
|
+
self,
|
|
88
|
+
type_key: str,
|
|
89
|
+
name: str | None = None,
|
|
90
|
+
x: float = 0.0,
|
|
91
|
+
y: float = 0.0,
|
|
92
|
+
parent_id: str | None = None,
|
|
93
|
+
properties: dict[str, str] | None = None,
|
|
94
|
+
) -> DeviceInstance:
|
|
95
|
+
dt = devices.get(type_key)
|
|
96
|
+
name = name or self._auto_name(dt)
|
|
97
|
+
props = dict(dt.default_properties)
|
|
98
|
+
if properties:
|
|
99
|
+
props.update(properties)
|
|
100
|
+
props["Name"] = name
|
|
101
|
+
inst = DeviceInstance(
|
|
102
|
+
id=self._new_id(dt.key + "-"),
|
|
103
|
+
type_key=type_key,
|
|
104
|
+
name=name,
|
|
105
|
+
x=x,
|
|
106
|
+
y=y,
|
|
107
|
+
parent_id=parent_id,
|
|
108
|
+
properties=props,
|
|
109
|
+
)
|
|
110
|
+
self.devices[inst.id] = inst
|
|
111
|
+
return inst
|
|
112
|
+
|
|
113
|
+
def add_link(self, source_id: str, target_id: str, label: str = "") -> Link:
|
|
114
|
+
if source_id not in self.devices or target_id not in self.devices:
|
|
115
|
+
raise KeyError("link endpoints must be existing devices")
|
|
116
|
+
link = Link(self._new_id("link-"), source_id, target_id, label)
|
|
117
|
+
self.links[link.id] = link
|
|
118
|
+
return link
|
|
119
|
+
|
|
120
|
+
def add_attach(self, rider_id: str, donor_id: str, label: str = "") -> Link:
|
|
121
|
+
"""Mount a rider (Source/Sink) onto its donor. Distinct from a network link: it carries no
|
|
122
|
+
traffic and is not compiled — the rider merely RUNS ON the donor. `rider_id` is source_id."""
|
|
123
|
+
if rider_id not in self.devices or donor_id not in self.devices:
|
|
124
|
+
raise KeyError("attach endpoints must be existing devices")
|
|
125
|
+
link = Link(self._new_id("attach-"), rider_id, donor_id, label, kind="attach")
|
|
126
|
+
self.links[link.id] = link
|
|
127
|
+
return link
|
|
128
|
+
|
|
129
|
+
# -- mutation ----------------------------------------------------------- #
|
|
130
|
+
def remove_device(self, device_id: str) -> None:
|
|
131
|
+
self.devices.pop(device_id, None)
|
|
132
|
+
for lid in [l.id for l in self.links.values()
|
|
133
|
+
if l.source_id == device_id or l.target_id == device_id]:
|
|
134
|
+
self.links.pop(lid, None)
|
|
135
|
+
|
|
136
|
+
def rename(self, device_id: str, name: str) -> None:
|
|
137
|
+
d = self.devices[device_id]
|
|
138
|
+
d.name = name
|
|
139
|
+
d.properties["Name"] = name
|
|
140
|
+
|
|
141
|
+
# -- queries ------------------------------------------------------------ #
|
|
142
|
+
def find_by_name(self, name: str) -> DeviceInstance | None:
|
|
143
|
+
for d in self.devices.values():
|
|
144
|
+
if d.name == name:
|
|
145
|
+
return d
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def neighbors(self, device_id: str) -> list[DeviceInstance]:
|
|
149
|
+
out = []
|
|
150
|
+
for l in self.links.values():
|
|
151
|
+
if l.source_id == device_id:
|
|
152
|
+
out.append(self.devices[l.target_id])
|
|
153
|
+
elif l.target_id == device_id:
|
|
154
|
+
out.append(self.devices[l.source_id])
|
|
155
|
+
return out
|
|
156
|
+
|
|
157
|
+
def degree(self, device_id: str) -> int:
|
|
158
|
+
return sum(1 for l in self.links.values()
|
|
159
|
+
if device_id in (l.source_id, l.target_id))
|
|
160
|
+
|
|
161
|
+
# -- riders / attach edges ---------------------------------------------- #
|
|
162
|
+
def net_links(self) -> list["Link"]:
|
|
163
|
+
"""Only the network cables — what the compiler wires. Attach edges are excluded."""
|
|
164
|
+
return [l for l in self.links.values() if l.kind != "attach"]
|
|
165
|
+
|
|
166
|
+
def donor_of(self, rider_id: str) -> "DeviceInstance | None":
|
|
167
|
+
"""The donor a rider is mounted on (the far end of its attach edge), or None."""
|
|
168
|
+
for l in self.links.values():
|
|
169
|
+
if l.kind == "attach" and l.source_id == rider_id:
|
|
170
|
+
return self.devices.get(l.target_id)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
def riders_on(self, donor_id: str) -> list["DeviceInstance"]:
|
|
174
|
+
"""Every Source/Sink rider mounted on this donor."""
|
|
175
|
+
return [self.devices[l.source_id] for l in self.links.values()
|
|
176
|
+
if l.kind == "attach" and l.target_id == donor_id
|
|
177
|
+
and l.source_id in self.devices]
|
|
178
|
+
|
|
179
|
+
def counts_by_category(self) -> dict[str, int]:
|
|
180
|
+
out: dict[str, int] = {}
|
|
181
|
+
for d in self.devices.values():
|
|
182
|
+
cat = d.type.category.value
|
|
183
|
+
out[cat] = out.get(cat, 0) + 1
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
# -- serialization (used by persistence + agent layer) ------------------ #
|
|
187
|
+
def to_dict(self) -> dict:
|
|
188
|
+
return {
|
|
189
|
+
"name": self.name,
|
|
190
|
+
"manual_addressing": self.manual_addressing,
|
|
191
|
+
"devices": [asdict(d) for d in self.devices.values()],
|
|
192
|
+
"links": [asdict(l) for l in self.links.values()],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_dict(cls, data: dict) -> "Topology":
|
|
197
|
+
t = cls(data.get("name", "untitled"))
|
|
198
|
+
t.manual_addressing = bool(data.get("manual_addressing", False))
|
|
199
|
+
max_n = 0
|
|
200
|
+
for d in data.get("devices", []):
|
|
201
|
+
inst = DeviceInstance(**d)
|
|
202
|
+
t.devices[inst.id] = inst
|
|
203
|
+
for token in inst.id.replace("-", " ").split():
|
|
204
|
+
if token.isdigit():
|
|
205
|
+
max_n = max(max_n, int(token))
|
|
206
|
+
for l in data.get("links", []):
|
|
207
|
+
link = Link(**l)
|
|
208
|
+
t.links[link.id] = link
|
|
209
|
+
t._ids = itertools.count(max_n + 1)
|
|
210
|
+
return t
|
|
211
|
+
|
|
212
|
+
def __repr__(self) -> str:
|
|
213
|
+
return f"<Topology {self.name!r}: {len(self.devices)} devices, {len(self.links)} links>"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""The engine's vocabulary — the discovery protocol (GINI_AUTHORING_DESIGN.md, "Discover").
|
|
2
|
+
|
|
3
|
+
A single, versioned manifest of everything a fragment can be built FROM on this engine: elements (+
|
|
4
|
+
their grammar), predicates, probes, and capability roles. gBuilder exports it; the Teaching Center
|
|
5
|
+
consumes it so it composes against a *declared version* rather than importing the live engine. This
|
|
6
|
+
is the forward-extensibility seam: when the engine gains a primitive, the exported vocabulary grows,
|
|
7
|
+
and authoring/composition see it automatically — no author-tool code change.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from . import capabilities as _caps
|
|
12
|
+
from . import devices as _devices
|
|
13
|
+
from .content import ENGINE_VERSION, FRAGMENT_SCHEMA
|
|
14
|
+
from .objectives import _TYPE_ARG_FUNCS
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def export() -> dict:
|
|
18
|
+
"""The full vocabulary manifest for this engine version."""
|
|
19
|
+
elements = []
|
|
20
|
+
for d in _devices.all_devices():
|
|
21
|
+
cat = getattr(d, "category", None)
|
|
22
|
+
elements.append({"key": d.key, "label": d.label,
|
|
23
|
+
"category": str(getattr(cat, "value", cat) or "")})
|
|
24
|
+
return {
|
|
25
|
+
"engine_version": ENGINE_VERSION,
|
|
26
|
+
"schema_version": FRAGMENT_SCHEMA,
|
|
27
|
+
"elements": sorted(elements, key=lambda e: e["key"]),
|
|
28
|
+
# structural predicates that take element type_keys as args
|
|
29
|
+
"predicates": sorted(_TYPE_ARG_FUNCS),
|
|
30
|
+
# behavioral probes the runtime can witness (measure = a Source/Sink measurement assertion)
|
|
31
|
+
"probes": ["reach", "ping", "http", "balances", "flow_installed", "measure"],
|
|
32
|
+
# capability roles (the provides/requires vocabulary), as an is-a map
|
|
33
|
+
"capabilities": dict(sorted(_caps.PARENTS.items())),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_compatible(fragment_engine: str) -> tuple[bool, str]:
|
|
38
|
+
"""Coarse version gate: can content authored on `fragment_engine` be trusted here? Empty = a
|
|
39
|
+
built-in (always fine). The FINE gate is validation against the real vocabulary (an unknown
|
|
40
|
+
primitive fails); this is the friendly up-front check + message."""
|
|
41
|
+
if not fragment_engine or fragment_engine == ENGINE_VERSION:
|
|
42
|
+
return True, ""
|
|
43
|
+
try:
|
|
44
|
+
theirs = tuple(int(x) for x in fragment_engine.split("."))
|
|
45
|
+
ours = tuple(int(x) for x in ENGINE_VERSION.split("."))
|
|
46
|
+
except ValueError:
|
|
47
|
+
return True, "" # unparseable → let validation be the judge
|
|
48
|
+
if theirs > ours:
|
|
49
|
+
return False, (f"authored on gBuilder {fragment_engine}, but this is {ENGINE_VERSION} — "
|
|
50
|
+
f"update the engine to use it")
|
|
51
|
+
return True, "" # older content is fine (this engine is a superset)
|