gini-toolkit 6.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""The capability vocabulary — the typed interface that lets mission fragments COMPOSE.
|
|
2
|
+
|
|
3
|
+
Every fragment declares what it `provides` and `requires` in these role names; assembly then joins
|
|
4
|
+
fragments by matching one's provides to another's requires. Roles form a small **is-a hierarchy**
|
|
5
|
+
(a role may have several super-roles), so a fragment can require a *broad* role (loose — many
|
|
6
|
+
providers satisfy it) or a *specific* leaf (tight). `satisfies(provided, required)` is true when the
|
|
7
|
+
required role is the provided role or any of its ancestors — i.e. providing a `web-endpoint`
|
|
8
|
+
satisfies a requirement for a `traffic-sink`.
|
|
9
|
+
|
|
10
|
+
Pure data + two small helpers; no Qt, no LLM. This is the vocabulary drafted in
|
|
11
|
+
GINI_MISSIONS_COMPOSABLE_DESIGN.md §4 — prune here as we learn.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# role -> its super-roles (is-a). Roots map to (). Multiple parents are allowed (e.g. a web tier is
|
|
16
|
+
# both a compute node and a traffic sink).
|
|
17
|
+
PARENTS: dict[str, tuple[str, ...]] = {
|
|
18
|
+
# F1 · network fabric
|
|
19
|
+
# `network` is the recursion hinge: a slot that requires a `network` is satisfied by a LAN
|
|
20
|
+
# (l2-fabric) OR by another routed network (routed-network) — so routers nest to any depth.
|
|
21
|
+
"network": (),
|
|
22
|
+
"l2-fabric": ("network",),
|
|
23
|
+
"switched-segment": ("l2-fabric",),
|
|
24
|
+
"sdn-fabric": ("l2-fabric",),
|
|
25
|
+
"routed-network": ("network",),
|
|
26
|
+
"l3-gateway": (),
|
|
27
|
+
"router-gateway": ("l3-gateway", "routed-network"), # a router exposes the routed network it fronts
|
|
28
|
+
"nat-gateway": ("l3-gateway",),
|
|
29
|
+
"network-boundary": (),
|
|
30
|
+
"vpc-boundary": ("network-boundary",),
|
|
31
|
+
"subnet-boundary": ("network-boundary",),
|
|
32
|
+
# F2 · edge / egress
|
|
33
|
+
"internet-egress": (),
|
|
34
|
+
"public-entrypoint": (),
|
|
35
|
+
# F3 · compute / workload
|
|
36
|
+
"compute-node": (),
|
|
37
|
+
"host-node": ("compute-node",),
|
|
38
|
+
"pod-workload": ("compute-node",),
|
|
39
|
+
"serverless-fn": ("compute-node",),
|
|
40
|
+
"orchestrated-compute": (),
|
|
41
|
+
"web-tier": ("compute-node", "traffic-sink"),
|
|
42
|
+
# F4 · traffic
|
|
43
|
+
"traffic-source": (),
|
|
44
|
+
"load-generator": ("traffic-source",),
|
|
45
|
+
"request-client": ("traffic-source",),
|
|
46
|
+
"traffic-sink": (),
|
|
47
|
+
"web-endpoint": ("traffic-sink",),
|
|
48
|
+
"api-endpoint": ("traffic-sink",),
|
|
49
|
+
"service-endpoint": ("traffic-sink",),
|
|
50
|
+
# F5 · steering / distribution
|
|
51
|
+
"load-distributor": (),
|
|
52
|
+
"inline-nf": (),
|
|
53
|
+
"flow-control": (),
|
|
54
|
+
# F6 · security / policy
|
|
55
|
+
"access-policy": (),
|
|
56
|
+
"perimeter-filter": ("access-policy",),
|
|
57
|
+
"micro-segmentation": ("access-policy",),
|
|
58
|
+
# F7 · data / state
|
|
59
|
+
"datastore": (),
|
|
60
|
+
"relational-store": ("datastore",),
|
|
61
|
+
"cache-tier": ("datastore",),
|
|
62
|
+
"object-store": ("datastore",),
|
|
63
|
+
"message-broker": (),
|
|
64
|
+
# F8 · observability
|
|
65
|
+
"metrics-source": (),
|
|
66
|
+
"metrics-collector": (),
|
|
67
|
+
"visualizer": (),
|
|
68
|
+
"dashboard-view": ("visualizer",),
|
|
69
|
+
"flow-inspector": ("visualizer",),
|
|
70
|
+
"packet-visualizer": ("visualizer",),
|
|
71
|
+
# F9 · fault / perturbation
|
|
72
|
+
"fault-injector": (),
|
|
73
|
+
"link-break": ("fault-injector",),
|
|
74
|
+
"misconfig": ("fault-injector",),
|
|
75
|
+
"overload": ("fault-injector",),
|
|
76
|
+
# F10 · operating systems (xv6 track) — standalone machine, no fabric
|
|
77
|
+
"kernel-host": (),
|
|
78
|
+
"workload-source": ("traffic-source",), # a process/syscall stimulus (a "source" for the OS)
|
|
79
|
+
"kernel-observer": ("visualizer",),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Which domain each role belongs to (for presentation + so the composer knows the track). ONE
|
|
83
|
+
# namespace, tagged three ways — a role like compute-node is shared, which is what lets fragments
|
|
84
|
+
# compose ACROSS domains (a cloud app on a network, an OS reached over a link).
|
|
85
|
+
NETWORKING, CLOUD, OS, SHARED = "networking", "cloud", "os", "shared"
|
|
86
|
+
DOMAIN: dict[str, str] = {
|
|
87
|
+
**{r: NETWORKING for r in (
|
|
88
|
+
"network", "routed-network",
|
|
89
|
+
"l2-fabric", "switched-segment", "sdn-fabric", "l3-gateway", "router-gateway", "nat-gateway",
|
|
90
|
+
"network-boundary", "vpc-boundary", "subnet-boundary", "internet-egress", "public-entrypoint",
|
|
91
|
+
"traffic-source", "load-generator", "request-client", "traffic-sink", "load-distributor",
|
|
92
|
+
"inline-nf", "flow-control", "access-policy", "perimeter-filter", "micro-segmentation",
|
|
93
|
+
"packet-visualizer", "flow-inspector", "fault-injector", "link-break", "misconfig", "overload")},
|
|
94
|
+
**{r: CLOUD for r in (
|
|
95
|
+
"compute-node", "host-node", "pod-workload", "serverless-fn", "orchestrated-compute",
|
|
96
|
+
"web-tier", "web-endpoint", "api-endpoint", "service-endpoint", "datastore",
|
|
97
|
+
"relational-store", "cache-tier", "object-store", "message-broker")},
|
|
98
|
+
**{r: SHARED for r in ("metrics-source", "metrics-collector", "visualizer", "dashboard-view")},
|
|
99
|
+
**{r: OS for r in ("kernel-host", "workload-source", "kernel-observer")},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Device type → the capability roles it can PLAY. Multi-role is deliberate: it gives the composer
|
|
103
|
+
# more ways to satisfy a requirement (a host is both a compute-node and, when it pings, a source).
|
|
104
|
+
DEVICE_ROLES: dict[str, tuple[str, ...]] = {
|
|
105
|
+
# networking fabric / edge
|
|
106
|
+
"router": ("router-gateway",), "switch": ("switched-segment",), "hub": ("switched-segment",),
|
|
107
|
+
"firewall": ("perimeter-filter",), "wap": ("switched-segment",), "vnf": ("inline-nf",),
|
|
108
|
+
"cloud": ("internet-egress",), "ovs": ("sdn-fabric",), "controller": ("flow-control",),
|
|
109
|
+
# compute
|
|
110
|
+
"host": ("host-node",), "container": ("compute-node",), "instance": ("compute-node",),
|
|
111
|
+
"kinstance": ("compute-node",), "web_app": ("web-endpoint", "compute-node"),
|
|
112
|
+
"pod": ("pod-workload",), "k8s_cluster": ("orchestrated-compute",), "k8s_node": ("compute-node",),
|
|
113
|
+
"instance_group": ("orchestrated-compute",),
|
|
114
|
+
# cloud networking
|
|
115
|
+
"vpc": ("vpc-boundary",), "cloud_subnet": ("subnet-boundary",),
|
|
116
|
+
"security_group": ("micro-segmentation",), "gateway": ("nat-gateway",),
|
|
117
|
+
"load_balancer": ("load-distributor",), "proxy": ("load-distributor",),
|
|
118
|
+
# data / serverless / streaming
|
|
119
|
+
"database": ("relational-store",), "nosql": ("datastore",), "cache": ("cache-tier",),
|
|
120
|
+
"object_store": ("object-store",), "function": ("serverless-fn",),
|
|
121
|
+
"api_gateway": ("api-endpoint", "public-entrypoint"),
|
|
122
|
+
"queue": ("message-broker",), "stream": ("message-broker",), "messaging": ("message-broker",),
|
|
123
|
+
# observability (sinks)
|
|
124
|
+
"metrics": ("metrics-collector",), "dashboard": ("dashboard-view",), "tracing": ("visualizer",),
|
|
125
|
+
# sources / sinks (riders)
|
|
126
|
+
"ping_probe": ("request-client",), "http_probe": ("request-client",),
|
|
127
|
+
"dns_probe": ("request-client",), "traceroute_probe": ("request-client",),
|
|
128
|
+
"iperf_client": ("load-generator",), "load_generator": ("load-generator",),
|
|
129
|
+
"iperf_server": ("traffic-sink",), "iface_stats": ("metrics-source",),
|
|
130
|
+
"packet_view": ("packet-visualizer",),
|
|
131
|
+
# operating systems (xv6)
|
|
132
|
+
"xv6": ("kernel-host",), "terminal": ("kernel-observer",), "storage_volume": ("kernel-observer",),
|
|
133
|
+
"xv6_shell": ("workload-source",), "xv6_workload": ("workload-source",),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def roles_for(device_key: str) -> tuple[str, ...]:
|
|
138
|
+
"""The capability roles a device type can play (may be several)."""
|
|
139
|
+
return DEVICE_ROLES.get(device_key, ())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def domain_of(role: str) -> str:
|
|
143
|
+
return DOMAIN.get(role, SHARED)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_role(name: str) -> bool:
|
|
147
|
+
return name in PARENTS
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ancestors(role: str) -> set[str]:
|
|
151
|
+
"""All super-roles of `role`, inclusive of itself (walks the is-a DAG)."""
|
|
152
|
+
seen: set[str] = set()
|
|
153
|
+
stack = [role]
|
|
154
|
+
while stack:
|
|
155
|
+
r = stack.pop()
|
|
156
|
+
if r in seen:
|
|
157
|
+
continue
|
|
158
|
+
seen.add(r)
|
|
159
|
+
stack.extend(PARENTS.get(r, ()))
|
|
160
|
+
return seen
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def satisfies(provided: str, required: str) -> bool:
|
|
164
|
+
"""True when a fragment providing `provided` meets a requirement for `required` — i.e. the
|
|
165
|
+
required role is the provided role or one of its ancestors (a specific provider satisfies a
|
|
166
|
+
broad requirement)."""
|
|
167
|
+
return required in ancestors(provided)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def any_satisfies(provided_roles, required: str) -> bool:
|
|
171
|
+
"""True when any of `provided_roles` satisfies the single `required` role."""
|
|
172
|
+
return any(satisfies(p, required) for p in provided_roles)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def unknown_roles(roles) -> list[str]:
|
|
176
|
+
"""Roles not in the vocabulary (authoring safety net)."""
|
|
177
|
+
return sorted({r for r in roles if r not in PARENTS})
|
gini/domain/catalog.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""The Game Catalog — an ADAPTER over the foundational fragments (now YAML packs under `missions/`).
|
|
2
|
+
|
|
3
|
+
Historically this module held the archetypes as Python literals. Those foundational templates now live
|
|
4
|
+
as data (YAML) and are loaded by `fragments`; this module presents the pickable/standalone fragments as
|
|
5
|
+
`Archetype`s so the older call sites (lesson.from_archetype, lesson_resolver, the mission picker) keep
|
|
6
|
+
working unchanged. Content moved to data; the API is stable.
|
|
7
|
+
|
|
8
|
+
`ObjectiveTemplate` is re-exported from `fragments` for back-compat. Objectives use the same type-based,
|
|
9
|
+
name-agnostic predicates (`exists`, `count`, `link`, `path`, `through`, `contains_type`).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
from . import fragments as _frag
|
|
17
|
+
from .fragments import ObjectiveTemplate # noqa: F401 (re-export for back-compat)
|
|
18
|
+
from .objectives import Objective
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Archetype:
|
|
23
|
+
id: str
|
|
24
|
+
teaches: str
|
|
25
|
+
spirit: str
|
|
26
|
+
summary: str
|
|
27
|
+
params: tuple[str, ...] = ()
|
|
28
|
+
objectives: tuple[ObjectiveTemplate, ...] = ()
|
|
29
|
+
misconceptions: tuple[str, ...] = ()
|
|
30
|
+
complete_when: str = "all"
|
|
31
|
+
difficulty: dict = field(default_factory=dict)
|
|
32
|
+
stage: dict = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Type-based objectives need no name bindings, so archetypes launch with no params.
|
|
36
|
+
DEMO_PARAMS: dict[str, dict] = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _arch_of(frag) -> Archetype:
|
|
40
|
+
return Archetype(id=frag.id, teaches=frag.teaches, spirit=frag.spirit, summary=frag.summary,
|
|
41
|
+
params=(), objectives=tuple(frag.objectives),
|
|
42
|
+
misconceptions=tuple(frag.misconceptions), complete_when=frag.complete_when,
|
|
43
|
+
stage=dict(frag.stage))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get(archetype_id: str) -> Archetype | None:
|
|
47
|
+
frag = _frag.get(archetype_id)
|
|
48
|
+
return _arch_of(frag) if (frag is not None and frag.catalog) else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def all_archetypes() -> list[Archetype]:
|
|
52
|
+
"""The standalone, pickable missions (fragments flagged `catalog`), preserving fragment order."""
|
|
53
|
+
return [_arch_of(f) for f in _frag.all_fragments() if f.catalog]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def demo_params(archetype_id: str) -> dict:
|
|
57
|
+
return dict(DEMO_PARAMS.get(archetype_id, {}))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# -- instantiation ---------------------------------------------------------- #
|
|
61
|
+
_PLACEHOLDER = re.compile(r"\{(\w+)\}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _bind(text: str, params: dict) -> str:
|
|
65
|
+
"""Replace {ref} placeholders with concrete names (a no-op for type-based checks)."""
|
|
66
|
+
return _PLACEHOLDER.sub(lambda m: str(params.get(m.group(1), m.group(0))), text)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def instantiate(archetype: Archetype, params: dict) -> list[Objective]:
|
|
70
|
+
from .objectives import by_level
|
|
71
|
+
out: list[Objective] = []
|
|
72
|
+
for t in archetype.objectives:
|
|
73
|
+
out.append(Objective(id=t.id, say=t.say, kind=t.kind,
|
|
74
|
+
check=_bind(t.check, params) if t.check else "",
|
|
75
|
+
probe=_bind(t.probe, params) if t.probe else "",
|
|
76
|
+
level=t.level))
|
|
77
|
+
return by_level(out) # progressive ladder: place → connect → group → prove live
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def unbound_refs(archetype: Archetype, params: dict) -> list[str]:
|
|
81
|
+
refs: set[str] = set()
|
|
82
|
+
for t in archetype.objectives:
|
|
83
|
+
refs.update(_PLACEHOLDER.findall(t.check))
|
|
84
|
+
refs.update(_PLACEHOLDER.findall(t.probe))
|
|
85
|
+
return sorted(r for r in refs if r not in params)
|
gini/domain/certify.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Fragment certification — the client-side quality gate before a block goes to the Teaching Center.
|
|
2
|
+
|
|
3
|
+
Certification has two organs, matching the generate⇄verify spine:
|
|
4
|
+
|
|
5
|
+
* **the compiler (deterministic, HARD gate)** — proves ground truth: the fragment validates, and
|
|
6
|
+
(once a live runner is wired) that a reference solution grades to complete. The AI never decides
|
|
7
|
+
correctness — a hallucinated "looks fine" on a broken block would poison every mission composed
|
|
8
|
+
from it.
|
|
9
|
+
* **the AI certifier (advisory, SOFT gate)** — reviews *composability*: the issues the composer
|
|
10
|
+
will hit when it recombines this block. Its strongest form is a **dry-run of the composition
|
|
11
|
+
engine** — actually attempt to compose the fragment against the library and report what fit,
|
|
12
|
+
what's orphaned, and what it can feed. The reasoning here is deterministic today (contract
|
|
13
|
+
matching via `assembly`); an LLM advisor can layer richer judgement on top later.
|
|
14
|
+
|
|
15
|
+
`certify()` returns a report of tiered issues. `blocked` (any BLOCK issue) means "not certified".
|
|
16
|
+
WARN issues are soft — the teacher may upload over them, but they travel with the fragment so the
|
|
17
|
+
composer knows the risk.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
from . import capabilities as _caps
|
|
25
|
+
from . import fragment_yaml as _fy
|
|
26
|
+
from . import fragments as _frag
|
|
27
|
+
|
|
28
|
+
BLOCK, WARN, INFO = "block", "warn", "info"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Issue:
|
|
33
|
+
level: str # BLOCK | WARN | INFO
|
|
34
|
+
code: str
|
|
35
|
+
message: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class CertReport:
|
|
40
|
+
fragment_id: str
|
|
41
|
+
issues: list[Issue] = field(default_factory=list)
|
|
42
|
+
composes_into: list[str] = field(default_factory=list) # fragments this one can feed (forward)
|
|
43
|
+
unmet_requires: list[str] = field(default_factory=list) # requires nothing in the library gives
|
|
44
|
+
|
|
45
|
+
def add(self, level: str, code: str, message: str) -> None:
|
|
46
|
+
self.issues.append(Issue(level, code, message))
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def blocked(self) -> bool:
|
|
50
|
+
return any(i.level == BLOCK for i in self.issues)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def certified(self) -> bool:
|
|
54
|
+
return not self.blocked
|
|
55
|
+
|
|
56
|
+
def of(self, level: str) -> list[Issue]:
|
|
57
|
+
return [i for i in self.issues if i.level == level]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class RuntimeGrade:
|
|
62
|
+
"""The outcome of grading the fragment's AUTHORING BOARD (the reference solution) on a live
|
|
63
|
+
stack. The caller runs `objectives.evaluate_all` with the fragment's sources auto-started, then
|
|
64
|
+
hands the summary here. `available=False` means the stack wasn't running, so nothing was proven.
|
|
65
|
+
"""
|
|
66
|
+
available: bool = False
|
|
67
|
+
unmet: list = field(default_factory=list) # objective ids that graded UNMET (a real failure)
|
|
68
|
+
pending: list = field(default_factory=list) # behavioral ids the runner couldn't witness
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def all_met(self) -> bool:
|
|
72
|
+
return self.available and not self.unmet and not self.pending
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_COUNT_RE = re.compile(r"^\s*count\(\s*(\w+)\s*\)\s*>=\s*(\d+)")
|
|
76
|
+
_PROBE_ENDS_RE = re.compile(r"(?:reach|ping|http)\(\s*(\w+)\s*->\s*(\w+)")
|
|
77
|
+
_MEASURE_RE = re.compile(r"measure\(\s*(\w+)\s*,")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _multi_types(objectives) -> set[str]:
|
|
81
|
+
"""Types the fragment asks for MORE THAN ONE of — the ones a type-based probe can be ambiguous
|
|
82
|
+
about in a merged board."""
|
|
83
|
+
out = set()
|
|
84
|
+
for o in objectives:
|
|
85
|
+
m = _COUNT_RE.match(o.get("check") or "")
|
|
86
|
+
if m and int(m.group(2)) >= 2:
|
|
87
|
+
out.add(m.group(1))
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _probe_types(objectives) -> set[str]:
|
|
92
|
+
out = set()
|
|
93
|
+
for o in objectives:
|
|
94
|
+
p = o.get("probe") or ""
|
|
95
|
+
m = _PROBE_ENDS_RE.search(p)
|
|
96
|
+
if m:
|
|
97
|
+
out.update((m.group(1), m.group(2)))
|
|
98
|
+
m = _MEASURE_RE.search(p)
|
|
99
|
+
if m:
|
|
100
|
+
out.add(m.group(1))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _dry_run_compose(frag, rep: CertReport, library) -> None:
|
|
105
|
+
"""A deterministic dry-run of the composition engine: can the fragment's requirements be met by
|
|
106
|
+
the library (backward), and what can consume what it provides (forward)? This is exactly the
|
|
107
|
+
contract-matching the assembler does when it chains fragments."""
|
|
108
|
+
lib = library if library is not None else _frag.all_fragments()
|
|
109
|
+
others = [f for f in lib if f.id != frag.id]
|
|
110
|
+
|
|
111
|
+
for req in frag.requires: # backward: who can satisfy what I need?
|
|
112
|
+
if any(_caps.any_satisfies(f.provides, req) for f in others):
|
|
113
|
+
continue
|
|
114
|
+
rep.unmet_requires.append(req)
|
|
115
|
+
rep.add(WARN, "orphan-requires",
|
|
116
|
+
f"Nothing in the library provides '{req}', so I can only ever use this block to "
|
|
117
|
+
f"START a chain, never in the middle.")
|
|
118
|
+
|
|
119
|
+
for f in others: # forward: who can consume what I provide?
|
|
120
|
+
if any(_caps.any_satisfies(frag.provides, r) for r in f.requires):
|
|
121
|
+
rep.composes_into.append(f.id)
|
|
122
|
+
if frag.provides and rep.composes_into:
|
|
123
|
+
rep.add(INFO, "composes-into",
|
|
124
|
+
f"Composes into: {', '.join(sorted(set(rep.composes_into)))}.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def certify(frag_dict: dict, *, library=None, runtime: "RuntimeGrade | None" = None) -> CertReport:
|
|
128
|
+
"""Certify an authored fragment dict. `runtime` is the result of grading the fragment's authoring
|
|
129
|
+
board on a live stack (None = not graded). A fragment with live/output checks is only certified
|
|
130
|
+
once that grade is clean — the HARD compiler gate; pure-structural fragments need no runtime."""
|
|
131
|
+
rep = CertReport(fragment_id=frag_dict.get("id", ""))
|
|
132
|
+
|
|
133
|
+
# -- compiler: gradable (BLOCK) ---------------------------------------- #
|
|
134
|
+
problems = _fy.validate(_fy.fragment_from_dict(frag_dict))
|
|
135
|
+
for p in problems:
|
|
136
|
+
rep.add(BLOCK, "invalid", p)
|
|
137
|
+
if rep.blocked:
|
|
138
|
+
return rep # a broken block: don't reason further
|
|
139
|
+
|
|
140
|
+
frag = _fy.fragment_from_dict(frag_dict)
|
|
141
|
+
objectives = frag_dict.get("objectives", [])
|
|
142
|
+
has_output = any((o.get("probe") or "").startswith("measure(") for o in objectives)
|
|
143
|
+
has_live = any((o.get("probe") or "").startswith(("reach", "ping", "http")) for o in objectives)
|
|
144
|
+
has_behavioral = has_output or has_live
|
|
145
|
+
|
|
146
|
+
# -- compiler: winnable / runtime-sound (HARD gate) -------------------- #
|
|
147
|
+
# The authoring board IS the reference solution: grade it live and it must pass. Behavioral /
|
|
148
|
+
# output checks need a running stack (with the Sources driving traffic); pure-structural doesn't.
|
|
149
|
+
if not has_behavioral:
|
|
150
|
+
rep.add(INFO, "structural-only",
|
|
151
|
+
"Structural fragment — no live checks, so no runtime playtest is needed.")
|
|
152
|
+
elif runtime is None or not runtime.available:
|
|
153
|
+
rep.add(BLOCK, "runtime-required",
|
|
154
|
+
"Run the topology and Certify to prove the live/output checks — required before "
|
|
155
|
+
"upload (its Sources drive traffic while the board is graded).")
|
|
156
|
+
elif runtime.unmet:
|
|
157
|
+
rep.add(BLOCK, "not-winnable",
|
|
158
|
+
f"The reference board doesn't satisfy: {', '.join(runtime.unmet)} — is it winnable?")
|
|
159
|
+
elif runtime.pending:
|
|
160
|
+
rep.add(BLOCK, "runtime-incomplete",
|
|
161
|
+
f"Couldn't witness: {', '.join(runtime.pending)} — is the whole stack running?")
|
|
162
|
+
else:
|
|
163
|
+
rep.add(INFO, "runtime-ok",
|
|
164
|
+
"Playtested live: the reference solution grades to complete.")
|
|
165
|
+
|
|
166
|
+
# -- AI certifier: composability review (WARN, soft gate) -------------- #
|
|
167
|
+
if not (frag_dict.get("spirit") or "").strip():
|
|
168
|
+
rep.add(WARN, "no-spirit",
|
|
169
|
+
"No spirit/intent — the composer can't generate variants or negotiate level without "
|
|
170
|
+
"a sentence on what success means.")
|
|
171
|
+
if not has_output and not has_live:
|
|
172
|
+
rep.add(WARN, "no-output",
|
|
173
|
+
"No live/output check — this grades only the picture, not that the system works. "
|
|
174
|
+
"Attach a Sink and add an output check.")
|
|
175
|
+
if not frag.forks:
|
|
176
|
+
rep.add(WARN, "no-difficulty",
|
|
177
|
+
"No difficulty knob — the composer can't offer an easier/harder variant. Add input "
|
|
178
|
+
"presets or a fork.")
|
|
179
|
+
multi, probed = _multi_types(objectives), _probe_types(objectives)
|
|
180
|
+
for tk in sorted(multi & probed):
|
|
181
|
+
# not actually ambiguous — type-based grading reads this as "SOME <tk>" by default (add
|
|
182
|
+
# ', all' if you mean EVERY one). Just a hint so the author picks the intended quantifier.
|
|
183
|
+
rep.add(INFO, "quantifier",
|
|
184
|
+
f"You have ≥2 '{tk}' and a check on '{tk}' — it grades as \"some {tk}\"; add ', all' "
|
|
185
|
+
f"to the probe if you mean every one.")
|
|
186
|
+
if len(objectives) > 8:
|
|
187
|
+
rep.add(WARN, "too-big",
|
|
188
|
+
f"{len(objectives)} objectives — big blocks recombine poorly; consider splitting "
|
|
189
|
+
f"into single-idea fragments.")
|
|
190
|
+
|
|
191
|
+
# -- AI certifier: dry-run the composition engine (WARN/INFO) ---------- #
|
|
192
|
+
_dry_run_compose(frag, rep, library)
|
|
193
|
+
return rep
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def runtime_from_results(results, *, available: bool = True) -> RuntimeGrade:
|
|
197
|
+
"""Summarise an `objectives.evaluate_all` result list into a RuntimeGrade for `certify()`."""
|
|
198
|
+
from . import objectives as _obj
|
|
199
|
+
unmet = [r.id for r in results if r.status == _obj.UNMET]
|
|
200
|
+
pending = [r.id for r in results if r.status == _obj.PENDING]
|
|
201
|
+
return RuntimeGrade(available=available, unmet=unmet, pending=pending)
|