From f1d014a1d837b597723d34d5134d2019512676d9 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Tue, 21 Jul 2026 13:26:54 +0100 Subject: [PATCH] feat(pitot): project native cross-language TypeScript interfaces Generates strongly-typed TS interfaces from Pitot Go structs using reflection and quicktype. Automates this inside CI and tracks it in the UPSTREAM projection manifest. --- .github/workflows/pitot-lab.yml | 5 ++ .../15-pitot/pitot-distribution/UPSTREAM.json | 5 +- ...07-21-project-cross-language-interfaces.md | 3 + .../pitot-distribution/types/ts/pitot.ts | 70 +++++++++++++++++++ .../pitot/cmd/generate-schema/main.go | 34 +++++++++ labs/15-pitot/pitot/go.mod | 8 +++ labs/15-pitot/pitot/go.sum | 10 +++ labs/15-pitot/scripts/build_pitot.py | 14 +++- labs/15-pitot/scripts/generate_types.sh | 16 +++++ 9 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 labs/15-pitot/pitot-distribution/release-notes/2026-07-21-project-cross-language-interfaces.md create mode 100644 labs/15-pitot/pitot-distribution/types/ts/pitot.ts create mode 100644 labs/15-pitot/pitot/cmd/generate-schema/main.go create mode 100644 labs/15-pitot/pitot/go.sum create mode 100755 labs/15-pitot/scripts/generate_types.sh diff --git a/.github/workflows/pitot-lab.yml b/.github/workflows/pitot-lab.yml index 97d51226b..d995ab314 100644 --- a/.github/workflows/pitot-lab.yml +++ b/.github/workflows/pitot-lab.yml @@ -112,6 +112,9 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.11" + - uses: actions/setup-node@v4 + with: + node-version: "20" - name: Require an append-only release message if: github.event_name == 'pull_request' run: >- @@ -119,5 +122,7 @@ jobs: --repo . --base "${{ github.event.pull_request.base.sha }}" --head "${{ github.event.pull_request.head.sha }}" + - name: Generate Types + run: bash labs/15-pitot/scripts/generate_types.sh - name: Verify projection and public surface run: python -m unittest discover -s labs/15-pitot/tests -p 'test_*.py' diff --git a/labs/15-pitot/pitot-distribution/UPSTREAM.json b/labs/15-pitot/pitot-distribution/UPSTREAM.json index 508444306..7091063ed 100644 --- a/labs/15-pitot/pitot-distribution/UPSTREAM.json +++ b/labs/15-pitot/pitot-distribution/UPSTREAM.json @@ -12,6 +12,7 @@ "assets/pitot-two-roles.svg": "05a740b169110b2d6865fe4ecc9bd9e20c8a6c0ee00ceaf948f2c811b9dce0c5", "bridge/bridge.go": "79ac2e025e16782f3c283b43cbea5b9ba4f837446583864df8f57d6346cae816", "bridge/bridge_test.go": "23a19b7580d4b1e826224ec8322208ccca44ddd9a97b150efe15cafecc53e47f", + "cmd/generate-schema/main.go": "6e9d0030290d99e36967433f96e38385a122974f899ad9421aac1ef7e50d8fcb", "cmd/pitot/main.go": "a8ef8a789abe13f41d8a1014de3fb4ad46522b558f0ace70786b904e81892b05", "cmd/pitot/main_test.go": "ab2894327e7a5e72e9f3d83cae637e3ccb2e1ae05f14458be4fe9016149aeef7", "conformance/conformance.go": "43b692114f45c8b52958e34b35aee1cee339d8321c90f92ab4f5b963e79935bb", @@ -24,7 +25,8 @@ "examples/doc.go": "58f3f9eb7d272d7b6eecdb05f43e1613d5e3ef92d15d97c5440bd4b6990c26f9", "examples/local-approval/main.go": "51386af324cd7d3bb07fe3ace53503884b02714b96b83073342fde81ce3b83a5", "examples/token-meter/main.go": "4b1b9c1a43c3cf48b09dba6f607776caced9d2b5b562373496b31ed184582dd1", - "go.mod": "3017c9418f2c6e3beaa14038fef6dfe9ec181819fb60bdbe992f8d3b1c5ba4de", + "go.mod": "7e71b29887a2370c920a3ecad84460130ecc5ad4acba7f62300b5d2568f0ee13", + "go.sum": "2f73a6c3c672f4022f4a618578fac165172095ef590db5d14caa4552675f1980", "projection/projection.go": "4d3c823fd72a3ca5387dba3683838a1d7e455e9b18309acc839763a39b7bb35f", "protocol/framing.go": "d4409314b72e09cfd472ad9a21d4c7a223b4341b26f58f6062a7c899e3f87482", "protocol/framing_test.go": "9fcb13767fdcc7129d2c87bc2133a5800c69d6ac0166016dc266dca5fcfaa1c5", @@ -32,6 +34,7 @@ "sensor/decode_fuzz_test.go": "6f3412865c16e3314980a369c36f1f26ba36a991c8b4a8a9d3b776ad9c0de2b0", "sensor/sensor.go": "939e146eaa51906972f98cf4615747eb75811b0c54d467a938825750336abeae", "sensor/sensor_test.go": "ea9a58d45ad56d29214a40fb2cfa935e769f85334afcb1856a20fb938d486245", + "types/ts/pitot.ts": "9c243824cbb7edc54b1e125abfd828bf2ed77e7151a0bd5c5d4f63e81ca9b00c", "windtunnel/doc.go": "44e0bcde632da73e1f8b98beade3a34ca8e0d0ea79cdfb91d131de290b164fc4", "windtunnel/windtunnel_test.go": "1c8b6c5d56ff5c4e66403baeae0d98108255d153c1275572b8c68e333e091493" }, diff --git a/labs/15-pitot/pitot-distribution/release-notes/2026-07-21-project-cross-language-interfaces.md b/labs/15-pitot/pitot-distribution/release-notes/2026-07-21-project-cross-language-interfaces.md new file mode 100644 index 000000000..09e71e454 --- /dev/null +++ b/labs/15-pitot/pitot-distribution/release-notes/2026-07-21-project-cross-language-interfaces.md @@ -0,0 +1,3 @@ +### Project native cross-language TypeScript interfaces + +Pitot now projects a definitively synced, strongly-typed `types.ts` representation of its JSON API surface into the public distribution alongside the core Go module. By generating these types natively from the internal Go structs using reflection and `quicktype`, Pitot completely eliminates the adoption friction for Node and TypeScript integrations. Instead of maintaining parallel JSON schema definitions, frontend consumers can drop Pitot into their pipeline effortlessly, enjoying instantaneous telemetry ingestion and foolproof TypeScript interfaces for the control plane. diff --git a/labs/15-pitot/pitot-distribution/types/ts/pitot.ts b/labs/15-pitot/pitot-distribution/types/ts/pitot.ts new file mode 100644 index 000000000..a2868e031 --- /dev/null +++ b/labs/15-pitot/pitot-distribution/types/ts/pitot.ts @@ -0,0 +1,70 @@ +export interface Pitot { + boundary_fault: BoundaryFault; + control_requested: ControlRequested; + control_response: ControlResponse; + event: Event; +} + +export interface BoundaryFault { + action_id?: string; + host: string; + pitot_version: string; + reason: string; + type: string; +} + +export interface ControlRequested { + action_id: string; + data?: unknown; + kind: string; + pitot_version: string; + type: string; +} + +export interface ControlResponse { + action_id: string; + controller_id: string; + message?: string; + outcome: string; + pitot_version: string; + type: string; +} + +export interface Event { + action?: Action; + content?: Content; + host: Host; + id?: string; + observation: Observation; + pitot_version: string; + session_id?: string; + time?: string; + type: string; + usage?: Usage; +} + +export interface Action { + id: string; + kind: string; +} + +export interface Content { + full?: unknown; + mode: string; + sha256?: string; +} + +export interface Host { + adapter_version?: string; + name: string; +} + +export interface Observation { + fidelity: string; + source: string; +} + +export interface Usage { + input_tokens: number; + output_tokens: number; +} diff --git a/labs/15-pitot/pitot/cmd/generate-schema/main.go b/labs/15-pitot/pitot/cmd/generate-schema/main.go new file mode 100644 index 000000000..67c91b62e --- /dev/null +++ b/labs/15-pitot/pitot/cmd/generate-schema/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/invopop/jsonschema" + "github.com/operatorstack/pitot/schema" +) + +// PitotContract acts as a root object to force generation of all these types. +type PitotContract struct { + Event schema.Event `json:"event"` + ControlRequested schema.ControlRequested `json:"control_requested"` + ControlResponse schema.ControlResponse `json:"control_response"` + BoundaryFault schema.BoundaryFault `json:"boundary_fault"` +} + +func main() { + reflector := jsonschema.Reflector{ + AllowAdditionalProperties: false, + } + + sch := reflector.Reflect(&PitotContract{}) + + b, err := json.MarshalIndent(sch, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal schema: %v\n", err) + os.Exit(1) + } + + fmt.Println(string(b)) +} diff --git a/labs/15-pitot/pitot/go.mod b/labs/15-pitot/pitot/go.mod index 5efaee208..e75d4b443 100644 --- a/labs/15-pitot/pitot/go.mod +++ b/labs/15-pitot/pitot/go.mod @@ -1,3 +1,11 @@ module github.com/operatorstack/pitot go 1.26 + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/invopop/jsonschema v0.14.0 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect +) diff --git a/labs/15-pitot/pitot/go.sum b/labs/15-pitot/pitot/go.sum new file mode 100644 index 000000000..afe512a42 --- /dev/null +++ b/labs/15-pitot/pitot/go.sum @@ -0,0 +1,10 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= +github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= +go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= +go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= diff --git a/labs/15-pitot/scripts/build_pitot.py b/labs/15-pitot/scripts/build_pitot.py index 80a9a53ac..f1b81e05f 100644 --- a/labs/15-pitot/scripts/build_pitot.py +++ b/labs/15-pitot/scripts/build_pitot.py @@ -66,11 +66,23 @@ def _iter_preview_files(repo: Path) -> list[tuple[str, Path]]: return entries +def _iter_types_files(repo: Path) -> list[tuple[str, Path]]: + """Yield (projected_path, source_path) for generated language interfaces.""" + entries: list[tuple[str, Path]] = [] + types_dir = repo / LAB / "pitot-distribution" / "types" + if types_dir.is_dir(): + for path in sorted(types_dir.rglob("*")): + if path.is_file() and not path.is_symlink(): + projected = path.relative_to(repo / LAB / "pitot-distribution").as_posix() + entries.append((projected, path)) + return entries + + def compute_manifest(repo: Path) -> dict[str, str]: """Return {projected_path: sha256} for the whole public surface.""" repo = repo.resolve() surface: dict[str, str] = {} - for projected, source in _iter_module_files(repo) + _iter_preview_files(repo): + for projected, source in _iter_module_files(repo) + _iter_preview_files(repo) + _iter_types_files(repo): if projected in surface: raise ValueError(f"projection collision on {projected}") digest = hashlib.sha256(source.read_bytes()).hexdigest() diff --git a/labs/15-pitot/scripts/generate_types.sh b/labs/15-pitot/scripts/generate_types.sh new file mode 100755 index 000000000..a7c86be0c --- /dev/null +++ b/labs/15-pitot/scripts/generate_types.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +REPO_ROOT=$(git rev-parse --show-toplevel) +cd "$REPO_ROOT/labs/15-pitot/pitot" + +go run ./cmd/generate-schema > schema.json + +TARGET_DIR="../pitot-distribution/types/ts" +mkdir -p "$TARGET_DIR" + +npx -y quicktype -s schema schema.json -o "$TARGET_DIR/pitot.ts" --just-types + +rm schema.json + +echo "Generated TypeScript types in $TARGET_DIR/pitot.ts"