diff --git a/.changeset/daring-deer-leap.md b/.changeset/daring-deer-leap.md new file mode 100644 index 000000000..310fb39bd --- /dev/null +++ b/.changeset/daring-deer-leap.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1802 +--- +**Internal: the declarative embedding adapter is now named + bound behind a minimal `HostIntegrationInterface`** — `createDeclarativeAdapter({runtime})` (new `src/adapter-declarative.cts`) delegates in-process to `install-engine`'s `installRuntimeArtifacts`/`uninstallRuntimeArtifacts`, formalizing today's projection path as one of the two embedding adapters behind a common contract (ADR-1239 Phase C-1 / #1680 AC1). Output is byte-identical to today's install (gated by `golden-install-parity`). The full 6-point interface binding surface is deferred until the imperative adapter (AC2) fixes the shape (ADR-1239 open wire-shape question). No user-facing change — the adapter is not yet wired to any runtime path. + + diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 923fed8cb..bc5a8e5e3 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -279,6 +279,7 @@ ], "cli_modules": [ "active-workstream-store.cjs", + "adapter-declarative.cjs", "adr-parser.cjs", "agent-command-router.cjs", "agent-install-check.cjs", @@ -324,6 +325,7 @@ "edge-probe.cjs", "eval-command-router.cjs", "eval.cjs", + "embedding-adapter.cjs", "fallow-runner.cjs", "federated-config.cjs", "frontmatter.cjs", diff --git a/eslint.config.mjs b/eslint.config.mjs index 53334498c..e4ba00cc2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -200,6 +200,9 @@ export default tseslint.config( 'gsd-core/bin/lib/teams-status.cjs', // ADR-1372: tsc-generated runtime artifact — lint the src/markdown-sectionizer.cts source. 'gsd-core/bin/lib/markdown-sectionizer.cjs', + // ADR-1239 Phase C-1 (#1680): tsc-generated — lint src/embedding-adapter.cts + src/adapter-declarative.cts. + 'gsd-core/bin/lib/embedding-adapter.cjs', + 'gsd-core/bin/lib/adapter-declarative.cjs', ], }, diff --git a/src/adapter-declarative.cts b/src/adapter-declarative.cts new file mode 100644 index 000000000..40c540fd3 --- /dev/null +++ b/src/adapter-declarative.cts @@ -0,0 +1,42 @@ +/** + * Declarative embedding adapter (ADR-1239 Phase C-1, AC1 / #1680). + * + * NAMES + BOUNDS today's projection path (file emission via install-engine) + * behind `HostIntegrationInterface`. The declarative adapter is lossy by + * design: it projects skills/agents/commands and does NOT drive the loop + * orchestration (that is the imperative adapter's job, AC2 / a later slice). + * + * `install`/`uninstall` delegate IN-PROCESS to install-engine's + * `installRuntimeArtifacts` / `uninstallRuntimeArtifacts` — the SAME engine + * functions `bin/install.js` uses — so the adapter's output is byte-identical to + * today's install (the link is gated by `tests/golden-install-parity.test.cjs`). + * The module-ref call style (`installEngine.fn`) keeps it monkeypatch-friendly + * for tests, mirroring the install-engine.cts:31-38 pattern. + */ +'use strict'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +import installEngine = require('./install-engine.cjs'); +import type { HostIntegrationInterface, AdapterInstallIntent, AdapterUninstallIntent } from './embedding-adapter.cjs'; + +export function createDeclarativeAdapter({ runtime }: { runtime: string }): HostIntegrationInterface { + if (!runtime || typeof runtime !== 'string') { + throw new TypeError('createDeclarativeAdapter: runtime is required (non-empty string)'); + } + return Object.freeze({ + kind: 'declarative' as const, + runtime, + install(intent: AdapterInstallIntent): void { + installEngine.installRuntimeArtifacts( + runtime, + intent.configDir, + intent.scope, + intent.resolvedProfile, + intent.resolveAttribution, + ); + }, + uninstall(intent: AdapterUninstallIntent): void { + installEngine.uninstallRuntimeArtifacts(runtime, intent.configDir, intent.scope); + }, + }); +} diff --git a/src/embedding-adapter.cts b/src/embedding-adapter.cts new file mode 100644 index 000000000..31cf23313 --- /dev/null +++ b/src/embedding-adapter.cts @@ -0,0 +1,74 @@ +/** + * Embedding adapter contract — the common `HostIntegrationInterface` both + * embedding adapters satisfy (ADR-1239 Phase C-1, #1680). + * + * INTENTIONALLY MINIMAL (Phase 3 slice 1). The full six-interface-point binding + * surface (command / dispatch / model / hooks / state / artifact) is DEFERRED + * until the imperative adapter (AC2) provides a real consumer that fixes the + * shape — ADR-1239 lists the wire-shape as an open question: + * "Exact wire-shape of the initialize handshake … Where precisely to cut the + * engine↔host boundary" + * (docs/adr/1239-gsd-embeddable-orchestration-engine.md#open-questions-narrowed-by-the-research). + * Freezing a 6-point contract before the imperative adapter exists would risk + * rework across Phases 3-6. This slice ships only what the declarative adapter + * (AC1) needs: the kind discriminator + runtime + install/uninstall entry. + * + * Both adapters bind the SAME engine (install-engine.cjs / the loop resolver); + * they differ in HOW — declarative projects files (lossy: drops loop + * orchestration), imperative drives host primitives in-process. See ADR-1239 + * "How a capability reaches a host (two adapters, one engine)". + */ +'use strict'; + +// --------------------------------------------------------------------------- +// Kinds +// --------------------------------------------------------------------------- + +export const ADAPTER_KINDS = Object.freeze(['declarative', 'imperative'] as const); +export type AdapterKind = (typeof ADAPTER_KINDS)[number]; +export type Scope = 'global' | 'local'; +// --------------------------------------------------------------------------- +// Intent shapes (minimal — grow when the imperative adapter fixes the shape) +// --------------------------------------------------------------------------- + +/** + * Install intent accepted by a `HostIntegrationInterface.install`. + * + * `resolvedProfile` + `resolveAttribution` mirror `installRuntimeArtifacts` + * (install-engine.cjs) — the declarative adapter passes them straight through to + * the engine. The imperative adapter (future) will source them from the host. + */ +export interface AdapterInstallIntent { + configDir: string; + scope: Scope; + resolvedProfile: unknown; + resolveAttribution?: (runtime: string) => unknown; +} + +export interface AdapterUninstallIntent { + configDir: string; + scope: Scope; +} + +// --------------------------------------------------------------------------- +// The contract +// --------------------------------------------------------------------------- + +/** + * The minimal contract both embedding adapters satisfy. `kind` discriminates + * declarative (projection) from imperative (in-process engine drive). Both + * `install`/`uninstall` delegate to the shared engine surface; neither + * reimplements the loop. The byte-identity of the declarative adapter's output + * to today's install is gated by `tests/golden-install-parity.test.cjs` + * (both route through the same `installRuntimeArtifacts` engine function). + */ +export interface HostIntegrationInterface { + readonly kind: AdapterKind; + readonly runtime: string; + install(intent: AdapterInstallIntent): void; + uninstall(intent: AdapterUninstallIntent): void; +} + +// NOTE: `ADAPTER_KINDS` is exported above as a runtime const so this module +// compiles to a non-empty .cjs (the interfaces above are erased by tsc) and so +// adapter implementors can reference the frozen kind set at runtime. diff --git a/tests/adapter-declarative-equivalence.test.cjs b/tests/adapter-declarative-equivalence.test.cjs new file mode 100644 index 000000000..b7adbb580 --- /dev/null +++ b/tests/adapter-declarative-equivalence.test.cjs @@ -0,0 +1,102 @@ +'use strict'; +/** + * Equivalence test for the declarative embedding adapter (ADR-1239 Phase C-1, + * AC1 / #1680). + * + * The declarative adapter NAMES + BOUNDS today's projection path behind + * `HostIntegrationInterface`. This test pins the three load-bearing properties: + * + * 1. KIND — every runtime's adapter classifies as `kind: 'declarative'` and + * echoes its runtime id. + * 2. DELEGATION (the byte-identity link) — `install`/`uninstall` delegate + * IN-PROCESS to install-engine's `installRuntimeArtifacts` / + * `uninstallRuntimeArtifacts` with the EXACT args passed through. Because + * the adapter calls the SAME engine functions `bin/install.js` uses, its + * output is byte-identical to today's install — the 16-runtime byte + * identity is gated by `tests/golden-install-parity.test.cjs` (which + * exercises these engine functions end-to-end). Asserting the delegation + * link here proves the adapter never diverges from the reference path. + * 3. FAIL-CLOSED CONSTRUCTION — missing/invalid runtime throws (no silent + * default adapter). + * + * Behavioral tests only; delegation verified via module-ref monkeypatch (the + * install-engine.cts:31-38 stub-compatible pattern — Node module cache shares + * the one module object, so patching it here is seen by the adapter). + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { createDeclarativeAdapter } = require('../gsd-core/bin/lib/adapter-declarative.cjs'); +const installEngine = require('../gsd-core/bin/lib/install-engine.cjs'); +const registry = require('../gsd-core/bin/lib/capability-registry.cjs'); + +const RUNTIMES = Object.keys(registry.runtimes); + +test('declarative adapter: kind === "declarative" + runtime echoed, for all 16 runtimes', () => { + assert.ok(RUNTIMES.length >= 16, `expected ≥16 runtimes in registry, got ${RUNTIMES.length}`); + for (const r of RUNTIMES) { + const adapter = createDeclarativeAdapter({ runtime: r }); + assert.strictEqual(adapter.kind, 'declarative', `${r}: kind must be 'declarative'`); + assert.strictEqual(adapter.runtime, r, `${r}: adapter must echo the runtime id`); + assert.strictEqual(typeof adapter.install, 'function', `${r}: install must be a function`); + assert.strictEqual(typeof adapter.uninstall, 'function', `${r}: uninstall must be a function`); + } +}); + +test('declarative adapter.install delegates in-process to installRuntimeArtifacts with exact args (byte-identity link)', () => { + const original = installEngine.installRuntimeArtifacts; + try { + for (const r of RUNTIMES) { + const adapter = createDeclarativeAdapter({ runtime: r }); + let captured = null; + installEngine.installRuntimeArtifacts = function (...args) { captured = args; return undefined; }; + const resolveAttribution = () => 'attr-' + r; + const intent = { + configDir: '/tmp/adapter-eq/' + r, + scope: 'global', + resolvedProfile: { profile: r }, + resolveAttribution, + }; + assert.doesNotThrow(() => adapter.install(intent), `${r}: install threw`); + assert.ok(captured, `${r}: install did not delegate to installRuntimeArtifacts`); + assert.deepStrictEqual( + captured, + [r, '/tmp/adapter-eq/' + r, 'global', { profile: r }, resolveAttribution], + `${r}: install delegation args diverged from the engine signature`, + ); + } + } finally { + installEngine.installRuntimeArtifacts = original; + } +}); + +test('declarative adapter.uninstall delegates in-process to uninstallRuntimeArtifacts with exact args', () => { + const original = installEngine.uninstallRuntimeArtifacts; + try { + for (const r of RUNTIMES) { + const adapter = createDeclarativeAdapter({ runtime: r }); + let captured = null; + installEngine.uninstallRuntimeArtifacts = function (...args) { captured = args; return undefined; }; + assert.doesNotThrow(() => adapter.uninstall({ configDir: '/tmp/adapter-eq/' + r, scope: 'local' }), `${r}: uninstall threw`); + assert.ok(captured, `${r}: uninstall did not delegate to uninstallRuntimeArtifacts`); + assert.deepStrictEqual( + captured, + [r, '/tmp/adapter-eq/' + r, 'local'], + `${r}: uninstall delegation args diverged from the engine signature`, + ); + } + } finally { + installEngine.uninstallRuntimeArtifacts = original; + } +}); + +test('createDeclarativeAdapter: throws on missing/invalid runtime (fail-closed construction)', () => { + for (const bad of ['', undefined, null]) { + assert.throws( + () => createDeclarativeAdapter({ runtime: bad }), + TypeError, + `runtime=${JSON.stringify(bad)} must throw TypeError`, + ); + } + assert.throws(() => createDeclarativeAdapter({}), TypeError, 'missing runtime must throw TypeError'); +});