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.
Files changed (278) hide show
  1. gini/__init__.py +12 -0
  2. gini/__main__.py +107 -0
  3. gini/_version.py +24 -0
  4. gini/agent/__init__.py +17 -0
  5. gini/agent/agent_gamemaster.py +140 -0
  6. gini/agent/api.py +291 -0
  7. gini/agent/ask.py +123 -0
  8. gini/agent/authoring.py +72 -0
  9. gini/agent/blackboard.py +114 -0
  10. gini/agent/contracts.py +142 -0
  11. gini/agent/domains.py +91 -0
  12. gini/agent/embed.py +123 -0
  13. gini/agent/gamemaster.py +256 -0
  14. gini/agent/kb.py +148 -0
  15. gini/agent/lesson_resolver.py +261 -0
  16. gini/agent/llm/__init__.py +5 -0
  17. gini/agent/llm/backend.py +43 -0
  18. gini/agent/llm/fake.py +25 -0
  19. gini/agent/llm/ollama.py +206 -0
  20. gini/agent/loop.py +258 -0
  21. gini/agent/mcp_server.py +86 -0
  22. gini/agent/meaning.py +225 -0
  23. gini/agent/mission.py +210 -0
  24. gini/agent/mission_controller.py +208 -0
  25. gini/agent/narration.py +116 -0
  26. gini/agent/notifier.py +86 -0
  27. gini/agent/personas.py +79 -0
  28. gini/agent/reasoning.py +172 -0
  29. gini/agent/recall.py +248 -0
  30. gini/agent/session.py +79 -0
  31. gini/agent/teaching_center.py +482 -0
  32. gini/agent/tools/__init__.py +3 -0
  33. gini/agent/tools/registry.py +193 -0
  34. gini/agent/twin/__init__.py +28 -0
  35. gini/agent/twin/authoring.py +71 -0
  36. gini/agent/twin/contracts.py +54 -0
  37. gini/agent/twin/dialectic.py +189 -0
  38. gini/agent/twin/harness.py +93 -0
  39. gini/agent/twin/justify.py +156 -0
  40. gini/agent/twin/learner.py +64 -0
  41. gini/agent/twin/mission.py +60 -0
  42. gini/agent/twin/os_coach.py +79 -0
  43. gini/agent/twin/salience.py +30 -0
  44. gini/agent/understand.py +250 -0
  45. gini/agent/verifiers.py +106 -0
  46. gini/agent/wizard.py +178 -0
  47. gini/agent/xv6_pack.py +74 -0
  48. gini/app/__init__.py +3 -0
  49. gini/app/context.py +368 -0
  50. gini/app/paths.py +121 -0
  51. gini/data/README.md +21 -0
  52. gini/domain/__init__.py +9 -0
  53. gini/domain/assembly.py +209 -0
  54. gini/domain/authoring.py +353 -0
  55. gini/domain/blueprints.py +5 -0
  56. gini/domain/capabilities.py +177 -0
  57. gini/domain/catalog.py +85 -0
  58. gini/domain/certify.py +201 -0
  59. gini/domain/compose.py +413 -0
  60. gini/domain/composition.py +88 -0
  61. gini/domain/concepts.py +383 -0
  62. gini/domain/connection_rules.py +269 -0
  63. gini/domain/constraints.py +153 -0
  64. gini/domain/content.py +59 -0
  65. gini/domain/cpu_journey.py +89 -0
  66. gini/domain/devices.py +747 -0
  67. gini/domain/diagnose.py +201 -0
  68. gini/domain/element_guide.py +327 -0
  69. gini/domain/explain.py +90 -0
  70. gini/domain/fingerprint.py +201 -0
  71. gini/domain/firewall.py +34 -0
  72. gini/domain/flowlog.py +61 -0
  73. gini/domain/flowtable.py +179 -0
  74. gini/domain/fragment_yaml.py +230 -0
  75. gini/domain/fragments.py +169 -0
  76. gini/domain/games/__init__.py +2 -0
  77. gini/domain/games/paging_games.py +119 -0
  78. gini/domain/games/policy_game.py +86 -0
  79. gini/domain/games/process_game.py +48 -0
  80. gini/domain/games/thrash_game.py +75 -0
  81. gini/domain/games/translate_game.py +60 -0
  82. gini/domain/games/trap_game.py +86 -0
  83. gini/domain/grader.py +155 -0
  84. gini/domain/grouping.py +67 -0
  85. gini/domain/legality.py +103 -0
  86. gini/domain/lesson.py +241 -0
  87. gini/domain/lexicon.py +150 -0
  88. gini/domain/machine_state.py +410 -0
  89. gini/domain/missions/networking/basic-lan.yaml +32 -0
  90. gini/domain/missions/networking/cache-in-front.yaml +23 -0
  91. gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
  92. gini/domain/missions/networking/drive-load.yaml +20 -0
  93. gini/domain/missions/networking/fix-the-address.yaml +75 -0
  94. gini/domain/missions/networking/fix-the-lan.yaml +43 -0
  95. gini/domain/missions/networking/inspect-flows.yaml +16 -0
  96. gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
  97. gini/domain/missions/networking/least-privilege.yaml +21 -0
  98. gini/domain/missions/networking/load-balanced-web.yaml +29 -0
  99. gini/domain/missions/networking/observe-it.yaml +24 -0
  100. gini/domain/missions/networking/put-in-vpc.yaml +30 -0
  101. gini/domain/missions/networking/reachability-boundary.yaml +56 -0
  102. gini/domain/missions/networking/sdn-reactive.yaml +35 -0
  103. gini/domain/missions/networking/send-request.yaml +19 -0
  104. gini/domain/missions/networking/serverless-api.yaml +25 -0
  105. gini/domain/missions/networking/service-chain.yaml +33 -0
  106. gini/domain/missions/os/lottery-fix.yaml +19 -0
  107. gini/domain/missions/os/priority-fix.yaml +24 -0
  108. gini/domain/missions.py +111 -0
  109. gini/domain/modulechain.py +36 -0
  110. gini/domain/objectives.py +488 -0
  111. gini/domain/os_zoo.py +79 -0
  112. gini/domain/paging_sim.py +141 -0
  113. gini/domain/pricing.py +199 -0
  114. gini/domain/probes.py +226 -0
  115. gini/domain/profile.py +142 -0
  116. gini/domain/recipes.py +738 -0
  117. gini/domain/riders.py +309 -0
  118. gini/domain/router_modules.py +224 -0
  119. gini/domain/routetable.py +67 -0
  120. gini/domain/scoring.py +76 -0
  121. gini/domain/staging.py +122 -0
  122. gini/domain/syscall_builder.py +144 -0
  123. gini/domain/topic_cloud.py +62 -0
  124. gini/domain/topology.py +213 -0
  125. gini/domain/vocabulary.py +51 -0
  126. gini/domain/xv6.py +808 -0
  127. gini/domain/xv6_fs.py +250 -0
  128. gini/domain/xv6_runner.py +113 -0
  129. gini/domain/xv6_vm.py +385 -0
  130. gini/gloader.py +17 -0
  131. gini/runtime/__init__.py +18 -0
  132. gini/runtime/cloudfabric_agent.py +370 -0
  133. gini/runtime/console.py +68 -0
  134. gini/runtime/control.py +70 -0
  135. gini/runtime/frame.py +138 -0
  136. gini/runtime/gbridge.py +638 -0
  137. gini/runtime/grouter.py +223 -0
  138. gini/runtime/hostsim.py +90 -0
  139. gini/runtime/shuttle.py +348 -0
  140. gini/runtime/switch.py +109 -0
  141. gini/runtime/transport.py +77 -0
  142. gini/runtime/xv6_bridge.py +312 -0
  143. gini/server/__init__.py +22 -0
  144. gini/server/__main__.py +74 -0
  145. gini/server/app.py +140 -0
  146. gini/server/auth.py +82 -0
  147. gini/server/policy.py +57 -0
  148. gini/server/session.py +23 -0
  149. gini/services/__init__.py +15 -0
  150. gini/services/boardflash.py +248 -0
  151. gini/services/boardsetup.py +374 -0
  152. gini/services/cloud_catalog.py +143 -0
  153. gini/services/compiler.py +1858 -0
  154. gini/services/discovery.py +324 -0
  155. gini/services/gloader.py +183 -0
  156. gini/services/orchestrator.py +1460 -0
  157. gini/services/persistence.py +28 -0
  158. gini/services/probe_runner.py +149 -0
  159. gini/services/project.py +217 -0
  160. gini/services/remote.py +93 -0
  161. gini/services/rider_runner.py +96 -0
  162. gini/services/rider_session.py +171 -0
  163. gini/services/shadow_store.py +52 -0
  164. gini/services/terminal.py +45 -0
  165. gini/setup/__init__.py +17 -0
  166. gini/setup/cli.py +109 -0
  167. gini/setup/images.py +33 -0
  168. gini/setup/marker.py +43 -0
  169. gini/setup/runtime.py +69 -0
  170. gini/ui/__init__.py +3 -0
  171. gini/ui/assets/app_icon.icns +0 -0
  172. gini/ui/assets/app_icon.ico +0 -0
  173. gini/ui/assets/app_icon.png +0 -0
  174. gini/ui/assets/app_icon_1024.png +0 -0
  175. gini/ui/assets/cue/_w.txt +1 -0
  176. gini/ui/assets/cue/ai.png +0 -0
  177. gini/ui/assets/cue/canvas.png +0 -0
  178. gini/ui/assets/cue/cloud.png +0 -0
  179. gini/ui/assets/cue/cost.png +0 -0
  180. gini/ui/assets/cue/dark/ai.png +0 -0
  181. gini/ui/assets/cue/dark/canvas.png +0 -0
  182. gini/ui/assets/cue/dark/cloud.png +0 -0
  183. gini/ui/assets/cue/dark/cost.png +0 -0
  184. gini/ui/assets/cue/dark/metrics.png +0 -0
  185. gini/ui/assets/cue/dark/router.png +0 -0
  186. gini/ui/assets/cue/dark/run.png +0 -0
  187. gini/ui/assets/cue/dark/serverless.png +0 -0
  188. gini/ui/assets/cue/dark/settings.png +0 -0
  189. gini/ui/assets/cue/dark/welcome.png +0 -0
  190. gini/ui/assets/cue/dark/wizard.png +0 -0
  191. gini/ui/assets/cue/ginibrand/ai.png +0 -0
  192. gini/ui/assets/cue/ginibrand/canvas.png +0 -0
  193. gini/ui/assets/cue/ginibrand/cloud.png +0 -0
  194. gini/ui/assets/cue/ginibrand/cost.png +0 -0
  195. gini/ui/assets/cue/ginibrand/metrics.png +0 -0
  196. gini/ui/assets/cue/ginibrand/router.png +0 -0
  197. gini/ui/assets/cue/ginibrand/run.png +0 -0
  198. gini/ui/assets/cue/ginibrand/serverless.png +0 -0
  199. gini/ui/assets/cue/ginibrand/settings.png +0 -0
  200. gini/ui/assets/cue/ginibrand/welcome.png +0 -0
  201. gini/ui/assets/cue/ginibrand/wizard.png +0 -0
  202. gini/ui/assets/cue/highcontrast/ai.png +0 -0
  203. gini/ui/assets/cue/highcontrast/canvas.png +0 -0
  204. gini/ui/assets/cue/highcontrast/cloud.png +0 -0
  205. gini/ui/assets/cue/highcontrast/cost.png +0 -0
  206. gini/ui/assets/cue/highcontrast/metrics.png +0 -0
  207. gini/ui/assets/cue/highcontrast/router.png +0 -0
  208. gini/ui/assets/cue/highcontrast/run.png +0 -0
  209. gini/ui/assets/cue/highcontrast/serverless.png +0 -0
  210. gini/ui/assets/cue/highcontrast/settings.png +0 -0
  211. gini/ui/assets/cue/highcontrast/welcome.png +0 -0
  212. gini/ui/assets/cue/highcontrast/wizard.png +0 -0
  213. gini/ui/assets/cue/light/ai.png +0 -0
  214. gini/ui/assets/cue/light/canvas.png +0 -0
  215. gini/ui/assets/cue/light/cloud.png +0 -0
  216. gini/ui/assets/cue/light/cost.png +0 -0
  217. gini/ui/assets/cue/light/metrics.png +0 -0
  218. gini/ui/assets/cue/light/router.png +0 -0
  219. gini/ui/assets/cue/light/run.png +0 -0
  220. gini/ui/assets/cue/light/serverless.png +0 -0
  221. gini/ui/assets/cue/light/settings.png +0 -0
  222. gini/ui/assets/cue/light/welcome.png +0 -0
  223. gini/ui/assets/cue/light/wizard.png +0 -0
  224. gini/ui/assets/cue/metrics.png +0 -0
  225. gini/ui/assets/cue/router.png +0 -0
  226. gini/ui/assets/cue/run.png +0 -0
  227. gini/ui/assets/cue/serverless.png +0 -0
  228. gini/ui/assets/cue/settings.png +0 -0
  229. gini/ui/assets/cue/welcome.png +0 -0
  230. gini/ui/assets/cue/wizard.png +0 -0
  231. gini/ui/assistant.py +2111 -0
  232. gini/ui/author_dialog.py +184 -0
  233. gini/ui/board_dialog.py +247 -0
  234. gini/ui/branding.py +21 -0
  235. gini/ui/canvas.py +2007 -0
  236. gini/ui/chat_panel.py +7 -0
  237. gini/ui/cpu_journey.py +212 -0
  238. gini/ui/cpu_lab.py +306 -0
  239. gini/ui/cue_cards.py +214 -0
  240. gini/ui/dashboard.py +222 -0
  241. gini/ui/diagnose_game.py +336 -0
  242. gini/ui/fingerprint_lab.py +219 -0
  243. gini/ui/flash_dialog.py +244 -0
  244. gini/ui/flow_layout.py +63 -0
  245. gini/ui/fragment_manager.py +1415 -0
  246. gini/ui/game_catalog.py +184 -0
  247. gini/ui/game_renderers.py +340 -0
  248. gini/ui/games_lab.py +90 -0
  249. gini/ui/inspector.py +1055 -0
  250. gini/ui/live_metrics.py +130 -0
  251. gini/ui/machine_lab.py +1412 -0
  252. gini/ui/main_window.py +3153 -0
  253. gini/ui/memory_lab.py +371 -0
  254. gini/ui/mission_panel.py +302 -0
  255. gini/ui/mode_indicator.py +227 -0
  256. gini/ui/palette.py +112 -0
  257. gini/ui/peripherals.py +218 -0
  258. gini/ui/process_tree.py +130 -0
  259. gini/ui/reset_dialog.py +179 -0
  260. gini/ui/router_lab.py +776 -0
  261. gini/ui/run_button.py +183 -0
  262. gini/ui/settings_dialog.py +234 -0
  263. gini/ui/signin_dialog.py +111 -0
  264. gini/ui/storage_lab.py +219 -0
  265. gini/ui/syscall_builder.py +235 -0
  266. gini/ui/syscall_lab.py +152 -0
  267. gini/ui/theme/__init__.py +5 -0
  268. gini/ui/theme/icons.py +145 -0
  269. gini/ui/theme/manager.py +291 -0
  270. gini/ui/theme/tokens.py +194 -0
  271. gini/ui/trap_lab.py +270 -0
  272. gini/ui/worker_host.py +102 -0
  273. gini/ui/zoo_lab.py +112 -0
  274. gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
  275. gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
  276. gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
  277. gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
  278. gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,336 @@
1
+ """Generic Diagnose-game UI — one widget that drives ANY game via a GameSpec + a case source + a
2
+ signature renderer. The confusion matrix, buttons, modes (practice/graded), and scoring are shared;
3
+ each game only supplies its classes, its cases (real telemetry), and how to draw a signature.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from PySide6.QtCore import Qt, QTimer
8
+ from PySide6.QtGui import QColor, QPainter
9
+ from PySide6.QtWidgets import (
10
+ QHBoxLayout, QLabel, QLineEdit, QListWidget, QPushButton, QVBoxLayout, QWidget,
11
+ )
12
+
13
+ from ..domain.diagnose import GRADED, PRACTICE, DiagnoseSession
14
+ from .theme.manager import scale_css as _scss
15
+
16
+
17
+ class ConfusionGrid(QWidget):
18
+ """true(row) × predicted(col) matrix; diagonal = correct. Axis ticks use the spec's abbrevs."""
19
+
20
+ def __init__(self, theme, parent=None) -> None:
21
+ super().__init__(parent)
22
+ self.theme = theme
23
+ self._m: dict = {}
24
+ self._classes: list = []
25
+ self._abbr: dict = {}
26
+ self.setMinimumSize(300, 240)
27
+
28
+ def set_matrix(self, m, classes, abbrev=None) -> None:
29
+ self._m = m or {}
30
+ self._classes = classes or []
31
+ self._abbr = abbrev or {}
32
+ self.update()
33
+
34
+ def _tick(self, cls) -> str:
35
+ return self._abbr.get(cls, cls)
36
+
37
+ def paintEvent(self, _e) -> None: # noqa: N802
38
+ t = self.theme.theme
39
+ p = QPainter(self)
40
+ p.setRenderHint(QPainter.Antialiasing)
41
+ cls = self._classes
42
+ n = len(cls)
43
+ if n == 0:
44
+ return
45
+ lab = 46
46
+ cell = min((self.width() - lab) / n, (self.height() - lab) / n)
47
+ mx = max([v for v in self._m.values()] + [1])
48
+ for r, tru in enumerate(cls):
49
+ p.setPen(QColor(t.muted))
50
+ p.drawText(0, int(lab + r * cell), lab - 4, int(cell),
51
+ Qt.AlignRight | Qt.AlignVCenter, self._tick(tru))
52
+ for c, pred in enumerate(cls):
53
+ v = self._m.get((tru, pred), 0)
54
+ x, y = lab + c * cell, lab + r * cell
55
+ base = t.accent_for("green" if r == c else "red")
56
+ col = QColor(base); col.setAlpha(int(30 + 200 * (v / mx)) if v else 12)
57
+ p.setBrush(col)
58
+ p.setPen(QColor(t.accent if r == c else t.line))
59
+ p.drawRect(int(x), int(y), int(cell) - 2, int(cell) - 2)
60
+ if v:
61
+ p.setPen(QColor(t.text))
62
+ p.drawText(int(x), int(y), int(cell) - 2, int(cell) - 2,
63
+ Qt.AlignCenter, str(v))
64
+ p.save()
65
+ p.translate(lab, lab - 6)
66
+ for c, pred in enumerate(cls):
67
+ p.setPen(QColor(t.muted))
68
+ p.drawText(int(c * cell), -14, int(cell), 14, Qt.AlignCenter, self._tick(pred))
69
+ p.restore()
70
+
71
+
72
+ class DiagnoseGameWidget(QWidget):
73
+ """spec + source()->[Case] + a renderer widget (show_signature(sig)). Reused by every game."""
74
+
75
+ def __init__(self, theme, spec, source, renderer, live=False, deck: int = 10) -> None:
76
+ super().__init__()
77
+ self.theme = theme
78
+ self.spec = spec
79
+ self._source = source
80
+ self._renderer = renderer
81
+ self.live = live
82
+ self._deck = deck
83
+ self._answer = spec.answer
84
+ self._estimate = (spec.answer == "estimate")
85
+ self._dynamic = spec.answer in ("spot", "rank") # buttons/chips rebuilt per case
86
+ self._has_matrix = (spec.answer == "class")
87
+ self._session = DiagnoseSession(spec, source(), mode=PRACTICE, deck=deck)
88
+ self._class_btns: list = []
89
+ self._opt_btns: list = []
90
+ self._opt_by: dict = {}
91
+ self._order: list = []
92
+
93
+ t = theme.theme
94
+ lay = QHBoxLayout(self)
95
+ # left: mystery + controls
96
+ col = QVBoxLayout()
97
+ col.addWidget(self._lbl(spec.prompt, t.accent_for("purple"), 12, True))
98
+ col.addWidget(renderer, 1)
99
+ self._msg = QLabel(spec.prompt)
100
+ self._msg.setWordWrap(True)
101
+ self._msg.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
102
+ col.addWidget(self._msg)
103
+ if self._estimate:
104
+ row = QHBoxLayout(); row.setSpacing(6)
105
+ self._input = QLineEdit()
106
+ self._input.setPlaceholderText("your estimate" + (f" ({spec.unit})" if spec.unit else ""))
107
+ self._input.setStyleSheet(
108
+ f"QLineEdit{{color:{t.text};background:{t.panel};border:1px solid {t.line};"
109
+ "border-radius:7px;padding:5px 9px;}")
110
+ self._input.returnPressed.connect(self._submit_estimate)
111
+ self._submit = QPushButton("Submit"); self._submit.setStyleSheet(self._btn_css())
112
+ self._submit.clicked.connect(self._submit_estimate)
113
+ row.addWidget(self._input, 1); row.addWidget(self._submit)
114
+ col.addLayout(row)
115
+ elif self._dynamic:
116
+ # candidates come from each case; a host widget we clear + refill per mystery
117
+ self._opts_host = QWidget()
118
+ self._opts_row = QHBoxLayout(self._opts_host)
119
+ self._opts_row.setContentsMargins(0, 0, 0, 0); self._opts_row.setSpacing(5)
120
+ col.addWidget(self._opts_host)
121
+ if self._answer == "rank":
122
+ self._order_lbl = QLabel("your order: —")
123
+ self._order_lbl.setStyleSheet(_scss(f"color:{t.text};font-size:12px;"))
124
+ col.addWidget(self._order_lbl)
125
+ self._clear_order_btn = QPushButton("Clear order")
126
+ self._clear_order_btn.setStyleSheet(self._btn_css())
127
+ self._clear_order_btn.clicked.connect(self._rank_clear)
128
+ col.addWidget(self._clear_order_btn, 0, Qt.AlignLeft)
129
+ else: # class: fixed buttons
130
+ btns = QHBoxLayout(); btns.setSpacing(5)
131
+ for cls in spec.classes:
132
+ b = QPushButton(cls); b.setStyleSheet(self._btn_css())
133
+ b.clicked.connect(lambda _c=False, k=cls: self._guess(k))
134
+ btns.addWidget(b); self._class_btns.append(b)
135
+ self._opt_btns = self._class_btns
136
+ col.addLayout(btns)
137
+ mode_row = QHBoxLayout(); mode_row.setSpacing(5)
138
+ mode_row.addWidget(self._lbl("mode:", t.muted, 11))
139
+ self._mode_btns = {}
140
+ for m in (PRACTICE, GRADED):
141
+ b = QPushButton(m); b.setCheckable(True); b.setStyleSheet(self._toggle_css())
142
+ b.clicked.connect(lambda _c=False, mm=m: self._set_mode(mm))
143
+ mode_row.addWidget(b); self._mode_btns[m] = b
144
+ self._mode_btns[PRACTICE].setChecked(True)
145
+ mode_row.addStretch(1)
146
+ col.addLayout(mode_row)
147
+ lay.addLayout(col, 1)
148
+
149
+ # right: score + scoreboard (confusion matrix for class, recent-guesses for estimate)
150
+ right = QVBoxLayout()
151
+ self._score = QLabel("score 0 / 0")
152
+ self._score.setStyleSheet(_scss(f"color:{t.text};font-size:13px;font-weight:600;"))
153
+ right.addWidget(self._score)
154
+ if self._has_matrix:
155
+ right.addWidget(self._lbl("confusion matrix · true (row) × your guess (col)",
156
+ t.muted, 11))
157
+ self._conf = ConfusionGrid(theme)
158
+ right.addWidget(self._conf, 1)
159
+ else:
160
+ right.addWidget(self._lbl("recent · your answer vs actual", t.muted, 11))
161
+ self._recent = QListWidget()
162
+ self._recent.setStyleSheet(
163
+ f"QListWidget{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
164
+ "border-radius:8px;font-family:monospace;font-size:12px;padding:4px;}")
165
+ right.addWidget(self._recent, 1)
166
+ self._conf = None
167
+ self._reset_btn = QPushButton("Reset")
168
+ self._reset_btn.setStyleSheet(self._btn_css())
169
+ self._reset_btn.clicked.connect(self._reset)
170
+ right.addWidget(self._reset_btn, 0, Qt.AlignRight)
171
+ lay.addLayout(right, 1)
172
+
173
+ self._advance = QTimer(self); self._advance.setSingleShot(True)
174
+ self._advance.timeout.connect(self._new_mystery)
175
+ self._new_mystery()
176
+
177
+ # -- styling ----------------------------------------------------------- #
178
+ def _lbl(self, text, color, size=11, bold=False) -> QLabel:
179
+ lbl = QLabel(text)
180
+ lbl.setStyleSheet(_scss(f"color:{color};font-size:{size}px;"
181
+ + ("font-weight:600;" if bold else "")))
182
+ return lbl
183
+
184
+ def _btn_css(self) -> str:
185
+ t = self.theme.theme
186
+ return (f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
187
+ "border-radius:7px;padding:5px 11px;}"
188
+ f"QPushButton:hover{{border-color:{t.accent};}}"
189
+ f"QPushButton:disabled{{color:{t.faint};}}")
190
+
191
+ def _toggle_css(self) -> str:
192
+ t = self.theme.theme
193
+ return (f"QPushButton{{color:{t.muted};background:{t.panel2};border:1px solid {t.line};"
194
+ "border-radius:7px;padding:3px 10px;font-size:11px;}"
195
+ f"QPushButton:checked{{color:{t.text};border-color:{t.accent};}}")
196
+
197
+ # -- flow -------------------------------------------------------------- #
198
+ @staticmethod
199
+ def _clear_layout(layout) -> None:
200
+ while layout.count():
201
+ it = layout.takeAt(0)
202
+ w = it.widget()
203
+ if w is not None:
204
+ w.setParent(None)
205
+
206
+ def _render_options(self, case) -> None:
207
+ """Rebuild the per-case candidate buttons/chips (spot + rank)."""
208
+ self._clear_layout(self._opts_row)
209
+ self._opt_btns = []; self._opt_by = {}; self._order = []
210
+ opts = case.options if case.options else self.spec.classes
211
+ for opt in opts:
212
+ b = QPushButton(str(opt)); b.setStyleSheet(self._btn_css())
213
+ if self._answer == "rank":
214
+ b.clicked.connect(lambda _c=False, o=opt: self._rank_click(o))
215
+ else:
216
+ b.clicked.connect(lambda _c=False, o=opt: self._guess(o))
217
+ self._opts_row.addWidget(b)
218
+ self._opt_btns.append(b); self._opt_by[opt] = b
219
+ self._opts_row.addStretch(1)
220
+ if self._answer == "rank":
221
+ self._order_lbl.setText("your order: —")
222
+
223
+ def _rank_click(self, opt) -> None:
224
+ if opt in self._order:
225
+ return
226
+ self._order.append(opt)
227
+ b = self._opt_by.get(opt)
228
+ if b is not None:
229
+ b.setEnabled(False)
230
+ self._order_lbl.setText("your order: " + " → ".join(str(o) for o in self._order))
231
+ if len(self._order) == len(self._opt_btns): # complete -> submit
232
+ self._guess(list(self._order))
233
+
234
+ def _rank_clear(self) -> None:
235
+ self._order = []
236
+ for b in self._opt_btns:
237
+ b.setEnabled(True)
238
+ self._order_lbl.setText("your order: —")
239
+
240
+ def _set_answer_enabled(self, on: bool) -> None:
241
+ if self._estimate:
242
+ self._input.setEnabled(on); self._submit.setEnabled(on)
243
+ if on:
244
+ self._input.clear(); self._input.setFocus()
245
+ return
246
+ for b in self._opt_btns:
247
+ b.setEnabled(on)
248
+ if self._answer == "rank":
249
+ self._clear_order_btn.setEnabled(on)
250
+
251
+ def _set_mode(self, mode) -> None:
252
+ for m, b in self._mode_btns.items():
253
+ b.setChecked(m == mode)
254
+ self._session = DiagnoseSession(self.spec, self._source(), mode=mode, deck=self._deck)
255
+ self._clear_board()
256
+ self._score.setText("score 0 / 0")
257
+ self._new_mystery()
258
+
259
+ def _new_mystery(self) -> None:
260
+ self._session.set_cases(self._source())
261
+ case = self._session.next()
262
+ if case is not None:
263
+ self._renderer.show_signature(case.signature)
264
+ self._msg.setText(self.spec.prompt)
265
+ if self._dynamic:
266
+ self._render_options(case)
267
+ self._set_answer_enabled(True)
268
+ return
269
+ self._renderer.show_signature(None)
270
+ self._set_answer_enabled(False)
271
+ if self._session.finished:
272
+ hit, tot = self._session.score()
273
+ self._msg.setText(f"Run complete — {hit}/{tot} "
274
+ f"({round(self._session.accuracy() * 100)}%). Reset for a new run.")
275
+ else:
276
+ self._msg.setText("No cases yet — launch some programs to play, or switch to Demo.")
277
+
278
+ def _submit_estimate(self) -> None:
279
+ raw = self._input.text().strip()
280
+ try:
281
+ val = int(raw, 0) # accepts 0x… hex and decimal
282
+ except ValueError:
283
+ try:
284
+ val = float(raw)
285
+ except ValueError:
286
+ self._msg.setText("Enter a number (decimal or 0x… hex).")
287
+ return
288
+ self._guess(val)
289
+
290
+ def _guess(self, answer) -> None:
291
+ v = self._session.guess(answer)
292
+ if not v:
293
+ return
294
+ sub = f" ({v['subtitle']})" if v.get("subtitle") else ""
295
+ hint = f" · hint: {v['hint']}" if v.get("hint") else ""
296
+ if self._has_matrix:
297
+ verdict = "✓ correct" if v["correct"] else f"✗ it was {v['truth']}"
298
+ self._msg.setText(verdict + sub + hint)
299
+ self._conf.set_matrix(self._session.matrix(), self.spec.classes, self.spec.abbrev)
300
+ elif self._answer == "rank":
301
+ pct = round((v.get("partial") or 0) * 100)
302
+ verdict = "✓ perfect order" if v["correct"] else f"✗ ({pct}% ordered)"
303
+ self._msg.setText(f"{verdict} — actual: {' → '.join(map(str, v['truth']))}{hint}")
304
+ self._recent.insertItem(0, f"{'✓' if v['correct'] else f'{pct}%'} "
305
+ f"{' '.join(map(str, answer))}")
306
+ else: # estimate + spot
307
+ verdict = "✓ correct" if v["correct"] else f"✗ it was {v['truth']}"
308
+ self._msg.setText(f"{verdict} — you said {answer}{sub}{hint}")
309
+ self._recent.insertItem(0, f"{'✓' if v['correct'] else '✗'} you {answer} "
310
+ f"actual {v['truth']}")
311
+ self._update_score()
312
+ self._advance.start(950)
313
+
314
+ def _update_score(self) -> None:
315
+ hit, tot = self._session.score()
316
+ txt = f"score {hit} / {tot} · accuracy {round(self._session.accuracy() * 100)}%"
317
+ if self._estimate:
318
+ txt += f" · mean error {round(self._session.mean_abs_error(), 1)}"
319
+ elif self._answer == "rank":
320
+ txt += f" · order {round(self._session.mean_order_score() * 100)}%"
321
+ rem = self._session.remaining()
322
+ if rem is not None:
323
+ txt += f" · {rem} left"
324
+ self._score.setText(txt)
325
+
326
+ def _clear_board(self) -> None:
327
+ if self._has_matrix:
328
+ self._conf.set_matrix({}, self.spec.classes, self.spec.abbrev)
329
+ else:
330
+ self._recent.clear()
331
+
332
+ def _reset(self) -> None:
333
+ self._session.reset()
334
+ self._clear_board()
335
+ self._score.setText("score 0 / 0")
336
+ self._new_mystery()
@@ -0,0 +1,219 @@
1
+ """Process Fingerprints — behavioral-signature view over the Machine Lab's real telemetry.
2
+
3
+ • Explore — a radar (signature) for the selected process + a scatter BOARD where programs self-sort
4
+ into CPU-bound vs IO-bound / compute vs kernel.
5
+ • Classify game — the shared DiagnoseGameWidget (domain/diagnose.py) fed by the process case source:
6
+ a mystery radar with the name hidden; guess the class, scored by a confusion matrix vs the oracle.
7
+
8
+ Live: a FingerprintAccumulator ingests procdump states + the syscall/trap rings each poll. Demo: the
9
+ canned demo_features(), so the panel and the game work fully offline.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from PySide6.QtCore import Qt, QTimer, Signal
14
+ from PySide6.QtWidgets import (
15
+ QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QTabWidget, QVBoxLayout, QWidget,
16
+ )
17
+
18
+ from ..domain.fingerprint import FingerprintAccumulator, demo_features, fingerprint, scatter_xy
19
+ from ..domain.games.process_game import PROCESS_SPEC, demo_cases, live_cases
20
+ from ..domain.xv6 import parse_sctrace, parse_traptrace
21
+ from .diagnose_game import DiagnoseGameWidget
22
+ from .game_renderers import RadarChart, ScatterBoard
23
+ from .theme import ThemeManager, icons
24
+ from .theme.manager import scale_css as _scss
25
+
26
+ _PALETTE = ["blue", "green", "purple", "amber", "teal", "pink", "orange", "indigo", "red", "cyan"]
27
+
28
+
29
+ class FingerprintLab(QWidget):
30
+ refreshed = Signal()
31
+
32
+ def __init__(self, parent, theme: ThemeManager, device, state, live=False) -> None:
33
+ super().__init__(parent)
34
+ self.setWindowFlag(Qt.Window, True)
35
+ self.theme = theme
36
+ self.device = device
37
+ self.state = state
38
+ self.live = live
39
+ self._closed = False
40
+ self._busy = False
41
+ self._acc = FingerprintAccumulator()
42
+ self._seen_sc: set = set()
43
+ self._seen_tr: set = set()
44
+
45
+ t = theme.theme
46
+ self.setWindowTitle(f"Process Fingerprints Lab — {device.name}")
47
+ self.resize(780, 620)
48
+ self.setStyleSheet(f"QWidget{{background:{t.bg};}}")
49
+ root = QVBoxLayout(self)
50
+
51
+ head = QHBoxLayout()
52
+ ic = QLabel(); ic.setPixmap(icons.render_pixmap("metrics", t.accent_for("purple"), 22))
53
+ title = QLabel(f" Process Fingerprints Lab — {device.name}")
54
+ title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
55
+ head.addWidget(ic); head.addWidget(title); head.addStretch(1)
56
+ self._reset_btn = QPushButton("Reset")
57
+ self._reset_btn.setStyleSheet(self._btn_css())
58
+ self._reset_btn.clicked.connect(self._reset)
59
+ head.addWidget(self._reset_btn)
60
+ chip = QLabel("live" if live else "offline demo")
61
+ chip.setStyleSheet(f"color:{t.success if live else t.muted};background:{t.panel2};"
62
+ f"border:1px solid {t.line};border-radius:9px;padding:2px 10px;"
63
+ "font-size:11px;")
64
+ head.addWidget(chip)
65
+ root.addLayout(head)
66
+
67
+ self._tabs = QTabWidget()
68
+ self._tabs.setStyleSheet(
69
+ f"QTabWidget::pane{{border:1px solid {t.line};border-radius:8px;}}"
70
+ f"QTabBar::tab{{background:{t.panel2};color:{t.muted};padding:6px 14px;"
71
+ f"border:1px solid {t.line};border-bottom:none;border-top-left-radius:7px;"
72
+ "border-top-right-radius:7px;margin-right:2px;}"
73
+ f"QTabBar::tab:selected{{color:{t.text};background:{t.panel};}}")
74
+ root.addWidget(self._tabs, 1)
75
+ self._build_explore()
76
+ # the classify game is the shared engine, fed by the process case source (live or demo)
77
+ self._game = DiagnoseGameWidget(theme, PROCESS_SPEC, self._game_source, RadarChart(theme),
78
+ live=self.live)
79
+ self._tabs.addTab(self._game, "Classify game")
80
+
81
+ self.refreshed.connect(self._refresh_views)
82
+ self._poll = QTimer(self); self._poll.timeout.connect(self._tick)
83
+ self._refresh_views()
84
+ if self.live:
85
+ self._poll.start(1200)
86
+
87
+ # -- styling ----------------------------------------------------------- #
88
+ def _btn_css(self) -> str:
89
+ t = self.theme.theme
90
+ return (f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
91
+ "border-radius:7px;padding:4px 12px;}"
92
+ f"QPushButton:hover{{border-color:{t.accent};}}")
93
+
94
+ def _lbl(self, text) -> QLabel:
95
+ lbl = QLabel(text)
96
+ lbl.setStyleSheet(_scss(f"color:{self.theme.theme.muted};font-size:11px;font-weight:600;"))
97
+ return lbl
98
+
99
+ # -- Explore tab ------------------------------------------------------- #
100
+ def _build_explore(self) -> None:
101
+ t = self.theme.theme
102
+ page = QWidget(); lay = QHBoxLayout(page)
103
+ left = QVBoxLayout()
104
+ left.addWidget(self._lbl("processes"))
105
+ self._list = QListWidget()
106
+ self._list.setStyleSheet(
107
+ f"QListWidget{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
108
+ "border-radius:8px;font-size:12px;padding:3px;}"
109
+ f"QListWidget::item:selected{{background:{t.panel2};color:{t.text};}}")
110
+ self._list.setFixedWidth(150)
111
+ self._list.currentRowChanged.connect(lambda _r: self._draw_radar())
112
+ left.addWidget(self._list, 1)
113
+ lay.addLayout(left)
114
+
115
+ mid = QVBoxLayout()
116
+ mid.addWidget(self._lbl("signature · radar"))
117
+ self._radar = RadarChart(self.theme)
118
+ mid.addWidget(self._radar, 1)
119
+ lay.addLayout(mid, 1)
120
+
121
+ right = QVBoxLayout()
122
+ right.addWidget(self._lbl("behavior map · all processes"))
123
+ self._board = ScatterBoard(self.theme)
124
+ right.addWidget(self._board, 1)
125
+ lay.addLayout(right, 1)
126
+ self._tabs.addTab(page, "Explore")
127
+
128
+ # -- data -------------------------------------------------------------- #
129
+ def _tick(self) -> None:
130
+ if self._closed or self._busy:
131
+ return
132
+ import threading
133
+ threading.Thread(target=self._accumulate, daemon=True).start()
134
+
135
+ def _accumulate(self) -> None:
136
+ self._busy = True
137
+ try:
138
+ self.state.refresh()
139
+ procs = self.state.latest.procs if self.state.latest else []
140
+ sc = self._new_events(self._read("sc"), parse_sctrace, self._seen_sc,
141
+ lambda e: (e.pid, e.num, e.a0, e.ret))
142
+ tr = self._new_events(self._read("traps"), parse_traptrace, self._seen_tr,
143
+ lambda e: (e.pid, e.kind, e.epc, e.tval))
144
+ self._acc.observe(procs, sc, tr, dt=1.2)
145
+ if not self._closed:
146
+ self.refreshed.emit()
147
+ except (Exception, RuntimeError):
148
+ pass
149
+ finally:
150
+ self._busy = False
151
+
152
+ def _read(self, method) -> str:
153
+ fn = getattr(self.state.provider, method, None)
154
+ try:
155
+ return fn() if callable(fn) else ""
156
+ except Exception:
157
+ return ""
158
+
159
+ @staticmethod
160
+ def _new_events(text, parse, seen, sig):
161
+ out = []
162
+ for e in parse(text):
163
+ s = sig(e)
164
+ if s not in seen:
165
+ seen.add(s); out.append(e)
166
+ if len(seen) > 4000:
167
+ seen.clear()
168
+ return out
169
+
170
+ def _collect(self) -> dict:
171
+ """{pid: (name, fp)} — accumulator (live) or the canned demo set (offline)."""
172
+ if self.live:
173
+ return {pid: (self._acc.feats[pid].name or f"pid{pid}", fp)
174
+ for pid, fp in self._acc.fingerprints().items()}
175
+ return {f.pid: (f.name, fingerprint(f)) for f in demo_features()}
176
+
177
+ def _game_source(self) -> list:
178
+ """Cases for the classify game — live fingerprints when running, else the demo deck."""
179
+ if self.live:
180
+ fps = self._acc.fingerprints()
181
+ names = {pid: self._acc.feats[pid].name for pid in fps}
182
+ return live_cases(fps, names)
183
+ return demo_cases()
184
+
185
+ def _color(self, i) -> str:
186
+ return _PALETTE[i % len(_PALETTE)]
187
+
188
+ def _refresh_views(self) -> None:
189
+ data = list(self._collect().items())
190
+ cur = self._list.currentRow()
191
+ self._list.blockSignals(True)
192
+ self._list.clear()
193
+ for pid, (name, _fp) in data:
194
+ QListWidgetItem(f"{name} · {pid}", self._list)
195
+ self._list.setCurrentRow(cur if 0 <= cur < len(data) else (0 if data else -1))
196
+ self._list.blockSignals(False)
197
+ self._draw_radar()
198
+ pts = [(*scatter_xy(fp), name, self._color(i))
199
+ for i, (pid, (name, fp)) in enumerate(data)]
200
+ self._board.set_points(pts)
201
+
202
+ def _draw_radar(self) -> None:
203
+ data = list(self._collect().items())
204
+ row = self._list.currentRow()
205
+ if 0 <= row < len(data):
206
+ _pid, (name, fp) = data[row]
207
+ self._radar.set_series([(name, fp, self._color(row))])
208
+ else:
209
+ self._radar.set_series([])
210
+
211
+ def _reset(self) -> None:
212
+ self._acc.reset(); self._seen_sc.clear(); self._seen_tr.clear()
213
+ self._game._reset()
214
+ self._refresh_views()
215
+
216
+ def closeEvent(self, e) -> None: # noqa: N802
217
+ self._closed = True
218
+ self._poll.stop()
219
+ super().closeEvent(e)