From 66c678c34ea61db1b38ae26f39ec0e3de64e8891 Mon Sep 17 00:00:00 2001 From: dovvnloading <157447210+dovvnloading@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:26:16 -0400 Subject: [PATCH 1/3] Send each node only the fields it uses Every scene node crossed the wire as the full flat superset of every node kind's fields: 157 keys per node, of which a chat node uses 13. Each new node kind widened every node on the canvas. Against the 2026-08-11 perf baseline the per-node payload had grown 32-47%, publish cost 40-119%, and the nightly perf check had failed every night since 2026-08-23. A node now sends its five identity keys plus only the fields that differ from the contract default. The client restores the rest from the same defaults, so every reader still sees the full row: - backend/domain/node_wire.py builds the row from one field table per node state class instead of ~150 isinstance checks per node. Omission is type-strict (0 is not False, 480 is not 480.0), so restoring never changes a value. - SceneNodeRow is marked WIRE_OMITS_DEFAULTS. codegen emits hydrateSceneNodeRow() from its declared defaults; validateSceneState restores snapshots before validating, and SceneStore restores patch upserts, which replace the row, so a field reset to its default comes back as the default rather than the old value. - graphlink_wire_schema's schema and validator treat a sparse row's defaulted fields as optional, with the restored value as `default`. Checked against the previous serializer: 22,703 nodes (the four perf fixtures plus 20,000 randomized nodes of every state class, including int/bool/float type confusions) restore to exactly the row it produced. Measured locally, median of 7: publish cost -73% to -80% versus the August baseline, full snapshot -64% to -80%, one node -65% to -94%. Also: perf timings are now the median of 7 publishes rather than one sample, and the Document View tests get a test timeout above the 15s lazy-body wait #417 added. The per-test 5000ms limit still applied, so that longer wait could never actually be used. Co-Authored-By: Claude --- backend/domain/graph.py | 647 +--------------- backend/domain/node_states.py | 12 +- backend/domain/node_wire.py | 715 ++++++++++++++++++ backend/tests/conftest.py | 25 + backend/tests/perf/measure_baselines.py | 36 +- backend/tests/test_canvas.py | 37 +- backend/tests/test_node_wire.py | 242 ++++++ backend/tests/test_plugin_grants.py | 5 +- backend/tests/test_plugin_sdk.py | 9 +- backend/tests/test_wire_schema_validation.py | 5 +- contracts/codegen.py | 166 +++- contracts/graphlink_scene_payload.py | 13 +- graphlink_wire_schema.py | 50 +- tests/test_node_state_migration.py | 15 +- .../src/app/canvas/DocumentViewPanel.test.tsx | 6 + web_ui/src/app/canvas/SceneCanvas.tsx | 7 +- web_ui/src/app/canvas/sceneStore.test.ts | 62 +- web_ui/src/app/canvas/sceneStore.ts | 14 +- .../generated/scene-state.schema.json | 288 +++---- .../lib/bridge-core/generated/scene-state.ts | 186 ++++- .../lib/bridge-core/sceneRowHydration.test.ts | 115 +++ 21 files changed, 1824 insertions(+), 831 deletions(-) create mode 100644 backend/domain/node_wire.py create mode 100644 backend/tests/test_node_wire.py create mode 100644 web_ui/src/lib/bridge-core/sceneRowHydration.test.ts diff --git a/backend/domain/graph.py b/backend/domain/graph.py index 317da424..810c9287 100644 --- a/backend/domain/graph.py +++ b/backend/domain/graph.py @@ -5,6 +5,10 @@ _content_parts_wire helper scene_payload calls) with exactly ONE deliberate edit, called out below; everything else is verbatim. +The per-node wire row builder (_node_wire's body, _content_parts_wire and +the Review Lens row builders) has since moved to backend/domain/node_wire.py, +where the row became sparse - see that module's docstring. + The one edit: SceneDocument.add_session_tokens used to call backend.token_counter.estimate_tokens, but backend/token_counter.py imports backend.events at module level, which tests/test_domain_purity.py @@ -34,7 +38,6 @@ from collections import deque import itertools -import json import logging from dataclasses import dataclass, field from typing import Any, Callable @@ -49,7 +52,6 @@ from backend.domain.branches import BranchOps from backend.domain.commands import CommandOps -from backend.domain.content_codec import _content_codec from backend.domain.groups import GroupOps from backend.domain.layout import LayoutOps from backend.domain.model import ( @@ -65,25 +67,8 @@ SceneNode, ) from backend.domain.node_access import is_node_of, require_node -from backend.domain.node_states import ( - ArtifactState, - ChartState, - ChatState, - CodeReviewState, - CodeSandboxState, - CodeState, - ContainerState, - DocumentState, - FrameState, - GitlinkState, - HarnessState, - HtmlState, - ImageState, - NodeState, - NoteState, - PlanState, - WebResearchState, -) +from backend.domain.node_states import CodeState, FrameState, NodeState +from backend.domain.node_wire import node_wire from backend.domain.nodes_agent_runs import AgentRunOps from backend.domain.nodes_code_review import CodeReviewOps from backend.domain.nodes_content import ContentOps @@ -100,104 +85,6 @@ def _estimate_tokens(text: str) -> int: return TokenEstimator().count_tokens(text) -# -- Review Lens nested wire rows ------------------------------------------ -# -# These five builders exist because the Review Lens node stores its nested -# rows as the review engine's own snake_case dicts (graphlink_plugins/ -# review_lens/review_engine.py) while the wire contract - and the generated -# client validator built from it - is camelCase, like every other nested -# row on SceneNodeRow. scene_payload used to forward them with a bare -# `dict(row)`, which shipped `patch_truncated`/`previous_path`/ -# `group_title` where CodeReviewFileRow and CodeReviewWalkthroughGroupRow -# declare `patchTruncated`/`previousPath`/`groupTitle`. -# -# That was not a cosmetic mismatch. validateSceneState treats a missing -# required field as a hard error, and web_ui/src/lib/api-contract/ -# bindTopic.ts DROPS a snapshot that fails validation, so from the first -# successful PR fetch onward every scene snapshot for the whole session -# was rejected client-side - the canvas froze for every node, not just -# this one. Building each row explicitly (the `toolCalls` precedent -# below) fixes the casing AND guarantees each row carries exactly the -# contract's fields with the contract's types, so a row that reached the -# state from an old save file cannot put an unexpected key on the wire. -# -# The domain keeps snake_case on purpose: it is what the engine emits and -# what session_save.py has already written to every existing save file. -# The conversion belongs at the wire builder, the same place -# codeReviewScores is coerced to dict[str, str]. - - -def _code_review_file_wire(row: dict[str, Any]) -> dict[str, Any]: - wire: dict[str, Any] = { - "path": str(row.get("path", "")), - "status": str(row.get("status", "modified")), - "additions": _non_negative_wire_int(row.get("additions")), - "deletions": _non_negative_wire_int(row.get("deletions")), - # `patch` is DELIBERATELY not forwarded - see the caller's comment. - "patch": "", - "patchTruncated": bool(row.get("patch_truncated", False)), - } - # Genuinely absent (not "") for every non-rename, which is why - # CodeReviewFileRow.previousPath is the one Optional field on the row. - previous = str(row.get("previous_path", "") or "") - if previous: - wire["previousPath"] = previous - return wire - - -def _code_review_walkthrough_wire(row: dict[str, Any]) -> dict[str, Any]: - raw_paths = row.get("paths") - return { - "groupTitle": str(row.get("group_title", "")), - "paths": [str(path) for path in raw_paths] if isinstance(raw_paths, list) else [], - "explanation": str(row.get("explanation", "")), - } - - -def _code_review_finding_wire(row: dict[str, Any]) -> dict[str, Any]: - return { - "id": str(row.get("id", "")), - "severity": str(row.get("severity", "")), - "tier": str(row.get("tier", "")), - "category": str(row.get("category", "")), - "path": str(row.get("path", "")), - "line": _non_negative_wire_int(row.get("line")), - "title": str(row.get("title", "")), - "evidence": str(row.get("evidence", "")), - "impact": str(row.get("impact", "")), - "recommendation": str(row.get("recommendation", "")), - } - - -def _code_review_error_wire(row: dict[str, Any]) -> dict[str, Any]: - return { - "id": str(row.get("id", "")), - "severity": str(row.get("severity", "")), - "tier": str(row.get("tier", "")), - "kind": str(row.get("kind", "")), - "path": str(row.get("path", "")), - "line": _non_negative_wire_int(row.get("line")), - "title": str(row.get("title", "")), - "evidence": str(row.get("evidence", "")), - "fix": str(row.get("fix", "")), - } - - -def _code_review_qa_wire(row: dict[str, Any]) -> dict[str, Any]: - return { - "question": str(row.get("question", "")), - "answer": str(row.get("answer", "")), - } - - -def _non_negative_wire_int(value: Any) -> int: - """int for the wire, never raising. A row can reach the wire from a - hand-edited save file as well as from the engine, and a ValueError - here would fail the whole scene republish, not just one row.""" - try: - return max(0, int(value)) # type: ignore[call-overload] - except (TypeError, ValueError, OverflowError): - return 0 @@ -1000,490 +887,16 @@ def organize(self) -> None: # -- snapshots --------------------------------------------------------- def _node_wire(self, n: SceneNode) -> dict[str, Any]: - """The per-node wire shape - ADR-003 stage 3.4 factored this out of - scene_payload() verbatim (byte-identical output) so both the full - snapshot AND take_dirty_patch_ops's upsertNode ops build a node's - payload from exactly ONE place, rather than a second copy drifting - out of sync with this one field by field.""" - return { - "id": n.id, - "x": n.x, - "y": n.y, - "title": n.title, - "kind": n.kind, - "content": n.content, - "isUser": n.state.is_user if isinstance(n.state, ChatState) else False, - "isCollapsed": n.is_collapsed, - "code": n.state.code if isinstance(n.state, CodeState) else "", - "language": n.state.language if isinstance(n.state, CodeState) else "", - "attachmentKind": n.state.attachment_kind if isinstance(n.state, DocumentState) else "", - "filePath": n.state.file_path if isinstance(n.state, DocumentState) else "", - "mimeType": n.state.mime_type if isinstance(n.state, DocumentState) else "", - "durationSeconds": n.state.duration_seconds if isinstance(n.state, DocumentState) else None, - "byteSize": n.state.byte_size if isinstance(n.state, DocumentState) else None, - "previewLabel": n.state.preview_label if isinstance(n.state, DocumentState) else "", - "isDocked": n.is_docked, - "imageAssetId": n.state.image_asset_id if isinstance(n.state, ImageState) else "", - "history": [ - # ADR-006 stage 6.4: the projection stays a strict allow-list - # (never spread the raw dict - legacy entries can carry - # arbitrary keys), widened by exactly one optional marker: - # "incomplete" flags a partial assistant reply whose stream - # died (see append_conversation_assistant_message). - { - "role": m["role"], - "content": m["content"], - "incomplete": bool(m.get("incomplete", False)), - } - for m in n.history - ], - "pendingRequestId": n.pending_request_id, - # ADR-002 Workstream 1 ("Synthesize Branches") - see - # ChatState's own comment, backend/domain/node_states.py. - "provider": n.state.provider if isinstance(n.state, ChatState) else None, - "model": n.state.model if isinstance(n.state, ChatState) else None, - # ADR-018 stage 18.3: see ChatState's own comment on override_ - # provider/override_model_id for why this is a distinct pair - # from provider/model directly above (output provenance vs. - # input routing pin). - "overrideProvider": n.state.override_provider if isinstance(n.state, ChatState) else "", - "overrideModelId": n.state.override_model_id if isinstance(n.state, ChatState) else "", - # ADR-017 stage 17.5: see ChatState's own comment on - # index_into_knowledge. - "indexIntoKnowledge": n.state.index_into_knowledge if isinstance(n.state, ChatState) else False, - "isBranchSynthesis": n.state.is_branch_synthesis if isinstance(n.state, ChatState) else False, - "synthesisInstructions": ( - n.state.synthesis_instructions if isinstance(n.state, ChatState) else "" - ), - # ADR-002 Workstream 1 ("Branch status and lifecycle") - - # see ChatState's own comment/SceneDocument. - # final_deliverable_node_id's own comment. "active" (not - # "") is the correct non-chat fallback - it is - # branch_status's own real pre-migration default, not an - # empty placeholder. - "branchStatus": n.state.branch_status if isinstance(n.state, ChatState) else "active", - # ADR-006 stage 6.4 (H5): partial reply preserved after a dead - # stream - see ChatState.response_incomplete's own comment. - "responseIncomplete": ( - n.state.response_incomplete if isinstance(n.state, ChatState) else False - ), - # ADR-006 stage 6.8: provider-reported usage stamped on chat - # replies - see ChatState's own comment. Nullable, like - # provider/model above. - "promptTokens": n.state.prompt_tokens if isinstance(n.state, ChatState) else None, - "completionTokens": ( - n.state.completion_tokens if isinstance(n.state, ChatState) else None - ), - # ADR-016 stage 16.2: the cost snapshot taken when usage was - # stamped - see ChatState.estimated_cost_usd's own comment. - "estimatedCostUsd": ( - n.state.estimated_cost_usd if isinstance(n.state, ChatState) else None - ), - # ADR-007 stage 7.4: see ChatState.tool_invocations' own comment - # and ToolInvocationRow's own docstring (contracts/graphlink_ - # scene_payload.py) for why `arguments` is JSON-encoded here - # rather than passed through as a nested object. - "toolCalls": ( - [ - { - "id": str(call.get("id", "")), - "name": str(call.get("name", "")), - "argumentsJson": json.dumps(call.get("arguments") or {}, sort_keys=True), - "result": str(call.get("result", "")), - "isError": bool(call.get("is_error", False)), - } - for call in n.state.tool_invocations - ] - if isinstance(n.state, ChatState) - else [] - ), - "isFinalDeliverable": n.id == self.final_deliverable_node_id, - "researchStage": n.state.research_stage if isinstance(n.state, WebResearchState) else "", - "researchCompleted": n.state.research_completed if isinstance(n.state, WebResearchState) else 0, - "researchTotal": n.state.research_total if isinstance(n.state, WebResearchState) else 0, - "researchActiveSourceId": ( - n.state.research_active_source_id if isinstance(n.state, WebResearchState) else None - ), - "researchError": n.state.research_error if isinstance(n.state, WebResearchState) else "", - "researchResult": n.state.research_result if isinstance(n.state, WebResearchState) else None, - "researchRetainToKnowledge": ( - n.state.research_retain_to_knowledge if isinstance(n.state, WebResearchState) else False - ), - "artifactContent": n.state.artifact_content if isinstance(n.state, ArtifactState) else "", - "artifactError": n.state.artifact_error if isinstance(n.state, ArtifactState) else "", - "gitlinkRepo": n.state.gitlink_repo if isinstance(n.state, GitlinkState) else "", - "gitlinkBranch": n.state.gitlink_branch if isinstance(n.state, GitlinkState) else "", - "gitlinkScopeMode": ( - n.state.gitlink_scope_mode if isinstance(n.state, GitlinkState) else "selected" - ), - "gitlinkLocalRoot": n.state.gitlink_local_root if isinstance(n.state, GitlinkState) else "", - "gitlinkRepoFilePaths": ( - list(n.state.gitlink_repo_file_paths) if isinstance(n.state, GitlinkState) else [] - ), - "gitlinkSelectedPaths": ( - list(n.state.gitlink_selected_paths) if isinstance(n.state, GitlinkState) else [] - ), - "gitlinkTaskPrompt": n.state.gitlink_task_prompt if isinstance(n.state, GitlinkState) else "", - # gitlinkContextXml is DELIBERATELY OMITTED - see - # GitlinkState's own comment. Served on demand via the - # fetchGitlinkContext intent instead. - "gitlinkContextStats": ( - dict(n.state.gitlink_context_stats) if isinstance(n.state, GitlinkState) else {} - ), - "gitlinkContextSummary": ( - n.state.gitlink_context_summary if isinstance(n.state, GitlinkState) else "" - ), - # R5.3 post-review FIX 6: UNLIKE gitlinkContextXml (and - # unlike gitlink_change_local_root, never on the wire at - # all), this genuinely needs to be here - see - # GitlinkState's own comment for why gitlinkContextSummary - # alone cannot be trusted as a lazy-fetch-once cache key. - "gitlinkContextVersion": ( - n.state.gitlink_context_version if isinstance(n.state, GitlinkState) else 0 - ), - "gitlinkProposalMarkdown": ( - n.state.gitlink_proposal_markdown if isinstance(n.state, GitlinkState) else "" - ), - "gitlinkPendingChanges": ( - [dict(c) for c in n.state.gitlink_pending_changes] if isinstance(n.state, GitlinkState) else [] - ), - "gitlinkPreviewText": n.state.gitlink_preview_text if isinstance(n.state, GitlinkState) else "", - "gitlinkChangeFingerprint": ( - n.state.gitlink_change_fingerprint if isinstance(n.state, GitlinkState) else None - ), - "gitlinkChangeState": ( - n.state.gitlink_change_state if isinstance(n.state, GitlinkState) else "draft" - ), - "gitlinkError": n.state.gitlink_error if isinstance(n.state, GitlinkState) else "", - "codeReviewPrUrl": n.state.code_review_pr_url if isinstance(n.state, CodeReviewState) else "", - "codeReviewRepo": n.state.code_review_repo if isinstance(n.state, CodeReviewState) else "", - "codeReviewPrNumber": ( - n.state.code_review_pr_number if isinstance(n.state, CodeReviewState) else 0 - ), - "codeReviewPrTitle": ( - n.state.code_review_pr_title if isinstance(n.state, CodeReviewState) else "" - ), - "codeReviewPrState": ( - n.state.code_review_pr_state if isinstance(n.state, CodeReviewState) else "" - ), - "codeReviewPrHtmlUrl": ( - n.state.code_review_pr_html_url if isinstance(n.state, CodeReviewState) else "" - ), - "codeReviewBaseRef": ( - n.state.code_review_base_ref if isinstance(n.state, CodeReviewState) else "" - ), - "codeReviewHeadRef": ( - n.state.code_review_head_ref if isinstance(n.state, CodeReviewState) else "" - ), - "codeReviewAdditions": ( - n.state.code_review_additions if isinstance(n.state, CodeReviewState) else 0 - ), - "codeReviewDeletions": ( - n.state.code_review_deletions if isinstance(n.state, CodeReviewState) else 0 - ), - "codeReviewChangedFiles": ( - n.state.code_review_changed_files if isinstance(n.state, CodeReviewState) else 0 - ), - # Rebuilt row by row rather than forwarded as `dict(f)` - see - # _code_review_file_wire's own comment for the casing bug that - # cost every scene snapshot after a PR fetch. - # - # `patch` rides as "" on purpose. The per-file patches are - # capped at MAX_FILE_PATCH_CHARS (6000) each and MAX_PR_FILES - # (100) of them, so forwarding them put up to ~600KB of diff - # text on EVERY scene republish - roughly ten times the - # 60KB codeReviewDiffText that was excluded from this payload - # for exactly that reason (see CodeReviewState's own comment). - # No frontend code reads it: CodeReviewNodeView renders the - # unified diff it lazily fetches via fetchCodeReviewDiffText, - # never these per-file patches. The field stays on the row - # because the contract declares it required; the engine still - # reads the real patches from node.state, which is where the - # fallback pre-screen scans them. - "codeReviewFiles": ( - [_code_review_file_wire(f) for f in n.state.code_review_files] - if isinstance(n.state, CodeReviewState) - else [] - ), - "codeReviewFilesTruncated": ( - n.state.code_review_files_truncated if isinstance(n.state, CodeReviewState) else False - ), - # codeReviewDiffText is DELIBERATELY OMITTED - see - # CodeReviewState's own comment (the gitlinkContextXml - # precedent). Served on demand via fetchCodeReviewDiffText. - "codeReviewDiffTruncated": ( - n.state.code_review_diff_truncated if isinstance(n.state, CodeReviewState) else False - ), - "codeReviewDiffChars": ( - n.state.code_review_diff_chars if isinstance(n.state, CodeReviewState) else 0 - ), - # The lazy-diff cache key (the R5.3 post-review FIX 6 - # precedent): bumped by every successful fetch, so the - # frontend never serves a previous fetch's text for this one. - "codeReviewDiffVersion": ( - n.state.code_review_diff_version if isinstance(n.state, CodeReviewState) else 0 - ), - "codeReviewWalkthrough": ( - [_code_review_walkthrough_wire(g) for g in n.state.code_review_walkthrough] - if isinstance(n.state, CodeReviewState) - else [] - ), - "codeReviewFindings": ( - [_code_review_finding_wire(f) for f in n.state.code_review_findings] - if isinstance(n.state, CodeReviewState) - else [] - ), - "codeReviewErrors": ( - [_code_review_error_wire(e) for e in n.state.code_review_errors] - if isinstance(n.state, CodeReviewState) - else [] - ), - "codeReviewDismissedIds": ( - list(n.state.code_review_dismissed_ids) - if isinstance(n.state, CodeReviewState) - else [] - ), - "codeReviewTitle": ( - n.state.code_review_title if isinstance(n.state, CodeReviewState) else "" - ), - "codeReviewOverview": ( - n.state.code_review_overview if isinstance(n.state, CodeReviewState) else "" - ), - "codeReviewConfidence": ( - n.state.code_review_confidence if isinstance(n.state, CodeReviewState) else "" - ), - # Scores ride the wire as dict[str, str] - coerced here, the - # store_gitlink_context str-coercion precedent for - # gitlinkContextStats (contracts only admit string-valued - # dicts on SceneNodeRow). - "codeReviewScores": ( - {str(k): str(v) for k, v in n.state.code_review_scores.items()} - if isinstance(n.state, CodeReviewState) - else {} - ), - "codeReviewQualityScore": ( - n.state.code_review_quality_score if isinstance(n.state, CodeReviewState) else 0 - ), - "codeReviewVerdict": ( - n.state.code_review_verdict if isinstance(n.state, CodeReviewState) else "none" - ), - "codeReviewRisk": n.state.code_review_risk if isinstance(n.state, CodeReviewState) else "", - "codeReviewQualitySummary": ( - n.state.code_review_quality_summary if isinstance(n.state, CodeReviewState) else "" - ), - "codeReviewQa": ( - [_code_review_qa_wire(entry) for entry in n.state.code_review_qa] - if isinstance(n.state, CodeReviewState) - else [] - ), - "codeReviewState": ( - n.state.code_review_state if isinstance(n.state, CodeReviewState) else "draft" - ), - "codeReviewError": n.state.code_review_error if isinstance(n.state, CodeReviewState) else "", - # codeSandboxSandboxId is DELIBERATELY OMITTED - see - # CodeSandboxState's own comment (pure internal - # directory-naming key, mirrors gitlink_imported_root's - # own "server-side bookkeeping only" precedent). - "codeSandboxRequirements": ( - n.state.code_sandbox_requirements if isinstance(n.state, CodeSandboxState) else "" - ), - "codeSandboxPrompt": ( - n.state.code_sandbox_prompt if isinstance(n.state, CodeSandboxState) else "" - ), - "codeSandboxCode": n.state.code_sandbox_code if isinstance(n.state, CodeSandboxState) else "", - "codeSandboxOutput": ( - n.state.code_sandbox_output if isinstance(n.state, CodeSandboxState) else "" - ), - "codeSandboxAnalysis": ( - n.state.code_sandbox_analysis if isinstance(n.state, CodeSandboxState) else "" - ), - "codeSandboxAwaitingApproval": ( - n.state.code_sandbox_awaiting_approval if isinstance(n.state, CodeSandboxState) else False - ), - # R5.4 CODESANDBOX FIX: the frozen-at-approval-time - # snapshot, deliberately distinct from - # codeSandboxRequirements above (that one is the user's - # still-live, still-editable draft for the NEXT run) - - # see CodeSandboxState's own comment. - "codeSandboxApprovalRequirements": ( - n.state.code_sandbox_approval_requirements if isinstance(n.state, CodeSandboxState) else "" - ), - # ADR-005 stage 5.5: the user's live source-build opt-in - # for the CURRENT pending approval - see CodeSandboxState. - # code_sandbox_approval_allow_source_builds's own comment. - "codeSandboxApprovalAllowSourceBuilds": ( - n.state.code_sandbox_approval_allow_source_builds - if isinstance(n.state, CodeSandboxState) - else False - ), - # ADR-005 stage 5.5 review-fix: True only during a - # repair-loop re-gate - see CodeSandboxState. - # code_sandbox_approval_is_repair's own comment. - "codeSandboxApprovalIsRepair": ( - n.state.code_sandbox_approval_is_repair - if isinstance(n.state, CodeSandboxState) - else False - ), - "codeSandboxError": n.state.code_sandbox_error if isinstance(n.state, CodeSandboxState) else "", - # R6.1: Notes/Frames/Containers. groupManualWidth/Height - # are DELIBERATELY OMITTED, same "server-side bookkeeping - # only" posture as codeSandboxSandboxId/gitlinkImportedRoot - # above - see those fields' own comments on - # CodeSandboxState/GitlinkState. - "color": n.color, - "headerColor": n.header_color, - "isSystemPrompt": n.state.is_system_prompt if isinstance(n.state, NoteState) else False, - "isSummaryNote": n.state.is_summary_note if isinstance(n.state, NoteState) else False, - # ADR-002 Workstream 1 ("Compare Branches") - see - # NoteState's own comment, backend/domain/node_states.py. - "isBranchComparison": n.state.is_branch_comparison if isinstance(n.state, NoteState) else False, - "itemIds": list(n.item_ids), - "isLocked": n.state.is_locked if isinstance(n.state, FrameState) else True, - "groupWidth": n.state.group_width if isinstance(n.state, (FrameState, ContainerState)) else None, - "groupHeight": ( - n.state.group_height if isinstance(n.state, (FrameState, ContainerState)) else None - ), - # R6.2: Chart node. - "chartType": n.state.chart_type if isinstance(n.state, ChartState) else "", - "chartData": dict(n.state.chart_data) if isinstance(n.state, ChartState) else {}, - "chartError": n.state.chart_error if isinstance(n.state, ChartState) else "", - "chartWidth": n.state.chart_width if isinstance(n.state, ChartState) else 480.0, - "chartHeight": n.state.chart_height if isinstance(n.state, ChartState) else 340.0, - "chartAspectLocked": n.state.chart_aspect_locked if isinstance(n.state, ChartState) else True, - "chartSourceNodeId": n.state.chart_source_node_id if isinstance(n.state, ChartState) else "", - # R6.3: HTML splitter + chat scroll gaps. - "htmlSplitterState": n.state.html_splitter_state if isinstance(n.state, HtmlState) else None, - "chatScrollValue": n.state.chat_scroll_value if isinstance(n.state, ChatState) else 0.0, - # R6.3: null (not []) when content_parts is None - "no - # multimodal content" must stay distinguishable from - # "multimodal content that happens to be empty". Any - # part carrying raw bytes under "data" gets that key - # base64-encoded for the wire via content_codec's own - # encode_image_bytes - the WIRE shape this produces - # matches content_codec.process_content_for_serialization's - # own output shape exactly, while n.content_parts itself - # (in memory) keeps holding real bytes, per the field's - # own contract. - "contentParts": _content_parts_wire(n.state.content_parts if isinstance(n.state, ChatState) else None), - # ADR-008 stage 8.3: plan node (the Builder's checklist). Steps - # cross as typed PlanStepRow dicts - see PlanState's own - # docstring for the state machine these render. - "planGoal": n.state.plan_goal if isinstance(n.state, PlanState) else "", - "planSteps": ( - [ - { - "id": s["id"], "title": s["title"], - "status": s["status"], "detail": s["detail"], - } - for s in n.state.plan_steps - ] - if isinstance(n.state, PlanState) else [] - ), - # ADR-008 stage 8.7: the run's own activity log - see PlanState's - # docstring for why this is untouched by undo. - "builderActivity": ( - [ - { - "tool": a["tool"], "summary": a["summary"], - "outcome": a["outcome"], "stepId": a["stepId"], - "elapsedMs": a["elapsedMs"], - } - for a in n.state.builder_activity - ] - if isinstance(n.state, PlanState) else [] - ), - "builderStatus": n.state.builder_status if isinstance(n.state, PlanState) else "", - "builderMode": n.state.builder_mode if isinstance(n.state, PlanState) else "", - "builderRunId": n.state.builder_run_id if isinstance(n.state, PlanState) else "", - "builderMaxSteps": n.state.builder_max_steps if isinstance(n.state, PlanState) else 0, - "builderMaxTokens": n.state.builder_max_tokens if isinstance(n.state, PlanState) else 0, - "builderMaxWallSeconds": n.state.builder_max_wall_seconds if isinstance(n.state, PlanState) else 0, - "builderSpentSteps": n.state.builder_spent_steps if isinstance(n.state, PlanState) else 0, - "builderSpentTokens": n.state.builder_spent_tokens if isinstance(n.state, PlanState) else 0, - "builderSpentWallSeconds": ( - n.state.builder_spent_wall_seconds if isinstance(n.state, PlanState) else 0 - ), - "builderAwaitingToolApproval": ( - n.state.builder_awaiting_tool_approval if isinstance(n.state, PlanState) else False - ), - "builderApprovalToolName": ( - n.state.builder_approval_tool_name if isinstance(n.state, PlanState) else "" - ), - "builderApprovalSummary": ( - n.state.builder_approval_summary if isinstance(n.state, PlanState) else "" - ), - "builderStatusDetail": n.state.builder_status_detail if isinstance(n.state, PlanState) else "", - # PLAN-2026-08-24 H1: harness node (the workspace agent). - # Conversation history deliberately does NOT cross the wire - - # the transcript lives in the workspace; only the render - # surface does (see HarnessState's own docstring). - "harnessGoal": n.state.harness_goal if isinstance(n.state, HarnessState) else "", - "harnessReply": n.state.harness_reply if isinstance(n.state, HarnessState) else "", - "harnessStatus": n.state.harness_status if isinstance(n.state, HarnessState) else "", - "harnessStatusDetail": ( - n.state.harness_status_detail if isinstance(n.state, HarnessState) else "" - ), - "harnessRunId": n.state.harness_run_id if isinstance(n.state, HarnessState) else "", - "harnessActivity": ( - [ - { - "tool": a["tool"], "summary": a["summary"], - "outcome": a["outcome"], "elapsedMs": a["elapsedMs"], - } - for a in n.state.harness_activity - ] - if isinstance(n.state, HarnessState) else [] - ), - "harnessAwaitingApproval": ( - n.state.harness_awaiting_approval if isinstance(n.state, HarnessState) else False - ), - "harnessApprovalToolName": ( - n.state.harness_approval_tool_name if isinstance(n.state, HarnessState) else "" - ), - "harnessApprovalSummary": ( - n.state.harness_approval_summary if isinstance(n.state, HarnessState) else "" - ), - "harnessApprovalSessionOffered": ( - n.state.harness_approval_session_offered - if isinstance(n.state, HarnessState) else False - ), - "harnessPlan": ( - [{"text": s["text"], "status": s["status"]} for s in n.state.harness_plan] - if isinstance(n.state, HarnessState) else [] - ), - "harnessAwaitingQuestion": ( - n.state.harness_awaiting_question if isinstance(n.state, HarnessState) else False - ), - "harnessQuestion": ( - n.state.harness_question if isinstance(n.state, HarnessState) else "" - ), - "harnessContextTokens": ( - n.state.harness_context_tokens if isinstance(n.state, HarnessState) else 0 - ), - "harnessMaxContextTokens": ( - n.state.harness_max_context_tokens if isinstance(n.state, HarnessState) else 0 - ), - "harnessCompactions": ( - n.state.harness_compactions if isinstance(n.state, HarnessState) else 0 - ), - "harnessWorkspacePath": ( - n.state.harness_workspace_path if isinstance(n.state, HarnessState) else "" - ), - "harnessWorkspaceActive": ( - n.state.harness_workspace_active if isinstance(n.state, HarnessState) else "" - ), - "harnessMaxTurns": n.state.harness_max_turns if isinstance(n.state, HarnessState) else 0, - "harnessSpentTurns": ( - n.state.harness_spent_turns if isinstance(n.state, HarnessState) else 0 - ), - "harnessSpentTokens": ( - n.state.harness_spent_tokens if isinstance(n.state, HarnessState) else 0 - ), - # ADR-014 stage 14.2: the Plugin SDK's generic live-wire - # fallback - see plugin_node_serializers' own field comment and - # _plugin_state_wire's own docstring below. - "pluginState": self._plugin_state_wire(n), - } + """The per-node wire row - the ONE place both the full snapshot and + take_dirty_patch_ops's upsertNode ops build a node's payload from + (ADR-003 stage 3.4), so the two can never drift field by field. + Sparse: see backend/domain/node_wire.py for the encoding and the + client-side restore that keeps every reader seeing the full row.""" + return node_wire( + n, + is_final_deliverable=n.id == self.final_deliverable_node_id, + plugin_state=self._plugin_state_wire(n), + ) def _plugin_state_wire(self, n: SceneNode) -> dict[str, str]: """ADR-014 stage 14.2: the live-WS-wire analog of session_save.py's @@ -1593,31 +1006,3 @@ def grid_payload(self) -> dict[str, Any]: "stylePresets": list(GRID_STYLE_PRESETS), "colorPresets": list(GRID_COLOR_PRESETS), } - - -def _content_parts_wire(parts: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None: - """R6.3: scene_payload()'s wire-side transform for ChatState's own - content_parts (backend/domain/node_states.py) - a pure mapping - function, NOT a SceneDocument method (same posture as - _research_result_wire below). None stays None (never []), so "no - multimodal content" and "multimodal content that happens to be empty" - remain distinguishable on the wire. Any part that is a dict carrying raw - bytes under its "data" key gets that key base64-encoded as a string via - content_codec.encode_image_bytes - matching content_codec. - process_content_for_serialization's own output shape exactly - while - leaving every other key/part untouched. Builds fresh dicts throughout; - never mutates the SceneNode's own in-memory parts (which must keep - holding real bytes, per content_parts's own field contract).""" - if parts is None: - return None - wire_parts: list[dict[str, Any]] = [] - for part in parts: - if isinstance(part, dict) and isinstance(part.get("data"), (bytes, bytearray)): - wire_part = dict(part) - wire_part["data"] = _content_codec.encode_image_bytes(bytes(part["data"])) - wire_parts.append(wire_part) - elif isinstance(part, dict): - wire_parts.append(dict(part)) - else: - wire_parts.append(part) - return wire_parts diff --git a/backend/domain/node_states.py b/backend/domain/node_states.py index 1ef40b37..748762c3 100644 --- a/backend/domain/node_states.py +++ b/backend/domain/node_states.py @@ -7,11 +7,13 @@ own relocation note); a kind not yet migrated still keeps its fields directly on SceneNode. -Wire-compatibility constraint: SceneDocument.scene_payload() must keep -emitting the exact same flat per-node shape it does today. This module -introduces no wire-layer change by itself - it only relocates where a -field lives in memory; scene_payload's own per-kind read expressions are -updated in lockstep with each kind's migration, in backend/domain/graph.py. +Wire-compatibility constraint: the client must keep reading the exact same +flat per-node shape. This module introduces no wire-layer change by itself - +it only relocates where a field lives in memory. The row itself is built by +backend/domain/node_wire.py from one field table per state class here (sent +sparse, restored to the flat shape client-side - see that module), so a +field added to a state class that should reach the frontend needs its entry +in that table too. kind values with no state class (no kind-specific fields at all, so node.state stays None for them permanently): placeholder, thinking, diff --git a/backend/domain/node_wire.py b/backend/domain/node_wire.py new file mode 100644 index 00000000..4461015a --- /dev/null +++ b/backend/domain/node_wire.py @@ -0,0 +1,715 @@ +"""The per-node wire row - sent sparse. + +Every node used to cross the wire as the FULL flat superset of every node +kind's fields: 157 keys, of which a chat node carries 13. Each new node kind +widened every node on the canvas, whatever its kind - measured against the +2026-08-11 perf baseline, the per-node payload had grown 32-47% and the +nightly perf check had failed every night since 2026-08-23. + +A node now sends its five identity keys (id/x/y/title/kind) plus only the +fields whose value differs from the wire default. The default for every key +is the one contracts/graphlink_scene_payload.py's SceneNodeRow declares, and +the client restores every omitted key from that same declaration: codegen +emits it as hydrateSceneNodeRow() (web_ui/src/lib/bridge-core/generated/ +scene-state.ts), which validateSceneState and the scene store's patch path +apply before anything else reads a row. The row the rest of the frontend sees +is therefore exactly the full row it always saw; only the bytes changed. + +WIRE_DEFAULTS below restates those contract defaults because contracts/ is +deliberately not shipped in the wheel, so the running backend cannot import +it. backend/tests/test_node_wire.py pins the two tables equal - a default +changed on one side only fails that test, never silently diverges the wire. + +"Differs from the default" is strict: same class AND equal. `0` and `False` +compare equal in Python but serialize differently, so a value of the wrong +type is always sent rather than folded into a default of another type. + +Only the fields a node's own state class owns are even computed: the +per-state tables below replace 150 isinstance checks per node, per publish, +with one type lookup - the build cost the perf baseline tracks as +payload_build_ms, paid on every publish because take_dirty_patch_ops diffs +every node's row. +""" + +from __future__ import annotations + +import json +from typing import Any, Callable + +from backend.domain.content_codec import _content_codec +from backend.domain.model import SceneNode +from backend.domain.node_states import ( + ArtifactState, + ChartState, + ChatState, + CodeReviewState, + CodeSandboxState, + CodeState, + ContainerState, + DocumentState, + FrameState, + GitlinkState, + HarnessState, + HtmlState, + ImageState, + NoteState, + PlanState, + WebResearchState, +) + +# The wire value of every non-identity key when a node does not set it - +# SceneNodeRow's own defaults (see the module docstring), plus `contentParts`, +# a real wire field the contract deliberately does not declare (no frontend +# code reads it; see contracts/graphlink_scene_payload.py's docstring). +# Containers here are never sent or mutated - they are comparison targets only. +WIRE_DEFAULTS: dict[str, Any] = { + "content": "", + "isUser": False, + "isCollapsed": False, + "code": "", + "language": "", + "attachmentKind": "", + "filePath": "", + "mimeType": "", + "durationSeconds": None, + "byteSize": None, + "previewLabel": "", + "isDocked": False, + "imageAssetId": "", + "history": [], + "pendingRequestId": None, + "provider": None, + "model": None, + "overrideProvider": "", + "overrideModelId": "", + "indexIntoKnowledge": False, + "isBranchSynthesis": False, + "synthesisInstructions": "", + "branchStatus": "active", + "responseIncomplete": False, + "promptTokens": None, + "completionTokens": None, + "estimatedCostUsd": None, + "toolCalls": [], + "isFinalDeliverable": False, + "researchStage": "", + "researchCompleted": 0, + "researchTotal": 0, + "researchActiveSourceId": None, + "researchError": "", + "researchResult": None, + "researchRetainToKnowledge": False, + "artifactContent": "", + "artifactError": "", + "gitlinkRepo": "", + "gitlinkBranch": "", + "gitlinkScopeMode": "selected", + "gitlinkLocalRoot": "", + "gitlinkRepoFilePaths": [], + "gitlinkSelectedPaths": [], + "gitlinkTaskPrompt": "", + "gitlinkContextStats": {}, + "gitlinkContextSummary": "", + "gitlinkContextVersion": 0, + "gitlinkProposalMarkdown": "", + "gitlinkPendingChanges": [], + "gitlinkPreviewText": "", + "gitlinkChangeFingerprint": None, + "gitlinkChangeState": "draft", + "gitlinkError": "", + "codeReviewPrUrl": "", + "codeReviewRepo": "", + "codeReviewPrNumber": 0, + "codeReviewPrTitle": "", + "codeReviewPrState": "", + "codeReviewPrHtmlUrl": "", + "codeReviewBaseRef": "", + "codeReviewHeadRef": "", + "codeReviewAdditions": 0, + "codeReviewDeletions": 0, + "codeReviewChangedFiles": 0, + "codeReviewFiles": [], + "codeReviewFilesTruncated": False, + "codeReviewDiffTruncated": False, + "codeReviewDiffChars": 0, + "codeReviewDiffVersion": 0, + "codeReviewWalkthrough": [], + "codeReviewFindings": [], + "codeReviewErrors": [], + "codeReviewDismissedIds": [], + "codeReviewTitle": "", + "codeReviewOverview": "", + "codeReviewConfidence": "", + "codeReviewScores": {}, + "codeReviewQualityScore": 0, + "codeReviewVerdict": "none", + "codeReviewRisk": "", + "codeReviewQualitySummary": "", + "codeReviewQa": [], + "codeReviewState": "draft", + "codeReviewError": "", + "codeSandboxRequirements": "", + "codeSandboxPrompt": "", + "codeSandboxCode": "", + "codeSandboxOutput": "", + "codeSandboxAnalysis": "", + "codeSandboxAwaitingApproval": False, + "codeSandboxApprovalRequirements": "", + "codeSandboxApprovalAllowSourceBuilds": False, + "codeSandboxApprovalIsRepair": False, + "codeSandboxError": "", + "color": None, + "headerColor": None, + "isSystemPrompt": False, + "isSummaryNote": False, + "isBranchComparison": False, + "itemIds": [], + "isLocked": True, + "groupWidth": None, + "groupHeight": None, + "chartType": "", + "chartData": {}, + "chartError": "", + "chartWidth": 480.0, + "chartHeight": 340.0, + "chartAspectLocked": True, + "chartSourceNodeId": "", + "htmlSplitterState": None, + "chatScrollValue": 0.0, + "contentParts": None, + "planGoal": "", + "planSteps": [], + "builderActivity": [], + "builderStatus": "", + "builderMode": "", + "builderRunId": "", + "builderMaxSteps": 0, + "builderMaxTokens": 0, + "builderMaxWallSeconds": 0, + "builderSpentSteps": 0, + "builderSpentTokens": 0, + "builderSpentWallSeconds": 0, + "builderAwaitingToolApproval": False, + "builderApprovalToolName": "", + "builderApprovalSummary": "", + "builderStatusDetail": "", + "harnessGoal": "", + "harnessReply": "", + "harnessStatus": "", + "harnessStatusDetail": "", + "harnessRunId": "", + "harnessActivity": [], + "harnessAwaitingApproval": False, + "harnessApprovalToolName": "", + "harnessApprovalSummary": "", + "harnessApprovalSessionOffered": False, + "harnessPlan": [], + "harnessAwaitingQuestion": False, + "harnessQuestion": "", + "harnessContextTokens": 0, + "harnessMaxContextTokens": 0, + "harnessCompactions": 0, + "harnessWorkspacePath": "", + "harnessWorkspaceActive": "", + "harnessMaxTurns": 0, + "harnessSpentTurns": 0, + "harnessSpentTokens": 0, + "pluginState": {}, +} + +# Always sent, never defaulted: SceneNodeRow's five fields with no default. +IDENTITY_KEYS = ("id", "x", "y", "title", "kind") + + +# -- Review Lens nested wire rows ------------------------------------------ +# +# These five builders exist because the Review Lens node stores its nested +# rows as the review engine's own snake_case dicts (graphlink_plugins/ +# review_lens/review_engine.py) while the wire contract - and the generated +# client validator built from it - is camelCase, like every other nested +# row on SceneNodeRow. scene_payload used to forward them with a bare +# `dict(row)`, which shipped `patch_truncated`/`previous_path`/ +# `group_title` where CodeReviewFileRow and CodeReviewWalkthroughGroupRow +# declare `patchTruncated`/`previousPath`/`groupTitle`. +# +# That was not a cosmetic mismatch. validateSceneState treats a missing +# required field as a hard error, and web_ui/src/lib/api-contract/ +# bindTopic.ts DROPS a snapshot that fails validation, so from the first +# successful PR fetch onward every scene snapshot for the whole session +# was rejected client-side - the canvas froze for every node, not just +# this one. Building each row explicitly (the `toolCalls` precedent +# below) fixes the casing AND guarantees each row carries exactly the +# contract's fields with the contract's types, so a row that reached the +# state from an old save file cannot put an unexpected key on the wire. +# +# The domain keeps snake_case on purpose: it is what the engine emits and +# what session_save.py has already written to every existing save file. +# The conversion belongs at the wire builder, the same place +# codeReviewScores is coerced to dict[str, str]. + + +def _code_review_file_wire(row: dict[str, Any]) -> dict[str, Any]: + wire: dict[str, Any] = { + "path": str(row.get("path", "")), + "status": str(row.get("status", "modified")), + "additions": _non_negative_wire_int(row.get("additions")), + "deletions": _non_negative_wire_int(row.get("deletions")), + # `patch` rides as "" on purpose. The per-file patches are capped at + # MAX_FILE_PATCH_CHARS (6000) each and MAX_PR_FILES (100) of them, so + # forwarding them put up to ~600KB of diff text on EVERY scene + # republish - roughly ten times the 60KB codeReviewDiffText that was + # excluded from this payload for exactly that reason (see + # CodeReviewState's own comment). No frontend code reads it: + # CodeReviewNodeView renders the unified diff it lazily fetches via + # fetchCodeReviewDiffText, never these per-file patches. The field + # stays on the row because the contract declares it required; the + # engine still reads the real patches from node.state, which is where + # the fallback pre-screen scans them. + "patch": "", + "patchTruncated": bool(row.get("patch_truncated", False)), + } + # Genuinely absent (not "") for every non-rename, which is why + # CodeReviewFileRow.previousPath is the one Optional field on the row. + previous = str(row.get("previous_path", "") or "") + if previous: + wire["previousPath"] = previous + return wire + + +def _code_review_walkthrough_wire(row: dict[str, Any]) -> dict[str, Any]: + raw_paths = row.get("paths") + return { + "groupTitle": str(row.get("group_title", "")), + "paths": [str(path) for path in raw_paths] if isinstance(raw_paths, list) else [], + "explanation": str(row.get("explanation", "")), + } + + +def _code_review_finding_wire(row: dict[str, Any]) -> dict[str, Any]: + return { + "id": str(row.get("id", "")), + "severity": str(row.get("severity", "")), + "tier": str(row.get("tier", "")), + "category": str(row.get("category", "")), + "path": str(row.get("path", "")), + "line": _non_negative_wire_int(row.get("line")), + "title": str(row.get("title", "")), + "evidence": str(row.get("evidence", "")), + "impact": str(row.get("impact", "")), + "recommendation": str(row.get("recommendation", "")), + } + + +def _code_review_error_wire(row: dict[str, Any]) -> dict[str, Any]: + return { + "id": str(row.get("id", "")), + "severity": str(row.get("severity", "")), + "tier": str(row.get("tier", "")), + "kind": str(row.get("kind", "")), + "path": str(row.get("path", "")), + "line": _non_negative_wire_int(row.get("line")), + "title": str(row.get("title", "")), + "evidence": str(row.get("evidence", "")), + "fix": str(row.get("fix", "")), + } + + +def _code_review_qa_wire(row: dict[str, Any]) -> dict[str, Any]: + return { + "question": str(row.get("question", "")), + "answer": str(row.get("answer", "")), + } + + +def _non_negative_wire_int(value: Any) -> int: + """int for the wire, never raising. A row can reach the wire from a + hand-edited save file as well as from the engine, and a ValueError + here would fail the whole scene republish, not just one row.""" + try: + return max(0, int(value)) # type: ignore[call-overload] + except (TypeError, ValueError, OverflowError): + return 0 + + +def _content_parts_wire(parts: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None: + """R6.3: the wire-side transform for ChatState's own content_parts + (backend/domain/node_states.py) - a pure mapping function (same posture + as backend/canvas.py's _research_result_wire). None stays None (never + []), so "no multimodal content" and "multimodal content that happens to + be empty" remain distinguishable on the wire. Any part that is a dict + carrying raw bytes under its "data" key gets that key base64-encoded as + a string via content_codec.encode_image_bytes - matching content_codec. + process_content_for_serialization's own output shape exactly - while + leaving every other key/part untouched. Builds fresh dicts throughout; + never mutates the SceneNode's own in-memory parts (which must keep + holding real bytes, per content_parts's own field contract).""" + if parts is None: + return None + wire_parts: list[dict[str, Any]] = [] + for part in parts: + if isinstance(part, dict) and isinstance(part.get("data"), (bytes, bytearray)): + wire_part = dict(part) + wire_part["data"] = _content_codec.encode_image_bytes(bytes(part["data"])) + wire_parts.append(wire_part) + elif isinstance(part, dict): + wire_parts.append(dict(part)) + else: + wire_parts.append(part) + return wire_parts + + +# -- Other nested-row transforms -------------------------------------------- + + +def _history_wire(history: list[dict[str, Any]]) -> list[dict[str, Any]]: + # ADR-006 stage 6.4: the projection stays a strict allow-list (never + # spread the raw dict - legacy entries can carry arbitrary keys), widened + # by exactly one optional marker: "incomplete" flags a partial assistant + # reply whose stream died (see append_conversation_assistant_message). + return [ + {"role": m["role"], "content": m["content"], "incomplete": bool(m.get("incomplete", False))} + for m in history + ] + + +def _tool_calls_wire(calls: list[dict[str, Any]]) -> list[dict[str, Any]]: + # ADR-007 stage 7.4: see ChatState.tool_invocations' own comment and + # ToolInvocationRow's docstring (contracts/graphlink_scene_payload.py) + # for why `arguments` is JSON-encoded rather than passed as an object. + return [ + { + "id": str(call.get("id", "")), + "name": str(call.get("name", "")), + "argumentsJson": json.dumps(call.get("arguments") or {}, sort_keys=True), + "result": str(call.get("result", "")), + "isError": bool(call.get("is_error", False)), + } + for call in calls + ] + + +def _dict_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [dict(row) for row in rows] + + +def _string_dict(values: dict[Any, Any]) -> dict[str, str]: + # Scores ride the wire as dict[str, str] - the store_gitlink_context + # str-coercion precedent (contracts only admit string-valued dicts on + # SceneNodeRow). + return {str(k): str(v) for k, v in values.items()} + + +def _plan_steps_wire(steps: list[dict[str, Any]]) -> list[dict[str, Any]]: + # ADR-008 stage 8.3: the Builder's checklist - see PlanState's docstring. + return [ + {"id": s["id"], "title": s["title"], "status": s["status"], "detail": s["detail"]} + for s in steps + ] + + +def _builder_activity_wire(activity: list[dict[str, Any]]) -> list[dict[str, Any]]: + # ADR-008 stage 8.7: the run's own activity log - see PlanState's + # docstring for why this is untouched by undo. + return [ + { + "tool": a["tool"], "summary": a["summary"], + "outcome": a["outcome"], "stepId": a["stepId"], + "elapsedMs": a["elapsedMs"], + } + for a in activity + ] + + +def _harness_activity_wire(activity: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [ + {"tool": a["tool"], "summary": a["summary"], "outcome": a["outcome"], "elapsedMs": a["elapsedMs"]} + for a in activity + ] + + +def _harness_plan_wire(plan: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [{"text": s["text"], "status": s["status"]} for s in plan] + + +def _code_review_rows(builder: Callable[[dict[str, Any]], dict[str, Any]]) -> Callable[[Any], Any]: + return lambda rows: [builder(row) for row in rows] + + +# -- Field tables ------------------------------------------------------------- +# +# (wire key, attribute, transform or None, default). Built once from compact +# specs; the default is looked up in WIRE_DEFAULTS so there is exactly one +# place a default is written down on this side. + +_Entry = tuple[str, str, Callable[[Any], Any] | None, Any] + + +def _table(*specs: tuple[Any, ...]) -> tuple[_Entry, ...]: + return tuple( + (spec[0], spec[1], spec[2] if len(spec) > 2 else None, WIRE_DEFAULTS[spec[0]]) + for spec in specs + ) + + +# Fields on SceneNode itself, sent for every kind. +_NODE_FIELDS = _table( + ("content", "content"), + ("isCollapsed", "is_collapsed"), + ("isDocked", "is_docked"), + ("history", "history", _history_wire), + # R4.3: generic across any kind with its own dispatch slot. + ("pendingRequestId", "pending_request_id"), + # R6.1: core fields on every kind (see SceneNodeRow's own comment). + ("color", "color"), + ("headerColor", "header_color"), + ("itemIds", "item_ids", list), +) + +_GROUP_SIZE_FIELDS = (("groupWidth", "group_width"), ("groupHeight", "group_height")) + +# Each state class's own fields. Keyed by the exact classes the old flat +# builder tested with isinstance; _fields_for() resolves a state's type +# against these the same way, subclasses included. +_STATE_FIELDS: dict[type, tuple[_Entry, ...]] = { + ChatState: _table( + ("isUser", "is_user"), + # ADR-002 Workstream 1 ("Synthesize Branches") - output provenance. + ("provider", "provider"), + ("model", "model"), + # ADR-018 stage 18.3: the input routing pin, distinct from the pair + # above - see ChatState's own comment. + ("overrideProvider", "override_provider"), + ("overrideModelId", "override_model_id"), + ("indexIntoKnowledge", "index_into_knowledge"), + ("isBranchSynthesis", "is_branch_synthesis"), + ("synthesisInstructions", "synthesis_instructions"), + ("branchStatus", "branch_status"), + ("responseIncomplete", "response_incomplete"), + ("promptTokens", "prompt_tokens"), + ("completionTokens", "completion_tokens"), + ("estimatedCostUsd", "estimated_cost_usd"), + ("toolCalls", "tool_invocations", _tool_calls_wire), + ("chatScrollValue", "chat_scroll_value"), + ("contentParts", "content_parts", _content_parts_wire), + ), + CodeState: _table(("code", "code"), ("language", "language")), + DocumentState: _table( + ("attachmentKind", "attachment_kind"), + ("filePath", "file_path"), + ("mimeType", "mime_type"), + ("durationSeconds", "duration_seconds"), + ("byteSize", "byte_size"), + ("previewLabel", "preview_label"), + ), + ImageState: _table(("imageAssetId", "image_asset_id")), + WebResearchState: _table( + ("researchStage", "research_stage"), + ("researchCompleted", "research_completed"), + ("researchTotal", "research_total"), + ("researchActiveSourceId", "research_active_source_id"), + ("researchError", "research_error"), + ("researchResult", "research_result"), + ("researchRetainToKnowledge", "research_retain_to_knowledge"), + ), + ArtifactState: _table(("artifactContent", "artifact_content"), ("artifactError", "artifact_error")), + # gitlinkContextXml is DELIBERATELY absent - see GitlinkState's own + # comment; served on demand via the fetchGitlinkContext intent. + GitlinkState: _table( + ("gitlinkRepo", "gitlink_repo"), + ("gitlinkBranch", "gitlink_branch"), + ("gitlinkScopeMode", "gitlink_scope_mode"), + ("gitlinkLocalRoot", "gitlink_local_root"), + ("gitlinkRepoFilePaths", "gitlink_repo_file_paths", list), + ("gitlinkSelectedPaths", "gitlink_selected_paths", list), + ("gitlinkTaskPrompt", "gitlink_task_prompt"), + ("gitlinkContextStats", "gitlink_context_stats", dict), + ("gitlinkContextSummary", "gitlink_context_summary"), + # R5.3 post-review FIX 6: the lazy-fetch cache key - see + # GitlinkState's own comment for why the summary alone cannot be. + ("gitlinkContextVersion", "gitlink_context_version"), + ("gitlinkProposalMarkdown", "gitlink_proposal_markdown"), + ("gitlinkPendingChanges", "gitlink_pending_changes", _dict_rows), + ("gitlinkPreviewText", "gitlink_preview_text"), + ("gitlinkChangeFingerprint", "gitlink_change_fingerprint"), + ("gitlinkChangeState", "gitlink_change_state"), + ("gitlinkError", "gitlink_error"), + ), + # codeReviewDiffText is DELIBERATELY absent - see CodeReviewState's own + # comment; served on demand via fetchCodeReviewDiffText. + CodeReviewState: _table( + ("codeReviewPrUrl", "code_review_pr_url"), + ("codeReviewRepo", "code_review_repo"), + ("codeReviewPrNumber", "code_review_pr_number"), + ("codeReviewPrTitle", "code_review_pr_title"), + ("codeReviewPrState", "code_review_pr_state"), + ("codeReviewPrHtmlUrl", "code_review_pr_html_url"), + ("codeReviewBaseRef", "code_review_base_ref"), + ("codeReviewHeadRef", "code_review_head_ref"), + ("codeReviewAdditions", "code_review_additions"), + ("codeReviewDeletions", "code_review_deletions"), + ("codeReviewChangedFiles", "code_review_changed_files"), + ("codeReviewFiles", "code_review_files", _code_review_rows(_code_review_file_wire)), + ("codeReviewFilesTruncated", "code_review_files_truncated"), + ("codeReviewDiffTruncated", "code_review_diff_truncated"), + ("codeReviewDiffChars", "code_review_diff_chars"), + # The lazy-diff cache key, bumped by every successful fetch. + ("codeReviewDiffVersion", "code_review_diff_version"), + ("codeReviewWalkthrough", "code_review_walkthrough", _code_review_rows(_code_review_walkthrough_wire)), + ("codeReviewFindings", "code_review_findings", _code_review_rows(_code_review_finding_wire)), + ("codeReviewErrors", "code_review_errors", _code_review_rows(_code_review_error_wire)), + ("codeReviewDismissedIds", "code_review_dismissed_ids", list), + ("codeReviewTitle", "code_review_title"), + ("codeReviewOverview", "code_review_overview"), + ("codeReviewConfidence", "code_review_confidence"), + ("codeReviewScores", "code_review_scores", _string_dict), + ("codeReviewQualityScore", "code_review_quality_score"), + ("codeReviewVerdict", "code_review_verdict"), + ("codeReviewRisk", "code_review_risk"), + ("codeReviewQualitySummary", "code_review_quality_summary"), + ("codeReviewQa", "code_review_qa", _code_review_rows(_code_review_qa_wire)), + ("codeReviewState", "code_review_state"), + ("codeReviewError", "code_review_error"), + ), + # codeSandboxSandboxId is DELIBERATELY absent - pure internal + # directory-naming key, see CodeSandboxState's own comment. + CodeSandboxState: _table( + ("codeSandboxRequirements", "code_sandbox_requirements"), + ("codeSandboxPrompt", "code_sandbox_prompt"), + ("codeSandboxCode", "code_sandbox_code"), + ("codeSandboxOutput", "code_sandbox_output"), + ("codeSandboxAnalysis", "code_sandbox_analysis"), + ("codeSandboxAwaitingApproval", "code_sandbox_awaiting_approval"), + # R5.4: the frozen-at-approval-time snapshot, distinct from the live + # draft above - see CodeSandboxState's own comment. + ("codeSandboxApprovalRequirements", "code_sandbox_approval_requirements"), + ("codeSandboxApprovalAllowSourceBuilds", "code_sandbox_approval_allow_source_builds"), + ("codeSandboxApprovalIsRepair", "code_sandbox_approval_is_repair"), + ("codeSandboxError", "code_sandbox_error"), + ), + NoteState: _table( + ("isSystemPrompt", "is_system_prompt"), + ("isSummaryNote", "is_summary_note"), + ("isBranchComparison", "is_branch_comparison"), + ), + # groupManualWidth/Height are DELIBERATELY absent - server-side + # bookkeeping only (R6.1). + FrameState: _table(("isLocked", "is_locked"), *_GROUP_SIZE_FIELDS), + ContainerState: _table(*_GROUP_SIZE_FIELDS), + ChartState: _table( + ("chartType", "chart_type"), + ("chartData", "chart_data", dict), + ("chartError", "chart_error"), + ("chartWidth", "chart_width"), + ("chartHeight", "chart_height"), + ("chartAspectLocked", "chart_aspect_locked"), + ("chartSourceNodeId", "chart_source_node_id"), + ), + HtmlState: _table(("htmlSplitterState", "html_splitter_state")), + PlanState: _table( + ("planGoal", "plan_goal"), + ("planSteps", "plan_steps", _plan_steps_wire), + ("builderActivity", "builder_activity", _builder_activity_wire), + ("builderStatus", "builder_status"), + ("builderMode", "builder_mode"), + ("builderRunId", "builder_run_id"), + ("builderMaxSteps", "builder_max_steps"), + ("builderMaxTokens", "builder_max_tokens"), + ("builderMaxWallSeconds", "builder_max_wall_seconds"), + ("builderSpentSteps", "builder_spent_steps"), + ("builderSpentTokens", "builder_spent_tokens"), + ("builderSpentWallSeconds", "builder_spent_wall_seconds"), + ("builderAwaitingToolApproval", "builder_awaiting_tool_approval"), + ("builderApprovalToolName", "builder_approval_tool_name"), + ("builderApprovalSummary", "builder_approval_summary"), + ("builderStatusDetail", "builder_status_detail"), + ), + # PLAN-2026-08-24 H1: the conversation history deliberately does NOT + # cross the wire - the transcript lives in the workspace; only the render + # surface does (see HarnessState's own docstring). + HarnessState: _table( + ("harnessGoal", "harness_goal"), + ("harnessReply", "harness_reply"), + ("harnessStatus", "harness_status"), + ("harnessStatusDetail", "harness_status_detail"), + ("harnessRunId", "harness_run_id"), + ("harnessActivity", "harness_activity", _harness_activity_wire), + ("harnessAwaitingApproval", "harness_awaiting_approval"), + ("harnessApprovalToolName", "harness_approval_tool_name"), + ("harnessApprovalSummary", "harness_approval_summary"), + ("harnessApprovalSessionOffered", "harness_approval_session_offered"), + ("harnessPlan", "harness_plan", _harness_plan_wire), + ("harnessAwaitingQuestion", "harness_awaiting_question"), + ("harnessQuestion", "harness_question"), + ("harnessContextTokens", "harness_context_tokens"), + ("harnessMaxContextTokens", "harness_max_context_tokens"), + ("harnessCompactions", "harness_compactions"), + ("harnessWorkspacePath", "harness_workspace_path"), + ("harnessWorkspaceActive", "harness_workspace_active"), + ("harnessMaxTurns", "harness_max_turns"), + ("harnessSpentTurns", "harness_spent_turns"), + ("harnessSpentTokens", "harness_spent_tokens"), + ), +} + +_FIELDS_BY_TYPE: dict[type, tuple[_Entry, ...]] = {} + + +def _fields_for(state_type: type) -> tuple[_Entry, ...]: + """The state-owned entries for one concrete state type, resolved like the + isinstance checks they replace (a subclass inherits its bases' fields) + and cached per type. A plugin's own NodeState subclass matches nothing + and gets the node-level fields only - exactly what the flat builder + gave it.""" + fields = _FIELDS_BY_TYPE.get(state_type) + if fields is None: + fields = tuple( + entry + for owner, entries in _STATE_FIELDS.items() + if issubclass(state_type, owner) + for entry in entries + ) + _FIELDS_BY_TYPE[state_type] = fields + return fields + + +def _put_non_default(wire: dict[str, Any], source: object, entries: tuple[_Entry, ...]) -> None: + for key, attr, transform, default in entries: + value = getattr(source, attr) + if transform is not None: + value = transform(value) + if value is default or (value.__class__ is default.__class__ and value == default): + continue + wire[key] = value + + +def node_wire(node: SceneNode, *, is_final_deliverable: bool, plugin_state: dict[str, str]) -> dict[str, Any]: + """One node's sparse wire row: identity keys, then every field whose + value differs from its wire default. See the module docstring for the + contract this relies on and the tests that pin it.""" + wire: dict[str, Any] = { + "id": node.id, + "x": node.x, + "y": node.y, + "title": node.title, + "kind": node.kind, + } + _put_non_default(wire, node, _NODE_FIELDS) + if is_final_deliverable: + wire["isFinalDeliverable"] = True + if node.state is not None: + _put_non_default(wire, node.state, _fields_for(type(node.state))) + if plugin_state: + # ADR-014 stage 14.2: the Plugin SDK's generic live-wire fallback - + # see SceneDocument._plugin_state_wire. + wire["pluginState"] = plugin_state + return wire + + +def wire_keys() -> frozenset[str]: + """Every key node_wire can ever emit - for the drift tests.""" + keys = set(IDENTITY_KEYS) + keys.update(entry[0] for entry in _NODE_FIELDS) + for entries in _STATE_FIELDS.values(): + keys.update(entry[0] for entry in entries) + keys.update(("isFinalDeliverable", "pluginState")) + return frozenset(keys) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 27a52c82..1e8155e6 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,3 +1,4 @@ +import json import os from pathlib import Path @@ -265,3 +266,27 @@ def busy_count(dispatcher, kind): awaited, single-shot" kind (chart, note) now living in AgentDispatcher._runs.""" return sum(1 for handle in dispatcher._runs.values() if handle.kind == kind) + + +def client_row(row): + """One scene node row as the CLIENT reads it. Rows cross the wire sparse + (backend/domain/node_wire.py): every field at its contract default is + omitted, and the generated hydrateSceneNodeRow restores it before any + frontend code reads the row. Tests about what a node looks like to the + frontend read this; tests about the bytes read scene_payload() raw. + `contentParts` is the one wire key the contract does not declare, so - + like the client - this never restores it.""" + from backend.domain.node_wire import WIRE_DEFAULTS + + restored = { + key: json.loads(json.dumps(default)) + for key, default in WIRE_DEFAULTS.items() + if key != "contentParts" + } + restored.update(row) + return restored + + +def client_rows(doc): + """scene_payload()'s node rows as the client reads them - see client_row.""" + return [client_row(row) for row in doc.scene_payload()["nodes"]] diff --git a/backend/tests/perf/measure_baselines.py b/backend/tests/perf/measure_baselines.py index 3179fd55..057ba272 100644 --- a/backend/tests/perf/measure_baselines.py +++ b/backend/tests/perf/measure_baselines.py @@ -16,23 +16,41 @@ from __future__ import annotations import json +import statistics import time from backend.tests.perf.graph_factory import ALL_WORKLOADS, Workload +# Each timing is the MEDIAN of this many publishes of the same document, not +# one sample. A single sample of a 2-8ms operation on a shared CI runner +# swings past check_baseline.py's 20% threshold on scheduling jitter alone - +# the same noise that made the ungated fixture_build_ms fail 5 of 13 nightly +# runs - while the median of several moves only when the code does. The first +# publish also pays one-off warm-up (per-type lookups cached on first use), +# which the median discards rather than bills to every run. +TIMING_REPEATS = 7 + def _measure(workload: Workload) -> dict: t0 = time.perf_counter() doc = workload.build() build_s = time.perf_counter() - t0 - t0 = time.perf_counter() - payload = doc.scene_payload() - payload_build_s = time.perf_counter() - t0 + payload_samples: list[float] = [] + serialize_samples: list[float] = [] + total_samples: list[float] = [] + for _ in range(TIMING_REPEATS): + t0 = time.perf_counter() + payload = doc.scene_payload() + payload_build_s = time.perf_counter() - t0 - t0 = time.perf_counter() - serialized = json.dumps(payload) - serialize_s = time.perf_counter() - t0 + t0 = time.perf_counter() + serialized = json.dumps(payload) + serialize_s = time.perf_counter() - t0 + + payload_samples.append(payload_build_s) + serialize_samples.append(serialize_s) + total_samples.append(payload_build_s + serialize_s) total_bytes = len(serialized.encode("utf-8")) one_node_bytes = len(json.dumps(payload["nodes"][-1]).encode("utf-8")) if payload["nodes"] else 0 @@ -42,9 +60,9 @@ def _measure(workload: Workload) -> dict: "node_count": len(doc.nodes), "edge_count": len(doc.edges), "fixture_build_ms": round(build_s * 1000, 2), - "payload_build_ms": round(payload_build_s * 1000, 2), - "serialize_ms": round(serialize_s * 1000, 2), - "publish_total_ms": round((payload_build_s + serialize_s) * 1000, 2), + "payload_build_ms": round(statistics.median(payload_samples) * 1000, 2), + "serialize_ms": round(statistics.median(serialize_samples) * 1000, 2), + "publish_total_ms": round(statistics.median(total_samples) * 1000, 2), "full_snapshot_kib": round(total_bytes / 1024, 1), "single_node_payload_kib": round(one_node_bytes / 1024, 3), } diff --git a/backend/tests/test_canvas.py b/backend/tests/test_canvas.py index e469c616..1bb93fce 100644 --- a/backend/tests/test_canvas.py +++ b/backend/tests/test_canvas.py @@ -39,6 +39,7 @@ from backend.notifications import NotificationState from backend.tests.conftest import ( chat_slots, + client_rows, code_sandbox_slots, drain_runs, gitlink_run_slots, @@ -315,7 +316,7 @@ def test_scene_payload_includes_chat_fields_defaulted_for_placeholders(): doc = SceneDocument() doc.add_node(0, 0, "plain") doc.add_chat_node(1, 1, "real content", True) - rows = {n["title"]: n for n in doc.scene_payload()["nodes"]} + rows = {n["title"]: n for n in client_rows(doc)} assert rows["plain"]["kind"] == "placeholder" assert rows["plain"]["content"] == "" assert rows["plain"]["isUser"] is False @@ -427,12 +428,12 @@ def test_set_node_docked_toggles_true_then_false(): doc.set_node_docked(node.id, True) assert doc.nodes[node.id].is_docked is True - row = {n["id"]: n for n in doc.scene_payload()["nodes"]}[node.id] + row = {n["id"]: n for n in client_rows(doc)}[node.id] assert row["isDocked"] is True doc.set_node_docked(node.id, False) assert doc.nodes[node.id].is_docked is False - row = {n["id"]: n for n in doc.scene_payload()["nodes"]}[node.id] + row = {n["id"]: n for n in client_rows(doc)}[node.id] assert row["isDocked"] is False @@ -706,7 +707,7 @@ def blocking_chat(task, messages, **kwargs): # Mid-flight: pendingRequestId surfaces as non-None both on the # domain node and in scene_payload. assert document.nodes[node_id].pending_request_id is not None - rows = {n["id"]: n for n in document.scene_payload()["nodes"]} + rows = {n["id"]: n for n in client_rows(document)} assert rows[node_id]["pendingRequestId"] is not None assert rows[node_id]["pendingRequestId"] == document.nodes[node_id].pending_request_id @@ -722,7 +723,7 @@ def blocking_chat(task, messages, **kwargs): notice = await bus.publish("notification") assert notice["visible"] is False, "a real reply landing is not a deferral - no notification fires" assert document.nodes[node_id].pending_request_id is None, "cleared again once the reply lands" - rows = {n["id"]: n for n in document.scene_payload()["nodes"]} + rows = {n["id"]: n for n in client_rows(document)} assert rows[node_id]["pendingRequestId"] is None asyncio.run(run()) @@ -3445,11 +3446,11 @@ def test_scene_payload_includes_artifact_content_for_artifact_kind_rows(): parent = doc.add_node(1, 1, "parent") node = doc.add_artifact_node(2, 2, parent.id) - rows = {n["id"]: n for n in doc.scene_payload()["nodes"]} + rows = {n["id"]: n for n in client_rows(doc)} assert rows["n0"]["artifactContent"] == "" doc.complete_artifact_generation(node.id, "the drafted document", "done") - row = {n["id"]: n for n in doc.scene_payload()["nodes"]}[node.id] + row = {n["id"]: n for n in client_rows(doc)}[node.id] assert row["kind"] == "artifact" assert row["artifactContent"] == "the drafted document" @@ -3611,7 +3612,7 @@ def test_store_gitlink_context_excluded_from_scene_payload(): assert node.state.gitlink_context_xml == huge_xml, "the domain object DOES hold the full text" - row = {n["id"]: n for n in doc.scene_payload()["nodes"]}[node.id] + row = {n["id"]: n for n in client_rows(doc)}[node.id] assert "gitlinkContextXml" not in row assert row["gitlinkContextSummary"] == "Scanned 3 files." assert row["gitlinkScopeMode"] == "selected" @@ -4258,7 +4259,7 @@ def test_scene_payload_code_sandbox_fields_default_correctly_and_excludes_sandbo doc = SceneDocument() parent = doc.add_node(0, 0, "parent") node = doc.add_code_sandbox_node(0, 0, parent.id) - row = {n["id"]: n for n in doc.scene_payload()["nodes"]}[node.id] + row = {n["id"]: n for n in client_rows(doc)}[node.id] assert "codeSandboxSandboxId" not in row, ( "the internal sandbox directory-naming key must never reach the wire" ) @@ -5800,7 +5801,7 @@ def test_scene_payload_exposes_note_frame_and_container_fields(): frame = doc.create_frame([m1.id]) doc.set_group_color(frame.id, "#4a7c59", "#2f5b3c") - payload_by_id = {n["id"]: n for n in doc.scene_payload()["nodes"]} + payload_by_id = {n["id"]: n for n in client_rows(doc)} note_row = payload_by_id[note.id] assert note_row["kind"] == "note" @@ -5986,8 +5987,7 @@ def test_scene_payload_exposes_all_chart_fields(): parent = doc.add_node(0, 0, "parent") chart = doc.add_chart_node(0, 0, parent.id, "bar", dict(_CHART_DATA), chart_error="degraded") - payload = doc.scene_payload() - row = next(n for n in payload["nodes"] if n["id"] == chart.id) + row = next(n for n in client_rows(doc) if n["id"] == chart.id) assert row["chartType"] == "bar" assert row["chartData"] == _CHART_DATA @@ -6354,17 +6354,20 @@ def test_scene_payload_round_trips_content_parts_with_base64_encoded_image_bytes assert isinstance(chat_node.state.content_parts[1]["data"], bytes) -def test_scene_payload_content_parts_is_none_not_empty_list_when_unset(): +def test_scene_payload_omits_content_parts_rather_than_sending_an_empty_list_when_unset(): # R6.3: "no multimodal content" (None) must stay distinguishable on the # wire from "multimodal content that happens to be empty" ([]) - the - # overwhelmingly common plain-text chat node must get contentParts: - # null, never []. + # overwhelmingly common plain-text chat node must not get contentParts: + # [] - it is left off the row entirely. doc = SceneDocument() chat_node = doc.add_chat_node(0, 0, "plain text only", True) assert chat_node.state.content_parts is None payload_node = next(n for n in doc.scene_payload()["nodes"] if n["id"] == chat_node.id) - assert payload_node["contentParts"] is None + # Sparse wire (backend/domain/node_wire.py): None is the default, so the + # key is left out - which still distinguishes it from an empty list, + # the one value that IS sent. + assert "contentParts" not in payload_node # -- R7.5e: Collapse All / Expand All ----------------------------------------- @@ -7034,7 +7037,7 @@ def test_scene_payload_includes_branch_status_and_final_deliverable(): doc.set_final_deliverable(node.id, True) other = doc.add_chat_node(0, 160, "other", True) - rows = {n["id"]: n for n in doc.scene_payload()["nodes"]} + rows = {n["id"]: n for n in client_rows(doc)} assert rows[node.id]["branchStatus"] == "accepted" assert rows[node.id]["isFinalDeliverable"] is True assert rows[other.id]["branchStatus"] == "active" diff --git a/backend/tests/test_node_wire.py b/backend/tests/test_node_wire.py new file mode 100644 index 00000000..0cd2b119 --- /dev/null +++ b/backend/tests/test_node_wire.py @@ -0,0 +1,242 @@ +"""The sparse per-node wire row (backend/domain/node_wire.py). + +A node sends its identity keys plus every field that differs from the +contract default, and the client restores the rest from the same defaults +(the generated hydrateSceneNodeRow). These tests pin the three things that +arrangement depends on: the backend's defaults ARE the contract's, every +contract field has a route onto the wire, and "differs from the default" is +decided strictly enough that restoring a row never changes a value. +""" + +from __future__ import annotations + +import dataclasses +import json +import sys +from pathlib import Path + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +import backend.domain.node_states as node_states +from backend.domain.graph import SceneDocument +from backend.domain.model import SceneNode +from backend.domain.node_wire import IDENTITY_KEYS, WIRE_DEFAULTS, _NODE_FIELDS, _fields_for, wire_keys +from backend.tests.conftest import client_row +from graphlink_wire_schema import NO_DEFAULT, wire_default + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "contracts")) + +from graphlink_scene_payload import SceneNodeRow # noqa: E402 + +STATE_CLASSES = ( + node_states.ArtifactState, + node_states.ChartState, + node_states.ChatState, + node_states.CodeReviewState, + node_states.CodeSandboxState, + node_states.CodeState, + node_states.ContainerState, + node_states.DocumentState, + node_states.FrameState, + node_states.GitlinkState, + node_states.HarnessState, + node_states.HtmlState, + node_states.ImageState, + node_states.NoteState, + node_states.PlanState, + node_states.WebResearchState, +) + + +def _node(state=None, **fields) -> tuple[SceneDocument, SceneNode]: + doc = SceneDocument() + node = SceneNode(id="n1", x=1.0, y=2.0, title="t", kind="k", state=state, **fields) + doc.nodes[node.id] = node + return doc, node + + +# -- the contract ------------------------------------------------------------ + + +def test_the_backend_defaults_are_the_contract_defaults(): + # The client restores omitted fields from SceneNodeRow's declared + # defaults, the backend omits by WIRE_DEFAULTS - they must be one table. + contract = { + field.name: default + for field in dataclasses.fields(SceneNodeRow) + if (default := wire_default(field)) is not NO_DEFAULT + } + backend = {key: value for key, value in WIRE_DEFAULTS.items() if key != "contentParts"} + assert backend == contract + # Equal is not enough: 0 == False and 480 == 480.0 in Python, and the + # omission test is type-strict, so a default of the wrong type would + # send values the client already has. + assert {k: type(v) for k, v in backend.items()} == {k: type(v) for k, v in contract.items()} + + +def test_every_contract_field_has_a_route_onto_the_wire(): + contract_keys = {field.name for field in dataclasses.fields(SceneNodeRow)} + assert wire_keys() == contract_keys | {"contentParts"} + no_default = {f.name for f in dataclasses.fields(SceneNodeRow) if wire_default(f) is NO_DEFAULT} + assert set(IDENTITY_KEYS) == no_default + + +def test_no_field_is_owned_twice(): + node_level = {entry[0] for entry in _NODE_FIELDS} + for state_class in STATE_CLASSES: + keys = [entry[0] for entry in _fields_for(state_class)] + assert len(keys) == len(set(keys)), state_class.__name__ + assert not node_level & set(keys), state_class.__name__ + + +# -- omission ------------------------------------------------------------------ + + +@pytest.mark.parametrize("state_class", [None, *STATE_CLASSES], ids=lambda c: getattr(c, "__name__", "None")) +def test_a_node_at_its_defaults_sends_little_more_than_its_identity(state_class): + doc, node = _node(state_class() if state_class else None) + row = doc._node_wire(node) + sent = set(row) - set(IDENTITY_KEYS) + # Two state classes start away from the contract default on purpose: a + # new Builder plan and a new harness node begin with real budgets and a + # real status, so those fields are information, not padding. + expected = { + node_states.PlanState: {"builderMaxSteps", "builderMaxTokens", "builderMaxWallSeconds", "builderMode", "builderStatus"}, + node_states.HarnessState: {"harnessMaxContextTokens", "harnessMaxTurns", "harnessStatus"}, + }.get(state_class, set()) + assert sent == expected + + +def test_a_field_moved_off_its_default_is_sent_and_left_out_again_when_it_returns(): + doc, node = _node(node_states.ChatState()) + node.state.branch_status = "accepted" + node.pending_request_id = "req-1" + row = doc._node_wire(node) + assert row["branchStatus"] == "accepted" + assert row["pendingRequestId"] == "req-1" + + node.state.branch_status = "active" + node.pending_request_id = None + row = doc._node_wire(node) + assert "branchStatus" not in row + assert "pendingRequestId" not in row + assert client_row(row)["branchStatus"] == "active" + assert client_row(row)["pendingRequestId"] is None + + +def test_omission_is_type_strict(): + # Each of these equals its default under Python's ==, but not as JSON: + # False is not 0, 1 is not true, 480 is not the float default. They + # must be sent as-is, never folded into the default. + doc, node = _node(node_states.WebResearchState()) + node.state.research_completed = False + assert doc._node_wire(node)["researchCompleted"] is False + + doc, node = _node(node_states.ChatState()) + node.state.is_user = 0 + assert doc._node_wire(node)["isUser"] == 0 + + doc, node = _node(node_states.FrameState()) + node.state.is_locked = 1 + assert doc._node_wire(node)["isLocked"] == 1 + + +def test_content_parts_none_is_left_out_but_an_empty_list_is_sent(): + doc, node = _node(node_states.ChatState()) + assert "contentParts" not in doc._node_wire(node) + node.state.content_parts = [] + assert doc._node_wire(node)["contentParts"] == [] + + +def test_a_field_reset_to_its_default_still_produces_a_patch(): + # take_dirty_patch_ops diffs whole rows. A field returning to its default + # REMOVES a key, and the upsert that carries the shorter row is what tells + # the client (which replaces the row and restores the default) to drop + # the old value - e.g. a node that finished running must stop looking busy. + doc = SceneDocument() + node = doc.add_node(0, 0, "n") + assert doc.take_dirty_patch_ops() is None # first publish: full snapshot + node.pending_request_id = "req-1" + ops = doc.take_dirty_patch_ops() + assert ops == [{"op": "upsertNode", "node": doc._node_wire(node)}] + assert ops[0]["node"]["pendingRequestId"] == "req-1" + + node.pending_request_id = None + ops = doc.take_dirty_patch_ops() + assert ops is not None and len(ops) == 1 + assert "pendingRequestId" not in ops[0]["node"] + assert client_row(ops[0]["node"])["pendingRequestId"] is None + + +# -- restoring a row changes nothing ------------------------------------------- + + +def _dense_row(doc: SceneDocument, node: SceneNode) -> dict: + """The row with NOTHING omitted - what every node used to send.""" + row = {key: getattr(node, key) for key in IDENTITY_KEYS} + for key, default in WIRE_DEFAULTS.items(): + row[key] = default + for key, attr, transform, _ in _NODE_FIELDS: + value = getattr(node, attr) + row[key] = transform(value) if transform else value + if node.state is not None: + for key, attr, transform, _ in _fields_for(type(node.state)): + value = getattr(node.state, attr) + row[key] = transform(value) if transform else value + row["isFinalDeliverable"] = node.id == doc.final_deliverable_node_id + row["pluginState"] = doc._plugin_state_wire(node) + return row + + +_SCALARS = st.one_of( + st.none(), st.booleans(), st.integers(-3, 3), st.sampled_from([0.0, 480.0, 340.0, 0.5]), + st.sampled_from(["", "active", "selected", "draft", "none", "x"]), +) + + +def _value_for(field: dataclasses.Field) -> st.SearchStrategy: + default = field.default_factory() if field.default_factory is not dataclasses.MISSING else field.default + if isinstance(default, list) or field.name == "content_parts": + # Every list the builders read holds dicts they index by name; an + # empty list is the case the omission logic has to get right. + return st.just([]) + if isinstance(default, dict): + return st.sampled_from([{}, {"k": "v"}]) + return _SCALARS + + +@st.composite +def _nodes(draw): + state_class = draw(st.sampled_from([None, *STATE_CLASSES])) + state = state_class() if state_class else None + if state is not None: + for field in dataclasses.fields(state): + if draw(st.booleans()): + setattr(state, field.name, draw(_value_for(field))) + doc, node = _node(state) + node.content = draw(st.sampled_from(["", "hello"])) + node.is_collapsed = draw(st.sampled_from([False, True, 0, 1])) + node.pending_request_id = draw(st.sampled_from([None, "", "req"])) + node.color = draw(st.sampled_from([None, "", "#fff"])) + node.item_ids = draw(st.sampled_from([[], ["a"]])) + if draw(st.booleans()): + doc.final_deliverable_node_id = node.id + return doc, node + + +@given(_nodes()) +def test_a_restored_row_is_exactly_the_row_that_used_to_be_sent(case): + doc, node = case + sparse = doc._node_wire(node) + dense = _dense_row(doc, node) + # Compared as JSON TEXT - what actually crosses the wire and what the + # client holds after restoring. Comparing the dicts would be blind to + # exactly the bug worth catching here: False == 0 in Python, but a row + # restored as `false` where `0` was sent is a changed value. + restored = client_row(json.loads(json.dumps(sparse))) + expected = json.loads(json.dumps(dense)) + if dense["contentParts"] is None: + del expected["contentParts"] + assert json.dumps(restored, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/backend/tests/test_plugin_grants.py b/backend/tests/test_plugin_grants.py index 16e1eafc..303763cc 100644 --- a/backend/tests/test_plugin_grants.py +++ b/backend/tests/test_plugin_grants.py @@ -43,6 +43,7 @@ ) from backend.plugins import plugins_payload, register_plugins from backend.session_load import restore_chat_into_document +from backend.tests.conftest import client_row from backend.session_save import build_chat_data from graphlink_settings_store import SettingsManager @@ -293,14 +294,14 @@ def test_revoking_a_plugins_grant_stops_its_live_wire_serializer_from_running(tm # Granted: the live wire carries the plugin's own custom state. wire = canvas_document.scene_payload() - node_wire = next(n for n in wire["nodes"] if n["id"] == node_id) + node_wire = next(client_row(n) for n in wire["nodes"] if n["id"] == node_id) assert node_wire["pluginState"] == {"clicks": "3"} # Revoke - the SAME live node, no re-registration, no session restart. settings_manager.set_plugin_grant("statefulgrant1", False) wire2 = canvas_document.scene_payload() - node_wire2 = next(n for n in wire2["nodes"] if n["id"] == node_id) + node_wire2 = next(client_row(n) for n in wire2["nodes"] if n["id"] == node_id) # Degrades to {} (the same "no plugin state to show" shape a plugin # with no serializer at all produces) - proving the wrapper re-checks # the CURRENT grant on every publish, not just at registration time. diff --git a/backend/tests/test_plugin_sdk.py b/backend/tests/test_plugin_sdk.py index 3e25821c..ecc2152b 100644 --- a/backend/tests/test_plugin_sdk.py +++ b/backend/tests/test_plugin_sdk.py @@ -28,6 +28,7 @@ from backend.plugins import register_plugins from backend.session_load import restore_chat_into_document from backend.session_save import build_chat_data +from backend.tests.conftest import client_row from graphlink_settings_store import SettingsManager # The three demo plugins (hello_node, counter_node, sandboxed_demo) live in @@ -743,13 +744,13 @@ def test_live_wire_scene_payload_includes_plugin_state_when_serializer_registere ) wire = canvas_document.scene_payload() - node_wire = next(n for n in wire["nodes"] if n["id"] == node_id) + node_wire = next(client_row(n) for n in wire["nodes"] if n["id"] == node_id) # Coerced to str on the wire (SceneNodeRow.pluginState is dict[str, str] # - see contracts/graphlink_scene_payload.py's own comment) even though # `clicks` is a real int in memory and in the save file. assert node_wire["pluginState"] == {"clicks": "3", "label": "initial"} - parent_wire = next(n for n in wire["nodes"] if n["id"] == parent.id) + parent_wire = next(client_row(n) for n in wire["nodes"] if n["id"] == parent.id) assert parent_wire["pluginState"] == {} @@ -764,7 +765,7 @@ def test_live_wire_plugin_state_is_empty_for_a_plugin_kind_with_no_serializer(tm ) wire = canvas_document.scene_payload() - node_wire = next(n for n in wire["nodes"] if n["id"] == node_id) + node_wire = next(client_row(n) for n in wire["nodes"] if n["id"] == node_id) assert node_wire["pluginState"] == {} @@ -800,7 +801,7 @@ def register(host: HostContext) -> None: # Live wire: a raising serializer degrades to {}, never crashes the # whole scene publish. wire = canvas_document.scene_payload() - node_wire = next(n for n in wire["nodes"] if n["id"] == node_id) + node_wire = next(client_row(n) for n in wire["nodes"] if n["id"] == node_id) assert node_wire["pluginState"] == {} # Save file: same degrade-not-crash posture - the node's universal diff --git a/backend/tests/test_wire_schema_validation.py b/backend/tests/test_wire_schema_validation.py index 24ace38b..4ace0a4d 100644 --- a/backend/tests/test_wire_schema_validation.py +++ b/backend/tests/test_wire_schema_validation.py @@ -270,8 +270,9 @@ def test_a_sankey_flow_with_a_wrong_typed_field_is_reported_at_its_own_indexed_n def test_a_non_chart_node_s_all_optional_default_chart_data_row_still_validates_with_no_errors(): - # The non-chart-node majority path: chartData is ALWAYS present on the - # wire (never omitted - see SceneNodeRow's own field comment) but every + # The non-chart-node majority path: a sparse row omits chartData for a + # non-chart node and the client restores it as `{}` (backend/domain/ + # node_wire.py) - the shape validated here - so it is present, but every # one of its fields is None for a node that was never a chart. payload = _chart_node_payload(kind="chat", chartType="") assert validate_payload(payload, SceneNodeRow) == [] diff --git a/contracts/codegen.py b/contracts/codegen.py index ba36b984..28692c2d 100644 --- a/contracts/codegen.py +++ b/contracts/codegen.py @@ -41,7 +41,7 @@ # added explicitly rather than relying on cwd or an editable install. sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from graphlink_wire_schema import json_schema_for +from graphlink_wire_schema import NO_DEFAULT, json_schema_for, omits_defaults, wire_default __all__ = ["schema_json_for", "typescript_for", "GENERATED_ARTIFACTS"] @@ -183,22 +183,168 @@ def typescript_for(dataclass_type: type, *, source: str) -> str: lines.append(_VALIDATOR_PREAMBLE) + sparse_fields = _root_sparse_fields(dataclass_type) + for row_type in dict.fromkeys(row for row, _ in sparse_fields.values()): + lines.append(_hydrator_for(row_type)) + if sparse_fields: + lines.append(_root_hydrator_for(root_name, sparse_fields)) + for name in ordered: lines.append(_validator_for(name, collected[name])) - lines.append( - f"export function validate{root_name}(value: unknown): ValidationResult<{root_name}> {{\n" - f" const errors: string[] = [];\n" - f" check{root_name}(value, \"$\", errors);\n" - f" return errors.length === 0\n" - f" ? {{ ok: true, value: value as {root_name} }}\n" - f" : {{ ok: false, errors }};\n" - f"}}\n" - ) + if sparse_fields: + # The one change a sparse row makes to its root's validator: restore + # the omitted fields FIRST, then validate - and hand back the restored + # value, so every reader downstream sees the full row. + lines.append( + f"export function validate{root_name}(value: unknown): ValidationResult<{root_name}> {{\n" + f" const hydrated = hydrate{root_name}(value);\n" + f" const errors: string[] = [];\n" + f" check{root_name}(hydrated, \"$\", errors);\n" + f" return errors.length === 0\n" + f" ? {{ ok: true, value: hydrated as {root_name} }}\n" + f" : {{ ok: false, errors }};\n" + f"}}\n" + ) + else: + lines.append( + f"export function validate{root_name}(value: unknown): ValidationResult<{root_name}> {{\n" + f" const errors: string[] = [];\n" + f" check{root_name}(value, \"$\", errors);\n" + f" return errors.length === 0\n" + f" ? {{ ok: true, value: value as {root_name} }}\n" + f" : {{ ok: false, errors }};\n" + f"}}\n" + ) return "\n".join(lines) +def _root_sparse_fields(root: type) -> dict[str, tuple[type, bool]]: + """{root field name: (sparse row type, held as a list)} for every root + field that holds a sparse row (graphlink_wire_schema.py's "SPARSE ROWS") directly or as a + list. Restoring is only generated at that depth; a sparse row anywhere + deeper raises here, so one can never ship with nothing restoring it.""" + import dataclasses + import typing + from typing import get_args, get_origin + + hints = typing.get_type_hints(root) + direct: dict[str, tuple[type, bool]] = {} + for field in dataclasses.fields(root): + annotation = hints[field.name] + is_list = get_origin(annotation) is list + if is_list: + (annotation,) = get_args(annotation) + if isinstance(annotation, type) and dataclasses.is_dataclass(annotation) and omits_defaults(annotation): + direct[field.name] = (annotation, is_list) + + def deeper(dataclass_type: type, seen: set[type]) -> None: + if dataclass_type in seen: + return + seen.add(dataclass_type) + nested_hints = typing.get_type_hints(dataclass_type) + for field in dataclasses.fields(dataclass_type): + for nested in _nested_dataclasses(_strip_optional(nested_hints[field.name])): + if omits_defaults(nested) and not (dataclass_type is root and field.name in direct): + raise ValueError( + f"{nested.__name__} omits defaults but sits below {root.__name__}.{field.name}; " + "codegen only restores sparse rows held directly by the root payload" + ) + deeper(nested, seen) + + deeper(root, set()) + return direct + + +def _strip_optional(annotation: Any) -> Any: + import types as _types + import typing + from typing import get_args, get_origin + + if get_origin(annotation) in (_types.UnionType, typing.Union): + return next(a for a in get_args(annotation) if a is not type(None)) + return annotation + + +def _screaming_snake(name: str) -> str: + """SceneNodeRow -> SCENE_NODE_ROW.""" + return "".join(f"_{c}" if c.isupper() and i else c for i, c in enumerate(name)).upper() + + +def _hydrator_for(row_type: type) -> str: + """The restore half of a sparse row: its declared defaults as a table, + and hydrate(), which fills in every ABSENT key. A present key - + including an explicit null - is data and is left for the validator to + judge. Containers restore as fresh empty values (wire_default guarantees + every container default is empty), never a shared instance. A row with + nothing missing comes back as the SAME object, so re-validating rows that + are already whole (the scene store re-checks the whole scene after every + patch) allocates nothing and keeps every untouched row's identity.""" + import dataclasses + + name = _ts_type_name(row_type.__name__) + table = f"{_screaming_snake(name)}_WIRE_DEFAULTS" + entries = [ + f" [{json.dumps(field.name)}, {json.dumps(default)}]," + for field in dataclasses.fields(row_type) + if (default := wire_default(field)) is not NO_DEFAULT + ] + return "\n".join([ + f"/** {name} crosses the wire sparse: the sender omits every field at its", + " * declared default (contracts: WIRE_OMITS_DEFAULTS). These are those", + " * defaults - the value every omitted field is restored to. */", + f"export const {table}: ReadonlyArray = [", + *entries, + "];", + "", + f"export function hydrate{name}(value: unknown): unknown {{", + " if (!isRecord(value)) return value;", + " let hydrated: Record | null = null;", + f" for (const [key, fallback] of {table}) {{", + " if (value[key] !== undefined) continue;", + " hydrated ??= { ...value };", + " hydrated[key] = Array.isArray(fallback) ? [] : isRecord(fallback) ? {} : fallback;", + " }", + " return hydrated ?? value;", + "}", + "", + ]) + + +def _root_hydrator_for(root_name: str, sparse_fields: dict[str, tuple[type, bool]]) -> str: + body = [ + f"function hydrate{root_name}(value: unknown): unknown {{", + " if (!isRecord(value)) return value;", + " let hydrated: Record | null = null;", + ] + # Only well-shaped fields are restored; anything else is left exactly as + # it arrived, for the validator to reject. + for field_name, (row_type, is_list) in sparse_fields.items(): + row = _ts_type_name(row_type.__name__) + key = json.dumps(field_name) + if is_list: + body += [ + " {", + f" const rows = value[{key}];", + " if (Array.isArray(rows)) {", + f" const restored = rows.map(hydrate{row});", + f" if (restored.some((row, i) => row !== rows[i])) (hydrated ??= {{ ...value }})[{key}] = restored;", + " }", + " }", + ] + else: + body += [ + " {", + f" const row = value[{key}];", + f" const restored = hydrate{row}(row);", + f" if (restored !== row) (hydrated ??= {{ ...value }})[{key}] = restored;", + " }", + ] + body += [" return hydrated ?? value;", "}", ""] + return "\n".join(body) + + _VALIDATOR_PREAMBLE = '''export type ValidationResult = | { ok: true; value: T } | { ok: false; errors: string[] }; diff --git a/contracts/graphlink_scene_payload.py b/contracts/graphlink_scene_payload.py index b9f49299..18105a99 100644 --- a/contracts/graphlink_scene_payload.py +++ b/contracts/graphlink_scene_payload.py @@ -143,7 +143,7 @@ fields (Source/Preview splitter position, chat scroll position) - populated for kind=="html"/kind=="chat" respectively, defaulted for every other kind. `contentParts` is DELIBERATELY NOT one of these 26: -it is a real wire field (backend/domain/graph.py's _content_parts_wire) +it is a real wire field (backend/domain/node_wire.py's _content_parts_wire) but SceneCanvas.tsx never reads it today (a backend-only multimodal round-trip capability, not a rendered feature) - C9 is specifically about fields an unsafe CAST reaches for, and no cast reaches for this @@ -189,7 +189,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Literal +from typing import ClassVar, Literal @dataclass @@ -464,6 +464,15 @@ class ChartDataRow: @dataclass class SceneNodeRow: + # Sparse on the wire: the sender (backend/domain/node_wire.py) leaves out + # every field that sits at its default below, and the client restores + # them from these same defaults - codegen emits hydrateSceneNodeRow(), + # which validateSceneState and the scene store's patch path run before + # anything reads a row. So every default here is ALSO the value a node + # of another kind reads for the field; change one deliberately. + # (graphlink_wire_schema.py: "SPARSE ROWS".) + WIRE_OMITS_DEFAULTS: ClassVar[bool] = True + id: str x: float y: float diff --git a/graphlink_wire_schema.py b/graphlink_wire_schema.py index a9cea228..47e5c8ed 100644 --- a/graphlink_wire_schema.py +++ b/graphlink_wire_schema.py @@ -39,6 +39,14 @@ The refusal to guess is the point: a future payload field typed as something outside this set fails loudly at generation time instead of producing a schema that quietly disagrees with the real payload. + +SPARSE ROWS: a payload dataclass that sets `WIRE_OMITS_DEFAULTS = True` (a +ClassVar, so never a field) is sent with every field that sits at its declared +default left out, and the receiver restores those from the same declaration - +the scene topic's SceneNodeRow, whose sender is backend/domain/node_wire.py and +whose client-side restore is generated by contracts/codegen.py. For such a row +a defaulted field is not required on the wire; its schema property carries the +restored value as `default`. Every other dataclass is unaffected. """ from __future__ import annotations @@ -51,6 +59,9 @@ __all__ = [ "json_schema_for", "validate_payload", + "omits_defaults", + "wire_default", + "NO_DEFAULT", "SchemaGenerationError", ] @@ -63,6 +74,36 @@ def _is_dataclass_type(annotation: Any) -> bool: return dataclasses.is_dataclass(annotation) and isinstance(annotation, type) +def omits_defaults(dataclass_type: type) -> bool: + """True for a sparse row - see this module's docstring.""" + return bool(getattr(dataclass_type, "WIRE_OMITS_DEFAULTS", False)) + + +NO_DEFAULT = object() + + +def wire_default(field: dataclasses.Field[Any]) -> Any: + """The JSON value a receiver restores for `field` when a sparse row omits + it, or NO_DEFAULT when the field declares none (always sent). A nested + dataclass default becomes its dict with every None field left out - the + shape the sender uses for an unset nested row (SceneNodeRow.chartData is + `{}` for every non-chart node). Container defaults must be EMPTY: a + receiver restores them as fresh empty values, never shared instances.""" + if field.default is not dataclasses.MISSING: + value = field.default + elif field.default_factory is not dataclasses.MISSING: + value = field.default_factory() + else: + return NO_DEFAULT + if dataclasses.is_dataclass(value) and not isinstance(value, type): + value = {k: v for k, v in dataclasses.asdict(value).items() if v is not None} + if isinstance(value, (list, dict)) and value: + raise SchemaGenerationError( + f"{field.name}: a sparse row's container default must be empty, got {value!r}" + ) + return value + + def _unwrap_optional(annotation: Any) -> tuple[Any, bool]: """Return (inner_type, is_optional) for `X | None` / Optional[X].""" origin = get_origin(annotation) @@ -145,12 +186,16 @@ def json_schema_for(dataclass_type: type, *, _path: str = "") -> dict[str, Any]: properties: dict[str, Any] = {} required: list[str] = [] + sparse = omits_defaults(dataclass_type) for field in dataclasses.fields(dataclass_type): annotation = hints[field.name] field_path = f"{_path}.{field.name}" if _path else f"{dataclass_type.__name__}.{field.name}" _, is_optional = _unwrap_optional(annotation) properties[field.name] = _schema_for_annotation(annotation, path=field_path) - if not is_optional: + default = wire_default(field) if sparse else NO_DEFAULT + if default is not NO_DEFAULT: + properties[field.name]["default"] = default + elif not is_optional: required.append(field.name) schema: dict[str, Any] = { @@ -181,6 +226,7 @@ def validate_payload(payload: Any, dataclass_type: type, *, _path: str = "$") -> hints = typing.get_type_hints(dataclass_type) known: set[str] = set() + sparse = omits_defaults(dataclass_type) for field in dataclasses.fields(dataclass_type): annotation = hints[field.name] @@ -189,7 +235,7 @@ def validate_payload(payload: Any, dataclass_type: type, *, _path: str = "$") -> field_path = f"{_path}.{field.name}" if field.name not in payload: - if not is_optional: + if not is_optional and not (sparse and wire_default(field) is not NO_DEFAULT): errors.append(f"{field_path}: missing required field") continue diff --git a/tests/test_node_state_migration.py b/tests/test_node_state_migration.py index 950b021f..2e239352 100644 --- a/tests/test_node_state_migration.py +++ b/tests/test_node_state_migration.py @@ -48,6 +48,8 @@ from pathlib import Path from backend.domain.graph import SceneDocument +from backend.domain.node_wire import wire_keys +from backend.tests.conftest import client_row from backend.domain.model import SceneNode REPO_ROOT = Path(__file__).resolve().parents[1] @@ -421,10 +423,16 @@ def _non_owning_kind_node(doc): def test_scene_payload_key_set_is_unchanged_by_the_migration(): + # Rows are sparse on the wire (backend/domain/node_wire.py) - a field at + # its default is left out - so the pinned set is every key the row + # builder can emit, not the keys of any one node's raw row... + assert sorted(wire_keys()) == _EXPECTED_SCENE_NODE_WIRE_KEYS + # ...and what the client reads once it restores a non-owning node's row + # is all of them, bar contentParts, which the contract never declared. doc = SceneDocument() _non_owning_kind_node(doc) - row = doc.scene_payload()["nodes"][-1] - assert sorted(row.keys()) == _EXPECTED_SCENE_NODE_WIRE_KEYS + row = client_row(doc.scene_payload()["nodes"][-1]) + assert sorted(row.keys()) == [key for key in _EXPECTED_SCENE_NODE_WIRE_KEYS if key != "contentParts"] # Every migrated field's wire VALUE for a node of a kind that does NOT own @@ -570,7 +578,8 @@ def test_scene_payload_key_set_is_unchanged_by_the_migration(): def test_migrated_field_wire_fallbacks_match_pre_migration_defaults(): doc = SceneDocument() _non_owning_kind_node(doc) - row = doc.scene_payload()["nodes"][-1] + # As the client reads it: a sparse row's omitted fields restored. + row = client_row(doc.scene_payload()["nodes"][-1]) mismatches = [ f"{key}: got {row[key]!r}, expected {expected!r}" for key, expected in _EXPECTED_NON_OWNING_KIND_WIRE_DEFAULTS.items() diff --git a/web_ui/src/app/canvas/DocumentViewPanel.test.tsx b/web_ui/src/app/canvas/DocumentViewPanel.test.tsx index c6fbfb71..aa8089af 100644 --- a/web_ui/src/app/canvas/DocumentViewPanel.test.tsx +++ b/web_ui/src/app/canvas/DocumentViewPanel.test.tsx @@ -32,6 +32,12 @@ beforeEach(() => { // the thing that broke. const LAZY_BODY_TIMEOUT = { timeout: 15_000 }; +// ...and each test must be allowed to outlive that wait. Raising only the +// wait left vitest's own 5000ms per-test timeout in charge, so a first import +// slower than 5s still failed the test ("Test timed out in 5000ms", a full +// `npm run check` on 2026-09-19) - the 15s wait could never actually be used. +vi.setConfig({ testTimeout: 20_000 }); + function renderPanel(overrides: Partial> = {}) { const props = { isOpen: true, diff --git a/web_ui/src/app/canvas/SceneCanvas.tsx b/web_ui/src/app/canvas/SceneCanvas.tsx index a27c0eb8..ddd96b5c 100644 --- a/web_ui/src/app/canvas/SceneCanvas.tsx +++ b/web_ui/src/app/canvas/SceneCanvas.tsx @@ -1273,9 +1273,10 @@ export function toFlowNodes( mimeType: n.mimeType, // Generated SceneNodeRow marks these `?: number | null` (optional // Python field), so the read can be `undefined`; DocumentNodeData - // is strictly `number | null` (matches the wire value, which is - // always present - the backend dataclass always serializes both - // keys). Coalesce here rather than loosening DocumentNodeData. + // is strictly `number | null` (matches the restored row, where + // both keys are always present - null when the sparse wire row + // left them out). Coalesce here rather than loosening + // DocumentNodeData. durationSeconds: n.durationSeconds ?? null, byteSize: n.byteSize ?? null, previewLabel: n.previewLabel, diff --git a/web_ui/src/app/canvas/sceneStore.test.ts b/web_ui/src/app/canvas/sceneStore.test.ts index b9ffcec6..39c6deff 100644 --- a/web_ui/src/app/canvas/sceneStore.test.ts +++ b/web_ui/src/app/canvas/sceneStore.test.ts @@ -124,7 +124,9 @@ function validScenePayload(overrides: Record = {}) { // WsTransport.handleMessage entirely (see makeFakeTransport above), so // nothing here actually enforces version compatibility, but keeping the // fixture honest matters for anyone reading it as "what scene actually - // looks like on the wire". + // looks like on the wire". One exception, on purpose: the node below is + // written out WHOLE, as the store holds it. On the wire rows are sparse + // (fields at their default left out) - see the SPARSE snapshot test. schemaVersion: 2, minCompatibleSchemaVersion: 2, revision: 3, @@ -312,6 +314,25 @@ describe("SceneStore", () => { expect(store.getScene().dragFactor).toBe(0.5); }); + it("accepts a SPARSE snapshot - rows carrying only what differs from the defaults", () => { + // What the backend actually sends (backend/domain/node_wire.py): every + // field at its contract default is left off the row, and the generated + // validator restores it before the store ever sees the row. + const { transport, listeners } = makeFakeTransport(); + const store = new SceneStore(transport); + store.connect(); + + listeners.get("scene")!( + validScenePayload({ nodes: [{ id: "n0", x: 1, y: 2, title: "A", kind: "chat", isUser: true }] }), + ); + const node = store.getScene().nodes[0]; + expect(node.isUser).toBe(true); + expect(node.content).toBe(""); + expect(node.branchStatus).toBe("active"); + expect(node.pendingRequestId).toBeNull(); + expect(node.history).toEqual([]); + }); + // ADR-003 stage 3.3 (C9) review-fix: every other test that touches // chartData only ever exercises the ALL-FIELDS-absent default ({}) the // base validScenePayload() node uses - the runtime validator's @@ -442,6 +463,45 @@ describe("SceneStore", () => { expect(store.getScene().revision).toBe(4); }); + it("restores a sparse upsertNode row's omitted fields before storing it", () => { + const { store, patchListeners } = connectedStoreAtRevision3(); + + patchListeners.get("scene")!({ + revision: 4, + baseRevision: 3, + ops: [{ op: "upsertNode", node: { id: "n1", x: 5, y: 6, title: "sparse", kind: "chat", content: "hi" } }], + }); + + const node = store.getScene().nodes.find((n) => n.id === "n1")!; + expect(node.content).toBe("hi"); + expect(node.isUser).toBe(false); + expect(node.isLocked).toBe(true); + expect(node.toolCalls).toEqual([]); + expect(store.getScene().revision).toBe(4); + }); + + it("an upsert that leaves a field out resets it to its default, never keeps the old value", () => { + // An upsert REPLACES the row. A node that finishes running arrives + // without pendingRequestId (null is its default) - if the old value + // survived, the node would look busy forever. + const { store, patchListeners } = connectedStoreAtRevision3(); + const existing = store.getScene().nodes[0]; + patchListeners.get("scene")!({ + revision: 4, + baseRevision: 3, + ops: [{ op: "upsertNode", node: { ...existing, pendingRequestId: "req-1" } }], + }); + expect(store.getScene().nodes[0].pendingRequestId).toBe("req-1"); + + const { id, x, y, title, kind } = existing; + patchListeners.get("scene")!({ + revision: 5, + baseRevision: 4, + ops: [{ op: "upsertNode", node: { id, x, y, title, kind } }], + }); + expect(store.getScene().nodes[0].pendingRequestId).toBeNull(); + }); + it("adds a node it has never seen, and removes one via removeNodes", () => { const { store, patchListeners } = connectedStoreAtRevision3(); const existing = store.getScene().nodes[0]; diff --git a/web_ui/src/app/canvas/sceneStore.ts b/web_ui/src/app/canvas/sceneStore.ts index b0b4e155..688a920a 100644 --- a/web_ui/src/app/canvas/sceneStore.ts +++ b/web_ui/src/app/canvas/sceneStore.ts @@ -18,7 +18,12 @@ import { TOPIC_VALIDATORS, type TopicName } from "../../lib/api-contract/topics"; import { bindTopic } from "../../lib/api-contract/bindTopic"; -import type { SceneEdgeRow, SceneNodeRow, SceneState } from "../../lib/bridge-core/generated/scene-state"; +import { + hydrateSceneNodeRow, + type SceneEdgeRow, + type SceneNodeRow, + type SceneState, +} from "../../lib/bridge-core/generated/scene-state"; import type { GridControlState } from "../../lib/bridge-core/generated/grid-control-state"; import type { DragSpeedState } from "../../lib/bridge-core/generated/drag-speed-state"; import type { FontControlState } from "../../lib/bridge-core/generated/font-control-state"; @@ -285,7 +290,12 @@ export class SceneStore { switch (op.op) { case "upsertNode": { if (!isWireRow(op.node)) return this.refusePatch("upsertNode.node is not a row", op); - const node = op.node; + // Rows arrive sparse - every field at its default is omitted + // (backend/domain/node_wire.py). Restored here, before anything + // reads the row, exactly as validateSceneState restores a + // snapshot's; an upsert REPLACES the old row, so a field reset to + // its default must come back as the default, not the old value. + const node = hydrateSceneNodeRow(op.node) as SceneNodeRow; const index = nodes.findIndex((n) => n.id === node.id); // ADR-012 stage 12.3: diffed against the row this patch is about // to REPLACE (undefined for a genuinely new node, which diff --git a/web_ui/src/lib/bridge-core/generated/scene-state.schema.json b/web_ui/src/lib/bridge-core/generated/scene-state.schema.json index 3d160260..78b49545 100644 --- a/web_ui/src/lib/bridge-core/generated/scene-state.schema.json +++ b/web_ui/src/lib/bridge-core/generated/scene-state.schema.json @@ -57,18 +57,23 @@ "additionalProperties": false, "properties": { "artifactContent": { + "default": "", "type": "string" }, "artifactError": { + "default": "", "type": "string" }, "attachmentKind": { + "default": "", "type": "string" }, "branchStatus": { + "default": "active", "type": "string" }, "builderActivity": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -100,52 +105,68 @@ "type": "array" }, "builderApprovalSummary": { + "default": "", "type": "string" }, "builderApprovalToolName": { + "default": "", "type": "string" }, "builderAwaitingToolApproval": { + "default": false, "type": "boolean" }, "builderMaxSteps": { + "default": 0, "type": "integer" }, "builderMaxTokens": { + "default": 0, "type": "integer" }, "builderMaxWallSeconds": { + "default": 0, "type": "integer" }, "builderMode": { + "default": "", "type": "string" }, "builderRunId": { + "default": "", "type": "string" }, "builderSpentSteps": { + "default": 0, "type": "integer" }, "builderSpentTokens": { + "default": 0, "type": "integer" }, "builderSpentWallSeconds": { + "default": 0, "type": "integer" }, "builderStatus": { + "default": "", "type": "string" }, "builderStatusDetail": { + "default": "", "type": "string" }, "byteSize": { + "default": null, "type": "integer" }, "chartAspectLocked": { + "default": true, "type": "boolean" }, "chartData": { "additionalProperties": false, + "default": {}, "properties": { "bins": { "type": "integer" @@ -212,60 +233,78 @@ "type": "object" }, "chartError": { + "default": "", "type": "string" }, "chartHeight": { + "default": 340.0, "type": "number" }, "chartSourceNodeId": { + "default": "", "type": "string" }, "chartType": { + "default": "", "type": "string" }, "chartWidth": { + "default": 480.0, "type": "number" }, "chatScrollValue": { + "default": 0.0, "type": "number" }, "code": { + "default": "", "type": "string" }, "codeReviewAdditions": { + "default": 0, "type": "integer" }, "codeReviewBaseRef": { + "default": "", "type": "string" }, "codeReviewChangedFiles": { + "default": 0, "type": "integer" }, "codeReviewConfidence": { + "default": "", "type": "string" }, "codeReviewDeletions": { + "default": 0, "type": "integer" }, "codeReviewDiffChars": { + "default": 0, "type": "integer" }, "codeReviewDiffTruncated": { + "default": false, "type": "boolean" }, "codeReviewDiffVersion": { + "default": 0, "type": "integer" }, "codeReviewDismissedIds": { + "default": [], "items": { "type": "string" }, "type": "array" }, "codeReviewError": { + "default": "", "type": "string" }, "codeReviewErrors": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -313,6 +352,7 @@ "type": "array" }, "codeReviewFiles": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -351,9 +391,11 @@ "type": "array" }, "codeReviewFilesTruncated": { + "default": false, "type": "boolean" }, "codeReviewFindings": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -405,27 +447,35 @@ "type": "array" }, "codeReviewHeadRef": { + "default": "", "type": "string" }, "codeReviewOverview": { + "default": "", "type": "string" }, "codeReviewPrHtmlUrl": { + "default": "", "type": "string" }, "codeReviewPrNumber": { + "default": 0, "type": "integer" }, "codeReviewPrState": { + "default": "", "type": "string" }, "codeReviewPrTitle": { + "default": "", "type": "string" }, "codeReviewPrUrl": { + "default": "", "type": "string" }, "codeReviewQa": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -445,33 +495,42 @@ "type": "array" }, "codeReviewQualityScore": { + "default": 0, "type": "integer" }, "codeReviewQualitySummary": { + "default": "", "type": "string" }, "codeReviewRepo": { + "default": "", "type": "string" }, "codeReviewRisk": { + "default": "", "type": "string" }, "codeReviewScores": { "additionalProperties": { "type": "string" }, + "default": {}, "type": "object" }, "codeReviewState": { + "default": "draft", "type": "string" }, "codeReviewTitle": { + "default": "", "type": "string" }, "codeReviewVerdict": { + "default": "none", "type": "string" }, "codeReviewWalkthrough": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -498,81 +557,106 @@ "type": "array" }, "codeSandboxAnalysis": { + "default": "", "type": "string" }, "codeSandboxApprovalAllowSourceBuilds": { + "default": false, "type": "boolean" }, "codeSandboxApprovalIsRepair": { + "default": false, "type": "boolean" }, "codeSandboxApprovalRequirements": { + "default": "", "type": "string" }, "codeSandboxAwaitingApproval": { + "default": false, "type": "boolean" }, "codeSandboxCode": { + "default": "", "type": "string" }, "codeSandboxError": { + "default": "", "type": "string" }, "codeSandboxOutput": { + "default": "", "type": "string" }, "codeSandboxPrompt": { + "default": "", "type": "string" }, "codeSandboxRequirements": { + "default": "", "type": "string" }, "color": { + "default": null, "type": "string" }, "completionTokens": { + "default": null, "type": "integer" }, "content": { + "default": "", "type": "string" }, "durationSeconds": { + "default": null, "type": "number" }, "estimatedCostUsd": { + "default": null, "type": "number" }, "filePath": { + "default": "", "type": "string" }, "gitlinkBranch": { + "default": "", "type": "string" }, "gitlinkChangeFingerprint": { + "default": null, "type": "string" }, "gitlinkChangeState": { + "default": "draft", "type": "string" }, "gitlinkContextStats": { "additionalProperties": { "type": "string" }, + "default": {}, "type": "object" }, "gitlinkContextSummary": { + "default": "", "type": "string" }, "gitlinkContextVersion": { + "default": 0, "type": "integer" }, "gitlinkError": { + "default": "", "type": "string" }, "gitlinkLocalRoot": { + "default": "", "type": "string" }, "gitlinkPendingChanges": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -599,39 +683,49 @@ "type": "array" }, "gitlinkPreviewText": { + "default": "", "type": "string" }, "gitlinkProposalMarkdown": { + "default": "", "type": "string" }, "gitlinkRepo": { + "default": "", "type": "string" }, "gitlinkRepoFilePaths": { + "default": [], "items": { "type": "string" }, "type": "array" }, "gitlinkScopeMode": { + "default": "selected", "type": "string" }, "gitlinkSelectedPaths": { + "default": [], "items": { "type": "string" }, "type": "array" }, "gitlinkTaskPrompt": { + "default": "", "type": "string" }, "groupHeight": { + "default": null, "type": "number" }, "groupWidth": { + "default": null, "type": "number" }, "harnessActivity": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -659,36 +753,47 @@ "type": "array" }, "harnessApprovalSessionOffered": { + "default": false, "type": "boolean" }, "harnessApprovalSummary": { + "default": "", "type": "string" }, "harnessApprovalToolName": { + "default": "", "type": "string" }, "harnessAwaitingApproval": { + "default": false, "type": "boolean" }, "harnessAwaitingQuestion": { + "default": false, "type": "boolean" }, "harnessCompactions": { + "default": 0, "type": "integer" }, "harnessContextTokens": { + "default": 0, "type": "integer" }, "harnessGoal": { + "default": "", "type": "string" }, "harnessMaxContextTokens": { + "default": 0, "type": "integer" }, "harnessMaxTurns": { + "default": 0, "type": "integer" }, "harnessPlan": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -708,36 +813,47 @@ "type": "array" }, "harnessQuestion": { + "default": "", "type": "string" }, "harnessReply": { + "default": "", "type": "string" }, "harnessRunId": { + "default": "", "type": "string" }, "harnessSpentTokens": { + "default": 0, "type": "integer" }, "harnessSpentTurns": { + "default": 0, "type": "integer" }, "harnessStatus": { + "default": "", "type": "string" }, "harnessStatusDetail": { + "default": "", "type": "string" }, "harnessWorkspaceActive": { + "default": "", "type": "string" }, "harnessWorkspacePath": { + "default": "", "type": "string" }, "headerColor": { + "default": null, "type": "string" }, "history": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -765,45 +881,58 @@ "type": "array" }, "htmlSplitterState": { + "default": null, "type": "number" }, "id": { "type": "string" }, "imageAssetId": { + "default": "", "type": "string" }, "indexIntoKnowledge": { + "default": false, "type": "boolean" }, "isBranchComparison": { + "default": false, "type": "boolean" }, "isBranchSynthesis": { + "default": false, "type": "boolean" }, "isCollapsed": { + "default": false, "type": "boolean" }, "isDocked": { + "default": false, "type": "boolean" }, "isFinalDeliverable": { + "default": false, "type": "boolean" }, "isLocked": { + "default": true, "type": "boolean" }, "isSummaryNote": { + "default": false, "type": "boolean" }, "isSystemPrompt": { + "default": false, "type": "boolean" }, "isUser": { + "default": false, "type": "boolean" }, "itemIds": { + "default": [], "items": { "type": "string" }, @@ -813,27 +942,35 @@ "type": "string" }, "language": { + "default": "", "type": "string" }, "mimeType": { + "default": "", "type": "string" }, "model": { + "default": null, "type": "string" }, "overrideModelId": { + "default": "", "type": "string" }, "overrideProvider": { + "default": "", "type": "string" }, "pendingRequestId": { + "default": null, "type": "string" }, "planGoal": { + "default": "", "type": "string" }, "planSteps": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -864,28 +1001,36 @@ "additionalProperties": { "type": "string" }, + "default": {}, "type": "object" }, "previewLabel": { + "default": "", "type": "string" }, "promptTokens": { + "default": null, "type": "integer" }, "provider": { + "default": null, "type": "string" }, "researchActiveSourceId": { + "default": null, "type": "string" }, "researchCompleted": { + "default": 0, "type": "integer" }, "researchError": { + "default": "", "type": "string" }, "researchResult": { "additionalProperties": false, + "default": null, "properties": { "answerMarkdown": { "type": "string" @@ -1015,24 +1160,30 @@ "type": "object" }, "researchRetainToKnowledge": { + "default": false, "type": "boolean" }, "researchStage": { + "default": "", "type": "string" }, "researchTotal": { + "default": 0, "type": "integer" }, "responseIncomplete": { + "default": false, "type": "boolean" }, "synthesisInstructions": { + "default": "", "type": "string" }, "title": { "type": "string" }, "toolCalls": { + "default": [], "items": { "additionalProperties": false, "properties": { @@ -1075,142 +1226,7 @@ "x", "y", "title", - "kind", - "content", - "isUser", - "isCollapsed", - "code", - "language", - "attachmentKind", - "filePath", - "mimeType", - "previewLabel", - "isDocked", - "imageAssetId", - "history", - "researchStage", - "researchCompleted", - "researchTotal", - "researchError", - "researchRetainToKnowledge", - "artifactContent", - "artifactError", - "gitlinkRepo", - "gitlinkBranch", - "gitlinkScopeMode", - "gitlinkLocalRoot", - "gitlinkRepoFilePaths", - "gitlinkSelectedPaths", - "gitlinkTaskPrompt", - "gitlinkContextStats", - "gitlinkContextSummary", - "gitlinkContextVersion", - "gitlinkProposalMarkdown", - "gitlinkPendingChanges", - "gitlinkPreviewText", - "gitlinkChangeState", - "gitlinkError", - "codeReviewPrUrl", - "codeReviewRepo", - "codeReviewPrNumber", - "codeReviewPrTitle", - "codeReviewPrState", - "codeReviewPrHtmlUrl", - "codeReviewBaseRef", - "codeReviewHeadRef", - "codeReviewAdditions", - "codeReviewDeletions", - "codeReviewChangedFiles", - "codeReviewFiles", - "codeReviewFilesTruncated", - "codeReviewDiffTruncated", - "codeReviewDiffChars", - "codeReviewDiffVersion", - "codeReviewWalkthrough", - "codeReviewFindings", - "codeReviewErrors", - "codeReviewDismissedIds", - "codeReviewTitle", - "codeReviewOverview", - "codeReviewConfidence", - "codeReviewScores", - "codeReviewQualityScore", - "codeReviewVerdict", - "codeReviewRisk", - "codeReviewQualitySummary", - "codeReviewQa", - "codeReviewState", - "codeReviewError", - "codeSandboxRequirements", - "codeSandboxPrompt", - "codeSandboxCode", - "codeSandboxOutput", - "codeSandboxAnalysis", - "codeSandboxAwaitingApproval", - "codeSandboxApprovalRequirements", - "codeSandboxApprovalAllowSourceBuilds", - "codeSandboxApprovalIsRepair", - "codeSandboxError", - "isBranchSynthesis", - "synthesisInstructions", - "branchStatus", - "responseIncomplete", - "isFinalDeliverable", - "isSystemPrompt", - "isSummaryNote", - "isBranchComparison", - "itemIds", - "isLocked", - "chartType", - "chartData", - "chartError", - "chartWidth", - "chartHeight", - "chartAspectLocked", - "chartSourceNodeId", - "chatScrollValue", - "toolCalls", - "overrideProvider", - "overrideModelId", - "indexIntoKnowledge", - "planGoal", - "planSteps", - "builderActivity", - "builderStatus", - "builderMode", - "builderRunId", - "builderMaxSteps", - "builderMaxTokens", - "builderMaxWallSeconds", - "builderSpentSteps", - "builderSpentTokens", - "builderSpentWallSeconds", - "builderAwaitingToolApproval", - "builderApprovalToolName", - "builderApprovalSummary", - "builderStatusDetail", - "harnessGoal", - "harnessReply", - "harnessStatus", - "harnessStatusDetail", - "harnessRunId", - "harnessActivity", - "harnessContextTokens", - "harnessMaxContextTokens", - "harnessCompactions", - "harnessAwaitingApproval", - "harnessApprovalToolName", - "harnessApprovalSummary", - "harnessApprovalSessionOffered", - "harnessPlan", - "harnessAwaitingQuestion", - "harnessQuestion", - "harnessWorkspacePath", - "harnessWorkspaceActive", - "harnessMaxTurns", - "harnessSpentTurns", - "harnessSpentTokens", - "pluginState" + "kind" ], "type": "object" }, diff --git a/web_ui/src/lib/bridge-core/generated/scene-state.ts b/web_ui/src/lib/bridge-core/generated/scene-state.ts index 21fe9038..c88aa047 100644 --- a/web_ui/src/lib/bridge-core/generated/scene-state.ts +++ b/web_ui/src/lib/bridge-core/generated/scene-state.ts @@ -358,6 +358,187 @@ function isRecord(value: unknown): value is Record { // defeat the additive-forward-compatibility the version negotiation exists to // provide. Missing or wrongly-typed KNOWN fields are still hard errors. +/** SceneNodeRow crosses the wire sparse: the sender omits every field at its + * declared default (contracts: WIRE_OMITS_DEFAULTS). These are those + * defaults - the value every omitted field is restored to. */ +export const SCENE_NODE_ROW_WIRE_DEFAULTS: ReadonlyArray = [ + ["content", ""], + ["isUser", false], + ["isCollapsed", false], + ["code", ""], + ["language", ""], + ["attachmentKind", ""], + ["filePath", ""], + ["mimeType", ""], + ["durationSeconds", null], + ["byteSize", null], + ["previewLabel", ""], + ["isDocked", false], + ["imageAssetId", ""], + ["history", []], + ["pendingRequestId", null], + ["researchStage", ""], + ["researchCompleted", 0], + ["researchTotal", 0], + ["researchActiveSourceId", null], + ["researchError", ""], + ["researchResult", null], + ["researchRetainToKnowledge", false], + ["artifactContent", ""], + ["artifactError", ""], + ["gitlinkRepo", ""], + ["gitlinkBranch", ""], + ["gitlinkScopeMode", "selected"], + ["gitlinkLocalRoot", ""], + ["gitlinkRepoFilePaths", []], + ["gitlinkSelectedPaths", []], + ["gitlinkTaskPrompt", ""], + ["gitlinkContextStats", {}], + ["gitlinkContextSummary", ""], + ["gitlinkContextVersion", 0], + ["gitlinkProposalMarkdown", ""], + ["gitlinkPendingChanges", []], + ["gitlinkPreviewText", ""], + ["gitlinkChangeFingerprint", null], + ["gitlinkChangeState", "draft"], + ["gitlinkError", ""], + ["codeReviewPrUrl", ""], + ["codeReviewRepo", ""], + ["codeReviewPrNumber", 0], + ["codeReviewPrTitle", ""], + ["codeReviewPrState", ""], + ["codeReviewPrHtmlUrl", ""], + ["codeReviewBaseRef", ""], + ["codeReviewHeadRef", ""], + ["codeReviewAdditions", 0], + ["codeReviewDeletions", 0], + ["codeReviewChangedFiles", 0], + ["codeReviewFiles", []], + ["codeReviewFilesTruncated", false], + ["codeReviewDiffTruncated", false], + ["codeReviewDiffChars", 0], + ["codeReviewDiffVersion", 0], + ["codeReviewWalkthrough", []], + ["codeReviewFindings", []], + ["codeReviewErrors", []], + ["codeReviewDismissedIds", []], + ["codeReviewTitle", ""], + ["codeReviewOverview", ""], + ["codeReviewConfidence", ""], + ["codeReviewScores", {}], + ["codeReviewQualityScore", 0], + ["codeReviewVerdict", "none"], + ["codeReviewRisk", ""], + ["codeReviewQualitySummary", ""], + ["codeReviewQa", []], + ["codeReviewState", "draft"], + ["codeReviewError", ""], + ["codeSandboxRequirements", ""], + ["codeSandboxPrompt", ""], + ["codeSandboxCode", ""], + ["codeSandboxOutput", ""], + ["codeSandboxAnalysis", ""], + ["codeSandboxAwaitingApproval", false], + ["codeSandboxApprovalRequirements", ""], + ["codeSandboxApprovalAllowSourceBuilds", false], + ["codeSandboxApprovalIsRepair", false], + ["codeSandboxError", ""], + ["provider", null], + ["model", null], + ["isBranchSynthesis", false], + ["synthesisInstructions", ""], + ["branchStatus", "active"], + ["responseIncomplete", false], + ["promptTokens", null], + ["completionTokens", null], + ["estimatedCostUsd", null], + ["isFinalDeliverable", false], + ["color", null], + ["headerColor", null], + ["isSystemPrompt", false], + ["isSummaryNote", false], + ["isBranchComparison", false], + ["itemIds", []], + ["isLocked", true], + ["groupWidth", null], + ["groupHeight", null], + ["chartType", ""], + ["chartData", {}], + ["chartError", ""], + ["chartWidth", 480.0], + ["chartHeight", 340.0], + ["chartAspectLocked", true], + ["chartSourceNodeId", ""], + ["htmlSplitterState", null], + ["chatScrollValue", 0.0], + ["toolCalls", []], + ["overrideProvider", ""], + ["overrideModelId", ""], + ["indexIntoKnowledge", false], + ["planGoal", ""], + ["planSteps", []], + ["builderActivity", []], + ["builderStatus", ""], + ["builderMode", ""], + ["builderRunId", ""], + ["builderMaxSteps", 0], + ["builderMaxTokens", 0], + ["builderMaxWallSeconds", 0], + ["builderSpentSteps", 0], + ["builderSpentTokens", 0], + ["builderSpentWallSeconds", 0], + ["builderAwaitingToolApproval", false], + ["builderApprovalToolName", ""], + ["builderApprovalSummary", ""], + ["builderStatusDetail", ""], + ["harnessGoal", ""], + ["harnessReply", ""], + ["harnessStatus", ""], + ["harnessStatusDetail", ""], + ["harnessRunId", ""], + ["harnessActivity", []], + ["harnessContextTokens", 0], + ["harnessMaxContextTokens", 0], + ["harnessCompactions", 0], + ["harnessAwaitingApproval", false], + ["harnessApprovalToolName", ""], + ["harnessApprovalSummary", ""], + ["harnessApprovalSessionOffered", false], + ["harnessPlan", []], + ["harnessAwaitingQuestion", false], + ["harnessQuestion", ""], + ["harnessWorkspacePath", ""], + ["harnessWorkspaceActive", ""], + ["harnessMaxTurns", 0], + ["harnessSpentTurns", 0], + ["harnessSpentTokens", 0], + ["pluginState", {}], +]; + +export function hydrateSceneNodeRow(value: unknown): unknown { + if (!isRecord(value)) return value; + let hydrated: Record | null = null; + for (const [key, fallback] of SCENE_NODE_ROW_WIRE_DEFAULTS) { + if (value[key] !== undefined) continue; + hydrated ??= { ...value }; + hydrated[key] = Array.isArray(fallback) ? [] : isRecord(fallback) ? {} : fallback; + } + return hydrated ?? value; +} + +function hydrateSceneState(value: unknown): unknown { + if (!isRecord(value)) return value; + let hydrated: Record | null = null; + { + const rows = value["nodes"]; + if (Array.isArray(rows)) { + const restored = rows.map(hydrateSceneNodeRow); + if (restored.some((row, i) => row !== rows[i])) (hydrated ??= { ...value })["nodes"] = restored; + } + } + return hydrated ?? value; +} + function checkSceneNodeRow(value: unknown, path: string, errors: string[]): void { if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } { @@ -1835,9 +2016,10 @@ function checkSceneState(value: unknown, path: string, errors: string[]): void { } export function validateSceneState(value: unknown): ValidationResult { + const hydrated = hydrateSceneState(value); const errors: string[] = []; - checkSceneState(value, "$", errors); + checkSceneState(hydrated, "$", errors); return errors.length === 0 - ? { ok: true, value: value as SceneState } + ? { ok: true, value: hydrated as SceneState } : { ok: false, errors }; } diff --git a/web_ui/src/lib/bridge-core/sceneRowHydration.test.ts b/web_ui/src/lib/bridge-core/sceneRowHydration.test.ts new file mode 100644 index 00000000..89a8793b --- /dev/null +++ b/web_ui/src/lib/bridge-core/sceneRowHydration.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from "vitest"; +import { + SCENE_NODE_ROW_WIRE_DEFAULTS, + hydrateSceneNodeRow, + validateSceneState, + type SceneNodeRow, +} from "./generated/scene-state"; + +// Scene node rows cross the wire sparse: the backend (backend/domain/ +// node_wire.py) leaves out every field at its contract default, and the +// generated hydrateSceneNodeRow restores them - inside validateSceneState for +// snapshots, and in SceneStore.applyScenePatch for upserts. Everything past +// those two points reads full rows, exactly as before the wire went sparse. + +const IDENTITY = { id: "n7", x: 10, y: 20, title: "Seven", kind: "chat" } as const; + +function scene(nodes: unknown[]) { + return { + schemaVersion: 2, + minCompatibleSchemaVersion: 2, + revision: 1, + nodes, + edges: [], + pins: [], + snapToGrid: false, + fadeConnectionsEnabled: false, + orthogonalRouting: false, + smartGuides: true, + hasSavedChat: false, + dragFactor: 0.5, + fontFamily: "Segoe UI", + fontSizePt: 10, + fontColor: "#ffffff", + canUndo: false, + canRedo: false, + undoLabel: "", + redoLabel: "", + }; +} + +describe("hydrateSceneNodeRow", () => { + it("restores every omitted field to its contract default", () => { + const row = hydrateSceneNodeRow({ ...IDENTITY }) as Record; + for (const [key, fallback] of SCENE_NODE_ROW_WIRE_DEFAULTS) { + expect(row[key], key).toEqual(fallback); + } + expect(row).toMatchObject(IDENTITY); + }); + + it("keeps every field the sender did send", () => { + const row = hydrateSceneNodeRow({ + ...IDENTITY, + branchStatus: "accepted", + isUser: true, + history: [{ role: "user", content: "hi", incomplete: false }], + }) as SceneNodeRow; + expect(row.branchStatus).toBe("accepted"); + expect(row.isUser).toBe(true); + expect(row.history).toEqual([{ role: "user", content: "hi", incomplete: false }]); + expect(row.chartWidth).toBe(480); + }); + + it("leaves an explicit null alone - only an ABSENT field is restored", () => { + // A null the sender actually sent is data; whether it is legal is the + // validator's call, not something hydration may paper over. + const row = hydrateSceneNodeRow({ ...IDENTITY, branchStatus: null }) as Record; + expect(row.branchStatus).toBeNull(); + }); + + it("returns the same object when nothing is missing", () => { + const whole = hydrateSceneNodeRow({ ...IDENTITY }); + expect(hydrateSceneNodeRow(whole)).toBe(whole); + }); + + it("never shares a restored container between rows", () => { + const a = hydrateSceneNodeRow({ ...IDENTITY }) as SceneNodeRow; + const b = hydrateSceneNodeRow({ ...IDENTITY, id: "n8" }) as SceneNodeRow; + expect(a.history).not.toBe(b.history); + expect(a.chartData).not.toBe(b.chartData); + }); + + it("passes a non-object through untouched for the validator to reject", () => { + expect(hydrateSceneNodeRow(null)).toBeNull(); + expect(hydrateSceneNodeRow("row")).toBe("row"); + }); +}); + +describe("validateSceneState on a sparse snapshot", () => { + it("accepts rows carrying only their identity and hands back restored rows", () => { + const result = validateSceneState(scene([{ ...IDENTITY }, { ...IDENTITY, id: "n8", kind: "note", isSystemPrompt: true }])); + expect(result.ok).toBe(true); + if (!result.ok) return; + const [chat, note] = result.value.nodes; + expect(chat.content).toBe(""); + expect(chat.pendingRequestId).toBeNull(); + expect(chat.isLocked).toBe(true); + expect(note.isSystemPrompt).toBe(true); + expect(note.gitlinkScopeMode).toBe("selected"); + }); + + it("still rejects a row missing a field that has no default", () => { + const { id: _id, ...noId } = IDENTITY; + expect(validateSceneState(scene([noId])).ok).toBe(false); + }); + + it("still rejects a sent field of the wrong type", () => { + expect(validateSceneState(scene([{ ...IDENTITY, isUser: "yes" }])).ok).toBe(false); + }); + + it("returns the very same payload when every row is already whole", () => { + const whole = scene([hydrateSceneNodeRow({ ...IDENTITY })]); + const result = validateSceneState(whole); + expect(result.ok && result.value).toBe(whole); + }); +}); From 8ef5a27e2338716c0be02999b7311e0ff36dc348 Mon Sep 17 00:00:00 2001 From: dovvnloading <157447210+dovvnloading@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:44:04 -0400 Subject: [PATCH 2/3] Get the initial chunk under the ADR-019 budget, and re-anchor the perf baseline The initial JS chunk was 599,947 bytes against ADR-019's 512,000-byte budget. A sourcemap attribution found the largest block of app source in it was the generated wire validators: ~87,000 bytes, 47,908 of them scene-state.ts, because codegen emitted one unrolled block of code per field (~190 bytes each). - codegen now emits each payload's shape as a field table (~25 bytes a field), compiled into a checker once at load by the new src/lib/bridge-core/wireCheck.ts. Checked against the unrolled validators on 52,000 fuzzed payloads across all 13 topics: identical accept/reject and identical error messages. Initial chunk 599,947 -> 529,180. - Scalar fields are checked inline without building a path string, and sparse rows are restored in table order so every restored row shares one shape. Scene rows that already passed are not re-checked; the store re-validates the whole scene after every patch, and stored rows are replaced, never edited. Re-validating a stored 500-node scene: 0.86 ms with the old validator -> 0.05 ms. Client-side cost of a 500-node snapshot (parse + restore + validate): 17.0 ms -> 8.1 ms. - Every chrome surface that only a click or shortcut opens (About, Builder/Harness launch, command palette, diagnostics, global and knowledge search, quick switcher, View) now loads on first open, on the existing LazySurface terms. Each was checked for work it does while closed; OnboardingDialog does some (it opens itself on a fresh machine) and stays eager. 529,180 -> 495,013. - check-bundle-size: the largest-chunk ceiling is now the budget itself, 512,000 (3.3% headroom). The total ceiling moves down to 1,440,000. Perf baseline: re-recorded after the sparse node rows landed, because the nightly check fails on any size change over 10% in either direction. Values are from the nightly's own runner, not a dev machine. Each timing is the slowest of three dispatched runs of the same commit, because runner-to-runner spread was up to 26%. Against the 2026-08-11 baseline: publish cost -71% to -79%, full snapshot -64% to -80%. Co-Authored-By: Claude --- backend/tests/perf/baseline.json | 52 +- contracts/codegen.py | 279 +-- web_ui/scripts/check-bundle-size.mjs | 36 +- web_ui/src/app/App.tsx | 89 +- .../bridge-core/generated/app-about-state.ts | 83 +- .../generated/app-chat-library-state.ts | 177 +- .../generated/app-composer-state.ts | 431 +--- .../generated/app-plugins-state.ts | 126 +- .../generated/app-settings-state.ts | 409 +--- .../generated/diagnostics-state.ts | 144 +- .../bridge-core/generated/drag-speed-state.ts | 60 +- .../generated/execution-limits-state.ts | 47 +- .../generated/font-control-state.ts | 67 +- .../generated/grid-control-state.ts | 86 +- .../generated/notification-state.ts | 59 +- .../lib/bridge-core/generated/scene-state.ts | 1993 +++-------------- .../generated/token-counter-state.ts | 104 +- web_ui/src/lib/bridge-core/wireCheck.test.ts | 138 ++ web_ui/src/lib/bridge-core/wireCheck.ts | 236 ++ 19 files changed, 1321 insertions(+), 3295 deletions(-) create mode 100644 web_ui/src/lib/bridge-core/wireCheck.test.ts create mode 100644 web_ui/src/lib/bridge-core/wireCheck.ts diff --git a/backend/tests/perf/baseline.json b/backend/tests/perf/baseline.json index f04d8c67..2054006e 100644 --- a/backend/tests/perf/baseline.json +++ b/backend/tests/perf/baseline.json @@ -1,51 +1,51 @@ { "_comment": "ADR-019 stage 19.3: the committed wall-clock baseline check_baseline.py compares fresh measure_baselines.py output against. Regenerate deliberately with `python -m backend.tests.perf.measure_baselines`, review the diff, and commit - never auto-overwritten by the nightly job itself (that would let a real regression quietly become the new normal).", - "measured_at": "2026-08-11", - "measured_on": "windows, Python 3.12.6, coverage-uninstrumented (see backend/tests/perf/test_loop_watchdog.py's own note on why coverage tracing corrupts these numbers)", + "measured_at": "2026-09-19", + "measured_on": "GitHub windows-latest - the nightly job's own runner - Python 3.12, coverage-uninstrumented. Each timing is the median of 7 publishes (measure_baselines.TIMING_REPEATS), and the value recorded is the SLOWEST of three workflow_dispatch runs of the same commit (runs 35464350837, 35464989364, 35464993252): runner-to-runner spread on one commit was up to 26% (stress.serialize_ms), so a baseline taken from one fast runner would fail the next slow one on noise. Sizes were identical across all three, as a deterministic metric should be.", "workloads": [ { "name": "small", "node_count": 25, "edge_count": 27, - "fixture_build_ms": 0.15, - "payload_build_ms": 0.37, - "serialize_ms": 0.44, - "publish_total_ms": 0.81, - "full_snapshot_kib": 76.2, - "single_node_payload_kib": 2.984 + "fixture_build_ms": 0.26, + "payload_build_ms": 0.1, + "serialize_ms": 0.07, + "publish_total_ms": 0.17, + "full_snapshot_kib": 15.1, + "single_node_payload_kib": 0.529 }, { "name": "typical", "node_count": 100, "edge_count": 137, - "fixture_build_ms": 0.7, - "payload_build_ms": 1.41, - "serialize_ms": 1.72, - "publish_total_ms": 3.14, - "full_snapshot_kib": 380.2, - "single_node_payload_kib": 2.604 + "fixture_build_ms": 2.64, + "payload_build_ms": 0.39, + "serialize_ms": 0.47, + "publish_total_ms": 0.83, + "full_snapshot_kib": 135.6, + "single_node_payload_kib": 0.168 }, { "name": "large", "node_count": 500, "edge_count": 697, - "fixture_build_ms": 8.75, - "payload_build_ms": 7.07, - "serialize_ms": 8.88, - "publish_total_ms": 15.95, - "full_snapshot_kib": 1902.3, - "single_node_payload_kib": 2.604 + "fixture_build_ms": 35.54, + "payload_build_ms": 1.99, + "serialize_ms": 2.67, + "publish_total_ms": 4.59, + "full_snapshot_kib": 679.3, + "single_node_payload_kib": 0.168 }, { "name": "stress", "node_count": 2000, "edge_count": 1999, - "fixture_build_ms": 61.85, - "payload_build_ms": 28.3, - "serialize_ms": 34.59, - "publish_total_ms": 62.89, - "full_snapshot_kib": 7645.0, - "single_node_payload_kib": 3.769 + "fixture_build_ms": 131.82, + "payload_build_ms": 7.34, + "serialize_ms": 11.13, + "publish_total_ms": 18.29, + "full_snapshot_kib": 2751.5, + "single_node_payload_kib": 1.33 } ] } diff --git a/contracts/codegen.py b/contracts/codegen.py index 28692c2d..bd1f4e2b 100644 --- a/contracts/codegen.py +++ b/contracts/codegen.py @@ -153,11 +153,29 @@ def _annotation_to_ts(annotation: Any) -> str: def typescript_for(dataclass_type: type, *, source: str) -> str: - """Emit TS interfaces plus a runtime validator for the payload graph.""" + """Emit TS interfaces plus a runtime validator for the payload graph. + + The validator is DATA: one `[name, type, optional]` table per dataclass, + compiled into a checker once at module load by web_ui/src/lib/bridge-core/ + wireCheck.ts. It used to be emitted as unrolled code, one block per field - + ~190 minified bytes a field, 87 KB across every topic, all of it in the + initial chunk. A table row is ~25 bytes; wireCheck.ts's own docstring + covers why the compiled checker behaves (and performs) like the unrolled + code did.""" collected: dict[str, dict[str, Any]] = {} _collect_object_types(dataclass_type, out=collected) + sparse_fields = _root_sparse_fields(dataclass_type) - lines: list[str] = [_HEADER.format(source=source)] + runtime = ["compileFields", "type ValidationResult", "type WireFields"] + if sparse_fields: + runtime += ["hydrateRow", "isRecord", "wireDefaults"] + lines: list[str] = [ + _HEADER.format(source=source), + f'import {{ {", ".join(sorted(runtime, key=lambda n: n.removeprefix("type ")))} }} from "../wireCheck";', + "", + "export type { ValidationResult };", + "", + ] # Interfaces, root last so the file reads bottom-up like the payload nests. root_name = _ts_type_name(dataclass_type.__name__) @@ -181,17 +199,19 @@ def typescript_for(dataclass_type: type, *, source: str) -> str: lines.append("}") lines.append("") - lines.append(_VALIDATOR_PREAMBLE) + # Field tables, each after every table it references - a const cannot be + # read before its declaration runs. + for row_type in _dataclasses_dependencies_first(dataclass_type): + lines.append(_field_table_for(row_type)) - sparse_fields = _root_sparse_fields(dataclass_type) for row_type in dict.fromkeys(row for row, _ in sparse_fields.values()): lines.append(_hydrator_for(row_type)) if sparse_fields: lines.append(_root_hydrator_for(root_name, sparse_fields)) - for name in ordered: - lines.append(_validator_for(name, collected[name])) - + table = _field_table_name(dataclass_type) + lines.append(f"const check{root_name} = compileFields({table});") + lines.append("") if sparse_fields: # The one change a sparse row makes to its root's validator: restore # the omitted fields FIRST, then validate - and hand back the restored @@ -220,11 +240,85 @@ def typescript_for(dataclass_type: type, *, source: str) -> str: return "\n".join(lines) +def _field_table_name(dataclass_type: type) -> str: + """SceneNodeRow -> SCENE_NODE_ROW_FIELDS.""" + return f"{_screaming_snake(_ts_type_name(dataclass_type.__name__))}_FIELDS" + + +def _dataclasses_dependencies_first(root: type) -> list[type]: + """Every dataclass in the payload graph, each after the ones it nests.""" + import dataclasses + import typing + + order: list[type] = [] + + def visit(dataclass_type: type, stack: tuple[type, ...]) -> None: + if dataclass_type in order: + return + if dataclass_type in stack: + raise ValueError(f"{dataclass_type.__name__} nests itself; a field table cannot express that") + hints = typing.get_type_hints(dataclass_type) + for field in dataclasses.fields(dataclass_type): + for nested in _nested_dataclasses(_strip_optional(hints[field.name])): + visit(nested, (*stack, dataclass_type)) + order.append(dataclass_type) + + visit(root, ()) + return order + + +def _wire_type(annotation: Any) -> str: + """One field's type as a wireCheck.ts WireType literal.""" + import dataclasses + from typing import Literal, get_args, get_origin + + if dataclasses.is_dataclass(annotation) and isinstance(annotation, type): + return f"{{ o: {_field_table_name(annotation)} }}" + if annotation is str: + return '"s"' + if annotation is bool: + return '"b"' + if annotation in (int, float): + return '"n"' + + origin = get_origin(annotation) + if origin is Literal: + return "{ e: [" + ", ".join(json.dumps(v) for v in get_args(annotation)) + "] }" + if origin is list: + (item,) = get_args(annotation) + return f"{{ a: {_wire_type(item)} }}" + if origin is dict: + _, value = get_args(annotation) + return f"{{ d: {_wire_type(value)} }}" + raise ValueError(f"cannot render a wire type for: {annotation!r}") + + +def _field_table_for(dataclass_type: type) -> str: + """`[name, type, optional]` per field - plus, for a sparse row, the wire + default its sender may leave the field out at.""" + import dataclasses + import typing + + hints = typing.get_type_hints(dataclass_type) + sparse = omits_defaults(dataclass_type) + rows: list[str] = [] + for field in dataclasses.fields(dataclass_type): + annotation = _strip_optional(hints[field.name]) + optional = 1 if annotation is not hints[field.name] else 0 + row = f"{json.dumps(field.name)}, {_wire_type(annotation)}, {optional}" + default = wire_default(field) if sparse else NO_DEFAULT + if default is not NO_DEFAULT: + row += f", {json.dumps(default)}" + rows.append(f" [{row}],") + return "\n".join([f"const {_field_table_name(dataclass_type)}: WireFields = [", *rows, "];", ""]) + + def _root_sparse_fields(root: type) -> dict[str, tuple[type, bool]]: """{root field name: (sparse row type, held as a list)} for every root - field that holds a sparse row (graphlink_wire_schema.py's "SPARSE ROWS") directly or as a - list. Restoring is only generated at that depth; a sparse row anywhere - deeper raises here, so one can never ship with nothing restoring it.""" + field that holds a sparse row (graphlink_wire_schema.py's "SPARSE ROWS") + directly or as a list. Restoring is only generated at that depth; a sparse + row anywhere deeper raises here, so one can never ship with nothing + restoring it.""" import dataclasses import typing from typing import get_args, get_origin @@ -239,10 +333,7 @@ def _root_sparse_fields(root: type) -> dict[str, tuple[type, bool]]: if isinstance(annotation, type) and dataclasses.is_dataclass(annotation) and omits_defaults(annotation): direct[field.name] = (annotation, is_list) - def deeper(dataclass_type: type, seen: set[type]) -> None: - if dataclass_type in seen: - return - seen.add(dataclass_type) + for dataclass_type in _dataclasses_dependencies_first(root): nested_hints = typing.get_type_hints(dataclass_type) for field in dataclasses.fields(dataclass_type): for nested in _nested_dataclasses(_strip_optional(nested_hints[field.name])): @@ -251,9 +342,6 @@ def deeper(dataclass_type: type, seen: set[type]) -> None: f"{nested.__name__} omits defaults but sits below {root.__name__}.{field.name}; " "codegen only restores sparse rows held directly by the root payload" ) - deeper(nested, seen) - - deeper(root, set()) return direct @@ -263,7 +351,10 @@ def _strip_optional(annotation: Any) -> Any: from typing import get_args, get_origin if get_origin(annotation) in (_types.UnionType, typing.Union): - return next(a for a in get_args(annotation) if a is not type(None)) + args = [a for a in get_args(annotation) if a is not type(None)] + if len(args) != 1: + raise ValueError(f"cannot render a wire type for a multi-type union: {annotation!r}") + return args[0] return annotation @@ -273,40 +364,18 @@ def _screaming_snake(name: str) -> str: def _hydrator_for(row_type: type) -> str: - """The restore half of a sparse row: its declared defaults as a table, - and hydrate(), which fills in every ABSENT key. A present key - - including an explicit null - is data and is left for the validator to - judge. Containers restore as fresh empty values (wire_default guarantees - every container default is empty), never a shared instance. A row with - nothing missing comes back as the SAME object, so re-validating rows that - are already whole (the scene store re-checks the whole scene after every - patch) allocates nothing and keeps every untouched row's identity.""" - import dataclasses - + """The restore half of a sparse row: its defaults, read off its own field + table, and hydrate() - see wireCheck.ts's hydrateRow.""" name = _ts_type_name(row_type.__name__) - table = f"{_screaming_snake(name)}_WIRE_DEFAULTS" - entries = [ - f" [{json.dumps(field.name)}, {json.dumps(default)}]," - for field in dataclasses.fields(row_type) - if (default := wire_default(field)) is not NO_DEFAULT - ] + defaults = f"{_screaming_snake(name)}_WIRE_DEFAULTS" return "\n".join([ f"/** {name} crosses the wire sparse: the sender omits every field at its", " * declared default (contracts: WIRE_OMITS_DEFAULTS). These are those", " * defaults - the value every omitted field is restored to. */", - f"export const {table}: ReadonlyArray = [", - *entries, - "];", + f"export const {defaults} = wireDefaults({_field_table_name(row_type)});", "", f"export function hydrate{name}(value: unknown): unknown {{", - " if (!isRecord(value)) return value;", - " let hydrated: Record | null = null;", - f" for (const [key, fallback] of {table}) {{", - " if (value[key] !== undefined) continue;", - " hydrated ??= { ...value };", - " hydrated[key] = Array.isArray(fallback) ? [] : isRecord(fallback) ? {} : fallback;", - " }", - " return hydrated ?? value;", + f" return hydrateRow({_field_table_name(row_type)}, {defaults}, value);", "}", "", ]) @@ -345,126 +414,6 @@ def _root_hydrator_for(root_name: str, sparse_fields: dict[str, tuple[type, bool return "\n".join(body) -_VALIDATOR_PREAMBLE = '''export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. -''' - - -def _ts_check_expr(annotation: Any, *, value_expr: str, path_expr: str) -> str: - """Inline runtime check for one value, appending to `errors`. - - `path_expr` is a complete JS expression evaluating to a string. Error - messages concatenate onto it rather than interpolating it into another - template literal, which would nest backticks and emit valid-but-ugly - `${`${path}.id`}` generated code. - """ - import dataclasses - from typing import Literal, get_args, get_origin - - if dataclasses.is_dataclass(annotation) and isinstance(annotation, type): - name = _ts_type_name(annotation.__name__) - return f"check{name}({value_expr}, {path_expr}, errors);" - - origin = get_origin(annotation) - - if origin is Literal: - allowed = list(get_args(annotation)) - allowed_ts = ", ".join(json.dumps(v) for v in allowed) - allowed_msg = json.dumps(", ".join(allowed)) - return ( - f"if (![{allowed_ts}].includes({value_expr} as string)) " - f"errors.push({path_expr} + `: ${{JSON.stringify({value_expr})}} is not one of " - f"[` + {allowed_msg} + `]`);" - ) - - if annotation is str: - return ( - f'if (typeof {value_expr} !== "string") ' - f'errors.push({path_expr} + ": expected string");' - ) - if annotation is bool: - return ( - f'if (typeof {value_expr} !== "boolean") ' - f'errors.push({path_expr} + ": expected boolean");' - ) - if annotation in (int, float): - return ( - f'if (typeof {value_expr} !== "number") ' - f'errors.push({path_expr} + ": expected number");' - ) - - if origin is list: - (item,) = get_args(annotation) - inner = _ts_check_expr( - item, value_expr="item", path_expr=f"{path_expr} + `[${{i}}]`" - ) - return ( - f"if (!Array.isArray({value_expr})) " - f'errors.push({path_expr} + ": expected array");\n' - f" else ({value_expr} as unknown[]).forEach((item, i) => {{ {inner} }});" - ) - - if origin is dict: - _, value_type = get_args(annotation) - # Bracket notation with a JSON-stringified key, not `.${k}` - a dict - # key is arbitrary string data (e.g. --gl-* custom property names - # today, but this generator has no way to know that won't ever be a - # key containing "." or "[0]"), and dot-appending it would make an - # error path indistinguishable from a genuinely deeper nested path. - # JSON.stringify also makes the key visible verbatim in the message - # rather than silently truncated at a special character. - inner = _ts_check_expr( - value_type, value_expr="v", path_expr=f"{path_expr} + `[${{JSON.stringify(k)}}]`" - ) - return ( - f"if (!isRecord({value_expr})) " - f'errors.push({path_expr} + ": expected object");\n' - f" else Object.entries({value_expr} as Record)" - f".forEach(([k, v]) => {{ {inner} }});" - ) - - raise ValueError(f"cannot render TS runtime check for: {annotation!r}") - - -def _validator_for(name: str, fields: dict[str, Any]) -> str: - body: list[str] = [ - f"function check{name}(value: unknown, path: string, errors: string[]): void {{", - " if (!isRecord(value)) { errors.push(`${path}: expected object`); return; }", - ] - for field_name, info in fields.items(): - annotation = info["annotation"] - optional = info["optional"] - access = f'value[{json.dumps(field_name)}]' - field_path = "`${path}." + field_name + "`" - check = _ts_check_expr(annotation, value_expr="fieldValue", path_expr=field_path) - body.append(" {") - body.append(f" const fieldValue = {access};") - if optional: - body.append(f" if (fieldValue !== undefined && fieldValue !== null) {{ {check} }}") - else: - body.append( - f" if (fieldValue === undefined || fieldValue === null) " - f"errors.push(`${{path}}.{field_name}: missing required field`);" - ) - body.append(f" else {{ {check} }}") - body.append(" }") - body.append("}") - body.append("") - return "\n".join(body) - - # Every generated artifact pair this repo currently ships, as # (dataclass, title, source label, output directory) - the registry a `--check` # invocation (and each island's own codegen call) walks. Deliberately a plain diff --git a/web_ui/scripts/check-bundle-size.mjs b/web_ui/scripts/check-bundle-size.mjs index 22dd800c..cb2b1083 100644 --- a/web_ui/scripts/check-bundle-size.mjs +++ b/web_ui/scripts/check-bundle-size.mjs @@ -1,10 +1,11 @@ // ADR-019 stage 19.2: the bundle-size CI counting gate. // // Fails the build when the built JS grows past a ratchet ceiling. This is a -// REGRESSION ratchet pinned ~5% above measured reality, NOT the ADR-019 -// budget itself (initial chunk <= 500 KiB, rest lazy-loaded) - this gate's -// job is to make sure the number only ever moves DOWN: nobody can add half a -// megabyte of dependencies without CI saying so. +// REGRESSION ratchet pinned a few percent above measured reality - and since +// 2026-09-19, when the initial chunk first fit, that ceiling IS the ADR-019 +// budget (initial chunk <= 500 KiB, rest lazy-loaded; see the last +// amendment). This gate's job is to make sure the number only ever moves +// DOWN: nobody can add half a megabyte of dependencies without CI saying so. // // ADR-011 stage 11.6 landed the code-splitting this file's own prior comment // was waiting on (React.lazy for SettingsDialog/ChatLibraryDialog/HelpDialog @@ -193,7 +194,27 @@ const ASSETS_DIR = join(HERE, "..", "dist", "app", "assets"); // lib/bridge-core (~84,000), app/canvas (~82,000) and app/chrome (~78,000). // Closing the last ~83,500 bytes means splitting app source, not removing a // dependency - which is a real option now that it is the only one left. -const LARGEST_CHUNK_CEILING_BYTES = 613_000; +// +// Deliberate, commented amendment - 2026-09-19 (ADR-019 section 4). +// +// The initial chunk is UNDER the ADR-019 budget, and this ceiling is now the +// budget itself: 512,000 bytes, ~3.4% over the measured 495,013 - the same +// headroom posture as every amendment above, except that from here the +// ratchet and the budget are the same number. +// +// Measured from 599,947 (this branch's starting point: 595,926 on main, plus +// ~4,000 for the sparse-row defaults table the scene contract now carries). +// A sourcemap attribution put the largest block of app source in the chunk in +// one place nobody had looked: the generated wire validators, ~87,000 bytes, +// 47,908 of them scene-state.ts alone - codegen emitted one unrolled block +// per field, ~190 bytes each. contracts/codegen.py now emits each payload's +// shape as a field table (~25 bytes a field) that +// src/lib/bridge-core/wireCheck.ts compiles into a checker once at load: +// 599,947 -> 529,180. The remaining ~34,000 came from deferring, on the +// existing LazySurface terms, every chrome surface only a click or shortcut +// opens (About, Builder/Harness launch, Command palette, Diagnostics, Global +// and Knowledge search, Quick switcher, View): 529,180 -> 495,013. +const LARGEST_CHUNK_CEILING_BYTES = 512_000; // Post-11.6 reality: six chunks (main + katex + highlight.js + the three // lazy dialogs) total 1,288,075 bytes - essentially unchanged from the // pre-split single-chunk total, as expected: splitting redistributes code @@ -214,7 +235,10 @@ const LARGEST_CHUNK_CEILING_BYTES = 613_000; // 2026-09-04: re-anchored to 1,456,011 with the same ~3% headroom, for the // per-chunk overhead the node-view split added. Recorded as a raise, small // as it is - the amendment above is what pays for it. -const TOTAL_JS_CEILING_BYTES = 1_500_000; +// 2026-09-19: moved DOWN to ~3% over the measured 1,397,864 (from +// 1,461,180) - the validator tables shrank the total, not just moved bytes +// between chunks, so the ratchet takes the gain. +const TOTAL_JS_CEILING_BYTES = 1_440_000; // Below this much headroom, say so. Both prior amendments record the same // story: the ceiling was set with ~3-5% room, ordinary week-to-week growth diff --git a/web_ui/src/app/App.tsx b/web_ui/src/app/App.tsx index b72177f7..118155ac 100644 --- a/web_ui/src/app/App.tsx +++ b/web_ui/src/app/App.tsx @@ -25,23 +25,14 @@ import { resolveTreeNavigationTarget, type TreeNavigationDirection } from "./can import { requestNewChat } from "./chrome/commands"; import { isGatedWhileTyping, resolveShortcut, type ShortcutId } from "./chrome/shortcuts"; import { DocumentViewPanel } from "./canvas/DocumentViewPanel"; -import { AboutDialog } from "./chrome/AboutDialog"; import { AppBar } from "./chrome/AppBar"; -import { CommandPalette } from "./chrome/CommandPalette"; import { Composer } from "./chrome/Composer"; import { ComposerStore } from "./chrome/composerStore"; -import { DiagnosticsDialog } from "./chrome/DiagnosticsDialog"; -import { GlobalSearchDialog } from "./chrome/GlobalSearchDialog"; -import { KnowledgeSearchDialog } from "./chrome/KnowledgeSearchDialog"; -import { QuickSwitcherDialog } from "./chrome/QuickSwitcherDialog"; -import { BuilderLaunchDialog } from "./chrome/BuilderLaunchDialog"; -import { HarnessLaunchDialog } from "./chrome/HarnessLaunchDialog"; import { NotificationBanner } from "./chrome/NotificationBanner"; import { OnboardingDialog } from "./chrome/OnboardingDialog"; import { PinOverlay } from "./chrome/PinOverlay"; import { PluginPicker } from "./chrome/PluginPicker"; import { SearchOverlay } from "./chrome/SearchOverlay"; -import { ViewPopover } from "./chrome/ViewPopover"; import { OverlayProvider, useOverlays } from "./overlays/overlays"; // ADR-011 stage 11.6: ChatLibraryDialog, HelpDialog (+ its 76-item static @@ -56,6 +47,35 @@ const ChatLibraryDialog = lazy(() => ); const HelpDialog = lazy(() => import("./chrome/HelpDialog").then((m) => ({ default: m.HelpDialog }))); const SettingsDialog = lazy(() => import("./chrome/SettingsDialog").then((m) => ({ default: m.SettingsDialog }))); +// ADR-019's 512,000-byte initial-chunk budget: every other surface that only +// a click or a shortcut opens, on the same LazySurface terms. Each was +// checked for work it does while CLOSED, which a deferred mount would +// silently stop: none has any. OnboardingDialog is the one that does (it +// opens itself on a fresh machine), so it stays eager - see its mount below. +// The topic-subscribing ones are safe mounted late: About and Diagnostics +// are their topic's only subscriber, so their first subscribe still fetches +// a snapshot, and QuickSwitcher already resubscribes for exactly that reason. +const AboutDialog = lazy(() => import("./chrome/AboutDialog").then((m) => ({ default: m.AboutDialog }))); +const BuilderLaunchDialog = lazy(() => + import("./chrome/BuilderLaunchDialog").then((m) => ({ default: m.BuilderLaunchDialog })), +); +const CommandPalette = lazy(() => import("./chrome/CommandPalette").then((m) => ({ default: m.CommandPalette }))); +const DiagnosticsDialog = lazy(() => + import("./chrome/DiagnosticsDialog").then((m) => ({ default: m.DiagnosticsDialog })), +); +const GlobalSearchDialog = lazy(() => + import("./chrome/GlobalSearchDialog").then((m) => ({ default: m.GlobalSearchDialog })), +); +const HarnessLaunchDialog = lazy(() => + import("./chrome/HarnessLaunchDialog").then((m) => ({ default: m.HarnessLaunchDialog })), +); +const KnowledgeSearchDialog = lazy(() => + import("./chrome/KnowledgeSearchDialog").then((m) => ({ default: m.KnowledgeSearchDialog })), +); +const QuickSwitcherDialog = lazy(() => + import("./chrome/QuickSwitcherDialog").then((m) => ({ default: m.QuickSwitcherDialog })), +); +const ViewPopover = lazy(() => import("./chrome/ViewPopover").then((m) => ({ default: m.ViewPopover }))); /** * The single-app shell (Qt-removal plan R0-R2). @@ -221,7 +241,18 @@ function GlobalShortcuts({ store }: { store: SceneStore }) { * (draft form fields, list scroll position, search text) survives being * closed and reopened exactly as it did when these were eagerly mounted. */ -function LazySurface({ overlayName, children }: { overlayName: string; children: ReactNode }) { +function LazySurface({ + overlayName, + children, + fallback = , +}: { + overlayName: string; + children: ReactNode; + /** What shows while the chunk loads. The dialog-shaped default suits a + * dialog; a popover passes null rather than flash a full scrim for the + * few milliseconds a local chunk takes. */ + fallback?: ReactNode; +}) { const overlays = useOverlays(); const isOpen = overlays.isOpen(overlayName); const [hasOpened, setHasOpened] = useState(isOpen); @@ -234,7 +265,7 @@ function LazySurface({ overlayName, children }: { overlayName: string; children: setHasOpened(true); } if (!hasOpened) return null; - return }>{children}; + return {children}; } /** Minimal Suspense fallback for the first-open chunk fetch above - same @@ -457,7 +488,9 @@ function App() { - + + +
@@ -469,9 +502,15 @@ function App() { showTokenCounter={settingsVisibility.showTokenCounter !== false} />
- - - + + + + + + + + + {/* ADR-012 stage 12.6: not lazy, unlike Help/Settings/ Library just below - it has to be mounted from the start so its own app-settings subscription can decide @@ -482,11 +521,21 @@ function App() { - - - - - + + + + + + + + + + + + + + + diff --git a/web_ui/src/lib/bridge-core/generated/app-about-state.ts b/web_ui/src/lib/bridge-core/generated/app-about-state.ts index f497fdad..5ec20396 100644 --- a/web_ui/src/lib/bridge-core/generated/app-about-state.ts +++ b/web_ui/src/lib/bridge-core/generated/app-about-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface AppAboutState { schemaVersion: number; revision: number; @@ -15,73 +19,20 @@ export interface AppAboutState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const APP_ABOUT_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["appName", "s", 0], + ["appVersion", "s", 0], + ["repositoryUrl", "s", 0], + ["developerName", "s", 0], + ["developerWebsiteUrl", "s", 0], + ["developerGithubUrl", "s", 0], + ["copyrightText", "s", 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkAppAboutState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["appName"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.appName: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.appName` + ": expected string"); } - } - { - const fieldValue = value["appVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.appVersion: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.appVersion` + ": expected string"); } - } - { - const fieldValue = value["repositoryUrl"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.repositoryUrl: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.repositoryUrl` + ": expected string"); } - } - { - const fieldValue = value["developerName"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.developerName: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.developerName` + ": expected string"); } - } - { - const fieldValue = value["developerWebsiteUrl"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.developerWebsiteUrl: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.developerWebsiteUrl` + ": expected string"); } - } - { - const fieldValue = value["developerGithubUrl"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.developerGithubUrl: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.developerGithubUrl` + ": expected string"); } - } - { - const fieldValue = value["copyrightText"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.copyrightText: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.copyrightText` + ": expected string"); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkAppAboutState = compileFields(APP_ABOUT_STATE_FIELDS); export function validateAppAboutState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/app-chat-library-state.ts b/web_ui/src/lib/bridge-core/generated/app-chat-library-state.ts index 159165cc..52c8df48 100644 --- a/web_ui/src/lib/bridge-core/generated/app-chat-library-state.ts +++ b/web_ui/src/lib/bridge-core/generated/app-chat-library-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface AppChatLibraryRow { id: number; title: string; @@ -35,151 +39,40 @@ export interface AppChatLibraryState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const APP_CHAT_LIBRARY_ROW_FIELDS: WireFields = [ + ["id", "n", 0], + ["title", "s", 0], + ["createdLabel", "s", 0], + ["updatedLabel", "s", 0], + ["createdAtIso", "s", 1], + ["updatedAtIso", "s", 1], + ["preview", "s", 0], + ["messageCount", "n", 0], + ["workspaceId", "n", 0], + ["favorite", "b", 0], + ["archived", "b", 0], + ["tags", { a: "s" }, 0], +]; -function checkAppChatLibraryRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.id` + ": expected number"); } - } - { - const fieldValue = value["title"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.title: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.title` + ": expected string"); } - } - { - const fieldValue = value["createdLabel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.createdLabel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.createdLabel` + ": expected string"); } - } - { - const fieldValue = value["updatedLabel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.updatedLabel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.updatedLabel` + ": expected string"); } - } - { - const fieldValue = value["createdAtIso"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.createdAtIso` + ": expected string"); } - } - { - const fieldValue = value["updatedAtIso"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.updatedAtIso` + ": expected string"); } - } - { - const fieldValue = value["preview"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.preview: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.preview` + ": expected string"); } - } - { - const fieldValue = value["messageCount"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.messageCount: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.messageCount` + ": expected number"); } - } - { - const fieldValue = value["workspaceId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.workspaceId: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.workspaceId` + ": expected number"); } - } - { - const fieldValue = value["favorite"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.favorite: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.favorite` + ": expected boolean"); } - } - { - const fieldValue = value["archived"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.archived: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.archived` + ": expected boolean"); } - } - { - const fieldValue = value["tags"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.tags: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.tags` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.tags` + `[${i}]` + ": expected string"); }); } - } -} +const APP_WORKSPACE_ROW_FIELDS: WireFields = [ + ["id", "n", 0], + ["name", "s", 0], + ["icon", "s", 0], + ["archived", "b", 0], + ["defaultModelProvider", "s", 0], + ["defaultModelId", "s", 0], +]; -function checkAppWorkspaceRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.id` + ": expected number"); } - } - { - const fieldValue = value["name"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.name: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.name` + ": expected string"); } - } - { - const fieldValue = value["icon"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.icon: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.icon` + ": expected string"); } - } - { - const fieldValue = value["archived"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.archived: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.archived` + ": expected boolean"); } - } - { - const fieldValue = value["defaultModelProvider"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.defaultModelProvider: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.defaultModelProvider` + ": expected string"); } - } - { - const fieldValue = value["defaultModelId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.defaultModelId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.defaultModelId` + ": expected string"); } - } -} +const APP_CHAT_LIBRARY_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["rows", { a: { o: APP_CHAT_LIBRARY_ROW_FIELDS } }, 0], + ["workspaces", { a: { o: APP_WORKSPACE_ROW_FIELDS } }, 0], + ["notice", "s", 1], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkAppChatLibraryState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["rows"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.rows: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.rows` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkAppChatLibraryRow(item, `${path}.rows` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["workspaces"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.workspaces: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.workspaces` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkAppWorkspaceRow(item, `${path}.workspaces` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["notice"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.notice` + ": expected string"); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkAppChatLibraryState = compileFields(APP_CHAT_LIBRARY_STATE_FIELDS); export function validateAppChatLibraryState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/app-composer-state.ts b/web_ui/src/lib/bridge-core/generated/app-composer-state.ts index a16b7a89..076a2832 100644 --- a/web_ui/src/lib/bridge-core/generated/app-composer-state.ts +++ b/web_ui/src/lib/bridge-core/generated/app-composer-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface AppComposerDraft { id: string; text: string; @@ -91,343 +95,96 @@ export interface AppComposerState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. - -function checkAppComposerDraft(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["text"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.text: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.text` + ": expected string"); } - } - { - const fieldValue = value["contextMode"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.contextMode: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.contextMode` + ": expected string"); } - } - { - const fieldValue = value["sendMode"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sendMode: missing required field`); - else { if (!["enter_to_send", "ctrl_enter_to_send"].includes(fieldValue as string)) errors.push(`${path}.sendMode` + `: ${JSON.stringify(fieldValue)} is not one of [` + "enter_to_send, ctrl_enter_to_send" + `]`); } - } -} - -function checkAppComposerContext(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["anchor"]; - if (fieldValue !== undefined && fieldValue !== null) { checkAppComposerContextAnchor(fieldValue, `${path}.anchor`, errors); } - } - { - const fieldValue = value["items"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.items: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.items` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkAppComposerAttachment(item, `${path}.items` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["totalTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.totalTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.totalTokens` + ": expected number"); } - } - { - const fieldValue = value["reviewAvailable"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.reviewAvailable: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.reviewAvailable` + ": expected boolean"); } - } -} - -function checkAppComposerContextAnchor(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["label"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.label: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.label` + ": expected string"); } - } - { - const fieldValue = value["type"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.type: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.type` + ": expected string"); } - } -} - -function checkAppComposerAttachment(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["name"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.name: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.name` + ": expected string"); } - } - { - const fieldValue = value["kind"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.kind: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.kind` + ": expected string"); } - } - { - const fieldValue = value["byteSize"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.byteSize: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.byteSize` + ": expected number"); } - } - { - const fieldValue = value["contextLabel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.contextLabel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.contextLabel` + ": expected string"); } - } - { - const fieldValue = value["tokenCount"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.tokenCount: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.tokenCount` + ": expected number"); } - } -} - -function checkAppComposerRoute(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["mode"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.mode: missing required field`); - else { if (!["ollama", "api", "llama_cpp"].includes(fieldValue as string)) errors.push(`${path}.mode` + `: ${JSON.stringify(fieldValue)} is not one of [` + "ollama, api, llama_cpp" + `]`); } - } - { - const fieldValue = value["provider"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.provider: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.provider` + ": expected string"); } - } - { - const fieldValue = value["modelId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.modelId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.modelId` + ": expected string"); } - } - { - const fieldValue = value["modelLabel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.modelLabel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.modelLabel` + ": expected string"); } - } - { - const fieldValue = value["modelOptions"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.modelOptions: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.modelOptions` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkAppComposerModelOption(item, `${path}.modelOptions` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["reasoning"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.reasoning: missing required field`); - else { checkAppComposerReasoning(fieldValue, `${path}.reasoning`, errors); } - } - { - const fieldValue = value["label"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.label: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.label` + ": expected string"); } - } - { - const fieldValue = value["available"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.available: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.available` + ": expected boolean"); } - } - { - const fieldValue = value["canChange"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.canChange: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.canChange` + ": expected boolean"); } - } - { - const fieldValue = value["modelValue"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.modelValue` + ": expected string"); } - } -} - -function checkAppComposerModelOption(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["label"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.label: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.label` + ": expected string"); } - } -} - -function checkAppComposerReasoning(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["level"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.level: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.level` + ": expected string"); } - } - { - const fieldValue = value["label"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.label: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.label` + ": expected string"); } - } - { - const fieldValue = value["options"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.options: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.options` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkAppComposerReasoningOption(item, `${path}.options` + `[${i}]`, errors); }); } - } -} - -function checkAppComposerReasoningOption(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["label"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.label: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.label` + ": expected string"); } - } - { - const fieldValue = value["description"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.description: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.description` + ": expected string"); } - } -} - -function checkAppComposerRequest(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["state"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.state: missing required field`); - else { if (!["idle", "preparing", "uploading", "waiting", "generating", "finalizing", "canceled", "failed", "succeeded"].includes(fieldValue as string)) errors.push(`${path}.state` + `: ${JSON.stringify(fieldValue)} is not one of [` + "idle, preparing, uploading, waiting, generating, finalizing, canceled, failed, succeeded" + `]`); } - } - { - const fieldValue = value["message"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.message: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.message` + ": expected string"); } - } - { - const fieldValue = value["canSend"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.canSend: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.canSend` + ": expected boolean"); } - } - { - const fieldValue = value["canCancel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.canCancel: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.canCancel` + ": expected boolean"); } - } - { - const fieldValue = value["canRetry"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.canRetry: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.canRetry` + ": expected boolean"); } - } -} - -function checkAppComposerCapabilities(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["attachments"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.attachments: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.attachments` + ": expected boolean"); } - } - { - const fieldValue = value["contextReview"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.contextReview: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.contextReview` + ": expected boolean"); } - } - { - const fieldValue = value["routeSelection"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.routeSelection: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.routeSelection` + ": expected boolean"); } - } - { - const fieldValue = value["modelSelection"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.modelSelection: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.modelSelection` + ": expected boolean"); } - } - { - const fieldValue = value["reasoningSelection"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.reasoningSelection: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.reasoningSelection` + ": expected boolean"); } - } - { - const fieldValue = value["settingsShortcut"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.settingsShortcut: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.settingsShortcut` + ": expected boolean"); } - } - { - const fieldValue = value["cancellation"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.cancellation: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.cancellation` + ": expected boolean"); } - } -} - -function checkAppComposerState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["draft"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.draft: missing required field`); - else { checkAppComposerDraft(fieldValue, `${path}.draft`, errors); } - } - { - const fieldValue = value["context"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.context: missing required field`); - else { checkAppComposerContext(fieldValue, `${path}.context`, errors); } - } - { - const fieldValue = value["route"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.route: missing required field`); - else { checkAppComposerRoute(fieldValue, `${path}.route`, errors); } - } - { - const fieldValue = value["request"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.request: missing required field`); - else { checkAppComposerRequest(fieldValue, `${path}.request`, errors); } - } - { - const fieldValue = value["capabilities"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.capabilities: missing required field`); - else { checkAppComposerCapabilities(fieldValue, `${path}.capabilities`, errors); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const APP_COMPOSER_DRAFT_FIELDS: WireFields = [ + ["id", "s", 0], + ["text", "s", 0], + ["contextMode", "s", 0], + ["sendMode", { e: ["enter_to_send", "ctrl_enter_to_send"] }, 0], +]; + +const APP_COMPOSER_CONTEXT_ANCHOR_FIELDS: WireFields = [ + ["id", "s", 0], + ["label", "s", 0], + ["type", "s", 0], +]; + +const APP_COMPOSER_ATTACHMENT_FIELDS: WireFields = [ + ["id", "s", 0], + ["name", "s", 0], + ["kind", "s", 0], + ["byteSize", "n", 0], + ["contextLabel", "s", 0], + ["tokenCount", "n", 0], +]; + +const APP_COMPOSER_CONTEXT_FIELDS: WireFields = [ + ["anchor", { o: APP_COMPOSER_CONTEXT_ANCHOR_FIELDS }, 1], + ["items", { a: { o: APP_COMPOSER_ATTACHMENT_FIELDS } }, 0], + ["totalTokens", "n", 0], + ["reviewAvailable", "b", 0], +]; + +const APP_COMPOSER_MODEL_OPTION_FIELDS: WireFields = [ + ["id", "s", 0], + ["label", "s", 0], +]; + +const APP_COMPOSER_REASONING_OPTION_FIELDS: WireFields = [ + ["id", "s", 0], + ["label", "s", 0], + ["description", "s", 0], +]; + +const APP_COMPOSER_REASONING_FIELDS: WireFields = [ + ["level", "s", 0], + ["label", "s", 0], + ["options", { a: { o: APP_COMPOSER_REASONING_OPTION_FIELDS } }, 0], +]; + +const APP_COMPOSER_ROUTE_FIELDS: WireFields = [ + ["mode", { e: ["ollama", "api", "llama_cpp"] }, 0], + ["provider", "s", 0], + ["modelId", "s", 0], + ["modelLabel", "s", 0], + ["modelOptions", { a: { o: APP_COMPOSER_MODEL_OPTION_FIELDS } }, 0], + ["reasoning", { o: APP_COMPOSER_REASONING_FIELDS }, 0], + ["label", "s", 0], + ["available", "b", 0], + ["canChange", "b", 0], + ["modelValue", "s", 1], +]; + +const APP_COMPOSER_REQUEST_FIELDS: WireFields = [ + ["id", "s", 1], + ["state", { e: ["idle", "preparing", "uploading", "waiting", "generating", "finalizing", "canceled", "failed", "succeeded"] }, 0], + ["message", "s", 0], + ["canSend", "b", 0], + ["canCancel", "b", 0], + ["canRetry", "b", 0], +]; + +const APP_COMPOSER_CAPABILITIES_FIELDS: WireFields = [ + ["attachments", "b", 0], + ["contextReview", "b", 0], + ["routeSelection", "b", 0], + ["modelSelection", "b", 0], + ["reasoningSelection", "b", 0], + ["settingsShortcut", "b", 0], + ["cancellation", "b", 0], +]; + +const APP_COMPOSER_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["draft", { o: APP_COMPOSER_DRAFT_FIELDS }, 0], + ["context", { o: APP_COMPOSER_CONTEXT_FIELDS }, 0], + ["route", { o: APP_COMPOSER_ROUTE_FIELDS }, 0], + ["request", { o: APP_COMPOSER_REQUEST_FIELDS }, 0], + ["capabilities", { o: APP_COMPOSER_CAPABILITIES_FIELDS }, 0], + ["minCompatibleSchemaVersion", "n", 1], +]; + +const checkAppComposerState = compileFields(APP_COMPOSER_STATE_FIELDS); export function validateAppComposerState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/app-plugins-state.ts b/web_ui/src/lib/bridge-core/generated/app-plugins-state.ts index 8a635a0d..6f869f9c 100644 --- a/web_ui/src/lib/bridge-core/generated/app-plugins-state.ts +++ b/web_ui/src/lib/bridge-core/generated/app-plugins-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface AppPluginCategory { name: string; description: string; @@ -28,109 +32,33 @@ export interface AppPluginsState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; +const APP_PLUGIN_ENTRY_FIELDS: WireFields = [ + ["name", "s", 0], + ["description", "s", 0], +]; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} +const APP_PLUGIN_CATEGORY_FIELDS: WireFields = [ + ["name", "s", 0], + ["description", "s", 0], + ["plugins", { a: { o: APP_PLUGIN_ENTRY_FIELDS } }, 0], +]; -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const APP_PLUGIN_GRANT_FIELDS: WireFields = [ + ["pluginId", "s", 0], + ["name", "s", 0], + ["scopes", { a: "s" }, 0], + ["granted", "b", 0], +]; -function checkAppPluginCategory(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["name"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.name: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.name` + ": expected string"); } - } - { - const fieldValue = value["description"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.description: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.description` + ": expected string"); } - } - { - const fieldValue = value["plugins"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.plugins: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.plugins` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkAppPluginEntry(item, `${path}.plugins` + `[${i}]`, errors); }); } - } -} +const APP_PLUGINS_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["categories", { a: { o: APP_PLUGIN_CATEGORY_FIELDS } }, 0], + ["grants", { a: { o: APP_PLUGIN_GRANT_FIELDS } }, 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkAppPluginEntry(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["name"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.name: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.name` + ": expected string"); } - } - { - const fieldValue = value["description"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.description: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.description` + ": expected string"); } - } -} - -function checkAppPluginGrant(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["pluginId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.pluginId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.pluginId` + ": expected string"); } - } - { - const fieldValue = value["name"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.name: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.name` + ": expected string"); } - } - { - const fieldValue = value["scopes"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.scopes: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.scopes` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.scopes` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["granted"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.granted: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.granted` + ": expected boolean"); } - } -} - -function checkAppPluginsState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["categories"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.categories: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.categories` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkAppPluginCategory(item, `${path}.categories` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["grants"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.grants: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.grants` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkAppPluginGrant(item, `${path}.grants` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkAppPluginsState = compileFields(APP_PLUGINS_STATE_FIELDS); export function validateAppPluginsState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/app-settings-state.ts b/web_ui/src/lib/bridge-core/generated/app-settings-state.ts index b9762271..50781c97 100644 --- a/web_ui/src/lib/bridge-core/generated/app-settings-state.ts +++ b/web_ui/src/lib/bridge-core/generated/app-settings-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface ApiModelDescriptor { modelId: string; provider: string; @@ -71,347 +75,76 @@ export interface AppSettingsState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const API_MODEL_DESCRIPTOR_FIELDS: WireFields = [ + ["modelId", "s", 0], + ["provider", "s", 0], + ["capabilities", { a: "s" }, 0], + ["ready", "b", 0], + ["available", "b", 0], +]; -function checkApiModelDescriptor(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["modelId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.modelId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.modelId` + ": expected string"); } - } - { - const fieldValue = value["provider"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.provider: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.provider` + ": expected string"); } - } - { - const fieldValue = value["capabilities"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.capabilities: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.capabilities` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.capabilities` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["ready"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ready: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.ready` + ": expected boolean"); } - } - { - const fieldValue = value["available"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.available: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.available` + ": expected boolean"); } - } -} +const MCP_SERVER_CONFIG_FIELDS: WireFields = [ + ["id", "s", 0], + ["name", "s", 0], + ["command", "s", 0], + ["args", { a: "s" }, 0], + ["scopes", { a: "s" }, 0], + ["approval", "s", 0], + ["enabledTools", { a: "s" }, 0], + ["enabled", "b", 0], + ["timeout", "n", 0], + ["envKeys", { a: "s" }, 0], +]; -function checkMcpServerConfig(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["name"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.name: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.name` + ": expected string"); } - } - { - const fieldValue = value["command"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.command: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.command` + ": expected string"); } - } - { - const fieldValue = value["args"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.args: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.args` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.args` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["scopes"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.scopes: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.scopes` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.scopes` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["approval"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.approval: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.approval` + ": expected string"); } - } - { - const fieldValue = value["enabledTools"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.enabledTools: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.enabledTools` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.enabledTools` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["enabled"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.enabled: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.enabled` + ": expected boolean"); } - } - { - const fieldValue = value["timeout"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.timeout: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.timeout` + ": expected number"); } - } - { - const fieldValue = value["envKeys"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.envKeys: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.envKeys` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.envKeys` + `[${i}]` + ": expected string"); }); } - } -} +const APP_SETTINGS_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["activeSection", "s", 0], + ["showTokenCounter", "b", 0], + ["enableSystemPrompt", "b", 0], + ["notificationPreferences", { d: "b" }, 0], + ["githubTokenConfigured", "b", 0], + ["secretsEncryptedAtRest", "b", 0], + ["logLevel", "s", 0], + ["autoModelPolicy", "s", 0], + ["theme", "s", 0], + ["hasCompletedOnboarding", "b", 0], + ["providerMode", "s", 0], + ["activeApiProvider", "s", 0], + ["viewingApiProvider", "s", 0], + ["apiBaseUrl", "s", 0], + ["apiKeyConfigured", { d: "b" }, 0], + ["apiKeySource", { d: "s" }, 0], + ["apiModels", { d: "s" }, 0], + ["apiModelCatalog", { a: { o: API_MODEL_DESCRIPTOR_FIELDS } }, 0], + ["apiCatalogStatus", "s", 0], + ["apiCatalogMessage", "s", 0], + ["geminiStaticModels", { a: "s" }, 0], + ["geminiStaticImageModels", { a: "s" }, 0], + ["ollamaReasoningLevel", "s", 0], + ["ollamaCurrentModel", "s", 0], + ["ollamaModelAssignments", { d: "s" }, 0], + ["ollamaScannedModels", { a: "s" }, 0], + ["ollamaScanSummary", "s", 0], + ["ollamaScanStatus", "s", 0], + ["ollamaPullStatus", "s", 0], + ["ollamaNotice", "s", 0], + ["llamaCppReasoningLevel", "s", 0], + ["llamaCppChatModelPath", "s", 0], + ["llamaCppTitleModelPath", "s", 0], + ["llamaCppChatFormat", "s", 0], + ["llamaCppNCtx", "n", 0], + ["llamaCppNGpuLayers", "n", 0], + ["llamaCppNThreads", "n", 0], + ["llamaCppScannedModels", { a: "s" }, 0], + ["llamaCppScanSummary", "s", 0], + ["llamaCppScanStatus", "s", 0], + ["llamaCppNotice", "s", 0], + ["mcpServers", { a: { o: MCP_SERVER_CONFIG_FIELDS } }, 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkAppSettingsState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["activeSection"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.activeSection: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.activeSection` + ": expected string"); } - } - { - const fieldValue = value["showTokenCounter"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.showTokenCounter: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.showTokenCounter` + ": expected boolean"); } - } - { - const fieldValue = value["enableSystemPrompt"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.enableSystemPrompt: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.enableSystemPrompt` + ": expected boolean"); } - } - { - const fieldValue = value["notificationPreferences"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.notificationPreferences: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.notificationPreferences` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "boolean") errors.push(`${path}.notificationPreferences` + `[${JSON.stringify(k)}]` + ": expected boolean"); }); } - } - { - const fieldValue = value["githubTokenConfigured"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.githubTokenConfigured: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.githubTokenConfigured` + ": expected boolean"); } - } - { - const fieldValue = value["secretsEncryptedAtRest"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.secretsEncryptedAtRest: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.secretsEncryptedAtRest` + ": expected boolean"); } - } - { - const fieldValue = value["logLevel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.logLevel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.logLevel` + ": expected string"); } - } - { - const fieldValue = value["autoModelPolicy"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.autoModelPolicy: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.autoModelPolicy` + ": expected string"); } - } - { - const fieldValue = value["theme"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.theme: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.theme` + ": expected string"); } - } - { - const fieldValue = value["hasCompletedOnboarding"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.hasCompletedOnboarding: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.hasCompletedOnboarding` + ": expected boolean"); } - } - { - const fieldValue = value["providerMode"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.providerMode: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.providerMode` + ": expected string"); } - } - { - const fieldValue = value["activeApiProvider"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.activeApiProvider: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.activeApiProvider` + ": expected string"); } - } - { - const fieldValue = value["viewingApiProvider"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.viewingApiProvider: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.viewingApiProvider` + ": expected string"); } - } - { - const fieldValue = value["apiBaseUrl"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.apiBaseUrl: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.apiBaseUrl` + ": expected string"); } - } - { - const fieldValue = value["apiKeyConfigured"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.apiKeyConfigured: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.apiKeyConfigured` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "boolean") errors.push(`${path}.apiKeyConfigured` + `[${JSON.stringify(k)}]` + ": expected boolean"); }); } - } - { - const fieldValue = value["apiKeySource"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.apiKeySource: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.apiKeySource` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "string") errors.push(`${path}.apiKeySource` + `[${JSON.stringify(k)}]` + ": expected string"); }); } - } - { - const fieldValue = value["apiModels"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.apiModels: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.apiModels` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "string") errors.push(`${path}.apiModels` + `[${JSON.stringify(k)}]` + ": expected string"); }); } - } - { - const fieldValue = value["apiModelCatalog"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.apiModelCatalog: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.apiModelCatalog` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkApiModelDescriptor(item, `${path}.apiModelCatalog` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["apiCatalogStatus"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.apiCatalogStatus: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.apiCatalogStatus` + ": expected string"); } - } - { - const fieldValue = value["apiCatalogMessage"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.apiCatalogMessage: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.apiCatalogMessage` + ": expected string"); } - } - { - const fieldValue = value["geminiStaticModels"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.geminiStaticModels: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.geminiStaticModels` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.geminiStaticModels` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["geminiStaticImageModels"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.geminiStaticImageModels: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.geminiStaticImageModels` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.geminiStaticImageModels` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["ollamaReasoningLevel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ollamaReasoningLevel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.ollamaReasoningLevel` + ": expected string"); } - } - { - const fieldValue = value["ollamaCurrentModel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ollamaCurrentModel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.ollamaCurrentModel` + ": expected string"); } - } - { - const fieldValue = value["ollamaModelAssignments"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ollamaModelAssignments: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.ollamaModelAssignments` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "string") errors.push(`${path}.ollamaModelAssignments` + `[${JSON.stringify(k)}]` + ": expected string"); }); } - } - { - const fieldValue = value["ollamaScannedModels"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ollamaScannedModels: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.ollamaScannedModels` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.ollamaScannedModels` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["ollamaScanSummary"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ollamaScanSummary: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.ollamaScanSummary` + ": expected string"); } - } - { - const fieldValue = value["ollamaScanStatus"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ollamaScanStatus: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.ollamaScanStatus` + ": expected string"); } - } - { - const fieldValue = value["ollamaPullStatus"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ollamaPullStatus: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.ollamaPullStatus` + ": expected string"); } - } - { - const fieldValue = value["ollamaNotice"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.ollamaNotice: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.ollamaNotice` + ": expected string"); } - } - { - const fieldValue = value["llamaCppReasoningLevel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppReasoningLevel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.llamaCppReasoningLevel` + ": expected string"); } - } - { - const fieldValue = value["llamaCppChatModelPath"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppChatModelPath: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.llamaCppChatModelPath` + ": expected string"); } - } - { - const fieldValue = value["llamaCppTitleModelPath"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppTitleModelPath: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.llamaCppTitleModelPath` + ": expected string"); } - } - { - const fieldValue = value["llamaCppChatFormat"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppChatFormat: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.llamaCppChatFormat` + ": expected string"); } - } - { - const fieldValue = value["llamaCppNCtx"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppNCtx: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.llamaCppNCtx` + ": expected number"); } - } - { - const fieldValue = value["llamaCppNGpuLayers"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppNGpuLayers: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.llamaCppNGpuLayers` + ": expected number"); } - } - { - const fieldValue = value["llamaCppNThreads"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppNThreads: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.llamaCppNThreads` + ": expected number"); } - } - { - const fieldValue = value["llamaCppScannedModels"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppScannedModels: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.llamaCppScannedModels` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.llamaCppScannedModels` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["llamaCppScanSummary"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppScanSummary: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.llamaCppScanSummary` + ": expected string"); } - } - { - const fieldValue = value["llamaCppScanStatus"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppScanStatus: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.llamaCppScanStatus` + ": expected string"); } - } - { - const fieldValue = value["llamaCppNotice"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.llamaCppNotice: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.llamaCppNotice` + ": expected string"); } - } - { - const fieldValue = value["mcpServers"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.mcpServers: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.mcpServers` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkMcpServerConfig(item, `${path}.mcpServers` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkAppSettingsState = compileFields(APP_SETTINGS_STATE_FIELDS); export function validateAppSettingsState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/diagnostics-state.ts b/web_ui/src/lib/bridge-core/generated/diagnostics-state.ts index e082d850..aa957df6 100644 --- a/web_ui/src/lib/bridge-core/generated/diagnostics-state.ts +++ b/web_ui/src/lib/bridge-core/generated/diagnostics-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface DiagnosticsRunRow { runId: string; kind: string; @@ -30,123 +34,35 @@ export interface DiagnosticsState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const DIAGNOSTICS_RUN_ROW_FIELDS: WireFields = [ + ["runId", "s", 0], + ["kind", "s", 0], + ["nodeId", "s", 1], + ["outcome", "s", 0], + ["durationSeconds", "n", 1], +]; -function checkDiagnosticsRunRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["runId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.runId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.runId` + ": expected string"); } - } - { - const fieldValue = value["kind"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.kind: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.kind` + ": expected string"); } - } - { - const fieldValue = value["nodeId"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.nodeId` + ": expected string"); } - } - { - const fieldValue = value["outcome"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.outcome: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.outcome` + ": expected string"); } - } - { - const fieldValue = value["durationSeconds"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.durationSeconds` + ": expected number"); } - } -} +const DIAGNOSTICS_PROVIDER_ERROR_FIELDS: WireFields = [ + ["provider", "s", 0], + ["message", "s", 0], + ["at", "n", 0], +]; -function checkDiagnosticsProviderError(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["provider"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.provider: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.provider` + ": expected string"); } - } - { - const fieldValue = value["message"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.message: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.message` + ": expected string"); } - } - { - const fieldValue = value["at"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.at: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.at` + ": expected number"); } - } -} +const DIAGNOSTICS_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["recentRuns", { a: { o: DIAGNOSTICS_RUN_ROW_FIELDS } }, 0], + ["publishCount", "n", 0], + ["publishBytesTotal", "n", 0], + ["lastPublishBytes", "n", 1], + ["lastPublishTopic", "s", 1], + ["publishBytesPerSecond", "n", 0], + ["sessionCount", "n", 1], + ["providerErrors", { a: { o: DIAGNOSTICS_PROVIDER_ERROR_FIELDS } }, 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkDiagnosticsState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["recentRuns"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.recentRuns: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.recentRuns` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkDiagnosticsRunRow(item, `${path}.recentRuns` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["publishCount"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.publishCount: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.publishCount` + ": expected number"); } - } - { - const fieldValue = value["publishBytesTotal"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.publishBytesTotal: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.publishBytesTotal` + ": expected number"); } - } - { - const fieldValue = value["lastPublishBytes"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.lastPublishBytes` + ": expected number"); } - } - { - const fieldValue = value["lastPublishTopic"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.lastPublishTopic` + ": expected string"); } - } - { - const fieldValue = value["publishBytesPerSecond"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.publishBytesPerSecond: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.publishBytesPerSecond` + ": expected number"); } - } - { - const fieldValue = value["sessionCount"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.sessionCount` + ": expected number"); } - } - { - const fieldValue = value["providerErrors"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.providerErrors: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.providerErrors` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkDiagnosticsProviderError(item, `${path}.providerErrors` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkDiagnosticsState = compileFields(DIAGNOSTICS_STATE_FIELDS); export function validateDiagnosticsState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/drag-speed-state.ts b/web_ui/src/lib/bridge-core/generated/drag-speed-state.ts index f646aa7e..d955f645 100644 --- a/web_ui/src/lib/bridge-core/generated/drag-speed-state.ts +++ b/web_ui/src/lib/bridge-core/generated/drag-speed-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface DragSpeedState { schemaVersion: number; revision: number; @@ -11,54 +15,16 @@ export interface DragSpeedState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const DRAG_SPEED_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["percentPresets", { a: "n" }, 0], + ["percentMin", "n", 0], + ["percentMax", "n", 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkDragSpeedState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["percentPresets"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.percentPresets: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.percentPresets` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "number") errors.push(`${path}.percentPresets` + `[${i}]` + ": expected number"); }); } - } - { - const fieldValue = value["percentMin"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.percentMin: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.percentMin` + ": expected number"); } - } - { - const fieldValue = value["percentMax"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.percentMax: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.percentMax` + ": expected number"); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkDragSpeedState = compileFields(DRAG_SPEED_STATE_FIELDS); export function validateDragSpeedState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/execution-limits-state.ts b/web_ui/src/lib/bridge-core/generated/execution-limits-state.ts index 2101e1e5..9218cfb2 100644 --- a/web_ui/src/lib/bridge-core/generated/execution-limits-state.ts +++ b/web_ui/src/lib/bridge-core/generated/execution-limits-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface ExecutionLimitsState { schemaVersion: number; revision: number; @@ -9,43 +13,14 @@ export interface ExecutionLimitsState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const EXECUTION_LIMITS_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["codeSandboxResourceLimitsText", "s", 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkExecutionLimitsState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["codeSandboxResourceLimitsText"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxResourceLimitsText: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeSandboxResourceLimitsText` + ": expected string"); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkExecutionLimitsState = compileFields(EXECUTION_LIMITS_STATE_FIELDS); export function validateExecutionLimitsState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/font-control-state.ts b/web_ui/src/lib/bridge-core/generated/font-control-state.ts index d50b272a..f7109bc0 100644 --- a/web_ui/src/lib/bridge-core/generated/font-control-state.ts +++ b/web_ui/src/lib/bridge-core/generated/font-control-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface FontControlState { schemaVersion: number; revision: number; @@ -12,60 +16,17 @@ export interface FontControlState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const FONT_CONTROL_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["fontFamilies", { a: "s" }, 0], + ["colorPresets", { a: "s" }, 0], + ["sizeMin", "n", 0], + ["sizeMax", "n", 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkFontControlState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["fontFamilies"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.fontFamilies: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.fontFamilies` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.fontFamilies` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["colorPresets"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.colorPresets: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.colorPresets` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.colorPresets` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["sizeMin"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sizeMin: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.sizeMin` + ": expected number"); } - } - { - const fieldValue = value["sizeMax"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sizeMax: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.sizeMax` + ": expected number"); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkFontControlState = compileFields(FONT_CONTROL_STATE_FIELDS); export function validateFontControlState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/grid-control-state.ts b/web_ui/src/lib/bridge-core/generated/grid-control-state.ts index 6eeccbb1..69a686c8 100644 --- a/web_ui/src/lib/bridge-core/generated/grid-control-state.ts +++ b/web_ui/src/lib/bridge-core/generated/grid-control-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface GridControlState { schemaVersion: number; revision: number; @@ -15,76 +19,20 @@ export interface GridControlState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const GRID_CONTROL_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["gridSize", "n", 0], + ["gridOpacityPercent", "n", 0], + ["gridStyle", "s", 0], + ["gridColor", "s", 0], + ["sizePresets", { a: "n" }, 0], + ["stylePresets", { a: "s" }, 0], + ["colorPresets", { a: "s" }, 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkGridControlState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["gridSize"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gridSize: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.gridSize` + ": expected number"); } - } - { - const fieldValue = value["gridOpacityPercent"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gridOpacityPercent: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.gridOpacityPercent` + ": expected number"); } - } - { - const fieldValue = value["gridStyle"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gridStyle: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gridStyle` + ": expected string"); } - } - { - const fieldValue = value["gridColor"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gridColor: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gridColor` + ": expected string"); } - } - { - const fieldValue = value["sizePresets"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sizePresets: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.sizePresets` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "number") errors.push(`${path}.sizePresets` + `[${i}]` + ": expected number"); }); } - } - { - const fieldValue = value["stylePresets"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.stylePresets: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.stylePresets` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.stylePresets` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["colorPresets"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.colorPresets: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.colorPresets` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.colorPresets` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkGridControlState = compileFields(GRID_CONTROL_STATE_FIELDS); export function validateGridControlState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/notification-state.ts b/web_ui/src/lib/bridge-core/generated/notification-state.ts index 6457ba30..82503acf 100644 --- a/web_ui/src/lib/bridge-core/generated/notification-state.ts +++ b/web_ui/src/lib/bridge-core/generated/notification-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface NotificationState { schemaVersion: number; revision: number; @@ -11,53 +15,16 @@ export interface NotificationState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const NOTIFICATION_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["visible", "b", 0], + ["message", "s", 0], + ["msgType", { e: ["info", "success", "warning", "error"] }, 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkNotificationState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["visible"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.visible: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.visible` + ": expected boolean"); } - } - { - const fieldValue = value["message"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.message: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.message` + ": expected string"); } - } - { - const fieldValue = value["msgType"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.msgType: missing required field`); - else { if (!["info", "success", "warning", "error"].includes(fieldValue as string)) errors.push(`${path}.msgType` + `: ${JSON.stringify(fieldValue)} is not one of [` + "info, success, warning, error" + `]`); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkNotificationState = compileFields(NOTIFICATION_STATE_FIELDS); export function validateNotificationState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/generated/scene-state.ts b/web_ui/src/lib/bridge-core/generated/scene-state.ts index c88aa047..45eee379 100644 --- a/web_ui/src/lib/bridge-core/generated/scene-state.ts +++ b/web_ui/src/lib/bridge-core/generated/scene-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields, hydrateRow, isRecord, wireDefaults } from "../wireCheck"; + +export type { ValidationResult }; + export interface SceneNodeRow { id: string; x: number; @@ -343,187 +347,354 @@ export interface SceneState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; +const CONVERSATION_MESSAGE_ROW_FIELDS: WireFields = [ + ["role", { e: ["user", "assistant"] }, 0], + ["content", "s", 0], + ["incomplete", "b", 0], +]; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} +const RESEARCH_SOURCE_ROW_FIELDS: WireFields = [ + ["sourceId", "s", 0], + ["title", "s", 0], + ["url", "s", 0], + ["canonicalUrl", "s", 0], + ["snippet", "s", 0], + ["rank", "n", 0], + ["provider", "s", 0], + ["finalUrl", "s", 0], + ["status", "s", 0], + ["errorCode", "s", 0], + ["errorMessage", "s", 0], + ["truncated", "b", 0], + ["contentHash", "s", 0], + ["citationCount", "n", 0], +]; + +const RESEARCH_CITATION_ROW_FIELDS: WireFields = [ + ["sourceId", "s", 0], + ["marker", "s", 0], + ["claimContext", "s", 0], +]; + +const RESEARCH_RESULT_ROW_FIELDS: WireFields = [ + ["requestId", "s", 0], + ["originalQuery", "s", 0], + ["effectiveQuery", "s", 0], + ["answerMarkdown", "s", 0], + ["sources", { a: { o: RESEARCH_SOURCE_ROW_FIELDS } }, 0], + ["citations", { a: { o: RESEARCH_CITATION_ROW_FIELDS } }, 0], + ["warnings", { a: "s" }, 0], + ["providerSnapshot", { d: "s" }, 0], +]; + +const GITLINK_PENDING_CHANGE_ROW_FIELDS: WireFields = [ + ["path", "s", 0], + ["operation", "s", 0], + ["reason", "s", 0], + ["content", "s", 1], +]; + +const CODE_REVIEW_FILE_ROW_FIELDS: WireFields = [ + ["path", "s", 0], + ["status", "s", 0], + ["additions", "n", 0], + ["deletions", "n", 0], + ["patch", "s", 0], + ["patchTruncated", "b", 0], + ["previousPath", "s", 1], +]; + +const CODE_REVIEW_WALKTHROUGH_GROUP_ROW_FIELDS: WireFields = [ + ["groupTitle", "s", 0], + ["paths", { a: "s" }, 0], + ["explanation", "s", 0], +]; + +const CODE_REVIEW_FINDING_ROW_FIELDS: WireFields = [ + ["id", "s", 0], + ["severity", "s", 0], + ["tier", "s", 0], + ["category", "s", 0], + ["path", "s", 0], + ["line", "n", 0], + ["title", "s", 0], + ["evidence", "s", 0], + ["impact", "s", 0], + ["recommendation", "s", 0], +]; + +const CODE_REVIEW_ERROR_ROW_FIELDS: WireFields = [ + ["id", "s", 0], + ["severity", "s", 0], + ["tier", "s", 0], + ["kind", "s", 0], + ["path", "s", 0], + ["line", "n", 0], + ["title", "s", 0], + ["evidence", "s", 0], + ["fix", "s", 0], +]; + +const CODE_REVIEW_QA_ROW_FIELDS: WireFields = [ + ["question", "s", 0], + ["answer", "s", 0], +]; + +const CHART_FLOW_ROW_FIELDS: WireFields = [ + ["source", "s", 0], + ["target", "s", 0], + ["value", "n", 0], +]; + +const CHART_DATA_ROW_FIELDS: WireFields = [ + ["version", "n", 1], + ["type", { e: ["bar", "line", "pie", "histogram", "sankey"] }, 1], + ["title", "s", 1], + ["labels", { a: "s" }, 1], + ["values", { a: "n" }, 1], + ["xAxis", "s", 1], + ["yAxis", "s", 1], + ["bins", "n", 1], + ["flows", { a: { o: CHART_FLOW_ROW_FIELDS } }, 1], +]; + +const TOOL_INVOCATION_ROW_FIELDS: WireFields = [ + ["id", "s", 0], + ["name", "s", 0], + ["argumentsJson", "s", 0], + ["result", "s", 0], + ["isError", "b", 0], +]; + +const PLAN_STEP_ROW_FIELDS: WireFields = [ + ["id", "s", 0], + ["title", "s", 0], + ["status", "s", 0], + ["detail", "s", 0], +]; + +const BUILDER_ACTIVITY_ROW_FIELDS: WireFields = [ + ["tool", "s", 0], + ["summary", "s", 0], + ["outcome", "s", 0], + ["stepId", "s", 0], + ["elapsedMs", "n", 0], +]; + +const HARNESS_ACTIVITY_ROW_FIELDS: WireFields = [ + ["tool", "s", 0], + ["summary", "s", 0], + ["outcome", "s", 0], + ["elapsedMs", "n", 0], +]; + +const HARNESS_PLAN_STEP_ROW_FIELDS: WireFields = [ + ["text", "s", 0], + ["status", "s", 0], +]; + +const SCENE_NODE_ROW_FIELDS: WireFields = [ + ["id", "s", 0], + ["x", "n", 0], + ["y", "n", 0], + ["title", "s", 0], + ["kind", "s", 0], + ["content", "s", 0, ""], + ["isUser", "b", 0, false], + ["isCollapsed", "b", 0, false], + ["code", "s", 0, ""], + ["language", "s", 0, ""], + ["attachmentKind", "s", 0, ""], + ["filePath", "s", 0, ""], + ["mimeType", "s", 0, ""], + ["durationSeconds", "n", 1, null], + ["byteSize", "n", 1, null], + ["previewLabel", "s", 0, ""], + ["isDocked", "b", 0, false], + ["imageAssetId", "s", 0, ""], + ["history", { a: { o: CONVERSATION_MESSAGE_ROW_FIELDS } }, 0, []], + ["pendingRequestId", "s", 1, null], + ["researchStage", "s", 0, ""], + ["researchCompleted", "n", 0, 0], + ["researchTotal", "n", 0, 0], + ["researchActiveSourceId", "s", 1, null], + ["researchError", "s", 0, ""], + ["researchResult", { o: RESEARCH_RESULT_ROW_FIELDS }, 1, null], + ["researchRetainToKnowledge", "b", 0, false], + ["artifactContent", "s", 0, ""], + ["artifactError", "s", 0, ""], + ["gitlinkRepo", "s", 0, ""], + ["gitlinkBranch", "s", 0, ""], + ["gitlinkScopeMode", "s", 0, "selected"], + ["gitlinkLocalRoot", "s", 0, ""], + ["gitlinkRepoFilePaths", { a: "s" }, 0, []], + ["gitlinkSelectedPaths", { a: "s" }, 0, []], + ["gitlinkTaskPrompt", "s", 0, ""], + ["gitlinkContextStats", { d: "s" }, 0, {}], + ["gitlinkContextSummary", "s", 0, ""], + ["gitlinkContextVersion", "n", 0, 0], + ["gitlinkProposalMarkdown", "s", 0, ""], + ["gitlinkPendingChanges", { a: { o: GITLINK_PENDING_CHANGE_ROW_FIELDS } }, 0, []], + ["gitlinkPreviewText", "s", 0, ""], + ["gitlinkChangeFingerprint", "s", 1, null], + ["gitlinkChangeState", "s", 0, "draft"], + ["gitlinkError", "s", 0, ""], + ["codeReviewPrUrl", "s", 0, ""], + ["codeReviewRepo", "s", 0, ""], + ["codeReviewPrNumber", "n", 0, 0], + ["codeReviewPrTitle", "s", 0, ""], + ["codeReviewPrState", "s", 0, ""], + ["codeReviewPrHtmlUrl", "s", 0, ""], + ["codeReviewBaseRef", "s", 0, ""], + ["codeReviewHeadRef", "s", 0, ""], + ["codeReviewAdditions", "n", 0, 0], + ["codeReviewDeletions", "n", 0, 0], + ["codeReviewChangedFiles", "n", 0, 0], + ["codeReviewFiles", { a: { o: CODE_REVIEW_FILE_ROW_FIELDS } }, 0, []], + ["codeReviewFilesTruncated", "b", 0, false], + ["codeReviewDiffTruncated", "b", 0, false], + ["codeReviewDiffChars", "n", 0, 0], + ["codeReviewDiffVersion", "n", 0, 0], + ["codeReviewWalkthrough", { a: { o: CODE_REVIEW_WALKTHROUGH_GROUP_ROW_FIELDS } }, 0, []], + ["codeReviewFindings", { a: { o: CODE_REVIEW_FINDING_ROW_FIELDS } }, 0, []], + ["codeReviewErrors", { a: { o: CODE_REVIEW_ERROR_ROW_FIELDS } }, 0, []], + ["codeReviewDismissedIds", { a: "s" }, 0, []], + ["codeReviewTitle", "s", 0, ""], + ["codeReviewOverview", "s", 0, ""], + ["codeReviewConfidence", "s", 0, ""], + ["codeReviewScores", { d: "s" }, 0, {}], + ["codeReviewQualityScore", "n", 0, 0], + ["codeReviewVerdict", "s", 0, "none"], + ["codeReviewRisk", "s", 0, ""], + ["codeReviewQualitySummary", "s", 0, ""], + ["codeReviewQa", { a: { o: CODE_REVIEW_QA_ROW_FIELDS } }, 0, []], + ["codeReviewState", "s", 0, "draft"], + ["codeReviewError", "s", 0, ""], + ["codeSandboxRequirements", "s", 0, ""], + ["codeSandboxPrompt", "s", 0, ""], + ["codeSandboxCode", "s", 0, ""], + ["codeSandboxOutput", "s", 0, ""], + ["codeSandboxAnalysis", "s", 0, ""], + ["codeSandboxAwaitingApproval", "b", 0, false], + ["codeSandboxApprovalRequirements", "s", 0, ""], + ["codeSandboxApprovalAllowSourceBuilds", "b", 0, false], + ["codeSandboxApprovalIsRepair", "b", 0, false], + ["codeSandboxError", "s", 0, ""], + ["provider", "s", 1, null], + ["model", "s", 1, null], + ["isBranchSynthesis", "b", 0, false], + ["synthesisInstructions", "s", 0, ""], + ["branchStatus", "s", 0, "active"], + ["responseIncomplete", "b", 0, false], + ["promptTokens", "n", 1, null], + ["completionTokens", "n", 1, null], + ["estimatedCostUsd", "n", 1, null], + ["isFinalDeliverable", "b", 0, false], + ["color", "s", 1, null], + ["headerColor", "s", 1, null], + ["isSystemPrompt", "b", 0, false], + ["isSummaryNote", "b", 0, false], + ["isBranchComparison", "b", 0, false], + ["itemIds", { a: "s" }, 0, []], + ["isLocked", "b", 0, true], + ["groupWidth", "n", 1, null], + ["groupHeight", "n", 1, null], + ["chartType", "s", 0, ""], + ["chartData", { o: CHART_DATA_ROW_FIELDS }, 0, {}], + ["chartError", "s", 0, ""], + ["chartWidth", "n", 0, 480.0], + ["chartHeight", "n", 0, 340.0], + ["chartAspectLocked", "b", 0, true], + ["chartSourceNodeId", "s", 0, ""], + ["htmlSplitterState", "n", 1, null], + ["chatScrollValue", "n", 0, 0.0], + ["toolCalls", { a: { o: TOOL_INVOCATION_ROW_FIELDS } }, 0, []], + ["overrideProvider", "s", 0, ""], + ["overrideModelId", "s", 0, ""], + ["indexIntoKnowledge", "b", 0, false], + ["planGoal", "s", 0, ""], + ["planSteps", { a: { o: PLAN_STEP_ROW_FIELDS } }, 0, []], + ["builderActivity", { a: { o: BUILDER_ACTIVITY_ROW_FIELDS } }, 0, []], + ["builderStatus", "s", 0, ""], + ["builderMode", "s", 0, ""], + ["builderRunId", "s", 0, ""], + ["builderMaxSteps", "n", 0, 0], + ["builderMaxTokens", "n", 0, 0], + ["builderMaxWallSeconds", "n", 0, 0], + ["builderSpentSteps", "n", 0, 0], + ["builderSpentTokens", "n", 0, 0], + ["builderSpentWallSeconds", "n", 0, 0], + ["builderAwaitingToolApproval", "b", 0, false], + ["builderApprovalToolName", "s", 0, ""], + ["builderApprovalSummary", "s", 0, ""], + ["builderStatusDetail", "s", 0, ""], + ["harnessGoal", "s", 0, ""], + ["harnessReply", "s", 0, ""], + ["harnessStatus", "s", 0, ""], + ["harnessStatusDetail", "s", 0, ""], + ["harnessRunId", "s", 0, ""], + ["harnessActivity", { a: { o: HARNESS_ACTIVITY_ROW_FIELDS } }, 0, []], + ["harnessContextTokens", "n", 0, 0], + ["harnessMaxContextTokens", "n", 0, 0], + ["harnessCompactions", "n", 0, 0], + ["harnessAwaitingApproval", "b", 0, false], + ["harnessApprovalToolName", "s", 0, ""], + ["harnessApprovalSummary", "s", 0, ""], + ["harnessApprovalSessionOffered", "b", 0, false], + ["harnessPlan", { a: { o: HARNESS_PLAN_STEP_ROW_FIELDS } }, 0, []], + ["harnessAwaitingQuestion", "b", 0, false], + ["harnessQuestion", "s", 0, ""], + ["harnessWorkspacePath", "s", 0, ""], + ["harnessWorkspaceActive", "s", 0, ""], + ["harnessMaxTurns", "n", 0, 0], + ["harnessSpentTurns", "n", 0, 0], + ["harnessSpentTokens", "n", 0, 0], + ["pluginState", { d: "s" }, 0, {}], +]; + +const SCENE_EDGE_ROW_FIELDS: WireFields = [ + ["id", "s", 0], + ["source", "s", 0], + ["target", "s", 0], +]; + +const SCENE_PIN_ROW_FIELDS: WireFields = [ + ["id", "s", 0], + ["title", "s", 0], + ["note", "s", 0], + ["x", "n", 0], + ["y", "n", 0], +]; -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const SCENE_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["nodes", { a: { o: SCENE_NODE_ROW_FIELDS } }, 0], + ["edges", { a: { o: SCENE_EDGE_ROW_FIELDS } }, 0], + ["pins", { a: { o: SCENE_PIN_ROW_FIELDS } }, 0], + ["snapToGrid", "b", 0], + ["fadeConnectionsEnabled", "b", 0], + ["orthogonalRouting", "b", 0], + ["smartGuides", "b", 0], + ["hasSavedChat", "b", 0], + ["dragFactor", "n", 0], + ["fontFamily", "s", 0], + ["fontSizePt", "n", 0], + ["fontColor", "s", 0], + ["canUndo", "b", 0], + ["canRedo", "b", 0], + ["undoLabel", "s", 0], + ["redoLabel", "s", 0], + ["minCompatibleSchemaVersion", "n", 1], +]; /** SceneNodeRow crosses the wire sparse: the sender omits every field at its * declared default (contracts: WIRE_OMITS_DEFAULTS). These are those * defaults - the value every omitted field is restored to. */ -export const SCENE_NODE_ROW_WIRE_DEFAULTS: ReadonlyArray = [ - ["content", ""], - ["isUser", false], - ["isCollapsed", false], - ["code", ""], - ["language", ""], - ["attachmentKind", ""], - ["filePath", ""], - ["mimeType", ""], - ["durationSeconds", null], - ["byteSize", null], - ["previewLabel", ""], - ["isDocked", false], - ["imageAssetId", ""], - ["history", []], - ["pendingRequestId", null], - ["researchStage", ""], - ["researchCompleted", 0], - ["researchTotal", 0], - ["researchActiveSourceId", null], - ["researchError", ""], - ["researchResult", null], - ["researchRetainToKnowledge", false], - ["artifactContent", ""], - ["artifactError", ""], - ["gitlinkRepo", ""], - ["gitlinkBranch", ""], - ["gitlinkScopeMode", "selected"], - ["gitlinkLocalRoot", ""], - ["gitlinkRepoFilePaths", []], - ["gitlinkSelectedPaths", []], - ["gitlinkTaskPrompt", ""], - ["gitlinkContextStats", {}], - ["gitlinkContextSummary", ""], - ["gitlinkContextVersion", 0], - ["gitlinkProposalMarkdown", ""], - ["gitlinkPendingChanges", []], - ["gitlinkPreviewText", ""], - ["gitlinkChangeFingerprint", null], - ["gitlinkChangeState", "draft"], - ["gitlinkError", ""], - ["codeReviewPrUrl", ""], - ["codeReviewRepo", ""], - ["codeReviewPrNumber", 0], - ["codeReviewPrTitle", ""], - ["codeReviewPrState", ""], - ["codeReviewPrHtmlUrl", ""], - ["codeReviewBaseRef", ""], - ["codeReviewHeadRef", ""], - ["codeReviewAdditions", 0], - ["codeReviewDeletions", 0], - ["codeReviewChangedFiles", 0], - ["codeReviewFiles", []], - ["codeReviewFilesTruncated", false], - ["codeReviewDiffTruncated", false], - ["codeReviewDiffChars", 0], - ["codeReviewDiffVersion", 0], - ["codeReviewWalkthrough", []], - ["codeReviewFindings", []], - ["codeReviewErrors", []], - ["codeReviewDismissedIds", []], - ["codeReviewTitle", ""], - ["codeReviewOverview", ""], - ["codeReviewConfidence", ""], - ["codeReviewScores", {}], - ["codeReviewQualityScore", 0], - ["codeReviewVerdict", "none"], - ["codeReviewRisk", ""], - ["codeReviewQualitySummary", ""], - ["codeReviewQa", []], - ["codeReviewState", "draft"], - ["codeReviewError", ""], - ["codeSandboxRequirements", ""], - ["codeSandboxPrompt", ""], - ["codeSandboxCode", ""], - ["codeSandboxOutput", ""], - ["codeSandboxAnalysis", ""], - ["codeSandboxAwaitingApproval", false], - ["codeSandboxApprovalRequirements", ""], - ["codeSandboxApprovalAllowSourceBuilds", false], - ["codeSandboxApprovalIsRepair", false], - ["codeSandboxError", ""], - ["provider", null], - ["model", null], - ["isBranchSynthesis", false], - ["synthesisInstructions", ""], - ["branchStatus", "active"], - ["responseIncomplete", false], - ["promptTokens", null], - ["completionTokens", null], - ["estimatedCostUsd", null], - ["isFinalDeliverable", false], - ["color", null], - ["headerColor", null], - ["isSystemPrompt", false], - ["isSummaryNote", false], - ["isBranchComparison", false], - ["itemIds", []], - ["isLocked", true], - ["groupWidth", null], - ["groupHeight", null], - ["chartType", ""], - ["chartData", {}], - ["chartError", ""], - ["chartWidth", 480.0], - ["chartHeight", 340.0], - ["chartAspectLocked", true], - ["chartSourceNodeId", ""], - ["htmlSplitterState", null], - ["chatScrollValue", 0.0], - ["toolCalls", []], - ["overrideProvider", ""], - ["overrideModelId", ""], - ["indexIntoKnowledge", false], - ["planGoal", ""], - ["planSteps", []], - ["builderActivity", []], - ["builderStatus", ""], - ["builderMode", ""], - ["builderRunId", ""], - ["builderMaxSteps", 0], - ["builderMaxTokens", 0], - ["builderMaxWallSeconds", 0], - ["builderSpentSteps", 0], - ["builderSpentTokens", 0], - ["builderSpentWallSeconds", 0], - ["builderAwaitingToolApproval", false], - ["builderApprovalToolName", ""], - ["builderApprovalSummary", ""], - ["builderStatusDetail", ""], - ["harnessGoal", ""], - ["harnessReply", ""], - ["harnessStatus", ""], - ["harnessStatusDetail", ""], - ["harnessRunId", ""], - ["harnessActivity", []], - ["harnessContextTokens", 0], - ["harnessMaxContextTokens", 0], - ["harnessCompactions", 0], - ["harnessAwaitingApproval", false], - ["harnessApprovalToolName", ""], - ["harnessApprovalSummary", ""], - ["harnessApprovalSessionOffered", false], - ["harnessPlan", []], - ["harnessAwaitingQuestion", false], - ["harnessQuestion", ""], - ["harnessWorkspacePath", ""], - ["harnessWorkspaceActive", ""], - ["harnessMaxTurns", 0], - ["harnessSpentTurns", 0], - ["harnessSpentTokens", 0], - ["pluginState", {}], -]; +export const SCENE_NODE_ROW_WIRE_DEFAULTS = wireDefaults(SCENE_NODE_ROW_FIELDS); export function hydrateSceneNodeRow(value: unknown): unknown { - if (!isRecord(value)) return value; - let hydrated: Record | null = null; - for (const [key, fallback] of SCENE_NODE_ROW_WIRE_DEFAULTS) { - if (value[key] !== undefined) continue; - hydrated ??= { ...value }; - hydrated[key] = Array.isArray(fallback) ? [] : isRecord(fallback) ? {} : fallback; - } - return hydrated ?? value; + return hydrateRow(SCENE_NODE_ROW_FIELDS, SCENE_NODE_ROW_WIRE_DEFAULTS, value); } function hydrateSceneState(value: unknown): unknown { @@ -539,1481 +710,7 @@ function hydrateSceneState(value: unknown): unknown { return hydrated ?? value; } -function checkSceneNodeRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["x"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.x: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.x` + ": expected number"); } - } - { - const fieldValue = value["y"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.y: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.y` + ": expected number"); } - } - { - const fieldValue = value["title"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.title: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.title` + ": expected string"); } - } - { - const fieldValue = value["kind"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.kind: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.kind` + ": expected string"); } - } - { - const fieldValue = value["content"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.content: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.content` + ": expected string"); } - } - { - const fieldValue = value["isUser"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isUser: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isUser` + ": expected boolean"); } - } - { - const fieldValue = value["isCollapsed"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isCollapsed: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isCollapsed` + ": expected boolean"); } - } - { - const fieldValue = value["code"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.code: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.code` + ": expected string"); } - } - { - const fieldValue = value["language"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.language: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.language` + ": expected string"); } - } - { - const fieldValue = value["attachmentKind"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.attachmentKind: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.attachmentKind` + ": expected string"); } - } - { - const fieldValue = value["filePath"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.filePath: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.filePath` + ": expected string"); } - } - { - const fieldValue = value["mimeType"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.mimeType: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.mimeType` + ": expected string"); } - } - { - const fieldValue = value["durationSeconds"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.durationSeconds` + ": expected number"); } - } - { - const fieldValue = value["byteSize"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.byteSize` + ": expected number"); } - } - { - const fieldValue = value["previewLabel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.previewLabel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.previewLabel` + ": expected string"); } - } - { - const fieldValue = value["isDocked"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isDocked: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isDocked` + ": expected boolean"); } - } - { - const fieldValue = value["imageAssetId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.imageAssetId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.imageAssetId` + ": expected string"); } - } - { - const fieldValue = value["history"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.history: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.history` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkConversationMessageRow(item, `${path}.history` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["pendingRequestId"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.pendingRequestId` + ": expected string"); } - } - { - const fieldValue = value["researchStage"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.researchStage: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.researchStage` + ": expected string"); } - } - { - const fieldValue = value["researchCompleted"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.researchCompleted: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.researchCompleted` + ": expected number"); } - } - { - const fieldValue = value["researchTotal"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.researchTotal: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.researchTotal` + ": expected number"); } - } - { - const fieldValue = value["researchActiveSourceId"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.researchActiveSourceId` + ": expected string"); } - } - { - const fieldValue = value["researchError"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.researchError: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.researchError` + ": expected string"); } - } - { - const fieldValue = value["researchResult"]; - if (fieldValue !== undefined && fieldValue !== null) { checkResearchResultRow(fieldValue, `${path}.researchResult`, errors); } - } - { - const fieldValue = value["researchRetainToKnowledge"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.researchRetainToKnowledge: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.researchRetainToKnowledge` + ": expected boolean"); } - } - { - const fieldValue = value["artifactContent"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.artifactContent: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.artifactContent` + ": expected string"); } - } - { - const fieldValue = value["artifactError"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.artifactError: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.artifactError` + ": expected string"); } - } - { - const fieldValue = value["gitlinkRepo"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkRepo: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkRepo` + ": expected string"); } - } - { - const fieldValue = value["gitlinkBranch"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkBranch: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkBranch` + ": expected string"); } - } - { - const fieldValue = value["gitlinkScopeMode"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkScopeMode: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkScopeMode` + ": expected string"); } - } - { - const fieldValue = value["gitlinkLocalRoot"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkLocalRoot: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkLocalRoot` + ": expected string"); } - } - { - const fieldValue = value["gitlinkRepoFilePaths"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkRepoFilePaths: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.gitlinkRepoFilePaths` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.gitlinkRepoFilePaths` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["gitlinkSelectedPaths"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkSelectedPaths: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.gitlinkSelectedPaths` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.gitlinkSelectedPaths` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["gitlinkTaskPrompt"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkTaskPrompt: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkTaskPrompt` + ": expected string"); } - } - { - const fieldValue = value["gitlinkContextStats"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkContextStats: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.gitlinkContextStats` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "string") errors.push(`${path}.gitlinkContextStats` + `[${JSON.stringify(k)}]` + ": expected string"); }); } - } - { - const fieldValue = value["gitlinkContextSummary"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkContextSummary: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkContextSummary` + ": expected string"); } - } - { - const fieldValue = value["gitlinkContextVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkContextVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.gitlinkContextVersion` + ": expected number"); } - } - { - const fieldValue = value["gitlinkProposalMarkdown"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkProposalMarkdown: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkProposalMarkdown` + ": expected string"); } - } - { - const fieldValue = value["gitlinkPendingChanges"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkPendingChanges: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.gitlinkPendingChanges` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkGitlinkPendingChangeRow(item, `${path}.gitlinkPendingChanges` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["gitlinkPreviewText"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkPreviewText: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkPreviewText` + ": expected string"); } - } - { - const fieldValue = value["gitlinkChangeFingerprint"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkChangeFingerprint` + ": expected string"); } - } - { - const fieldValue = value["gitlinkChangeState"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkChangeState: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkChangeState` + ": expected string"); } - } - { - const fieldValue = value["gitlinkError"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.gitlinkError: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.gitlinkError` + ": expected string"); } - } - { - const fieldValue = value["codeReviewPrUrl"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewPrUrl: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewPrUrl` + ": expected string"); } - } - { - const fieldValue = value["codeReviewRepo"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewRepo: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewRepo` + ": expected string"); } - } - { - const fieldValue = value["codeReviewPrNumber"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewPrNumber: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.codeReviewPrNumber` + ": expected number"); } - } - { - const fieldValue = value["codeReviewPrTitle"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewPrTitle: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewPrTitle` + ": expected string"); } - } - { - const fieldValue = value["codeReviewPrState"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewPrState: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewPrState` + ": expected string"); } - } - { - const fieldValue = value["codeReviewPrHtmlUrl"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewPrHtmlUrl: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewPrHtmlUrl` + ": expected string"); } - } - { - const fieldValue = value["codeReviewBaseRef"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewBaseRef: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewBaseRef` + ": expected string"); } - } - { - const fieldValue = value["codeReviewHeadRef"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewHeadRef: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewHeadRef` + ": expected string"); } - } - { - const fieldValue = value["codeReviewAdditions"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewAdditions: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.codeReviewAdditions` + ": expected number"); } - } - { - const fieldValue = value["codeReviewDeletions"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewDeletions: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.codeReviewDeletions` + ": expected number"); } - } - { - const fieldValue = value["codeReviewChangedFiles"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewChangedFiles: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.codeReviewChangedFiles` + ": expected number"); } - } - { - const fieldValue = value["codeReviewFiles"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewFiles: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.codeReviewFiles` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkCodeReviewFileRow(item, `${path}.codeReviewFiles` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["codeReviewFilesTruncated"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewFilesTruncated: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.codeReviewFilesTruncated` + ": expected boolean"); } - } - { - const fieldValue = value["codeReviewDiffTruncated"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewDiffTruncated: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.codeReviewDiffTruncated` + ": expected boolean"); } - } - { - const fieldValue = value["codeReviewDiffChars"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewDiffChars: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.codeReviewDiffChars` + ": expected number"); } - } - { - const fieldValue = value["codeReviewDiffVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewDiffVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.codeReviewDiffVersion` + ": expected number"); } - } - { - const fieldValue = value["codeReviewWalkthrough"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewWalkthrough: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.codeReviewWalkthrough` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkCodeReviewWalkthroughGroupRow(item, `${path}.codeReviewWalkthrough` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["codeReviewFindings"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewFindings: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.codeReviewFindings` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkCodeReviewFindingRow(item, `${path}.codeReviewFindings` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["codeReviewErrors"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewErrors: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.codeReviewErrors` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkCodeReviewErrorRow(item, `${path}.codeReviewErrors` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["codeReviewDismissedIds"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewDismissedIds: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.codeReviewDismissedIds` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.codeReviewDismissedIds` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["codeReviewTitle"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewTitle: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewTitle` + ": expected string"); } - } - { - const fieldValue = value["codeReviewOverview"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewOverview: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewOverview` + ": expected string"); } - } - { - const fieldValue = value["codeReviewConfidence"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewConfidence: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewConfidence` + ": expected string"); } - } - { - const fieldValue = value["codeReviewScores"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewScores: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.codeReviewScores` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "string") errors.push(`${path}.codeReviewScores` + `[${JSON.stringify(k)}]` + ": expected string"); }); } - } - { - const fieldValue = value["codeReviewQualityScore"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewQualityScore: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.codeReviewQualityScore` + ": expected number"); } - } - { - const fieldValue = value["codeReviewVerdict"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewVerdict: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewVerdict` + ": expected string"); } - } - { - const fieldValue = value["codeReviewRisk"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewRisk: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewRisk` + ": expected string"); } - } - { - const fieldValue = value["codeReviewQualitySummary"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewQualitySummary: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewQualitySummary` + ": expected string"); } - } - { - const fieldValue = value["codeReviewQa"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewQa: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.codeReviewQa` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkCodeReviewQaRow(item, `${path}.codeReviewQa` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["codeReviewState"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewState: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewState` + ": expected string"); } - } - { - const fieldValue = value["codeReviewError"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeReviewError: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeReviewError` + ": expected string"); } - } - { - const fieldValue = value["codeSandboxRequirements"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxRequirements: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeSandboxRequirements` + ": expected string"); } - } - { - const fieldValue = value["codeSandboxPrompt"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxPrompt: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeSandboxPrompt` + ": expected string"); } - } - { - const fieldValue = value["codeSandboxCode"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxCode: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeSandboxCode` + ": expected string"); } - } - { - const fieldValue = value["codeSandboxOutput"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxOutput: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeSandboxOutput` + ": expected string"); } - } - { - const fieldValue = value["codeSandboxAnalysis"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxAnalysis: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeSandboxAnalysis` + ": expected string"); } - } - { - const fieldValue = value["codeSandboxAwaitingApproval"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxAwaitingApproval: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.codeSandboxAwaitingApproval` + ": expected boolean"); } - } - { - const fieldValue = value["codeSandboxApprovalRequirements"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxApprovalRequirements: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeSandboxApprovalRequirements` + ": expected string"); } - } - { - const fieldValue = value["codeSandboxApprovalAllowSourceBuilds"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxApprovalAllowSourceBuilds: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.codeSandboxApprovalAllowSourceBuilds` + ": expected boolean"); } - } - { - const fieldValue = value["codeSandboxApprovalIsRepair"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxApprovalIsRepair: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.codeSandboxApprovalIsRepair` + ": expected boolean"); } - } - { - const fieldValue = value["codeSandboxError"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.codeSandboxError: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.codeSandboxError` + ": expected string"); } - } - { - const fieldValue = value["provider"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.provider` + ": expected string"); } - } - { - const fieldValue = value["model"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.model` + ": expected string"); } - } - { - const fieldValue = value["isBranchSynthesis"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isBranchSynthesis: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isBranchSynthesis` + ": expected boolean"); } - } - { - const fieldValue = value["synthesisInstructions"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.synthesisInstructions: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.synthesisInstructions` + ": expected string"); } - } - { - const fieldValue = value["branchStatus"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.branchStatus: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.branchStatus` + ": expected string"); } - } - { - const fieldValue = value["responseIncomplete"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.responseIncomplete: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.responseIncomplete` + ": expected boolean"); } - } - { - const fieldValue = value["promptTokens"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.promptTokens` + ": expected number"); } - } - { - const fieldValue = value["completionTokens"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.completionTokens` + ": expected number"); } - } - { - const fieldValue = value["estimatedCostUsd"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.estimatedCostUsd` + ": expected number"); } - } - { - const fieldValue = value["isFinalDeliverable"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isFinalDeliverable: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isFinalDeliverable` + ": expected boolean"); } - } - { - const fieldValue = value["color"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.color` + ": expected string"); } - } - { - const fieldValue = value["headerColor"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.headerColor` + ": expected string"); } - } - { - const fieldValue = value["isSystemPrompt"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isSystemPrompt: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isSystemPrompt` + ": expected boolean"); } - } - { - const fieldValue = value["isSummaryNote"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isSummaryNote: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isSummaryNote` + ": expected boolean"); } - } - { - const fieldValue = value["isBranchComparison"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isBranchComparison: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isBranchComparison` + ": expected boolean"); } - } - { - const fieldValue = value["itemIds"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.itemIds: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.itemIds` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.itemIds` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["isLocked"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isLocked: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isLocked` + ": expected boolean"); } - } - { - const fieldValue = value["groupWidth"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.groupWidth` + ": expected number"); } - } - { - const fieldValue = value["groupHeight"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.groupHeight` + ": expected number"); } - } - { - const fieldValue = value["chartType"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.chartType: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.chartType` + ": expected string"); } - } - { - const fieldValue = value["chartData"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.chartData: missing required field`); - else { checkChartDataRow(fieldValue, `${path}.chartData`, errors); } - } - { - const fieldValue = value["chartError"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.chartError: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.chartError` + ": expected string"); } - } - { - const fieldValue = value["chartWidth"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.chartWidth: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.chartWidth` + ": expected number"); } - } - { - const fieldValue = value["chartHeight"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.chartHeight: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.chartHeight` + ": expected number"); } - } - { - const fieldValue = value["chartAspectLocked"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.chartAspectLocked: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.chartAspectLocked` + ": expected boolean"); } - } - { - const fieldValue = value["chartSourceNodeId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.chartSourceNodeId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.chartSourceNodeId` + ": expected string"); } - } - { - const fieldValue = value["htmlSplitterState"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.htmlSplitterState` + ": expected number"); } - } - { - const fieldValue = value["chatScrollValue"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.chatScrollValue: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.chatScrollValue` + ": expected number"); } - } - { - const fieldValue = value["toolCalls"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.toolCalls: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.toolCalls` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkToolInvocationRow(item, `${path}.toolCalls` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["overrideProvider"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.overrideProvider: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.overrideProvider` + ": expected string"); } - } - { - const fieldValue = value["overrideModelId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.overrideModelId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.overrideModelId` + ": expected string"); } - } - { - const fieldValue = value["indexIntoKnowledge"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.indexIntoKnowledge: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.indexIntoKnowledge` + ": expected boolean"); } - } - { - const fieldValue = value["planGoal"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.planGoal: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.planGoal` + ": expected string"); } - } - { - const fieldValue = value["planSteps"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.planSteps: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.planSteps` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkPlanStepRow(item, `${path}.planSteps` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["builderActivity"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderActivity: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.builderActivity` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkBuilderActivityRow(item, `${path}.builderActivity` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["builderStatus"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderStatus: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.builderStatus` + ": expected string"); } - } - { - const fieldValue = value["builderMode"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderMode: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.builderMode` + ": expected string"); } - } - { - const fieldValue = value["builderRunId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderRunId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.builderRunId` + ": expected string"); } - } - { - const fieldValue = value["builderMaxSteps"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderMaxSteps: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.builderMaxSteps` + ": expected number"); } - } - { - const fieldValue = value["builderMaxTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderMaxTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.builderMaxTokens` + ": expected number"); } - } - { - const fieldValue = value["builderMaxWallSeconds"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderMaxWallSeconds: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.builderMaxWallSeconds` + ": expected number"); } - } - { - const fieldValue = value["builderSpentSteps"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderSpentSteps: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.builderSpentSteps` + ": expected number"); } - } - { - const fieldValue = value["builderSpentTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderSpentTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.builderSpentTokens` + ": expected number"); } - } - { - const fieldValue = value["builderSpentWallSeconds"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderSpentWallSeconds: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.builderSpentWallSeconds` + ": expected number"); } - } - { - const fieldValue = value["builderAwaitingToolApproval"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderAwaitingToolApproval: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.builderAwaitingToolApproval` + ": expected boolean"); } - } - { - const fieldValue = value["builderApprovalToolName"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderApprovalToolName: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.builderApprovalToolName` + ": expected string"); } - } - { - const fieldValue = value["builderApprovalSummary"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderApprovalSummary: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.builderApprovalSummary` + ": expected string"); } - } - { - const fieldValue = value["builderStatusDetail"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.builderStatusDetail: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.builderStatusDetail` + ": expected string"); } - } - { - const fieldValue = value["harnessGoal"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessGoal: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessGoal` + ": expected string"); } - } - { - const fieldValue = value["harnessReply"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessReply: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessReply` + ": expected string"); } - } - { - const fieldValue = value["harnessStatus"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessStatus: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessStatus` + ": expected string"); } - } - { - const fieldValue = value["harnessStatusDetail"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessStatusDetail: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessStatusDetail` + ": expected string"); } - } - { - const fieldValue = value["harnessRunId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessRunId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessRunId` + ": expected string"); } - } - { - const fieldValue = value["harnessActivity"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessActivity: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.harnessActivity` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkHarnessActivityRow(item, `${path}.harnessActivity` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["harnessContextTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessContextTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.harnessContextTokens` + ": expected number"); } - } - { - const fieldValue = value["harnessMaxContextTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessMaxContextTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.harnessMaxContextTokens` + ": expected number"); } - } - { - const fieldValue = value["harnessCompactions"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessCompactions: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.harnessCompactions` + ": expected number"); } - } - { - const fieldValue = value["harnessAwaitingApproval"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessAwaitingApproval: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.harnessAwaitingApproval` + ": expected boolean"); } - } - { - const fieldValue = value["harnessApprovalToolName"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessApprovalToolName: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessApprovalToolName` + ": expected string"); } - } - { - const fieldValue = value["harnessApprovalSummary"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessApprovalSummary: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessApprovalSummary` + ": expected string"); } - } - { - const fieldValue = value["harnessApprovalSessionOffered"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessApprovalSessionOffered: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.harnessApprovalSessionOffered` + ": expected boolean"); } - } - { - const fieldValue = value["harnessPlan"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessPlan: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.harnessPlan` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkHarnessPlanStepRow(item, `${path}.harnessPlan` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["harnessAwaitingQuestion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessAwaitingQuestion: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.harnessAwaitingQuestion` + ": expected boolean"); } - } - { - const fieldValue = value["harnessQuestion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessQuestion: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessQuestion` + ": expected string"); } - } - { - const fieldValue = value["harnessWorkspacePath"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessWorkspacePath: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessWorkspacePath` + ": expected string"); } - } - { - const fieldValue = value["harnessWorkspaceActive"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessWorkspaceActive: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.harnessWorkspaceActive` + ": expected string"); } - } - { - const fieldValue = value["harnessMaxTurns"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessMaxTurns: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.harnessMaxTurns` + ": expected number"); } - } - { - const fieldValue = value["harnessSpentTurns"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessSpentTurns: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.harnessSpentTurns` + ": expected number"); } - } - { - const fieldValue = value["harnessSpentTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.harnessSpentTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.harnessSpentTokens` + ": expected number"); } - } - { - const fieldValue = value["pluginState"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.pluginState: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.pluginState` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "string") errors.push(`${path}.pluginState` + `[${JSON.stringify(k)}]` + ": expected string"); }); } - } -} - -function checkConversationMessageRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["role"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.role: missing required field`); - else { if (!["user", "assistant"].includes(fieldValue as string)) errors.push(`${path}.role` + `: ${JSON.stringify(fieldValue)} is not one of [` + "user, assistant" + `]`); } - } - { - const fieldValue = value["content"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.content: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.content` + ": expected string"); } - } - { - const fieldValue = value["incomplete"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.incomplete: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.incomplete` + ": expected boolean"); } - } -} - -function checkResearchResultRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["requestId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.requestId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.requestId` + ": expected string"); } - } - { - const fieldValue = value["originalQuery"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.originalQuery: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.originalQuery` + ": expected string"); } - } - { - const fieldValue = value["effectiveQuery"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.effectiveQuery: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.effectiveQuery` + ": expected string"); } - } - { - const fieldValue = value["answerMarkdown"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.answerMarkdown: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.answerMarkdown` + ": expected string"); } - } - { - const fieldValue = value["sources"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sources: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.sources` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkResearchSourceRow(item, `${path}.sources` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["citations"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.citations: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.citations` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkResearchCitationRow(item, `${path}.citations` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["warnings"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.warnings: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.warnings` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.warnings` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["providerSnapshot"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.providerSnapshot: missing required field`); - else { if (!isRecord(fieldValue)) errors.push(`${path}.providerSnapshot` + ": expected object"); - else Object.entries(fieldValue as Record).forEach(([k, v]) => { if (typeof v !== "string") errors.push(`${path}.providerSnapshot` + `[${JSON.stringify(k)}]` + ": expected string"); }); } - } -} - -function checkResearchSourceRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["sourceId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sourceId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.sourceId` + ": expected string"); } - } - { - const fieldValue = value["title"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.title: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.title` + ": expected string"); } - } - { - const fieldValue = value["url"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.url: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.url` + ": expected string"); } - } - { - const fieldValue = value["canonicalUrl"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.canonicalUrl: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.canonicalUrl` + ": expected string"); } - } - { - const fieldValue = value["snippet"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.snippet: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.snippet` + ": expected string"); } - } - { - const fieldValue = value["rank"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.rank: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.rank` + ": expected number"); } - } - { - const fieldValue = value["provider"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.provider: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.provider` + ": expected string"); } - } - { - const fieldValue = value["finalUrl"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.finalUrl: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.finalUrl` + ": expected string"); } - } - { - const fieldValue = value["status"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.status: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.status` + ": expected string"); } - } - { - const fieldValue = value["errorCode"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.errorCode: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.errorCode` + ": expected string"); } - } - { - const fieldValue = value["errorMessage"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.errorMessage: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.errorMessage` + ": expected string"); } - } - { - const fieldValue = value["truncated"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.truncated: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.truncated` + ": expected boolean"); } - } - { - const fieldValue = value["contentHash"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.contentHash: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.contentHash` + ": expected string"); } - } - { - const fieldValue = value["citationCount"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.citationCount: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.citationCount` + ": expected number"); } - } -} - -function checkResearchCitationRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["sourceId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sourceId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.sourceId` + ": expected string"); } - } - { - const fieldValue = value["marker"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.marker: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.marker` + ": expected string"); } - } - { - const fieldValue = value["claimContext"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.claimContext: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.claimContext` + ": expected string"); } - } -} - -function checkGitlinkPendingChangeRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["path"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.path: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.path` + ": expected string"); } - } - { - const fieldValue = value["operation"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.operation: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.operation` + ": expected string"); } - } - { - const fieldValue = value["reason"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.reason: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.reason` + ": expected string"); } - } - { - const fieldValue = value["content"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.content` + ": expected string"); } - } -} - -function checkCodeReviewFileRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["path"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.path: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.path` + ": expected string"); } - } - { - const fieldValue = value["status"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.status: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.status` + ": expected string"); } - } - { - const fieldValue = value["additions"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.additions: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.additions` + ": expected number"); } - } - { - const fieldValue = value["deletions"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.deletions: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.deletions` + ": expected number"); } - } - { - const fieldValue = value["patch"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.patch: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.patch` + ": expected string"); } - } - { - const fieldValue = value["patchTruncated"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.patchTruncated: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.patchTruncated` + ": expected boolean"); } - } - { - const fieldValue = value["previousPath"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.previousPath` + ": expected string"); } - } -} - -function checkCodeReviewWalkthroughGroupRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["groupTitle"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.groupTitle: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.groupTitle` + ": expected string"); } - } - { - const fieldValue = value["paths"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.paths: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.paths` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.paths` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["explanation"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.explanation: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.explanation` + ": expected string"); } - } -} - -function checkCodeReviewFindingRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["severity"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.severity: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.severity` + ": expected string"); } - } - { - const fieldValue = value["tier"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.tier: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.tier` + ": expected string"); } - } - { - const fieldValue = value["category"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.category: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.category` + ": expected string"); } - } - { - const fieldValue = value["path"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.path: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.path` + ": expected string"); } - } - { - const fieldValue = value["line"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.line: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.line` + ": expected number"); } - } - { - const fieldValue = value["title"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.title: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.title` + ": expected string"); } - } - { - const fieldValue = value["evidence"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.evidence: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.evidence` + ": expected string"); } - } - { - const fieldValue = value["impact"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.impact: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.impact` + ": expected string"); } - } - { - const fieldValue = value["recommendation"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.recommendation: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.recommendation` + ": expected string"); } - } -} - -function checkCodeReviewErrorRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["severity"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.severity: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.severity` + ": expected string"); } - } - { - const fieldValue = value["tier"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.tier: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.tier` + ": expected string"); } - } - { - const fieldValue = value["kind"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.kind: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.kind` + ": expected string"); } - } - { - const fieldValue = value["path"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.path: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.path` + ": expected string"); } - } - { - const fieldValue = value["line"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.line: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.line` + ": expected number"); } - } - { - const fieldValue = value["title"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.title: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.title` + ": expected string"); } - } - { - const fieldValue = value["evidence"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.evidence: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.evidence` + ": expected string"); } - } - { - const fieldValue = value["fix"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.fix: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.fix` + ": expected string"); } - } -} - -function checkCodeReviewQaRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["question"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.question: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.question` + ": expected string"); } - } - { - const fieldValue = value["answer"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.answer: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.answer` + ": expected string"); } - } -} - -function checkChartDataRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["version"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.version` + ": expected number"); } - } - { - const fieldValue = value["type"]; - if (fieldValue !== undefined && fieldValue !== null) { if (!["bar", "line", "pie", "histogram", "sankey"].includes(fieldValue as string)) errors.push(`${path}.type` + `: ${JSON.stringify(fieldValue)} is not one of [` + "bar, line, pie, histogram, sankey" + `]`); } - } - { - const fieldValue = value["title"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.title` + ": expected string"); } - } - { - const fieldValue = value["labels"]; - if (fieldValue !== undefined && fieldValue !== null) { if (!Array.isArray(fieldValue)) errors.push(`${path}.labels` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "string") errors.push(`${path}.labels` + `[${i}]` + ": expected string"); }); } - } - { - const fieldValue = value["values"]; - if (fieldValue !== undefined && fieldValue !== null) { if (!Array.isArray(fieldValue)) errors.push(`${path}.values` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { if (typeof item !== "number") errors.push(`${path}.values` + `[${i}]` + ": expected number"); }); } - } - { - const fieldValue = value["xAxis"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.xAxis` + ": expected string"); } - } - { - const fieldValue = value["yAxis"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "string") errors.push(`${path}.yAxis` + ": expected string"); } - } - { - const fieldValue = value["bins"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.bins` + ": expected number"); } - } - { - const fieldValue = value["flows"]; - if (fieldValue !== undefined && fieldValue !== null) { if (!Array.isArray(fieldValue)) errors.push(`${path}.flows` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkChartFlowRow(item, `${path}.flows` + `[${i}]`, errors); }); } - } -} - -function checkChartFlowRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["source"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.source: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.source` + ": expected string"); } - } - { - const fieldValue = value["target"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.target: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.target` + ": expected string"); } - } - { - const fieldValue = value["value"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.value: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.value` + ": expected number"); } - } -} - -function checkToolInvocationRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["name"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.name: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.name` + ": expected string"); } - } - { - const fieldValue = value["argumentsJson"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.argumentsJson: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.argumentsJson` + ": expected string"); } - } - { - const fieldValue = value["result"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.result: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.result` + ": expected string"); } - } - { - const fieldValue = value["isError"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.isError: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.isError` + ": expected boolean"); } - } -} - -function checkPlanStepRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["title"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.title: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.title` + ": expected string"); } - } - { - const fieldValue = value["status"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.status: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.status` + ": expected string"); } - } - { - const fieldValue = value["detail"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.detail: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.detail` + ": expected string"); } - } -} - -function checkBuilderActivityRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["tool"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.tool: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.tool` + ": expected string"); } - } - { - const fieldValue = value["summary"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.summary: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.summary` + ": expected string"); } - } - { - const fieldValue = value["outcome"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.outcome: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.outcome` + ": expected string"); } - } - { - const fieldValue = value["stepId"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.stepId: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.stepId` + ": expected string"); } - } - { - const fieldValue = value["elapsedMs"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.elapsedMs: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.elapsedMs` + ": expected number"); } - } -} - -function checkHarnessActivityRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["tool"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.tool: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.tool` + ": expected string"); } - } - { - const fieldValue = value["summary"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.summary: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.summary` + ": expected string"); } - } - { - const fieldValue = value["outcome"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.outcome: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.outcome` + ": expected string"); } - } - { - const fieldValue = value["elapsedMs"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.elapsedMs: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.elapsedMs` + ": expected number"); } - } -} - -function checkHarnessPlanStepRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["text"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.text: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.text` + ": expected string"); } - } - { - const fieldValue = value["status"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.status: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.status` + ": expected string"); } - } -} - -function checkSceneEdgeRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["source"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.source: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.source` + ": expected string"); } - } - { - const fieldValue = value["target"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.target: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.target` + ": expected string"); } - } -} - -function checkScenePinRow(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["id"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.id: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.id` + ": expected string"); } - } - { - const fieldValue = value["title"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.title: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.title` + ": expected string"); } - } - { - const fieldValue = value["note"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.note: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.note` + ": expected string"); } - } - { - const fieldValue = value["x"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.x: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.x` + ": expected number"); } - } - { - const fieldValue = value["y"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.y: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.y` + ": expected number"); } - } -} - -function checkSceneState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["nodes"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.nodes: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.nodes` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkSceneNodeRow(item, `${path}.nodes` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["edges"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.edges: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.edges` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkSceneEdgeRow(item, `${path}.edges` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["pins"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.pins: missing required field`); - else { if (!Array.isArray(fieldValue)) errors.push(`${path}.pins` + ": expected array"); - else (fieldValue as unknown[]).forEach((item, i) => { checkScenePinRow(item, `${path}.pins` + `[${i}]`, errors); }); } - } - { - const fieldValue = value["snapToGrid"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.snapToGrid: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.snapToGrid` + ": expected boolean"); } - } - { - const fieldValue = value["fadeConnectionsEnabled"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.fadeConnectionsEnabled: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.fadeConnectionsEnabled` + ": expected boolean"); } - } - { - const fieldValue = value["orthogonalRouting"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.orthogonalRouting: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.orthogonalRouting` + ": expected boolean"); } - } - { - const fieldValue = value["smartGuides"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.smartGuides: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.smartGuides` + ": expected boolean"); } - } - { - const fieldValue = value["hasSavedChat"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.hasSavedChat: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.hasSavedChat` + ": expected boolean"); } - } - { - const fieldValue = value["dragFactor"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.dragFactor: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.dragFactor` + ": expected number"); } - } - { - const fieldValue = value["fontFamily"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.fontFamily: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.fontFamily` + ": expected string"); } - } - { - const fieldValue = value["fontSizePt"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.fontSizePt: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.fontSizePt` + ": expected number"); } - } - { - const fieldValue = value["fontColor"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.fontColor: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.fontColor` + ": expected string"); } - } - { - const fieldValue = value["canUndo"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.canUndo: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.canUndo` + ": expected boolean"); } - } - { - const fieldValue = value["canRedo"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.canRedo: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.canRedo` + ": expected boolean"); } - } - { - const fieldValue = value["undoLabel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.undoLabel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.undoLabel` + ": expected string"); } - } - { - const fieldValue = value["redoLabel"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.redoLabel: missing required field`); - else { if (typeof fieldValue !== "string") errors.push(`${path}.redoLabel` + ": expected string"); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkSceneState = compileFields(SCENE_STATE_FIELDS); export function validateSceneState(value: unknown): ValidationResult { const hydrated = hydrateSceneState(value); diff --git a/web_ui/src/lib/bridge-core/generated/token-counter-state.ts b/web_ui/src/lib/bridge-core/generated/token-counter-state.ts index 041b0909..1b56e9ec 100644 --- a/web_ui/src/lib/bridge-core/generated/token-counter-state.ts +++ b/web_ui/src/lib/bridge-core/generated/token-counter-state.ts @@ -2,6 +2,10 @@ * Regenerate with codegen.py; a pytest fails if this file * drifts from what regenerating it now would produce. */ +import { type ValidationResult, type WireFields, compileFields } from "../wireCheck"; + +export type { ValidationResult }; + export interface TokenCounterState { schemaVersion: number; revision: number; @@ -19,90 +23,24 @@ export interface TokenCounterState { minCompatibleSchemaVersion?: number | null; } -export type ValidationResult = - | { ok: true; value: T } - | { ok: false; errors: string[] }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -// Unknown keys are tolerated on purpose. The JSON Schema marks the contract -// additionalProperties:false because Python and the schema must not drift, but -// an incoming payload carrying a field this build has never heard of is the -// normal, expected shape of a NEWER compatible sender - rejecting it here would -// defeat the additive-forward-compatibility the version negotiation exists to -// provide. Missing or wrongly-typed KNOWN fields are still hard errors. +const TOKEN_COUNTER_STATE_FIELDS: WireFields = [ + ["schemaVersion", "n", 0], + ["revision", "n", 0], + ["inputTokens", "n", 0], + ["outputTokens", "n", 0], + ["contextTokens", "n", 0], + ["totalTokens", "n", 0], + ["promptTokens", "n", 1], + ["completionTokens", "n", 1], + ["usageIsReal", "b", 0], + ["estimatedCostUsd", "n", 1], + ["sessionPromptTokens", "n", 0], + ["sessionCompletionTokens", "n", 0], + ["sessionEstimatedCostUsd", "n", 0], + ["minCompatibleSchemaVersion", "n", 1], +]; -function checkTokenCounterState(value: unknown, path: string, errors: string[]): void { - if (!isRecord(value)) { errors.push(`${path}: expected object`); return; } - { - const fieldValue = value["schemaVersion"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.schemaVersion: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.schemaVersion` + ": expected number"); } - } - { - const fieldValue = value["revision"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.revision: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.revision` + ": expected number"); } - } - { - const fieldValue = value["inputTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.inputTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.inputTokens` + ": expected number"); } - } - { - const fieldValue = value["outputTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.outputTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.outputTokens` + ": expected number"); } - } - { - const fieldValue = value["contextTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.contextTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.contextTokens` + ": expected number"); } - } - { - const fieldValue = value["totalTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.totalTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.totalTokens` + ": expected number"); } - } - { - const fieldValue = value["promptTokens"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.promptTokens` + ": expected number"); } - } - { - const fieldValue = value["completionTokens"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.completionTokens` + ": expected number"); } - } - { - const fieldValue = value["usageIsReal"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.usageIsReal: missing required field`); - else { if (typeof fieldValue !== "boolean") errors.push(`${path}.usageIsReal` + ": expected boolean"); } - } - { - const fieldValue = value["estimatedCostUsd"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.estimatedCostUsd` + ": expected number"); } - } - { - const fieldValue = value["sessionPromptTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sessionPromptTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.sessionPromptTokens` + ": expected number"); } - } - { - const fieldValue = value["sessionCompletionTokens"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sessionCompletionTokens: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.sessionCompletionTokens` + ": expected number"); } - } - { - const fieldValue = value["sessionEstimatedCostUsd"]; - if (fieldValue === undefined || fieldValue === null) errors.push(`${path}.sessionEstimatedCostUsd: missing required field`); - else { if (typeof fieldValue !== "number") errors.push(`${path}.sessionEstimatedCostUsd` + ": expected number"); } - } - { - const fieldValue = value["minCompatibleSchemaVersion"]; - if (fieldValue !== undefined && fieldValue !== null) { if (typeof fieldValue !== "number") errors.push(`${path}.minCompatibleSchemaVersion` + ": expected number"); } - } -} +const checkTokenCounterState = compileFields(TOKEN_COUNTER_STATE_FIELDS); export function validateTokenCounterState(value: unknown): ValidationResult { const errors: string[] = []; diff --git a/web_ui/src/lib/bridge-core/wireCheck.test.ts b/web_ui/src/lib/bridge-core/wireCheck.test.ts new file mode 100644 index 00000000..6f28a973 --- /dev/null +++ b/web_ui/src/lib/bridge-core/wireCheck.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, it } from "vitest"; +import { compileFields, hydrateRow, wireDefaults, type WireFields } from "./wireCheck"; + +// The generated validators (./generated/*.ts) are field tables compiled by +// this runtime. It replaced unrolled per-field code that had been checked in +// for every topic, and was proven against it input-for-input (52,000 fuzzed +// payloads across all 13 topics, identical results and messages) before that +// code was deleted - these tests pin the behaviour that proof established. + +const ROW: WireFields = [["label", "s", 0]]; +const SHAPE: WireFields = [ + ["name", "s", 0], + ["on", "b", 0], + ["count", "n", 0], + ["mode", { e: ["a", "b"] }, 0], + ["tags", { a: "s" }, 0], + ["stats", { d: "s" }, 0], + ["rows", { a: { o: ROW } }, 0], + ["nested", { o: ROW }, 1], + ["note", "s", 1], +]; + +function run(fields: WireFields, value: unknown): string[] { + const errors: string[] = []; + compileFields(fields)(value, "$", errors); + return errors; +} + +const VALID = { name: "n", on: true, count: 1, mode: "a", tags: ["x"], stats: { k: "v" }, rows: [{ label: "r" }] }; + +describe("compileFields", () => { + it("accepts a valid payload, with or without its optional fields", () => { + expect(run(SHAPE, VALID)).toEqual([]); + expect(run(SHAPE, { ...VALID, nested: { label: "x" }, note: "y" })).toEqual([]); + }); + + it("reports each wrong scalar type at the field's path", () => { + expect(run(SHAPE, { ...VALID, name: 1, on: "yes", count: "2" })).toEqual([ + "$.name: expected string", + "$.on: expected boolean", + "$.count: expected number", + ]); + }); + + it("treats a missing and a null required field alike, and skips a missing or null optional one", () => { + const { name: _name, ...noName } = VALID; + expect(run(SHAPE, noName)).toEqual(["$.name: missing required field"]); + expect(run(SHAPE, { ...VALID, name: null })).toEqual(["$.name: missing required field"]); + expect(run(SHAPE, { ...VALID, note: null, nested: null })).toEqual([]); + }); + + it("names the allowed values when a literal is off the list", () => { + expect(run(SHAPE, { ...VALID, mode: "c" })).toEqual(['$.mode: "c" is not one of [a, b]']); + }); + + it("checks arrays, records and nested rows at indexed paths", () => { + expect( + run(SHAPE, { ...VALID, tags: ["x", 2], stats: { "a.b": 3 }, rows: [{ label: "ok" }, { label: 5 }, 7] }), + ).toEqual([ + "$.tags[1]: expected string", + '$.stats["a.b"]: expected string', + "$.rows[1].label: expected string", + "$.rows[2]: expected object", + ]); + expect(run(SHAPE, { ...VALID, tags: "x", stats: [], rows: {} })).toEqual([ + "$.tags: expected array", + "$.stats: expected object", + "$.rows: expected array", + ]); + }); + + it("rejects a non-object outright and tolerates keys it does not know", () => { + expect(run(SHAPE, [])).toEqual(["$: expected object"]); + expect(run(SHAPE, { ...VALID, fromANewerSender: 1 })).toEqual([]); + }); +}); + +describe("sparse rows", () => { + const SPARSE: WireFields = [ + ["id", "s", 0], + ["title", "s", 0, ""], + ["items", { a: "s" }, 0, []], + ["meta", { d: "s" }, 0, {}], + ["owner", "s", 1, null], + ]; + const DEFAULTS = wireDefaults(SPARSE); + + it("restores absent fields, in table order, keeping unknown keys after them", () => { + const row = hydrateRow(SPARSE, DEFAULTS, { extra: 1, id: "r1" }) as Record; + expect(row).toEqual({ id: "r1", title: "", items: [], meta: {}, owner: null, extra: 1 }); + expect(Object.keys(row)).toEqual(["id", "title", "items", "meta", "owner", "extra"]); + }); + + it("returns a row that is already whole as the same object", () => { + const whole = { id: "r1", title: "t", items: [], meta: {}, owner: null }; + expect(hydrateRow(SPARSE, DEFAULTS, whole)).toBe(whole); + }); + + it("does not treat an explicit null as absent", () => { + const row = hydrateRow(SPARSE, DEFAULTS, { id: "r1", title: null }) as Record; + expect(row.title).toBeNull(); + expect(run(SPARSE, row)).toEqual(["$.title: missing required field"]); + }); + + it("gives every restored row its own containers", () => { + const a = hydrateRow(SPARSE, DEFAULTS, { id: "a" }) as { items: unknown[] }; + const b = hydrateRow(SPARSE, DEFAULTS, { id: "b" }) as { items: unknown[] }; + expect(a.items).not.toBe(b.items); + }); + + it("re-validating a row object that already passed costs nothing, but a failing row is always re-checked", () => { + const check = compileFields(SPARSE); + const good = { id: "r", title: "", items: [], meta: {}, owner: null }; + const errors: string[] = []; + check(good, "$", errors); + expect(errors).toEqual([]); + // Stored rows are replaced, never edited - the memo trusts that. This + // edit exists only to prove the second call really was skipped. + (good as Record).title = 5; + check(good, "$", errors); + expect(errors).toEqual([]); + + const bad = { id: "r", title: 5, items: [], meta: {}, owner: null }; + check(bad, "$", errors); + check(bad, "$", errors); + expect(errors).toEqual(["$.title: expected string", "$.title: expected string"]); + }); + + it("only sparse rows are memoized - any other shape is checked every time", () => { + const check = compileFields(ROW); + const row: Record = { label: "x" }; + const errors: string[] = []; + check(row, "$", errors); + row.label = 5; + check(row, "$", errors); + expect(errors).toEqual(["$.label: expected string"]); + }); +}); diff --git a/web_ui/src/lib/bridge-core/wireCheck.ts b/web_ui/src/lib/bridge-core/wireCheck.ts new file mode 100644 index 00000000..2a250f9c --- /dev/null +++ b/web_ui/src/lib/bridge-core/wireCheck.ts @@ -0,0 +1,236 @@ +/** + * The runtime half of the wire validators contracts/codegen.py generates. + * + * Each generated file (./generated/*.ts) holds its payload's shape as DATA - + * one `[name, type, optional]` row per field - and builds its validator from + * that table with `compileFields` once, at module load. The validators used to + * be emitted as unrolled code, one block per field: ~190 minified bytes a + * field, 87 KB across every topic, all of it in the initial chunk (every topic + * is subscribed at startup), the largest single piece of app code there. A + * table row costs ~25 bytes, and the checker compiled from it keeps the + * unrolled code's shape at run time: one closure per field, no per-value type + * dispatch. + * + * Behaviour is exactly the unrolled validators', message for message: a + * missing or null required field is "missing required field", a present one + * is type-checked, an absent or null optional field is skipped, and unknown + * keys are tolerated on purpose - the JSON Schema marks the contract + * additionalProperties:false because Python and the schema must not drift, + * but an incoming payload carrying a field this build has never heard of is + * the normal, expected shape of a NEWER compatible sender, and rejecting it + * would defeat the additive-forward-compatibility the version negotiation + * exists to provide. + */ + +export type ValidationResult = + | { ok: true; value: T } + | { ok: false; errors: string[] }; + +/** A field's type: "s" string, "b" boolean, "n" number; `e` a string + * literal union, `a` an array of, `d` a string-keyed record of, `o` a nested + * object with its own field table. */ +export type WireType = + | "s" + | "b" + | "n" + | { readonly e: readonly string[] } + | { readonly a: WireType } + | { readonly d: WireType } + | { readonly o: WireFields }; + +/** [name, type, optional (1) or required (0), wire default - sparse rows only] */ +export type WireField = readonly [string, WireType, 0 | 1, unknown?]; +export type WireFields = readonly WireField[]; + +type Check = (value: unknown, path: string, errors: string[]) => void; + +export function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function compileType(type: WireType): Check { + if (type === "s") { + return (value, path, errors) => { + if (typeof value !== "string") errors.push(path + ": expected string"); + }; + } + if (type === "b") { + return (value, path, errors) => { + if (typeof value !== "boolean") errors.push(path + ": expected boolean"); + }; + } + if (type === "n") { + return (value, path, errors) => { + if (typeof value !== "number") errors.push(path + ": expected number"); + }; + } + if ("e" in type) { + const allowed = type.e; + const listed = allowed.join(", "); + return (value, path, errors) => { + if (!allowed.includes(value as string)) { + errors.push(path + `: ${JSON.stringify(value)} is not one of [` + listed + `]`); + } + }; + } + if ("a" in type) { + const item = compileType(type.a); + return (value, path, errors) => { + if (!Array.isArray(value)) errors.push(path + ": expected array"); + else value.forEach((entry: unknown, i) => item(entry, path + `[${i}]`, errors)); + }; + } + if ("d" in type) { + const entryCheck = compileType(type.d); + return (value, path, errors) => { + if (!isRecord(value)) errors.push(path + ": expected object"); + // Bracket notation with a JSON-stringified key: a dict key is + // arbitrary data, and dot-appending one containing "." or "[0]" would + // make its error path look like a deeper nested field. + else Object.entries(value).forEach(([k, v]) => entryCheck(v, path + `[${JSON.stringify(k)}]`, errors)); + }; + } + return compileFields(type.o); +} + +// Field kinds the per-object loop checks inline - no call, and no path string +// built unless the check fails (a valid payload builds none at all for its +// scalar fields, the overwhelming majority). +const KIND_STRING = 1; +const KIND_BOOLEAN = 2; +const KIND_NUMBER = 3; +const KIND_OTHER = 0; + +interface CompiledField { + readonly name: string; + readonly optional: boolean; + readonly kind: number; + readonly check: Check | null; + readonly missing: string; + readonly suffix: string; +} + +/** The checker for one object shape - build it once, call it per payload. */ +export function compileFields(fields: WireFields): Check { + const compiled: CompiledField[] = fields.map(([name, type, optional]) => { + const kind = type === "s" ? KIND_STRING : type === "b" ? KIND_BOOLEAN : type === "n" ? KIND_NUMBER : KIND_OTHER; + return { + name, + optional: optional === 1, + kind, + check: kind === KIND_OTHER ? compileType(type) : null, + missing: `.${name}: missing required field`, + suffix: `.${name}: expected ${type === "s" ? "string" : type === "b" ? "boolean" : "number"}`, + }; + }); + const count = compiled.length; + const check: Check = (value, path, errors) => { + if (!isRecord(value)) { + errors.push(`${path}: expected object`); + return; + } + for (let i = 0; i < count; i++) { + const field = compiled[i]; + const fieldValue = value[field.name]; + if (fieldValue === undefined || fieldValue === null) { + if (!field.optional) errors.push(path + field.missing); + continue; + } + switch (field.kind) { + case KIND_STRING: + if (typeof fieldValue !== "string") errors.push(path + field.suffix); + break; + case KIND_BOOLEAN: + if (typeof fieldValue !== "boolean") errors.push(path + field.suffix); + break; + case KIND_NUMBER: + if (typeof fieldValue !== "number") errors.push(path + field.suffix); + break; + default: + field.check!(fieldValue, `${path}.${field.name}`, errors); + } + } + }; + if (!fields.some((field) => field.length > 3)) return check; + // A sparse row - the scene's node rows. The scene store re-validates the + // WHOLE scene after every patch, so without this each patch re-checked + // every row on the canvas, though a patch replaces only the rows it + // touches: a row object that has passed once is unchanged by construction + // (stored rows are replaced, never edited in place). Remembering the rows + // that passed makes a patch's validation cost the rows it carries. + return (value, path, errors) => { + if (isRecord(value) && validRows.has(value)) return; + const before = errors.length; + check(value, path, errors); + if (errors.length === before && isRecord(value)) validRows.add(value); + }; +} + +// Sparse rows that have passed their check - see the end of compileFields. +const validRows = new WeakSet(); + +/** The `[name, default]` pairs of a sparse row's table (graphlink_wire_schema + * .py's "SPARSE ROWS"): every field its sender may leave out. */ +export function wireDefaults(fields: WireFields): ReadonlyArray { + return fields.filter((field) => field.length > 3).map((field) => [field[0], field[3]] as const); +} + +/** Restore every field a sparse row's sender left out. + * + * Only an ABSENT field is restored - a present one, explicit null included, + * is data and is left for the validator to judge. Containers restore as + * fresh empty values (every container default is empty - wire_default + * enforces it), never a shared instance. + * + * A row with nothing missing comes back as the SAME object, so re-validating + * rows that are already whole (the scene store re-checks the whole scene + * after every patch) allocates nothing and keeps each untouched row's + * identity. A row that needs restoring is rebuilt with every field in TABLE + * order, whichever ones its sender happened to include: every restored row of + * a kind then has one key order - one hidden class to the JS engine - as the + * full rows the wire used to carry did, so property reads across the app stay + * monomorphic. A key the table does not know (a newer sender's field) is + * kept, after the known ones. + * + * Rows this function has already produced or found whole are remembered, so + * the scene store's re-check of every stored row after each patch skips the + * per-key scan for them. That is sound because stored rows are replaced, + * never edited in place - and if one ever were, a defaulted field deleted + * from it would still be caught: those fields are required by the + * validator that runs right after. */ +const wholeRows = new WeakSet(); + +export function hydrateRow( + fields: WireFields, + defaults: ReadonlyArray, + value: unknown, +): unknown { + if (!isRecord(value)) return value; + if (wholeRows.has(value)) return value; + let whole = true; + for (const [key] of defaults) { + if (value[key] === undefined) { + whole = false; + break; + } + } + if (whole) { + wholeRows.add(value); + return value; + } + const hydrated: Record = {}; + for (const field of fields) { + const key = field[0]; + const sent = value[key]; + if (sent !== undefined) hydrated[key] = sent; + else if (field.length > 3) { + const fallback = field[3]; + hydrated[key] = Array.isArray(fallback) ? [] : isRecord(fallback) ? {} : fallback; + } + } + for (const key of Object.keys(value)) { + if (!(key in hydrated)) hydrated[key] = value[key]; + } + wholeRows.add(hydrated); + return hydrated; +} From 29309202cb0d58aab53b55bdc89924836bc20d34 Mon Sep 17 00:00:00 2001 From: dovvnloading <157447210+dovvnloading@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:10:08 -0400 Subject: [PATCH 3/3] Bump anyio and soupsieve past five new advisories pip-audit now fails every PR, main's lockfile included: anyio 4.14.1 has three published advisories (CVE-2026-63374, CVE-2026-64847, CVE-2026-63349; fixed in 4.14.2) and soupsieve 2.8.4 has two (CVE-2026-85999, CVE-2026-86000; fixed in 2.9.0). Regenerated with the lockfile's own command, upgrading only those two packages (pip-compile --generate-hashes --upgrade-package ...), so the diff is two versions and their hashes. 4.14.2 is a patch release on the 4.14 line, not the 4.15 line whose BlockingPortal deprecation breaks starlette's testclient. pip-audit on the updated lockfile: no known vulnerabilities. Full suite on the new versions: 3,362 passed. Co-Authored-By: Claude --- requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index efca2474..a2dd9abc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,9 +16,9 @@ anthropic==0.116.0 \ --hash=sha256:5fc248fbb9fe03ef686f8a774f81586bca31a043260aab88b387ea3660f4a396 \ --hash=sha256:6c0a7698e8d652455da3499978279bb2588c7264d0a35be3666009a4258c8256 # via -r requirements.in -anyio==4.14.1 \ - --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \ - --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # anthropic # httpx @@ -1443,9 +1443,9 @@ socksio==1.0.0 \ --hash=sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3 \ --hash=sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac # via httpx -soupsieve==2.8.4 \ - --hash=sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e \ - --hash=sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65 +soupsieve==2.9 \ + --hash=sha256:a2b2c76d67df2382d245409fd71e321a571717e58463efa32ace87dcadac2c12 \ + --hash=sha256:acee8417325c5653e1377dc31eccad59eb82cbc65942afe6174c53b3aaad63fc # via beautifulsoup4 starlette==1.3.1 \ --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \