From cdf949877ac1d8a803a5d3b846a46393c43fa76c Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:07:15 -0700 Subject: [PATCH] fix(contracts): generator emits prettier-style TS directly (no Node in the Python CI lane) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The staleness test regenerates in the Python lane, which has no node_modules; prettier-dependent output would make the check pass locally and fail in CI (or the reverse). Single-quoted literals, bare identifier keys, no trailing commas or whitespace — prettier --check is clean on the committed file. --- apps/shared/src/gateway-contract.generated.ts | 52 +++++++------- scripts/gen_gateway_contracts.py | 70 +++++++++---------- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/apps/shared/src/gateway-contract.generated.ts b/apps/shared/src/gateway-contract.generated.ts index af22fe41b2..c19d4f3f10 100644 --- a/apps/shared/src/gateway-contract.generated.ts +++ b/apps/shared/src/gateway-contract.generated.ts @@ -515,15 +515,15 @@ export interface ConfigSetResult { } /** ``tui_gateway/server.py::_session_info`` — the ``session.info`` event and the ``info`` field of ``session.create`` / ``session.resume`` / ``session.activate`` results. */ export interface SessionLiveInfo { - model?: string + model: string provider?: string reasoning_effort?: string service_tier?: string fast?: boolean yolo?: boolean approval_mode?: string - tools?: Record - skills?: Record + tools: Record + skills: Record cwd?: string branch?: string | null project?: ProjectRef | null @@ -2416,16 +2416,17 @@ export interface SessionCreateResult { messages: TranscriptMessage[] info: SessionLiveInfo } -/** One stored transcript row as ``session.history`` / resume ``messages`` deliver it. */ +/** One transcript row as the gateway PROJECTS it for renderers (``session_history._project_history``): ``text`` (never ``content``), display-only ``timestamp`` / ``display_kind`` / ``display_metadata``, the durable ``row_id`` rewind targets, and for tool rows ``name`` + ``context`` preview + full ``args``. Assistant detail sidecars (``reasoning``, …) ride as extra keys. */ export interface TranscriptMessage { role: string - content?: unknown + text?: string | null timestamp?: number | null - tool_calls?: Record[] | null - tool_call_id?: string | null - name?: string | null - display_kind?: string | null row_id?: number | null + display_kind?: string | null + display_metadata?: unknown | null + name?: string | null + context?: string | null + args?: Record | null reasoning?: string | null [key: string]: unknown } @@ -2614,15 +2615,15 @@ export interface SessionCwdSetParams { } /** The refreshed ``session.info`` view (full agent view, or the lazy shape). */ export interface SessionCwdSetResult { - model?: string + model: string provider?: string reasoning_effort?: string service_tier?: string fast?: boolean yolo?: boolean approval_mode?: string - tools?: Record - skills?: Record + tools: Record + skills: Record cwd?: string branch?: string | null project?: ProjectRef | null @@ -3727,15 +3728,16 @@ export interface GatewayReadyPayload { } /** ``tui_gateway/change_watcher.py::resolve_skin`` — the resolved active skin (``HermesSkin``). ``{}`` when the skin engine failed to load. Colour maps are token → colour string. */ export interface SkinPayload { - name?: string | null - colors?: Record | null - light_colors?: Record | null - dark_colors?: Record | null - branding?: Record | null - banner_logo?: string | null - banner_hero?: string | null - tool_prefix?: string | null - help_header?: string | null + name?: string + description?: string + colors?: Record + light_colors?: Record + dark_colors?: Record + branding?: Record + banner_logo?: string + banner_hero?: string + tool_prefix?: string + help_header?: string [key: string]: unknown } /** ``hermes_cli/free_tier_bootstrap.py::SetupRecord.as_payload``. */ @@ -3791,7 +3793,7 @@ export interface BillingBlock { provider: string provider_label: string model: string - billing_url?: string | null + billing_url: string | null is_nous: boolean message: string unverified?: boolean | null @@ -4775,7 +4777,7 @@ export const SERVER_REQUEST_METHODS = [ ] as const satisfies readonly ServerRequestMethod[] // ── Notifications (`event` frames) ── -export interface GatewayEventMap { +export interface BackendGatewayEventMap { /** Output chunk from an agent-owned background process. */ 'agent.terminal.output': TerminalOutputPayload /** A /background side agent finished. */ @@ -4911,7 +4913,7 @@ export interface GatewayEventMap { /** A wake phrase fired. */ 'wake.detected': WakeDetectedPayload } -export type GatewayEventType = keyof GatewayEventMap +export type BackendGatewayEventName = keyof BackendGatewayEventMap export const GATEWAY_EVENT_TYPES = [ 'agent.terminal.output', 'background.complete', @@ -4980,4 +4982,4 @@ export const GATEWAY_EVENT_TYPES = [ 'voice.status', 'voice.transcript', 'wake.detected' -] as const satisfies readonly GatewayEventType[] +] as const satisfies readonly BackendGatewayEventName[] diff --git a/scripts/gen_gateway_contracts.py b/scripts/gen_gateway_contracts.py index a1774100c6..2c71c72bd8 100644 --- a/scripts/gen_gateway_contracts.py +++ b/scripts/gen_gateway_contracts.py @@ -5,7 +5,7 @@ by a deliberately small JSON-Schema-subset renderer — object/properties/requir enum, const, anyOf-with-null, array/items, ``$ref``, oneOf + discriminator, additionalProperties. Anything else raises at generation time so an unsupported model is fixed at the model, never worked around in the output. Prettier runs on the TS when a node_modules binary is present -(the committed file is the prettier-formatted one either way — CI regenerates and diffs). +(output is already in the repo's prettier style; the Python CI lane regenerates and diffs it). """ from __future__ import annotations @@ -73,9 +73,9 @@ class Renderer: self.ensure(name) return name if "const" in schema: - return json.dumps(schema["const"]) + return _lit(schema["const"]) if "enum" in schema: - return " | ".join(json.dumps(v) for v in schema["enum"]) + return " | ".join(_lit(v) for v in schema["enum"]) if "anyOf" in schema or "oneOf" in schema: variants = schema.get("anyOf") or schema.get("oneOf") or [] rendered = list(dict.fromkeys(self.type_of(v, inline_depth=inline_depth) for v in variants)) @@ -143,14 +143,26 @@ _IDENT = re.compile(r"^[A-Za-z_$][A-Za-z0-9_$]*$") def _prop(key: str) -> str: - return key if _IDENT.match(key) else json.dumps(key) + return key if _IDENT.match(key) else _lit(key) -def _doc(text: str | None) -> str: +def _const_items(names: list[str]) -> str: + return ",\n".join(f" {_lit(n)}" for n in names) + "\n" + + +def _lit(value) -> str: + """A TS literal in the repo's prettier style (single quotes) so the committed file needs no + Node-side formatting pass — the Python CI lane regenerates and diffs it.""" + if isinstance(value, str): + return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'" + return json.dumps(value) + + +def _doc(text: str | None, indent: str = "") -> str: if not text: return "" clean = " ".join(text.split()) - return f"/** {clean} */\n" + return f"{indent}/** {clean} */\n" def _pascal(name: str) -> str: @@ -183,50 +195,38 @@ def render_ts() -> str: out.append("\n// ── Client→server methods ──\n") out.append("export interface RpcMethods {\n") for m in sorted(METHODS.values(), key=lambda x: x.name): - out.append(_doc(m.doc)) - out.append(f" {json.dumps(m.name)}: {{ params: {name_of[m.params]}; result: {name_of[m.result]} }}\n") + out.append(_doc(m.doc, " ")) + out.append(f" {_prop(m.name)}: {{ params: {name_of[m.params]}; result: {name_of[m.result]} }}\n") out.append("}\n") out.append("export type RpcMethod = keyof RpcMethods\n") - out.append("export const RPC_METHODS = [\n") - out.extend(f" {json.dumps(n)},\n" for n in sorted(METHODS)) - out.append("] as const satisfies readonly RpcMethod[]\n") + out.append("export const RPC_METHODS = [\n" + _const_items(sorted(METHODS)) + "] as const satisfies readonly RpcMethod[]\n") out.append("\n// ── Server→client requests ──\n") out.append("export interface ServerRequestMap {\n") for s in sorted(SERVER_REQUESTS.values(), key=lambda x: x.name): - out.append(_doc(s.doc)) - out.append(f" {json.dumps(s.name)}: {{ params: {name_of[s.params]}; result: {name_of[s.result]} }}\n") + out.append(_doc(s.doc, " ")) + out.append(f" {_prop(s.name)}: {{ params: {name_of[s.params]}; result: {name_of[s.result]} }}\n") out.append("}\n") out.append("export type ServerRequestMethod = keyof ServerRequestMap\n") - out.append("export const SERVER_REQUEST_METHODS = [\n") - out.extend(f" {json.dumps(n)},\n" for n in sorted(SERVER_REQUESTS)) - out.append("] as const satisfies readonly ServerRequestMethod[]\n") + out.append("export const SERVER_REQUEST_METHODS = [\n" + _const_items(sorted(SERVER_REQUESTS)) + + "] as const satisfies readonly ServerRequestMethod[]\n") out.append("\n// ── Notifications (`event` frames) ──\n") - out.append("export interface GatewayEventMap {\n") + out.append("export interface BackendGatewayEventMap {\n") for e in sorted(EVENTS.values(), key=lambda x: x.name): - out.append(_doc(e.doc)) + out.append(_doc(e.doc, " ")) payload = name_of[e.payload] if e.payload is not None else "Record" - out.append(f" {json.dumps(e.name)}: {payload}\n") + out.append(f" {_prop(e.name)}: {payload}\n") out.append("}\n") - out.append("export type GatewayEventType = keyof GatewayEventMap\n") - out.append("export const GATEWAY_EVENT_TYPES = [\n") - out.extend(f" {json.dumps(n)},\n" for n in sorted(EVENTS)) - out.append("] as const satisfies readonly GatewayEventType[]\n") + out.append("export type BackendGatewayEventName = keyof BackendGatewayEventMap\n") + out.append("export const GATEWAY_EVENT_TYPES = [\n" + _const_items(sorted(EVENTS)) + + "] as const satisfies readonly BackendGatewayEventName[]\n") return "".join(out) -def prettier(text: str) -> str: - bin_ = ROOT / "node_modules" / ".bin" / "prettier" - if not bin_.exists(): - return text - proc = subprocess.run( - [str(bin_), "--stdin-filepath", str(TS_OUT)], input=text, capture_output=True, text=True, - encoding="utf-8", cwd=str(ROOT), check=False, - ) - if proc.returncode != 0: - raise RuntimeError(f"prettier failed: {proc.stderr}") - return proc.stdout +def _tidy(text: str) -> str: + """No trailing whitespace, single trailing newline (matches `git diff --check` + prettier).""" + return "\n".join(line.rstrip() for line in text.splitlines()).rstrip("\n") + "\n" # ── OpenRPC rendering ──────────────────────────────────────────────────────────────────────────── @@ -290,7 +290,7 @@ def render_openrpc() -> str: def render_all() -> dict[Path, str]: - return {TS_OUT: prettier(render_ts()), OPENRPC_OUT: render_openrpc()} + return {TS_OUT: _tidy(render_ts()), OPENRPC_OUT: render_openrpc()} def main(argv: list[str] | None = None) -> int: