diff --git a/codev/projects/1357-codev-sdk-controller-subpath-d/status.yaml b/codev/projects/1357-codev-sdk-controller-subpath-d/status.yaml new file mode 100644 index 000000000..0802b6f4f --- /dev/null +++ b/codev/projects/1357-codev-sdk-controller-subpath-d/status.yaml @@ -0,0 +1,17 @@ +id: '1357' +title: codev-sdk-controller-subpath-d +protocol: air +phase: pr +plan_phases: [] +current_plan_phase: null +gates: + pr: + status: approved + requested_at: '2026-08-05T22:37:24.073Z' + approved_at: '2026-08-05T22:39:37.067Z' +iteration: 1 +build_complete: false +history: [] +started_at: '2026-08-05T22:31:40.320Z' +updated_at: '2026-08-05T22:39:37.068Z' +pr_ready_for_human: false diff --git a/codev/state/air-1357_thread.md b/codev/state/air-1357_thread.md new file mode 100644 index 000000000..8bdffb034 --- /dev/null +++ b/codev/state/air-1357_thread.md @@ -0,0 +1,47 @@ +# air-1357 — codev-sdk controller subpath: overview wire type re-exports + +## 2026-08-06 — Implement phase + +AIR strict mode, issue #1357. Two-part scope per architect instruction: + +1. **Type-only re-exports** of `OverviewData`, `OverviewBuilder`, `OverviewPR`, + `OverviewBacklogItem` from `@cluesmith/codev-types` on the sdk's + `./controller` subpath (the streamdeck migration's import-boundary + criterion in #1347 depends on this). Also added `OverviewData` to + `./tower-client` since `getOverview` returns it. Both use the + whole-statement `export type { ... } from` form — the mixed + `export { type X } from` form still emits a runtime re-export statement, + so the boundary test pins the erased form specifically. + +2. **Packaging fix**: `@cluesmith/codev-types` moved from devDependencies to + dependencies. Published `.d.ts` files reference it (`import type ... from + '@cluesmith/codev-types'` in tower-client.d.ts), so a fresh npm consumer + running tsc could not resolve it as a devDep. codev-types is published + (3.2.4 on npm) and types-only, so the zero-RUNTIME-deps posture holds. + The import-boundary test's `dependencies` assertion restated: exactly + `['@cluesmith/codev-types']` allowed, with the intent documented (zero + runtime deps; contract-types dep permitted for .d.ts resolution). + +Boundary test extensions: +- New UNIVERSAL rule: value re-exports of codev-types forbidden + (`export type` form required), mirroring the existing `import type` rule. +- New pin test: controller.ts must re-export the four overview types via + `export type { ... } from '@cluesmith/codev-types'`. + +Lockfile updated (dep group move). Building + testing next. + +## PR phase + +- Implement checks green (build 8.4s, tests 28.1s). Emit inspection confirmed + the re-exports are erased from dist JS and present in the .d.ts. +- PR #1358 created with review in the body (AIR: no spec/plan/review files). +- Running porch PR-phase checks (pr_exists + e2e_tests), then notifying the + architect and stopping at the pr gate. + +## Gate approved, merge held + +- PR-phase checks green; pr gate approved by the architect after review. +- Merge NOT yet authorized: workspace policy requires Amr's explicit per-PR + word. Holding until the architect relays it. +- Architect confirmed the consult-skip was within AIR's builder discretion; + the protocol-doc contradiction that made it ambiguous is tracked as #1359. diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f45a31f87..136dcc6b4 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -70,8 +70,10 @@ "test": "vitest run", "prepublishOnly": "pnpm build" }, + "dependencies": { + "@cluesmith/codev-types": "workspace:*" + }, "devDependencies": { - "@cluesmith/codev-types": "workspace:*", "@types/node": "22.x", "typescript": "catalog:", "vitest": "^4.0.15" diff --git a/packages/sdk/src/__tests__/import-boundary.test.ts b/packages/sdk/src/__tests__/import-boundary.test.ts index bf890f522..dc487da2a 100644 --- a/packages/sdk/src/__tests__/import-boundary.test.ts +++ b/packages/sdk/src/__tests__/import-boundary.test.ts @@ -8,8 +8,12 @@ import { fileURLToPath } from 'node:url'; * Node, and React Native (Metro). Shipped source therefore has zero Node * builtins, zero vscode imports, zero direct fetch calls (transport is an * injected adapter), zero DOM-global usage, and zero runtime dependencies. - * `@cluesmith/codev-types` is type-only (erased at build). `@cluesmith/codev-core` - * is forbidden outright: core and sdk never import each other. + * `@cluesmith/codev-types` is type-only (erased at build): it may appear in + * `dependencies` so published `.d.ts` files resolve for npm consumers (issue + * #1357), but no runtime code from it may ever load — every import and + * re-export of it must use the `import type` / `export type` form. Nothing + * else belongs in `dependencies`. `@cluesmith/codev-core` is forbidden + * outright: core and sdk never import each other. * * The single exception is `src/node/`, the explicitly Node-only adapter subpath * (`@cluesmith/codev-sdk/node`). It may use Node builtins; nothing outside it @@ -56,6 +60,10 @@ const UNIVERSAL: Array<{ label: string; pattern: RegExp }> = [ { label: 'fetch()', pattern: /\bfetch\s*\(/ }, { label: '@cluesmith/codev-core', pattern: /['"]@cluesmith\/codev-core/ }, { label: 'runtime import of @cluesmith/codev-types (must be `import type`)', pattern: /^import\s+(?!type\b)[^;]*['"]@cluesmith\/codev-types/m }, + // `export { type X } from` is NOT equivalent: it emits a runtime re-export + // statement. Only the whole-statement `export type { ... } from` form is + // fully erased. + { label: 'runtime re-export of @cluesmith/codev-types (must be `export type`)', pattern: /^export\s+(?!type\b)[^;]*['"]@cluesmith\/codev-types/m }, ]; /** Additional rules for the environment-agnostic graph (everything outside src/node/). */ @@ -90,10 +98,32 @@ describe('import boundary', () => { expect(violations).toEqual([]); }); - it('declares zero runtime dependencies', () => { + /** + * Intent: zero RUNTIME dependencies. `@cluesmith/codev-types` is the one + * allowed entry — it is a regular dependency only so published `.d.ts` + * files typecheck for npm consumers (issue #1357); the source rules above + * guarantee it is erased at build and never loaded at runtime. + */ + it('declares no dependencies beyond the type-only contract package', () => { const pkg = JSON.parse(readFileSync(join(srcRoot, '..', 'package.json'), 'utf8')) as { dependencies?: Record; }; - expect(Object.keys(pkg.dependencies ?? {})).toEqual([]); + expect(Object.keys(pkg.dependencies ?? {})).toEqual(['@cluesmith/codev-types']); + }); + + /** + * Issue #1357: the controller subpath ships the overview wire types its + * consumers read (`getOverview` returns `OverviewData`), so integrations + * need no direct `@cluesmith/codev-types` dependency. Pin both presence + * and the erased `export type` form. + */ + it('controller subpath re-exports the overview wire types type-only', () => { + const text = readFileSync(join(srcRoot, 'controller.ts'), 'utf8'); + const match = text.match(/^export type \{([^}]*)\} from ['"]@cluesmith\/codev-types['"]/m); + expect(match, 'controller.ts must `export type { ... } from "@cluesmith/codev-types"`').not.toBeNull(); + const names = match![1].split(',').map((name) => name.trim()).filter(Boolean); + for (const name of ['OverviewData', 'OverviewBuilder', 'OverviewPR', 'OverviewBacklogItem']) { + expect(names).toContain(name); + } }); }); diff --git a/packages/sdk/src/controller.ts b/packages/sdk/src/controller.ts index f1f3be462..625a11955 100644 --- a/packages/sdk/src/controller.ts +++ b/packages/sdk/src/controller.ts @@ -19,3 +19,17 @@ export { } from './tower-client.js'; export { parseSseText, type SseEnvelope } from './sse.js'; export { DEFAULT_TOWER_PORT } from './constants.js'; + +/* + * The overview wire types a controller reads (issue #1357): `getOverview` + * returns `OverviewData`, so the contract ships with the client — one import, + * no direct `@cluesmith/codev-types` dependency for integrations. Must stay + * the `export type` form (erased at build) so the sdk keeps zero runtime + * dependencies; the import-boundary test pins this. + */ +export type { + OverviewData, + OverviewBuilder, + OverviewPR, + OverviewBacklogItem, +} from '@cluesmith/codev-types'; diff --git a/packages/sdk/src/tower-client.ts b/packages/sdk/src/tower-client.ts index 1afe0eb9c..1eb21a769 100644 --- a/packages/sdk/src/tower-client.ts +++ b/packages/sdk/src/tower-client.ts @@ -17,6 +17,13 @@ import type { DashboardState, OverviewData, IssueView, IssueSearchResponse, Reso import { DEFAULT_TOWER_PORT } from './constants.js'; import { parseSseText, type SseEnvelope } from './sse.js'; +/* + * `getOverview` returns `OverviewData`, so the subpath carries the contract + * with the client (issue #1357). `export type` only — erased at build, keeping + * the zero-runtime-deps posture; the import-boundary test pins the form. + */ +export type { OverviewData } from '@cluesmith/codev-types'; + const REQUEST_TIMEOUT_MS = 10000; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b174ffafe..7c258e7b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -294,10 +294,11 @@ importers: version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@28.1.0)(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)) packages/sdk: - devDependencies: + dependencies: '@cluesmith/codev-types': specifier: workspace:* version: link:../types + devDependencies: '@types/node': specifier: 22.x version: 22.19.17