inscript-lang 2.11.0__tar.gz → 2.12.0__tar.gz

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 (38) hide show
  1. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/PKG-INFO +1 -1
  2. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/errors.py +18 -0
  3. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript.py +64 -8
  4. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_lang.egg-info/PKG-INFO +1 -1
  5. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_lang.egg-info/SOURCES.txt +3 -0
  6. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_lang.egg-info/top_level.txt +3 -0
  7. inscript_lang-2.12.0/inscript_studio_api.py +295 -0
  8. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/pyproject.toml +2 -1
  9. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/setup.py +1 -0
  10. inscript_lang-2.12.0/studio_bridge.py +239 -0
  11. inscript_lang-2.12.0/studio_readiness.py +408 -0
  12. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/README.md +0 -0
  13. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/analyzer.py +0 -0
  14. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/ast_nodes.py +0 -0
  15. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/compiler.py +0 -0
  16. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/environment.py +0 -0
  17. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/export_pipeline.py +0 -0
  18. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/hot_reload.py +0 -0
  19. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_dap.py +0 -0
  20. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_fmt.py +0 -0
  21. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
  22. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_lang.egg-info/entry_points.txt +0 -0
  23. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_lang.egg-info/requires.txt +0 -0
  24. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/inscript_test.py +0 -0
  25. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/interpreter.py +0 -0
  26. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/lexer.py +0 -0
  27. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/parser.py +0 -0
  28. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/pygame_backend.py +0 -0
  29. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/repl.py +0 -0
  30. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/scene_tree.py +0 -0
  31. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/setup.cfg +0 -0
  32. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/stdlib.py +0 -0
  33. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/stdlib_assets.py +0 -0
  34. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/stdlib_extended.py +0 -0
  35. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/stdlib_extended_2.py +0 -0
  36. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/stdlib_game.py +0 -0
  37. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/stdlib_values.py +0 -0
  38. {inscript_lang-2.11.0 → inscript_lang-2.12.0}/vm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.11.0
3
+ Version: 2.12.0
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -85,6 +85,24 @@ class InScriptError(Exception):
85
85
  self.code = code or ERROR_CODES.get(cls_name, "E0000")
86
86
  super().__init__(self._format())
87
87
 
88
+ def to_dict(self) -> dict:
89
+ """v2.12.0: Structured dict for --json-errors / Studio error panel."""
90
+ return {
91
+ "type": "error",
92
+ "code": self.code,
93
+ "class": type(self).__name__,
94
+ "message": self.message,
95
+ "line": self.line,
96
+ "col": self.col,
97
+ "source_line": self.source_line,
98
+ "hint": self.hint,
99
+ "call_trace": self.call_trace,
100
+ }
101
+
102
+ def to_json(self) -> str:
103
+ import json as _j
104
+ return _j.dumps(self.to_dict(), separators=(",", ":"))
105
+
88
106
  def _format(self) -> str:
89
107
  code = self.code
90
108
  cls = type(self).__name__
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
24
24
  SemanticError, InScriptRuntimeError,
25
25
  MultiError, InScriptWarning)
26
26
 
27
- VERSION = "2.11.0"
27
+ VERSION = "2.12.0"
28
28
 
29
29
  MANIFEST_FILENAME = "inscript.toml"
30
30
  LOCK_FILENAME = "inscript.lock"
@@ -2385,21 +2385,34 @@ def run_source(source: str, filename: str = "<stdin>",
2385
2385
  no_warn_unused: bool = False,
2386
2386
  warn_as_error: bool = False,
2387
2387
  strict: bool = False,
2388
- profile: bool = False) -> int:
2388
+ profile: bool = False,
2389
+ json_errors: bool = False) -> int:
2389
2390
  """Lex, parse, analyze, and interpret InScript source code."""
2391
+
2392
+ def _emit_error(e):
2393
+ """v2.12.0: emit as JSON or plain text depending on flag."""
2394
+ if json_errors:
2395
+ from inscript_studio_api import error_stream
2396
+ from errors import InScriptError as _ISE
2397
+ if isinstance(e, _ISE):
2398
+ error_stream([e])
2399
+ else:
2400
+ error_stream([{"type": "error", "code": "E0000",
2401
+ "class": type(e).__name__, "message": str(e),
2402
+ "line": 0, "col": 0}])
2403
+ else:
2404
+ print(e, file=sys.stderr)
2390
2405
  # ── 1. Lex ────────────────────────────────────────────────────────────
2391
2406
  try:
2392
2407
  tokens = tokenize(source, filename)
2393
2408
  except LexerError as e:
2394
- print(e, file=sys.stderr)
2395
- return 1
2409
+ _emit_error(e); return 1
2396
2410
 
2397
2411
  # ── 2. Parse ──────────────────────────────────────────────────────────
2398
2412
  try:
2399
2413
  program = parse(source, filename)
2400
2414
  except ParseError as e:
2401
- print(e, file=sys.stderr)
2402
- return 1
2415
+ _emit_error(e); return 1
2403
2416
 
2404
2417
  # ── 3. Analyze (optional) — Phase 3: multi-error + warnings ──────────
2405
2418
  if type_check:
@@ -2442,7 +2455,7 @@ def run_source(source: str, filename: str = "<stdin>",
2442
2455
  interp.run(program)
2443
2456
  return 0
2444
2457
  except InScriptError as e:
2445
- print(e, file=sys.stderr)
2458
+ _emit_error(e)
2446
2459
  return 1
2447
2460
  except KeyboardInterrupt:
2448
2461
  print("\n[InScript] Interrupted.", file=sys.stderr)
@@ -2728,6 +2741,17 @@ Examples:
2728
2741
  help="v2.11.0: Project root for --build (default: current dir)")
2729
2742
  parser.add_argument("--new", metavar="NAME",
2730
2743
  help="v2.11.0: Scaffold a new InScript project: inscript --new mygame")
2744
+ # v2.12.0: studio readiness
2745
+ parser.add_argument("--json-errors", action="store_true",
2746
+ help="v2.12.0: Output errors as newline-delimited JSON for IDE panels")
2747
+ parser.add_argument("--inspect", metavar="DIR", nargs="?", const=".",
2748
+ help="v2.12.0: Project introspection → JSON (scenes, nodes, assets, fns)")
2749
+ parser.add_argument("--check-v3", action="store_true",
2750
+ help="v2.12.0: Run v3.0.0 readiness gates; exits 0 if all pass")
2751
+ parser.add_argument("--studio-bridge", action="store_true",
2752
+ help="v2.12.0: Start Electron bridge JSON-RPC server")
2753
+ parser.add_argument("--bridge-port", type=int, default=8765,
2754
+ help="v2.12.0: Port for --studio-bridge (default: 8765)")
2731
2755
  parser.add_argument("--lsp", action="store_true",
2732
2756
  help="Start the Language Server (requires: pip install pygls)")
