Skip to content

chore: Enforce Side-Effects in SDK Pipeline#350

Merged
davideast merged 2 commits into
mainfrom
fix/constructor-visibility
Apr 28, 2026
Merged

chore: Enforce Side-Effects in SDK Pipeline#350
davideast merged 2 commits into
mainfrom
fix/constructor-visibility

Conversation

@davideast
Copy link
Copy Markdown
Collaborator

@davideast davideast commented Apr 28, 2026

This PR formalizes the boundary between generated and handwritten code in the SDK. It fixes three concrete problems and adds six architectural guardrails to prevent future drift.

Fixes #317

The Problem

The SDK has handwritten extensions (project-ext.ts) that add methods the generator can't express — filesystem I/O, binary uploads, private REST. These extensions worked, but relied on two unsafe patterns:

  1. as any casts — Extension code used (this as any).client to access the base class client, bypassing TypeScript's type system entirely.
  2. Implicit inheritance — The generator hardcoded all imports to use the generated base class, even when an extensionPath was configured. Other generated files importing Project got the base class without uploadImage or downloadAssets.
  3. Undocumented boundary — Nothing in the codebase declared which methods were handwritten, why they couldn't be generated, or where their contracts lived. The membrane between generated and handwritten code was invisible.

What Changed

Constructor Visibility Fix

The generator now emits protected scope (instead of private) for the client parameter when a class has an extensionPath. This lets -ext.ts files access this.client without as any.

- parameters: [{ name: "client", type: "StitchToolClient", scope: Scope.Private }],
+ const clientScope = config.extensionPath ? Scope.Protected : Scope.Private;
+ parameters: [{ name: "client", type: "StitchToolClient", scope: clientScope }],

Inheritance Depth Resolution

When a generated class (e.g., Stitch) returns instances of another class (e.g., Project) that has an extension, the generator now imports the extension class instead of the base:

- moduleSpecifier: `./${rc.toLowerCase()}.js`,
+ moduleSpecifier: targetClassConfig.extensionPath,  // ../../src/project-ext.js

Scan-Based Projections (find)

Added a find keyword to the projection IR that replaces brittle indexed array access with a scan pattern:

// Before: raw?.outputComponents?.[0]?.design?.screens?.[0]  (breaks if structure changes)
// After:  (raw?.outputComponents ?? []).find(c => c?.design?.screens != null)?.design?.screens?.[0]

Side-Effect Membrane (IR + Generator)

Added SideEffectSpec to ir-schema.ts and sideEffects declarations to domain-map.json. Each handwritten extension method now formally declares:

  • method — the method name
  • reason — why it can't be generated (filesystem_io, binary_data, private_rest, complex_orchestration)
  • specPath — path to its typed service contract

The generator validates at Stage 3:

  • No sideEffect.method collides with a generated binding method
  • Each specPath resolves to an existing file

Passthrough Removal

Deleted stitch-ext.ts, an empty passthrough re-export of the generated Stitch class. index.ts and singleton.ts now import Stitch directly from generated/src/stitch.js.

Architectural Guardrails Added

Test Guards Against
constructor-visibility.test.ts as any casts to access private members
circular-deps.test.ts Import cycles between extensions and generated code
ghost-method-guard.test.ts Barrel exports referencing missing extension files
inheritance-depth.test.ts Generated files importing base class instead of extension
extension-resolution.test.ts Returned instances missing extension methods
sideeffect-manifest.test.ts Undeclared methods on extensions, or declared-but-missing methods

Files Changed

Generator & IR (pipeline)

  • scripts/ir-schema.ts — Added SideEffectSpec schema, sideEffects on DomainClassConfig
  • scripts/generate-sdk.tsfind projection, protected scope, extension imports, side-effect validation
  • scripts/test/ir-schema.test.ts — 8 new contract tests for SideEffectSpec

Domain Map

  • packages/sdk/generated/domain-map.jsonsideEffects entries for uploadImage and downloadAssets
  • packages/sdk/generated/src/*.ts — Regenerated (protected constructors, extension imports)

SDK Source

  • packages/sdk/src/project-ext.ts — Removed as any casts
  • packages/sdk/src/index.ts — Import Stitch from generated (not passthrough)
  • packages/sdk/src/singleton.ts — Import Stitch from generated (not passthrough)
  • packages/sdk/src/stitch-ext.tsDeleted
  • packages/sdk/src/download-handler.ts — Minor cleanup
  • packages/sdk/package.json — Added prepublishOnly script

Documentation

  • .agents/skills/stitch-sdk-development/SKILL.md — Documented Side-Effect Membrane and extension pattern

- Add SideEffectSpec to ir-schema.ts with typed reason enum
- Add sideEffects declarations to domain-map.json for Project
- Add generator validation: method collision check + spec file existence
- Remove stitch-ext.ts passthrough (import Stitch from generated directly)
- Add sideeffect-manifest.test.ts (bidirectional consistency guard)
- Add prepublishOnly script to package.json
- Document Side-Effect Membrane in stitch-sdk-development skill
- 8 new IR contract tests, 3 new manifest guard tests
@davideast davideast merged commit b023c76 into main Apr 28, 2026
6 checks passed
@davideast davideast deleted the fix/constructor-visibility branch April 28, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fleet Execution] Fix hardcoded index in Project.generate projection

1 participant