diff --git a/.github/workflows/pitot-lab.yml b/.github/workflows/pitot-lab.yml index d995ab314..62f233c4d 100644 --- a/.github/workflows/pitot-lab.yml +++ b/.github/workflows/pitot-lab.yml @@ -124,5 +124,7 @@ jobs: --head "${{ github.event.pull_request.head.sha }}" - name: Generate Types run: bash labs/15-pitot/scripts/generate_types.sh + - name: Verify generated Python SDK syntax + run: python -m py_compile labs/15-pitot/pitot-distribution/sdk/python/pitot/types.py - name: Verify projection and public surface run: python -m unittest discover -s labs/15-pitot/tests -p 'test_*.py' diff --git a/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-21-value-translation-readme.md b/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-21-value-translation-readme.md new file mode 100644 index 000000000..f4c2e207e --- /dev/null +++ b/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-21-value-translation-readme.md @@ -0,0 +1,6 @@ +### Restructure README for faster value comprehension + +- **Outcome-focused hero:** Replaces mechanism-first opening with a clear explanation of what happens to a delivery process when coding agents change. +- **Portability consequences:** Updates the portability table to describe retained operational value (e.g., "The same path from approved plan to reviewed PR") instead of just artifacts. +- **Explicit compounding:** Adds a section explaining how retained durable artifacts make the next feature easier to start without reconstructing intent from chat. +- **Reordered content:** Moves technical feature lists and installation instructions below the primary outcomes to respect evaluator attention. diff --git a/labs/15-pitot/pitot-distribution/UPSTREAM.json b/labs/15-pitot/pitot-distribution/UPSTREAM.json index 7091063ed..220e63615 100644 --- a/labs/15-pitot/pitot-distribution/UPSTREAM.json +++ b/labs/15-pitot/pitot-distribution/UPSTREAM.json @@ -31,10 +31,16 @@ "protocol/framing.go": "d4409314b72e09cfd472ad9a21d4c7a223b4341b26f58f6062a7c899e3f87482", "protocol/framing_test.go": "9fcb13767fdcc7129d2c87bc2133a5800c69d6ac0166016dc266dca5fcfaa1c5", "schema/schema.go": "fd5c3b76979c88aeed75fadb7e3c94abcad62e067d77595f021fb422a60f2211", + "sdk/python/pitot/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "sdk/python/pitot/types.py": "c9a7221f1ad6627f152f26148155d3c74f249c52262c73a515251f469e8eb4ad", + "sdk/python/pyproject.toml": "3f42de8223640978ad88e3f7b4ad1464053c43a393f0edf48445cacd31dd537c", + "sdk/typescript/package.json": "c26c726559ff0bc7c5d92b68303efc3c35314613248fe346fa125aea06c8dba6", + "sdk/typescript/src/index.ts": "de43e6654eac51afd09a3a74c4c6992dcf2306d6c41b9be626ded1c6d6cc7833", + "sdk/typescript/src/pitot.ts": "9c243824cbb7edc54b1e125abfd828bf2ed77e7151a0bd5c5d4f63e81ca9b00c", + "sdk/typescript/tsconfig.json": "e4d7ecb203fcb7d93cd9b9fb235d7fd75d1b6fb2b28eff773f4aecfdc1924d5d", "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-restructure-polyglot-sdk.md b/labs/15-pitot/pitot-distribution/release-notes/2026-07-21-restructure-polyglot-sdk.md new file mode 100644 index 000000000..0cc13a4f0 --- /dev/null +++ b/labs/15-pitot/pitot-distribution/release-notes/2026-07-21-restructure-polyglot-sdk.md @@ -0,0 +1,9 @@ +### Restructure public distribution as a polyglot SDK + +The Pitot distribution surface has been reorganized to prevent the Go module root from becoming cluttered with multi-language build configurations. Generated TypeScript and Python primitives are now strictly isolated inside an `sdk/` directory. + +- **Go**: Remains at the root of the repository (`go.mod`, `go.sum`). +- **TypeScript**: Housed in `sdk/typescript/`, complete with its own `package.json` and `tsconfig.json`. +- **Python**: Housed in `sdk/python/`, complete with a `pyproject.toml` and autogenerated types. + +This structure allows downstream users to seamlessly import the exact Pitot SDK they need for their environment without dependency conflict or configuration ambiguity. diff --git a/labs/15-pitot/pitot-distribution/sdk/python/pitot/__init__.py b/labs/15-pitot/pitot-distribution/sdk/python/pitot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/labs/15-pitot/pitot-distribution/sdk/python/pitot/types.py b/labs/15-pitot/pitot-distribution/sdk/python/pitot/types.py new file mode 100644 index 000000000..cd221edaa --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/python/pitot/types.py @@ -0,0 +1,307 @@ +from dataclasses import dataclass +from typing import Any, TypeVar, Type, cast + + +T = TypeVar("T") + + +def from_str(x: Any) -> str: + assert isinstance(x, str) + return x + + +def from_none(x: Any) -> Any: + assert x is None + return x + + +def from_union(fs, x): + for f in fs: + try: + return f(x) + except: + pass + assert False + + +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x + + +def to_class(c: Type[T], x: Any) -> dict: + assert isinstance(x, c) + return cast(Any, x).to_dict() + + +@dataclass +class BoundaryFault: + host: str + pitot_version: str + reason: str + type: str + action_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'BoundaryFault': + assert isinstance(obj, dict) + host = from_str(obj.get("host")) + pitot_version = from_str(obj.get("pitot_version")) + reason = from_str(obj.get("reason")) + type = from_str(obj.get("type")) + action_id = from_union([from_str, from_none], obj.get("action_id")) + return BoundaryFault(host, pitot_version, reason, type, action_id) + + def to_dict(self) -> dict: + result: dict = {} + result["host"] = from_str(self.host) + result["pitot_version"] = from_str(self.pitot_version) + result["reason"] = from_str(self.reason) + result["type"] = from_str(self.type) + if self.action_id is not None: + result["action_id"] = from_union([from_str, from_none], self.action_id) + return result + + +@dataclass +class ControlRequested: + action_id: str + kind: str + pitot_version: str + type: str + data: Any = None + + @staticmethod + def from_dict(obj: Any) -> 'ControlRequested': + assert isinstance(obj, dict) + action_id = from_str(obj.get("action_id")) + kind = from_str(obj.get("kind")) + pitot_version = from_str(obj.get("pitot_version")) + type = from_str(obj.get("type")) + data = obj.get("data") + return ControlRequested(action_id, kind, pitot_version, type, data) + + def to_dict(self) -> dict: + result: dict = {} + result["action_id"] = from_str(self.action_id) + result["kind"] = from_str(self.kind) + result["pitot_version"] = from_str(self.pitot_version) + result["type"] = from_str(self.type) + if self.data is not None: + result["data"] = self.data + return result + + +@dataclass +class ControlResponse: + action_id: str + controller_id: str + outcome: str + pitot_version: str + type: str + message: str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'ControlResponse': + assert isinstance(obj, dict) + action_id = from_str(obj.get("action_id")) + controller_id = from_str(obj.get("controller_id")) + outcome = from_str(obj.get("outcome")) + pitot_version = from_str(obj.get("pitot_version")) + type = from_str(obj.get("type")) + message = from_union([from_str, from_none], obj.get("message")) + return ControlResponse(action_id, controller_id, outcome, pitot_version, type, message) + + def to_dict(self) -> dict: + result: dict = {} + result["action_id"] = from_str(self.action_id) + result["controller_id"] = from_str(self.controller_id) + result["outcome"] = from_str(self.outcome) + result["pitot_version"] = from_str(self.pitot_version) + result["type"] = from_str(self.type) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + return result + + +@dataclass +class Action: + id: str + kind: str + + @staticmethod + def from_dict(obj: Any) -> 'Action': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + kind = from_str(obj.get("kind")) + return Action(id, kind) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["kind"] = from_str(self.kind) + return result + + +@dataclass +class Content: + mode: str + full: Any = None + sha256: str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'Content': + assert isinstance(obj, dict) + mode = from_str(obj.get("mode")) + full = obj.get("full") + sha256 = from_union([from_str, from_none], obj.get("sha256")) + return Content(mode, full, sha256) + + def to_dict(self) -> dict: + result: dict = {} + result["mode"] = from_str(self.mode) + if self.full is not None: + result["full"] = self.full + if self.sha256 is not None: + result["sha256"] = from_union([from_str, from_none], self.sha256) + return result + + +@dataclass +class Host: + name: str + adapter_version: str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'Host': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + adapter_version = from_union([from_str, from_none], obj.get("adapter_version")) + return Host(name, adapter_version) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.adapter_version is not None: + result["adapter_version"] = from_union([from_str, from_none], self.adapter_version) + return result + + +@dataclass +class Observation: + fidelity: str + source: str + + @staticmethod + def from_dict(obj: Any) -> 'Observation': + assert isinstance(obj, dict) + fidelity = from_str(obj.get("fidelity")) + source = from_str(obj.get("source")) + return Observation(fidelity, source) + + def to_dict(self) -> dict: + result: dict = {} + result["fidelity"] = from_str(self.fidelity) + result["source"] = from_str(self.source) + return result + + +@dataclass +class Usage: + input_tokens: int + output_tokens: int + + @staticmethod + def from_dict(obj: Any) -> 'Usage': + assert isinstance(obj, dict) + input_tokens = from_int(obj.get("input_tokens")) + output_tokens = from_int(obj.get("output_tokens")) + return Usage(input_tokens, output_tokens) + + def to_dict(self) -> dict: + result: dict = {} + result["input_tokens"] = from_int(self.input_tokens) + result["output_tokens"] = from_int(self.output_tokens) + return result + + +@dataclass +class Event: + host: Host + observation: Observation + pitot_version: str + type: str + action: Action | None = None + content: Content | None = None + id: str | None = None + session_id: str | None = None + time: str | None = None + usage: Usage | None = None + + @staticmethod + def from_dict(obj: Any) -> 'Event': + assert isinstance(obj, dict) + host = Host.from_dict(obj.get("host")) + observation = Observation.from_dict(obj.get("observation")) + pitot_version = from_str(obj.get("pitot_version")) + type = from_str(obj.get("type")) + action = from_union([Action.from_dict, from_none], obj.get("action")) + content = from_union([Content.from_dict, from_none], obj.get("content")) + id = from_union([from_str, from_none], obj.get("id")) + session_id = from_union([from_str, from_none], obj.get("session_id")) + time = from_union([from_str, from_none], obj.get("time")) + usage = from_union([Usage.from_dict, from_none], obj.get("usage")) + return Event(host, observation, pitot_version, type, action, content, id, session_id, time, usage) + + def to_dict(self) -> dict: + result: dict = {} + result["host"] = to_class(Host, self.host) + result["observation"] = to_class(Observation, self.observation) + result["pitot_version"] = from_str(self.pitot_version) + result["type"] = from_str(self.type) + if self.action is not None: + result["action"] = from_union([lambda x: to_class(Action, x), from_none], self.action) + if self.content is not None: + result["content"] = from_union([lambda x: to_class(Content, x), from_none], self.content) + if self.id is not None: + result["id"] = from_union([from_str, from_none], self.id) + if self.session_id is not None: + result["session_id"] = from_union([from_str, from_none], self.session_id) + if self.time is not None: + result["time"] = from_union([from_str, from_none], self.time) + if self.usage is not None: + result["usage"] = from_union([lambda x: to_class(Usage, x), from_none], self.usage) + return result + + +@dataclass +class Types: + boundary_fault: BoundaryFault + control_requested: ControlRequested + control_response: ControlResponse + event: Event + + @staticmethod + def from_dict(obj: Any) -> 'Types': + assert isinstance(obj, dict) + boundary_fault = BoundaryFault.from_dict(obj.get("boundary_fault")) + control_requested = ControlRequested.from_dict(obj.get("control_requested")) + control_response = ControlResponse.from_dict(obj.get("control_response")) + event = Event.from_dict(obj.get("event")) + return Types(boundary_fault, control_requested, control_response, event) + + def to_dict(self) -> dict: + result: dict = {} + result["boundary_fault"] = to_class(BoundaryFault, self.boundary_fault) + result["control_requested"] = to_class(ControlRequested, self.control_requested) + result["control_response"] = to_class(ControlResponse, self.control_response) + result["event"] = to_class(Event, self.event) + return result + + +def types_from_dict(s: Any) -> Types: + return Types.from_dict(s) + + +def types_to_dict(x: Types) -> Any: + return to_class(Types, x) diff --git a/labs/15-pitot/pitot-distribution/sdk/python/pyproject.toml b/labs/15-pitot/pitot-distribution/sdk/python/pyproject.toml new file mode 100644 index 000000000..291e90a3c --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/python/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "pitot" +version = "0.1.0" +description = "Pitot: The passive, protocol-first measurement boundary for coding agents." +authors = [ + { name = "Operator Stack" } +] +license = { text = "MIT" } +requires-python = ">=3.8" +dependencies = [] + +[project.urls] +Homepage = "https://github.com/operatorstack/pitot" +Repository = "https://github.com/operatorstack/pitot" diff --git a/labs/15-pitot/pitot-distribution/sdk/typescript/package.json b/labs/15-pitot/pitot-distribution/sdk/typescript/package.json new file mode 100644 index 000000000..cab900e36 --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/typescript/package.json @@ -0,0 +1,25 @@ +{ + "name": "@operatorstack/pitot", + "version": "0.1.0", + "description": "Pitot: The passive, protocol-first measurement boundary for coding agents.", + "main": "src/index.js", + "types": "src/index.d.ts", + "files": [ + "src/**/*.ts", + "src/**/*.js", + "src/**/*.d.ts" + ], + "scripts": { + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "https://github.com/operatorstack/pitot.git", + "directory": "sdk/typescript" + }, + "author": "Operator Stack", + "license": "MIT", + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/labs/15-pitot/pitot-distribution/sdk/typescript/src/index.ts b/labs/15-pitot/pitot-distribution/sdk/typescript/src/index.ts new file mode 100644 index 000000000..df19fcedd --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/typescript/src/index.ts @@ -0,0 +1 @@ +export * from "./pitot"; diff --git a/labs/15-pitot/pitot-distribution/sdk/typescript/src/pitot.ts b/labs/15-pitot/pitot-distribution/sdk/typescript/src/pitot.ts new file mode 100644 index 000000000..a2868e031 --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/typescript/src/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-distribution/sdk/typescript/tsconfig.json b/labs/15-pitot/pitot-distribution/sdk/typescript/tsconfig.json new file mode 100644 index 000000000..2e549458e --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/typescript/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "commonjs", + "declaration": true, + "outDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +} diff --git a/labs/15-pitot/scripts/build_pitot.py b/labs/15-pitot/scripts/build_pitot.py index f1b81e05f..77cfafc32 100644 --- a/labs/15-pitot/scripts/build_pitot.py +++ b/labs/15-pitot/scripts/build_pitot.py @@ -66,13 +66,15 @@ 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.""" +def _iter_sdk_files(repo: Path) -> list[tuple[str, Path]]: + """Yield (projected_path, source_path) for generated language SDKs.""" entries: list[tuple[str, Path]] = [] - types_dir = repo / LAB / "pitot-distribution" / "types" - if types_dir.is_dir(): - for path in sorted(types_dir.rglob("*")): + sdk_dir = repo / LAB / "pitot-distribution" / "sdk" + if sdk_dir.is_dir(): + for path in sorted(sdk_dir.rglob("*")): if path.is_file() and not path.is_symlink(): + if "__pycache__" in path.parts or path.suffix == ".pyc": + continue projected = path.relative_to(repo / LAB / "pitot-distribution").as_posix() entries.append((projected, path)) return entries @@ -82,7 +84,7 @@ 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) + _iter_types_files(repo): + for projected, source in _iter_module_files(repo) + _iter_preview_files(repo) + _iter_sdk_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 index a7c86be0c..5ad77ad7e 100755 --- a/labs/15-pitot/scripts/generate_types.sh +++ b/labs/15-pitot/scripts/generate_types.sh @@ -6,11 +6,19 @@ cd "$REPO_ROOT/labs/15-pitot/pitot" go run ./cmd/generate-schema > schema.json -TARGET_DIR="../pitot-distribution/types/ts" -mkdir -p "$TARGET_DIR" +# TypeScript +TS_DIR="../pitot-distribution/sdk/typescript/src" +mkdir -p "$TS_DIR" +npx -y quicktype -s schema schema.json -o "$TS_DIR/pitot.ts" --just-types +echo "export * from \"./pitot\";" > "$TS_DIR/index.ts" +echo "Generated TypeScript types in $TS_DIR" -npx -y quicktype -s schema schema.json -o "$TARGET_DIR/pitot.ts" --just-types +# Python +PY_DIR="../pitot-distribution/sdk/python/pitot" +mkdir -p "$PY_DIR" +npx -y quicktype -s schema schema.json -o "$PY_DIR/types.py" +# Create empty __init__.py if it doesn't exist +touch "$PY_DIR/__init__.py" +echo "Generated Python types in $PY_DIR" rm schema.json - -echo "Generated TypeScript types in $TARGET_DIR/pitot.ts"