2733
2757
  parser.add_argument("--game", action="store_true",
@@ -3012,6 +3036,37 @@ Examples:
3012
3036
  except Exception as _bld:
3013
3037
  print(f"[build] {_bld}", file=sys.stderr); return 1
3014
3038
 
3039
+ # ── v2.12.0 studio readiness ───────────────────────────────────────────
3040
+ if getattr(args, 'check_v3', False):
3041
+ from studio_readiness import run_all_gates
3042
+ base = getattr(args, 'project_dir', '.') or '.'
3043
+ _, all_passed = run_all_gates(base_dir=base)
3044
+ return 0 if all_passed else 1
3045
+
3046
+ if getattr(args, 'inspect', None) is not None:
3047
+ try:
3048
+ from inscript_studio_api import project_inspect
3049
+ import json as _j
3050
+ data = project_inspect(args.inspect)
3051
+ print(_j.dumps(data, indent=2))
3052
+ except Exception as _ie:
3053
+ print(f"[inspect] {_ie}", file=sys.stderr); return 1
3054
+ return 0
3055
+
3056
+ if getattr(args, 'studio_bridge', False):
3057
+ from studio_bridge import StudioBridge
3058
+ import time as _time
3059
+ port = getattr(args, 'bridge_port', 8765)
3060
+ bridge = StudioBridge(port=port)
3061
+ bridge.start()
3062
+ print(f"[studio-bridge] Listening on {bridge.url}")
3063
+ print(f"[studio-bridge] Ctrl+C to stop")
3064
+ try:
3065
+ while True: _time.sleep(1)
3066
+ except KeyboardInterrupt:
3067
+ bridge.stop()
3068
+ return 0
3069
+
3015
3070
  # v1.9.1: --no-typecheck is deprecated — emit a warning and honour it
3016
3071
  if getattr(args, 'no_typecheck', False):
3017
3072
  print(
@@ -3097,7 +3152,8 @@ Examples:
3097
3152
  return run_source(_source, filename=args.file, type_check=type_check,
3098
3153
  no_warn=no_warn, no_warn_unused=no_warn_unused,
3099
3154
  warn_as_error=warn_as_error, strict=strict,
3100
- profile=profile)
3155
+ profile=profile,
3156
+ json_errors=getattr(args, 'json_errors', False))
3101
3157
 
3102
3158
 
3103
3159
  # ─────────────────────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript-lang
3
- Version: 2.11.0
3
+ Version: 2.12.0
4
4
  Summary: InScript — a game-focused scripting language with 59 game modules and a bytecode VM
5
5
  Author: Shreyasi Sarkar
6
6
  License: MIT
@@ -9,6 +9,7 @@ hot_reload.py
9
9
  inscript.py
10
10
  inscript_dap.py
11
11
  inscript_fmt.py
12
+ inscript_studio_api.py
12
13
  inscript_test.py
13
14
  interpreter.py
14
15
  lexer.py
@@ -24,6 +25,8 @@ stdlib_extended.py
24
25
  stdlib_extended_2.py
25
26
  stdlib_game.py
26
27
  stdlib_values.py
28
+ studio_bridge.py
29
+ studio_readiness.py
27
30
  vm.py
28
31
  inscript_lang.egg-info/PKG-INFO
29
32
  inscript_lang.egg-info/SOURCES.txt
@@ -8,6 +8,7 @@ hot_reload
8
8
  inscript
9
9
  inscript_dap
10
10
  inscript_fmt
11
+ inscript_studio_api
11
12
  inscript_test
12
13
  interpreter
13
14
  lexer
@@ -21,4 +22,6 @@ stdlib_extended
21
22
  stdlib_extended_2
22
23
  stdlib_game
23
24
  stdlib_values
25
+ studio_bridge
26
+ studio_readiness
24
27
  vm
@@ -0,0 +1,295 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ inscript_studio_api.py — InScript v2.12.0 Studio Plugin API
4
+ ================================================================
5
+ Stable extension points that InScript Studio (Electron) and third-party
6
+ plugins use to integrate with the runtime.
7
+
8
+ StudioPlugin — base class for plugins; override hook methods
9
+ StudioAPI — registry; emit events to all registered plugins
10
+ project_inspect — static analysis → JSON scene/node/fn/asset list
11
+ error_stream — format errors as newline-delimited JSON for the IDE panel
12
+ """
13
+
14
+ from __future__ import annotations
15
+ import os, re, json
16
+ from typing import Any, Dict, List, Optional
17
+
18
+
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ # Plugin base class
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ class StudioPlugin:
24
+ """
25
+ Base class for Studio plugins. Override any hook you care about.
26
+
27
+ Plugins are registered with the global StudioAPI singleton:
28
+ from inscript_studio_api import get_api
29
+ get_api().register(MyPlugin())
30
+ """
31
+ name: str = "unnamed_plugin"
32
+
33
+ def on_scene_save(self, scene_path: str, tree) -> None:
34
+ """Called when a .inscene file is saved from the editor."""
35
+
36
+ def on_asset_import(self, handle) -> None:
37
+ """Called when an AssetHandle is registered in the AssetRegistry."""
38
+
39
+ def on_error(self, error: dict) -> None:
40
+ """Called when the interpreter raises an InScriptError.
41
+ error is a dict matching InScriptError.to_dict()."""
42
+
43
+ def on_breakpoint(self, file: str, line: int, env: dict) -> None:
44
+ """Called when a breakpoint is hit (DAP integration)."""
45
+
46
+ def on_reload(self, result) -> None:
47
+ """Called after every successful hot reload."""
48
+
49
+ def on_build(self, target: str, output_dir: str, success: bool) -> None:
50
+ """Called after inscript build completes."""
51
+
52
+ def __repr__(self):
53
+ return f"<StudioPlugin {self.name}>"
54
+
55
+
56
+ # ─────────────────────────────────────────────────────────────────────────────
57
+ # StudioAPI registry
58
+ # ─────────────────────────────────────────────────────────────────────────────
59
+
60
+ class StudioAPI:
61
+ """
62
+ Central registry for Studio plugins. Emits lifecycle events to all
63
+ registered plugins. Singleton — use get_api() rather than instantiating.
64
+ """
65
+ def __init__(self):
66
+ self._plugins: List[StudioPlugin] = []
67
+
68
+ def register(self, plugin: StudioPlugin) -> "StudioAPI":
69
+ if plugin not in self._plugins:
70
+ self._plugins.append(plugin)
71
+ return self
72
+
73
+ def unregister(self, plugin: StudioPlugin) -> bool:
74
+ if plugin in self._plugins:
75
+ self._plugins.remove(plugin)
76
+ return True
77
+ return False
78
+
79
+ def plugin_count(self) -> int:
80
+ return len(self._plugins)
81
+
82
+ def emit(self, event: str, *args, **kwargs) -> List[Any]:
83
+ """
84
+ Emit an event to all registered plugins.
85
+ Returns list of return values (Nones filtered out).
86
+ """
87
+ results = []
88
+ for plugin in list(self._plugins):
89
+ handler = getattr(plugin, event, None)
90
+ if handler is None:
91
+ continue
92
+ try:
93
+ rv = handler(*args, **kwargs)
94
+ if rv is not None:
95
+ results.append(rv)
96
+ except Exception as e:
97
+ print(f"[StudioAPI] Plugin '{plugin.name}' error in {event}: {e}")
98
+ return results
99
+
100
+ # Convenience emitters matching StudioPlugin hooks
101
+ def emit_scene_save(self, scene_path: str, tree) -> None:
102
+ self.emit("on_scene_save", scene_path, tree)
103
+
104
+ def emit_asset_import(self, handle) -> None:
105
+ self.emit("on_asset_import", handle)
106
+
107
+ def emit_error(self, error: dict) -> None:
108
+ self.emit("on_error", error)
109
+
110
+ def emit_breakpoint(self, file: str, line: int, env: dict) -> None:
111
+ self.emit("on_breakpoint", file, line, env)
112
+
113
+ def emit_reload(self, result) -> None:
114
+ self.emit("on_reload", result)
115
+
116
+ def emit_build(self, target: str, output_dir: str, success: bool) -> None:
117
+ self.emit("on_build", target, output_dir, success)
118
+
119
+ def __repr__(self):
120
+ return f"<StudioAPI plugins={len(self._plugins)}>"
121
+
122
+
123
+ # Module-level singleton
124
+ _GLOBAL_API = StudioAPI()
125
+
126
+ def get_api() -> StudioAPI:
127
+ return _GLOBAL_API
128
+
129
+
130
+ # ─────────────────────────────────────────────────────────────────────────────
131
+ # Project introspection — static analysis without running the interpreter
132
+ # ─────────────────────────────────────────────────────────────────────────────
133
+
134
+ # Patterns for extracting declarations from .ins source (fast, no full parse)
135
+ _RE_NODE = re.compile(r'^\s*node\s+(\w+)\s*\{', re.MULTILINE)
136
+ _RE_SCENE = re.compile(r'^\s*scene\s+(\w+)\s*\{', re.MULTILINE)
137
+ _RE_STRUCT = re.compile(r'^\s*struct\s+(\w+)\s*\{', re.MULTILINE)
138
+ _RE_FN = re.compile(r'^\s*(?:export\s+)?fn\s+(\w+)\s*\(', re.MULTILINE)
139
+ _RE_TEXTURE = re.compile(r'@texture\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
140
+ _RE_SOUND = re.compile(r'@sound\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
141
+ _RE_TILEMAP = re.compile(r'@tilemap\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
142
+ _RE_FONT = re.compile(r'@font\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
143
+ _RE_IMPORT = re.compile(r'^\s*import\s+["\']([^"\']+)["\']', re.MULTILINE)
144
+
145
+
146
+ def _scan_ins_file(file_path: str) -> dict:
147
+ """
148
+ Scan a single .ins file and return a summary dict:
149
+ {file, nodes, scenes, structs, functions, assets, imports}
150
+ """
151
+ try:
152
+ with open(file_path, encoding="utf-8") as f:
153
+ src = f.read()
154
+ except OSError:
155
+ return {}
156
+
157
+ rel = file_path # caller normalises
158
+
159
+ assets = []
160
+ for kind, pat in (("texture", _RE_TEXTURE), ("sound", _RE_SOUND),
161
+ ("tilemap", _RE_TILEMAP), ("font", _RE_FONT)):
162
+ for m in pat.finditer(src):
163
+ assets.append({"type": kind, "path": m.group(1)})
164
+
165
+ return {
166
+ "file": rel,
167
+ "nodes": [m.group(1) for m in _RE_NODE.finditer(src)],
168
+ "scenes": [m.group(1) for m in _RE_SCENE.finditer(src)],
169
+ "structs": [m.group(1) for m in _RE_STRUCT.finditer(src)],
170
+ "functions": [m.group(1) for m in _RE_FN.finditer(src)],
171
+ "assets": assets,
172
+ "imports": [m.group(1) for m in _RE_IMPORT.finditer(src)],
173
+ }
174
+
175
+
176
+ def _scan_inscene_file(file_path: str) -> Optional[dict]:
177
+ """Read a .inscene file and return {file, scene_name, root, nodes}."""
178
+ try:
179
+ with open(file_path, encoding="utf-8") as f:
180
+ content = f.read()
181
+ except OSError:
182
+ return None
183
+
184
+ name_m = re.search(r'^name\s*=\s*"([^"]+)"', content, re.MULTILINE)
185
+ root_m = re.search(r'^root\s*=\s*"([^"]+)"', content, re.MULTILINE)
186
+ nodes = re.findall(r'^type\s*=\s*"([^"]+)"', content, re.MULTILINE)
187
+
188
+ return {
189
+ "file": file_path,
190
+ "scene_name": name_m.group(1) if name_m else "",
191
+ "root": root_m.group(1) if root_m else "",
192
+ "nodes": nodes,
193
+ }
194
+
195
+
196
+ def project_inspect(project_dir: str = ".") -> dict:
197
+ """
198
+ Statically analyse a project directory and return a JSON-serialisable
199
+ summary of all declarations. Used by the Studio scene panel, node
200
+ inspector, and asset browser.
201
+
202
+ Returns:
203
+ {
204
+ "project_dir": str,
205
+ "version": str, # inscript.VERSION
206
+ "sources": [...], # per-file scan results
207
+ "scenes": [...], # merged list of scene names
208
+ "nodes": [...], # merged list of node names
209
+ "structs": [...],
210
+ "functions": [...],
211
+ "assets": [...],
212
+ "scene_files": [...], # .inscene file summaries
213
+ "imports": [...],
214
+ }
215
+ """
216
+ from inscript import VERSION
217
+
218
+ project_dir = os.path.abspath(project_dir)
219
+ sources = []
220
+ scene_files = []
221
+
222
+ all_scenes = []
223
+ all_nodes = []
224
+ all_structs = []
225
+ all_functions = []
226
+ all_assets = []
227
+ all_imports = set()
228
+
229
+ # Walk the project for .ins and .inscene files
230
+ for root, dirs, files in os.walk(project_dir):
231
+ # Skip build output and hidden dirs
232
+ dirs[:] = [d for d in dirs
233
+ if d not in ("build", "__pycache__", "inscript_runtime")
234
+ and not d.startswith(".")]
235
+ for fn in sorted(files):
236
+ full = os.path.join(root, fn)
237
+ rel = os.path.relpath(full, project_dir)
238
+ if fn.endswith(".ins"):
239
+ scan = _scan_ins_file(full)
240
+ scan["file"] = rel
241
+ sources.append(scan)
242
+ all_scenes += [{"name": n, "file": rel} for n in scan["nodes"]]
243
+ all_nodes += [{"name": n, "file": rel} for n in scan["nodes"]]
244
+ all_structs += [{"name": n, "file": rel} for n in scan["structs"]]
245
+ all_functions += [{"name": n, "file": rel} for n in scan["functions"]]
246
+ all_assets += [dict(a, file=rel) for a in scan["assets"]]
247
+ all_imports |= set(scan["imports"])
248
+ elif fn.endswith(".inscene"):
249
+ sf = _scan_inscene_file(full)
250
+ if sf:
251
+ sf["file"] = rel
252
+ scene_files.append(sf)
253
+ all_scenes.append({"name": sf["scene_name"], "file": rel,
254
+ "root": sf["root"]})
255
+
256
+ return {
257
+ "project_dir": project_dir,
258
+ "version": VERSION,
259
+ "sources": sources,
260
+ "scenes": all_scenes,
261
+ "nodes": all_nodes,
262
+ "structs": all_structs,
263
+ "functions": all_functions,
264
+ "assets": all_assets,
265
+ "scene_files": scene_files,
266
+ "imports": sorted(all_imports),
267
+ }
268
+
269
+
270
+ # ─────────────────────────────────────────────────────────────────────────────
271
+ # Error stream — newline-delimited JSON for IDE panels
272
+ # ─────────────────────────────────────────────────────────────────────────────
273
+
274
+ def error_stream(errors: list, file=None) -> None:
275
+ """
276
+ Write errors as newline-delimited JSON to `file` (default: sys.stderr).
277
+ Each line is a self-contained JSON object.
278
+
279
+ Format:
280
+ {"type":"error","code":"E0042","message":"...","line":5,"col":3}
281
+ {"type":"error","code":"E0001","message":"...","line":12,"col":1}
282
+ """
283
+ import sys as _sys
284
+ out = file or _sys.stderr
285
+ for err in errors:
286
+ if hasattr(err, "to_dict"):
287
+ d = err.to_dict()
288
+ elif isinstance(err, dict):
289
+ d = err
290
+ else:
291
+ d = {"type": "error", "code": "E0000",
292
+ "class": type(err).__name__, "message": str(err),
293
+ "line": 0, "col": 0}
294
+ out.write(json.dumps(d, separators=(",", ":")) + "\n")
295
+ out.flush()
@@ -61,7 +61,8 @@ py-modules = [
61
61
  # ── Backends ──────────────────────────────────────────────────────────────
62
62
  "pygame_backend",
63
63
  # ── v2.7.0+ runtime modules ───────────────────────────────────────────────
64
- "scene_tree", "hot_reload", "export_pipeline"
64
+ "scene_tree", "hot_reload", "export_pipeline",
65
+ "studio_bridge", "inscript_studio_api", "studio_readiness"
65
66
  ]
66
67
 
67
68
  [tool.setuptools.package-data]
@@ -56,6 +56,7 @@ setup(
56
56
  "pygame_backend",
57
57
  # ── v2.7.0+ runtime modules ───────────────────────────────────────────
58
58
  "scene_tree", "hot_reload", "export_pipeline",
59
+ "studio_bridge", "inscript_studio_api", "studio_readiness",
59
60
  ],
60
61
  package_data = {"": ["examples/*.ins", "lsp/*.py", "*.md"]},
61
62
  install_requires = [],
@@ -0,0 +1,239 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ studio_bridge.py — InScript v2.12.0 Electron Bridge
4
+ ================================================================
5
+ A lightweight JSON-RPC HTTP server that the InScript Studio Electron
6
+ shell calls to drive the runtime.
7
+
8
+ Start from CLI:
9
+ inscript --studio-bridge [--bridge-port 8765]
10
+
11
+ Or from Python:
12
+ from studio_bridge import StudioBridge
13
+ bridge = StudioBridge(port=8765)
14
+ bridge.start() # non-blocking daemon thread
15
+ bridge.stop()
16
+
17
+ JSON-RPC protocol (HTTP POST to /rpc):
18
+ Request: { "method": "run", "id": 1, "params": {"file": "src/main.ins"} }
19
+ Response: { "result": "ok", "id": 1 }
20
+ Error: { "error": "...", "id": 1 }
21
+
22
+ Methods
23
+ ───────
24
+ run — execute a .ins file; returns {"status": "ok"|"error", "output": [...]}
25
+ stop — stop a running interpreter
26
+ hot_reload — trigger hot reload on a file
27
+ scene_list — return all scene/node declarations in project
28
+ inspect — full project introspection (scenes, nodes, fns, assets)
29
+ eval — evaluate an InScript expression in live env
30
+ ping — health check; returns {"pong": true}
31
+ version — returns {"version": "2.12.0"}
32
+ """
33
+
34
+ from __future__ import annotations
35
+ import json, threading, sys, os, io
36
+ from http.server import BaseHTTPRequestHandler, HTTPServer
37
+ from typing import Any, Dict, Optional
38
+
39
+ DEFAULT_PORT = 8765
40
+
41
+
42
+ class _RpcHandler(BaseHTTPRequestHandler):
43
+ """HTTP handler — all requests are POST to /rpc."""
44
+
45
+ def log_message(self, fmt, *args):
46
+ pass # suppress default access log
47
+
48
+ def do_POST(self):
49
+ if self.path != "/rpc":
50
+ self._reply(404, {"error": "Not found — POST to /rpc"})
51
+ return
52
+ try:
53
+ length = int(self.headers.get("Content-Length", 0))
54
+ body = self.rfile.read(length)
55
+ request = json.loads(body)
56
+ except Exception as e:
57
+ self._reply(400, {"error": f"Bad JSON: {e}", "id": None})
58
+ return
59
+
60
+ req_id = request.get("id")
61
+ method = request.get("method", "")
62
+ params = request.get("params", {})
63
+
64
+ try:
65
+ result = self.server._bridge._dispatch(method, params)
66
+ self._reply(200, {"result": result, "id": req_id})
67
+ except Exception as e:
68
+ self._reply(200, {"error": str(e), "id": req_id})
69
+
70
+ def _reply(self, status: int, data: dict):
71
+ body = json.dumps(data, default=str).encode()
72
+ self.send_response(status)
73
+ self.send_header("Content-Type", "application/json")
74
+ self.send_header("Content-Length", str(len(body)))
75
+ self.send_header("Access-Control-Allow-Origin", "*")
76
+ self.end_headers()
77
+ self.wfile.write(body)
78
+
79
+
80
+ class StudioBridge:
81
+ """
82
+ Lightweight JSON-RPC bridge between the InScript runtime and
83
+ the Electron Studio shell.
84
+ """
85
+ def __init__(self, port: int = DEFAULT_PORT):
86
+ self._port = port
87
+ self._server: Optional[HTTPServer] = None
88
+ self._thread: Optional[threading.Thread] = None
89
+ self._interp = None # live Interpreter (created on first run)
90
+ self._lock = threading.Lock()
91
+ self._running = False
92
+
93
+ # ── lifecycle ──────────────────────────────────────────────────────────────
94
+
95
+ def start(self) -> "StudioBridge":
96
+ """Start the HTTP server in a daemon thread. Non-blocking."""
97
+ self._server = HTTPServer(("localhost", self._port), _RpcHandler)
98
+ self._server._bridge = self
99
+ self._running = True
100
+ self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
101
+ self._thread.start()
102
+ return self
103
+
104
+ def stop(self):
105
+ """Shut down the HTTP server."""
106
+ self._running = False
107
+ if self._server:
108
+ self._server.shutdown()
109
+
110
+ @property
111
+ def port(self) -> int:
112
+ return self._port
113
+
114
+ @property
115
+ def url(self) -> str:
116
+ return f"http://localhost:{self._port}/rpc"
117
+
118
+ # ── RPC dispatch ──────────────────────────────────────────────────────────
119
+
120
+ def _dispatch(self, method: str, params: dict) -> Any:
121
+ handlers = {
122
+ "ping": self._rpc_ping,
123
+ "version": self._rpc_version,
124
+ "run": self._rpc_run,
125
+ "stop": self._rpc_stop,
126
+ "hot_reload": self._rpc_hot_reload,
127
+ "scene_list": self._rpc_scene_list,
128
+ "inspect": self._rpc_inspect,
129
+ "eval": self._rpc_eval,
130
+ }
131
+ handler = handlers.get(method)
132
+ if handler is None:
133
+ raise ValueError(f"Unknown method '{method}'. Available: {sorted(handlers)}")
134
+ return handler(params)
135
+
136
+ # ── RPC handlers ──────────────────────────────────────────────────────────
137
+
138
+ def _rpc_ping(self, _params: dict) -> dict:
139
+ return {"pong": True, "port": self._port}
140
+
141
+ def _rpc_version(self, _params: dict) -> dict:
142
+ from inscript import VERSION
143
+ return {"version": VERSION, "bridge": "1.0"}
144
+
145
+ def _rpc_run(self, params: dict) -> dict:
146
+ """Run a .ins file and return captured output + errors."""
147
+ file_path = params.get("file", "")
148
+ if not file_path:
149
+ raise ValueError("'file' parameter required")
150
+ if not os.path.isfile(file_path):
151
+ raise FileNotFoundError(f"File not found: {file_path}")
152
+
153
+ with open(file_path, encoding="utf-8") as f:
154
+ source = f.read()
155
+
156
+ output_lines: list = []
157
+ errors: list = []
158
+
159
+ # Redirect stdout
160
+ old_stdout = sys.stdout
161
+ sys.stdout = io.StringIO()
162
+
163
+ try:
164
+ from interpreter import Interpreter
165
+ from errors import InScriptError
166
+ with self._lock:
167
+ self._interp = Interpreter()
168
+ self._interp.execute(source)
169
+ output_lines = sys.stdout.getvalue().splitlines()
170
+ except Exception as e:
171
+ from errors import InScriptError
172
+ if isinstance(e, InScriptError):
173
+ errors.append(e.to_dict())
174
+ else:
175
+ errors.append({"type": "error", "code": "E0000",
176
+ "class": type(e).__name__, "message": str(e),
177
+ "line": 0, "col": 0})
178
+ output_lines = sys.stdout.getvalue().splitlines()
179
+ finally:
180
+ sys.stdout = old_stdout
181
+
182
+ return {
183
+ "status": "error" if errors else "ok",
184
+ "output": output_lines,
185
+ "errors": errors,
186
+ }
187
+
188
+ def _rpc_stop(self, _params: dict) -> dict:
189
+ with self._lock:
190
+ self._interp = None
191
+ return {"stopped": True}
192
+
193
+ def _rpc_hot_reload(self, params: dict) -> dict:
194
+ """Trigger a hot reload on a running file."""
195
+ file_path = params.get("file", "")
196
+ if not file_path:
197
+ raise ValueError("'file' parameter required")
198
+ with self._lock:
199
+ if self._interp is None:
200
+ raise RuntimeError("No running interpreter — call 'run' first")
201
+ from hot_reload import HotReloader
202
+ reloader = HotReloader(file_path, self._interp)
203
+ result = reloader.tick()
204
+ return {
205
+ "changed": result.changed,
206
+ "success": result.success,
207
+ "patched": result.patched,
208
+ "restored": len(result.restored),
209
+ "errors": result.errors,
210
+ "elapsed_ms": result.elapsed_ms,
211
+ }
212
+
213
+ def _rpc_scene_list(self, params: dict) -> dict:
214
+ """Return all scene and node declarations in a project directory."""
215
+ project_dir = params.get("dir", ".")
216
+ from inscript_studio_api import project_inspect
217
+ data = project_inspect(project_dir)
218
+ return {"scenes": data["scenes"], "nodes": data["nodes"]}
219
+
220
+ def _rpc_inspect(self, params: dict) -> dict:
221
+ """Full project introspection."""
222
+ project_dir = params.get("dir", ".")
223
+ from inscript_studio_api import project_inspect
224
+ return project_inspect(project_dir)
225
+
226
+ def _rpc_eval(self, params: dict) -> dict:
227
+ """Evaluate an InScript expression in the live interpreter environment."""
228
+ expr = params.get("expr", "")
229
+ if not expr:
230
+ raise ValueError("'expr' parameter required")
231
+ with self._lock:
232
+ if self._interp is None:
233
+ raise RuntimeError("No running interpreter — call 'run' first")
234
+ result = self._interp.execute(expr)
235
+ return {"result": result, "type": type(result).__name__}
236
+
237
+ def __repr__(self):
238
+ status = "running" if self._running else "stopped"
239
+ return f"<StudioBridge port={self._port} {status}>"
@@ -0,0 +1,408 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ studio_readiness.py — InScript v2.12.0 Studio Readiness Gate
4
+ ================================================================
5
+ `inscript --check-v3` runs all seven readiness gates and exits 0
6
+ only when every gate passes. This is the final checkpoint before
7
+ v3.0.0 InScript Studio development can safely begin.
8
+
9
+ Gates
10
+ ─────
11
+ G1 60fps performance gate — 1000 NodeInstance._update calls in < 33ms
12
+ G2 Memory gate — < 2MB growth over 5000 game-loop iterations
13
+ G3 Language stability audit — all CI test files found and importable
14
+ G4 Error JSON stream — InScriptError.to_dict() / to_json() work
15
+ G5 Electron bridge — StudioBridge starts + responds to ping
16
+ G6 Plugin API — StudioAPI register/emit round-trip
17
+ G7 Project introspection — project_inspect returns well-formed JSON
18
+
19
+ If all pass, prints a v3.0.0 green-light banner.
20
+ """
21
+
22
+ from __future__ import annotations
23
+ import sys, os, time, tracemalloc, json, threading
24
+ from typing import List, Tuple
25
+
26
+ GATE_COUNT = 7
27
+
28
+ # ─────────────────────────────────────────────────────────────────────────────
29
+ # GateResult
30
+ # ─────────────────────────────────────────────────────────────────────────────
31
+
32
+ class GateResult:
33
+ def __init__(self, gate_id: int, name: str, passed: bool,
34
+ detail: str = "", elapsed_ms: float = 0.0):
35
+ self.gate_id = gate_id
36
+ self.name = name
37
+ self.passed = passed
38
+ self.detail = detail
39
+ self.elapsed_ms = elapsed_ms
40
+
41
+ def __repr__(self):
42
+ icon = "✅" if self.passed else "❌"
43
+ ms = f" ({self.elapsed_ms:.1f}ms)" if self.elapsed_ms else ""
44
+ return f" {icon} G{self.gate_id}: {self.name}{ms} {self.detail}"
45
+
46
+
47
+ # ─────────────────────────────────────────────────────────────────────────────
48
+ # G1 — 60fps performance gate
49
+ # ─────────────────────────────────────────────────────────────────────────────
50
+
51
+ # Threshold: 1000 NodeInstance._update calls must complete in under 33ms
52
+ # (= 30fps minimum; any modern machine will be well above 60fps)
53
+ PERF_THRESHOLD_MS = 33.0
54
+ PERF_NODE_COUNT = 1000
55
+
56
+ def gate_performance() -> GateResult:
57
+ """
58
+ Instantiate PERF_NODE_COUNT NodeInstances each with an _update(dt) hook
59
+ and measure how long one full update pass takes.
60
+ """
61
+ t0 = time.perf_counter()
62
+ try:
63
+ from interpreter import Interpreter
64
+ from scene_tree import NodeBlueprint, NodeInstance
65
+
66
+ src = "node PerfNode { let c: int = 0\n_update(dt) { c = c + 1 } }"
67
+ interp = Interpreter()
68
+ interp.execute(src)
69
+ bp = interp._globals._store.get("PerfNode")
70
+ if bp is None:
71
+ return GateResult(1, "60fps performance gate", False,
72
+ "PerfNode blueprint not found", 0.0)
73
+
74
+ # Create PERF_NODE_COUNT instances
75
+ instances = [bp.instantiate(f"n{i}") for i in range(PERF_NODE_COUNT)]
76
+
77
+ # Time one full update pass
78
+ t_start = time.perf_counter()
79
+ for inst in instances:
80
+ inst.call_update(0.016)
81
+ t_end = time.perf_counter()
82
+ elapsed = (t_end - t_start) * 1000
83
+
84
+ fps_equiv = 1000.0 / elapsed if elapsed > 0 else float("inf")
85
+ detail = (f"{PERF_NODE_COUNT} nodes updated in {elapsed:.1f}ms "
86
+ f"(≈{fps_equiv:.0f}fps equiv)")
87
+ passed = elapsed < PERF_THRESHOLD_MS
88
+ if not passed:
89
+ detail += f" — SLOW (threshold {PERF_THRESHOLD_MS}ms)"
90
+ return GateResult(1, "60fps performance gate", passed, detail,
91
+ (time.perf_counter() - t0) * 1000)
92
+ except Exception as e:
93
+ return GateResult(1, "60fps performance gate", False, str(e),
94
+ (time.perf_counter() - t0) * 1000)
95
+
96
+
97
+ # ─────────────────────────────────────────────────────────────────────────────
98
+ # G2 — Memory gate
99
+ # ─────────────────────────────────────────────────────────────────────────────
100
+
101
+ MEMORY_ITERATIONS = 5000
102
+ MEMORY_LIMIT_BYTES = 2 * 1024 * 1024 # 2 MB
103
+
104
+ def gate_memory() -> GateResult:
105
+ """
106
+ Run MEMORY_ITERATIONS of a minimal game loop and measure growth via tracemalloc.
107
+ A leaky interpreter would show unbounded growth.
108
+ """
109
+ t0 = time.perf_counter()
110
+ try:
111
+ from interpreter import Interpreter
112
+
113
+ interp = Interpreter()
114
+ interp.execute("let tick: int = 0\nfn update(dt) { tick = tick + 1 }")
115
+ update_fn = interp._globals._store.get("update")
116
+
117
+ tracemalloc.start()
118
+ snapshot_before = tracemalloc.take_snapshot()
119
+
120
+ for _ in range(MEMORY_ITERATIONS):
121
+ interp._call_fn(update_fn, [0.016])
122
+
123
+ snapshot_after = tracemalloc.take_snapshot()
124
+ tracemalloc.stop()
125
+
126
+ stats = snapshot_after.compare_to(snapshot_before, "lineno")
127
+ growth = sum(s.size_diff for s in stats if s.size_diff > 0)
128
+
129
+ detail = f"{growth / 1024:.1f}KB growth over {MEMORY_ITERATIONS} iterations"
130
+ passed = growth < MEMORY_LIMIT_BYTES
131
+ if not passed:
132
+ detail += f" — LEAK (limit {MEMORY_LIMIT_BYTES // 1024}KB)"
133
+ return GateResult(2, "Memory gate", passed, detail,
134
+ (time.perf_counter() - t0) * 1000)
135
+ except Exception as e:
136
+ return GateResult(2, "Memory gate", False, str(e),
137
+ (time.perf_counter() - t0) * 1000)
138
+
139
+
140
+ # ─────────────────────────────────────────────────────────────────────────────
141
+ # G3 — Language stability audit
142
+ # ─────────────────────────────────────────────────────────────────────────────
143
+
144
+ REQUIRED_TEST_FILES = [
145
+ "test_v260.py", "test_v270.py", "test_v280.py",
146
+ "test_v290.py", "test_v2100.py", "test_v2110.py",
147
+ ]
148
+
149
+ def gate_stability(base_dir: str = ".") -> GateResult:
150
+ """
151
+ Verify that all required CI test files exist and are importable.
152
+ Full execution is handled by the publish.yml workflow.
153
+ """
154
+ t0 = time.perf_counter()
155
+ missing = []
156
+ for fn in REQUIRED_TEST_FILES:
157
+ if not os.path.isfile(os.path.join(base_dir, fn)):
158
+ missing.append(fn)
159
+
160
+ if missing:
161
+ detail = f"Missing test files: {missing}"
162
+ return GateResult(3, "Language stability audit", False, detail,
163
+ (time.perf_counter() - t0) * 1000)
164
+
165
+ # Quick import check
166
+ import_errors = []
167
+ for fn in ["lexer", "parser", "interpreter", "scene_tree",
168
+ "hot_reload", "stdlib_assets", "export_pipeline",
169
+ "studio_bridge", "inscript_studio_api"]:
170
+ try:
171
+ __import__(fn)
172
+ except Exception as e:
173
+ import_errors.append(f"{fn}: {e}")
174
+
175
+ passed = not missing and not import_errors
176
+ detail = (f"All {len(REQUIRED_TEST_FILES)} test files present; "
177
+ f"{len(import_errors)} import error(s)")
178
+ if import_errors:
179
+ detail += f": {import_errors}"
180
+ return GateResult(3, "Language stability audit", passed, detail,
181
+ (time.perf_counter() - t0) * 1000)
182
+
183
+
184
+ # ─────────────────────────────────────────────────────────────────────────────
185
+ # G4 — Error JSON stream
186
+ # ─────────────────────────────────────────────────────────────────────────────
187
+
188
+ def gate_error_json() -> GateResult:
189
+ """Verify InScriptError.to_dict() and to_json() produce valid JSON."""
190
+ t0 = time.perf_counter()
191
+ try:
192
+ from errors import NameError_ as _NE
193
+
194
+ # Raise a real InScript error and check serialisation
195
+ err = _NE("Test variable not found", line=5, col=3,
196
+ source_line="let x = undefined_var", hint="Did you mean 'x'?")
197
+ d = err.to_dict()
198
+
199
+ # Validate required fields
200
+ required = {"type", "code", "class", "message", "line", "col"}
201
+ missing = required - set(d.keys())
202
+ if missing:
203
+ return GateResult(4, "Error JSON stream", False,
204
+ f"Missing keys: {missing}",
205
+ (time.perf_counter() - t0) * 1000)
206
+
207
+ # Validate to_json() is valid JSON
208
+ raw = err.to_json()
209
+ reparsed = json.loads(raw)
210
+
211
+ checks = [
212
+ d["type"] == "error",
213
+ d["line"] == 5,
214
+ d["col"] == 3,
215
+ d["hint"] == "Did you mean 'x'?",
216
+ reparsed["code"] == d["code"],
217
+ ]
218
+ passed = all(checks)
219
+ detail = f"to_dict() has {len(d)} keys; to_json() round-trips OK"
220
+ return GateResult(4, "Error JSON stream", passed, detail,
221
+ (time.perf_counter() - t0) * 1000)
222
+ except Exception as e:
223
+ return GateResult(4, "Error JSON stream", False, str(e),
224
+ (time.perf_counter() - t0) * 1000)
225
+
226
+
227
+ # ─────────────────────────────────────────────────────────────────────────────
228
+ # G5 — Electron bridge
229
+ # ─────────────────────────────────────────────────────────────────────────────
230
+
231
+ BRIDGE_TEST_PORT = 18999
232
+
233
+ def gate_bridge() -> GateResult:
234
+ """Start StudioBridge on a test port and verify ping round-trip."""
235
+ t0 = time.perf_counter()
236
+ try:
237
+ import urllib.request as _ur
238
+ from studio_bridge import StudioBridge
239
+
240
+ bridge = StudioBridge(port=BRIDGE_TEST_PORT)
241
+ bridge.start()
242
+ time.sleep(0.1) # allow server thread to start
243
+
244
+ req = _ur.Request(
245
+ bridge.url,
246
+ data=json.dumps({"method": "ping", "id": 1}).encode(),
247
+ headers={"Content-Type": "application/json"},
248
+ )
249
+ resp = _ur.urlopen(req, timeout=3)
250
+ body = json.loads(resp.read().decode())
251
+ bridge.stop()
252
+
253
+ passed = body.get("result", {}).get("pong") is True
254
+ detail = f"Ping response: {body.get('result')}"
255
+ return GateResult(5, "Electron bridge (ping)", passed, detail,
256
+ (time.perf_counter() - t0) * 1000)
257
+ except Exception as e:
258
+ return GateResult(5, "Electron bridge (ping)", False, str(e),
259
+ (time.perf_counter() - t0) * 1000)
260
+
261
+
262
+ # ─────────────────────────────────────────────────────────────────────────────
263
+ # G6 — Plugin API
264
+ # ─────────────────────────────────────────────────────────────────────────────
265
+
266
+ def gate_plugin_api() -> GateResult:
267
+ """Register a plugin, emit events, verify it received them."""
268
+ t0 = time.perf_counter()
269
+ try:
270
+ from inscript_studio_api import StudioAPI, StudioPlugin
271
+
272
+ received: list = []
273
+
274
+ class _TestPlugin(StudioPlugin):
275
+ name = "test_plugin"
276
+ def on_error(self, error): received.append(("error", error))
277
+ def on_reload(self, result): received.append(("reload", result))
278
+ def on_build(self, t, d, s): received.append(("build", t, s))
279
+
280
+ api = StudioAPI()
281
+ p = _TestPlugin()
282
+ api.register(p)
283
+
284
+ api.emit_error({"code": "E0042", "message": "test"})
285
+ api.emit_reload({"success": True})
286
+ api.emit_build("desktop", "/out", True)
287
+
288
+ checks = [
289
+ api.plugin_count() == 1,
290
+ len(received) == 3,
291
+ received[0][0] == "error",
292
+ received[1][0] == "reload",
293
+ received[2][0] == "build",
294
+ ]
295
+ api.unregister(p)
296
+ checks.append(api.plugin_count() == 0)
297
+
298
+ passed = all(checks)
299
+ detail = f"3/3 events received; register/unregister OK"
300
+ return GateResult(6, "Plugin API", passed, detail,
301
+ (time.perf_counter() - t0) * 1000)
302
+ except Exception as e:
303
+ return GateResult(6, "Plugin API", False, str(e),
304
+ (time.perf_counter() - t0) * 1000)
305
+
306
+
307
+ # ─────────────────────────────────────────────────────────────────────────────
308
+ # G7 — Project introspection
309
+ # ─────────────────────────────────────────────────────────────────────────────
310
+
311
+ def gate_introspection(base_dir: str = ".") -> GateResult:
312
+ """Run project_inspect on a minimal temp project and verify JSON output."""
313
+ t0 = time.perf_counter()
314
+ try:
315
+ import tempfile, shutil
316
+ from inscript_studio_api import project_inspect
317
+
318
+ tmp = tempfile.mkdtemp(prefix="inscript_gate7_")
319
+ try:
320
+ os.makedirs(os.path.join(tmp, "src"))
321
+ os.makedirs(os.path.join(tmp, "scenes"))
322
+ with open(os.path.join(tmp, "src", "main.ins"), "w") as f:
323
+ f.write('node Player { let hp: int = 100\n_update(dt) { } }\n'
324
+ 'fn spawn(x, y) { }\n'
325
+ '@texture("sprites/hero.png")\nlet hero_tex\n')
326
+ with open(os.path.join(tmp, "scenes", "level1.inscene"), "w") as f:
327
+ f.write('[scene]\nname = "level1"\nroot = "Player"\n')
328
+
329
+ data = project_inspect(tmp)
330
+
331
+ # Validate structure
332
+ required_keys = {"project_dir", "version", "sources", "scenes",
333
+ "nodes", "functions", "assets", "scene_files"}
334
+ missing = required_keys - set(data.keys())
335
+ if missing:
336
+ return GateResult(7, "Project introspection", False,
337
+ f"Missing keys: {missing}",
338
+ (time.perf_counter() - t0) * 1000)
339
+
340
+ # Validate JSON-serialisable
341
+ json.dumps(data)
342
+
343
+ has_player = any(n["name"] == "Player" for n in data["nodes"])
344
+ has_spawn = any(f["name"] == "spawn" for f in data["functions"])
345
+ has_texture = any(a["type"] == "texture" for a in data["assets"])
346
+ has_scene = any(s.get("scene_name") == "level1" for s in data["scene_files"])
347
+
348
+ checks = [has_player, has_spawn, has_texture, has_scene]
349
+ passed = all(checks)
350
+ detail = (f"nodes={len(data['nodes'])} fns={len(data['functions'])} "
351
+ f"assets={len(data['assets'])} scene_files={len(data['scene_files'])}")
352
+ return GateResult(7, "Project introspection", passed, detail,
353
+ (time.perf_counter() - t0) * 1000)
354
+ finally:
355
+ shutil.rmtree(tmp, ignore_errors=True)
356
+ except Exception as e:
357
+ return GateResult(7, "Project introspection", False, str(e),
358
+ (time.perf_counter() - t0) * 1000)
359
+
360
+
361
+ # ─────────────────────────────────────────────────────────────────────────────
362
+ # Main gate runner
363
+ # ─────────────────────────────────────────────────────────────────────────────
364
+
365
+ def run_all_gates(base_dir: str = ".") -> Tuple[List[GateResult], bool]:
366
+ """Run all 7 gates. Returns (results, all_passed)."""
367
+ print("\n╔══════════════════════════════════════════════════════════╗")
368
+ print("║ InScript v3.0.0 Readiness Gate (inscript check-v3) ║")
369
+ print("╚══════════════════════════════════════════════════════════╝\n")
370
+
371
+ runners = [
372
+ lambda: gate_performance(),
373
+ lambda: gate_memory(),
374
+ lambda: gate_stability(base_dir),
375
+ lambda: gate_error_json(),
376
+ lambda: gate_bridge(),
377
+ lambda: gate_plugin_api(),
378
+ lambda: gate_introspection(base_dir),
379
+ ]
380
+
381
+ results = []
382
+ for runner in runners:
383
+ r = runner()
384
+ print(repr(r))
385
+ results.append(r)
386
+
387
+ passed_count = sum(1 for r in results if r.passed)
388
+ all_passed = passed_count == GATE_COUNT
389
+
390
+ print(f"\n{'─'*60}")
391
+ print(f"Gates passed: {passed_count}/{GATE_COUNT}")
392
+
393
+ if all_passed:
394
+ print("""
395
+ ╔══════════════════════════════════════════════════════════╗
396
+ ║ 🟢 ALL GATES PASS — v3.0.0 InScript Studio is GO! ║
397
+ ║ ║
398
+ ║ Scene system, asset pipeline, hot reload, physics, ║
399
+ ║ multiplayer, export pipeline, and Studio bridge are ║
400
+ ║ all in place. Begin v3.0.0 development. ║
401
+ ╚══════════════════════════════════════════════════════════╝""")
402
+ else:
403
+ failed = [r for r in results if not r.passed]
404
+ print(f"\n🔴 {len(failed)} gate(s) failed — NOT ready for v3.0.0:")
405
+ for r in failed:
406
+ print(f" G{r.gate_id}: {r.name} — {r.detail}")
407
+
408
+ return results, all_passed
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes