From 1388e7a36279b3bc798131787a90d1c515f27db2 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 2 Jul 2026 18:27:06 -0400 Subject: [PATCH] =?UTF-8?q?feat(#1683):=20published=20Host-Integration=20S?= =?UTF-8?q?DK=20surface=20+=20smoke=20test=20=E2=80=94=20Slice=202=20(#193?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK entry (src/host-integration-sdk.cts) is the single PUBLIC surface a host-plugin author imports: the negotiated schema + classification, the five adapters (declarative/imperative/model/hook/state), and the serialized handshake. Frozen so the public shape cannot be mutated. Everything else in gsd-core stays internal. tests/sdk-smoke.test.cjs imports ONLY from the SDK entry and builds a third-party host-plugin end-to-end (compose adapters + handshake + classify) — proving an external author can wire a host without gsd-core internals (#1683 AC). ESLint-ignore + inventory manifest kept in sync for the new tsc-emitted module. --- docs/INVENTORY-MANIFEST.json | 1 + eslint.config.mjs | 1 + src/host-integration-sdk.cts | 58 +++++++++++++++++++++++++++++++ tests/sdk-smoke.test.cjs | 67 ++++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 src/host-integration-sdk.cts create mode 100644 tests/sdk-smoke.test.cjs diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index dd3fa08fb..7e2930467 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -341,6 +341,7 @@ "handshake-serialized.cjs", "hook-bus.cjs", "host-integration.cjs", + "host-integration-sdk.cjs", "init-command-router.cjs", "init.cjs", "install-engine.cjs", diff --git a/eslint.config.mjs b/eslint.config.mjs index 06038d37f..3f3f2b1df 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -59,6 +59,7 @@ export default tseslint.config( 'gsd-core/bin/lib/semver-compare.cjs', 'gsd-core/bin/lib/host-integration.cjs', 'gsd-core/bin/lib/handshake-serialized.cjs', + 'gsd-core/bin/lib/host-integration-sdk.cjs', 'gsd-core/bin/lib/install-engine.cjs', 'gsd-core/bin/lib/capability-loader.cjs', 'gsd-core/bin/lib/capability-source.cjs', diff --git a/src/host-integration-sdk.cts b/src/host-integration-sdk.cts new file mode 100644 index 000000000..73c52ece7 --- /dev/null +++ b/src/host-integration-sdk.cts @@ -0,0 +1,58 @@ +/** + * Host-Integration SDK — the published public surface (ADR-1239 Phase E / #1683). + * + * External host-plugin authors import ONLY from this entry. It IS the contract: + * everything it re-exports is public + versioned (PROTOCOL_VERSION governs the + * set); everything else in gsd-core is internal. An SDK smoke test + * (tests/sdk-smoke.test.cjs) builds a new host-plugin against this surface only + * — proving an external author can wire a host without reading gsd-core internals. + * + * Surface: the negotiated schema + classification, the five adapters + * (declarative/imperative/model/hook/state), and the serialized handshake. + * Frozen so the public shape cannot be mutated by consumers. + */ +'use strict'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import hostIntegration = require('./host-integration.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +import adapterDeclarative = require('./adapter-declarative.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +import adapterImperative = require('./adapter-imperative.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +import modelAdapter = require('./model-adapter.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +import hookBus = require('./hook-bus.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +import stateIo = require('./state-io.cjs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +import handshake = require('./handshake-serialized.cjs'); + +const SDK = Object.freeze({ + // ── Schema + protocol version ──────────────────────────────────────────── + PROTOCOL_VERSION: hostIntegration.PROTOCOL_VERSION, + HOST_INTEGRATION_AXES: hostIntegration.HOST_INTEGRATION_AXES, + INTERFACE_POINTS: hostIntegration.INTERFACE_POINTS, + PROFILE_BASELINES: hostIntegration.PROFILE_BASELINES, + + // ── Negotiation + classification ───────────────────────────────────────── + negotiateHostCapabilities: hostIntegration.negotiateHostCapabilities, + profileOf: hostIntegration.profileOf, + degradationFor: hostIntegration.degradationFor, + hookEventSurfaceFor: hostIntegration.hookEventSurfaceFor, + shouldFlattenDispatch: hostIntegration.shouldFlattenDispatch, + + // ── Embedding + engine adapters ────────────────────────────────────────── + createDeclarativeAdapter: adapterDeclarative.createDeclarativeAdapter, + createImperativeAdapter: adapterImperative.createImperativeAdapter, + createModelAdapter: modelAdapter.createModelAdapter, + createHookBus: hookBus.createHookBus, + createStateIO: stateIo.createStateIO, + + // ── Serialized handshake (out-of-process SDK hosts: pi / VS Code) ───────── + HANDSHAKE_METHOD: handshake.HANDSHAKE_METHOD, + buildHandshakeRequest: handshake.buildHandshakeRequest, + handleHandshakeRequest: handshake.handleHandshakeRequest, +}); + +export = SDK; diff --git a/tests/sdk-smoke.test.cjs b/tests/sdk-smoke.test.cjs new file mode 100644 index 000000000..abd3c6834 --- /dev/null +++ b/tests/sdk-smoke.test.cjs @@ -0,0 +1,67 @@ +'use strict'; + +/** + * SDK smoke test — ADR-1239 Phase E / #1683 Slice 2. + * + * Proves the Host-Integration Interface is PUBLISHED: an external developer can + * author + wire a new host-plugin against the SDK surface ONLY (no imports of + * gsd-core internal paths). This test imports exclusively from the SDK entry — + * if a host-plugin author needs a symbol that ISN'T here, the SDK is incomplete. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); + +// EXTERNAL-AUTHOR CONTRACT: import ONLY from the published SDK entry. +const SDK = require('../gsd-core/bin/lib/host-integration-sdk.cjs'); + +test('SDK exports the full public surface a host-plugin author needs', () => { + assert.equal(typeof SDK.PROTOCOL_VERSION, 'number'); + assert.ok(SDK.HOST_INTEGRATION_AXES, 'negotiated schema (axes)'); + assert.ok(SDK.INTERFACE_POINTS, 'interface points'); + assert.ok(SDK.PROFILE_BASELINES, 'profile baselines'); + + const fns = [ + 'negotiateHostCapabilities', 'profileOf', 'degradationFor', + 'hookEventSurfaceFor', 'shouldFlattenDispatch', + 'createDeclarativeAdapter', 'createImperativeAdapter', + 'createModelAdapter', 'createHookBus', 'createStateIO', + 'buildHandshakeRequest', 'handleHandshakeRequest', + ]; + for (const fn of fns) { + assert.equal(typeof SDK[fn], 'function', `SDK must export function: ${fn}`); + } + assert.equal(typeof SDK.HANDSHAKE_METHOD, 'string'); +}); + +test('the SDK surface is frozen (immutable public contract)', () => { + assert.equal(Object.isFrozen(SDK), true); +}); + +test('an external author can build a host-plugin against the SDK surface ONLY', () => { + // Compose a minimal host-plugin using EXCLUSIVELY the SDK entry — mirrors what + // an external author writes. No require() of any internal path succeeds here. + const model = SDK.createModelAdapter({ modelMode: 'passive' }); + const bus = SDK.createHookBus({ bus: 'engine' }); + const io = SDK.createStateIO({ io: 'filesystem' }); + const adapter = SDK.createImperativeAdapter({ runtime: 'my-third-party-host' }); + + // Negotiate this host's capabilities via the SDK handshake. + const req = SDK.buildHandshakeRequest({ embeddingMode: 'imperative', runtime: 'node' }); + const result = SDK.handleHandshakeRequest(req); + const profile = SDK.profileOf(result.effective); + + assert.equal(model.mode, 'passive'); + assert.equal(bus.bus, 'engine'); + assert.equal(io.io, 'filesystem'); + assert.equal(adapter.kind, 'imperative'); + assert.equal(adapter.runtime, 'my-third-party-host'); + assert.equal(profile, 'programmatic-cli'); + assert.ok(result.effective && result.points, 'handshake yields a negotiated result'); +}); + +test('the SDK handshake round-trips for a declarative host authored externally', () => { + const req = SDK.buildHandshakeRequest({ embeddingMode: 'declarative', commandSurface: 'slash-file' }); + const result = SDK.handleHandshakeRequest(req); + assert.equal(SDK.profileOf(result.effective), 'declarative-cli'); +});