From c642ed0ec5c852535eaab0498c93ab7eb9143479 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 28 Jun 2026 02:12:06 -0400 Subject: [PATCH] =?UTF-8?q?feat(#1680):=20ADR-1239=20Phase=20C-1=20?= =?UTF-8?q?=E2=80=94=20declarative=20embedding=20adapter=20+=20minimal=20H?= =?UTF-8?q?ostIntegrationInterface=20[AC1]=20(#1802)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(#1680): ADR-1239 Phase C-1 — declarative embedding adapter + minimal HostIntegrationInterface [AC1] Phase 3 slice 1 (AC1). Names + bounds today's projection path behind the common HostIntegrationInterface that both declarative + imperative adapters will satisfy. - src/embedding-adapter.cts: minimal HostIntegrationInterface (kind + runtime + install/uninstall) + ADAPTER_KINDS. The full 6-point binding surface (command/dispatch/model/hooks/state/artifact) is DEFERRED until the imperative adapter (AC2) fixes the shape — ADR-1239 lists the wire-shape as an open question; freezing it now risks rework across Phases 3-6. - src/adapter-declarative.cts: createDeclarativeAdapter({runtime}) factory. Delegates in-process to install-engine installRuntimeArtifacts / uninstallRuntimeArtifacts (the SAME engine functions bin/install.js uses), so output is byte-identical to today's install (gated by golden-install-parity). Module-ref call style = monkeypatch-friendly for tests. Lossy by design: projects files, does not drive the loop (that's the imperative adapter, AC2). - tests/adapter-declarative-equivalence.test.cjs: kind classification (all 16 runtimes), install/uninstall delegation with exact args (the byte-identity link), fail-closed construction (missing/invalid runtime throws). Purely additive — no install.js/install-engine changes. Unblocks AC2 (imperative adapter) + AC3/AC4 (model/hook/state seams) as follow-up slices. * chore(changeset): add Changed fragment for declarative embedding adapter (#1680) * fix(lint): ignore tsc-emitted embedding-adapter/adapter-declarative .cjs (ADR-457) The new src/embedding-adapter.cts + src/adapter-declarative.cts modules' emitted gsd-core/bin/lib/*.cjs artifacts must join the ADR-457 ignores list (lint the src/*.cts source, not the emitted .cjs). Without this, the .cjs is linted under js.recommended where @typescript-eslint/no-require-imports is undefined, so the verbatim-copied eslint-disable directive surfaces as 'Definition for rule not found' — failing the lint-tests CI job. Also restores the clean line-level disable in adapter-declarative.cts (valid in the .cts source context where the rule IS defined). * fix(docs): add adapter-declarative + embedding-adapter to INVENTORY-MANIFEST cli_modules The new ADR-1239 Phase C-1 modules' built .cjs artifacts must be registered in docs/INVENTORY-MANIFEST.json's cli_modules array or the 'docs/INVENTORY-MANIFEST.json matches the filesystem' drift test fails on CI. Mirrors the existing sorted entries. --- .changeset/daring-deer-leap.md | 7 ++ docs/INVENTORY-MANIFEST.json | 2 + eslint.config.mjs | 3 + src/adapter-declarative.cts | 42 ++++++++ src/embedding-adapter.cts | 74 +++++++++++++ .../adapter-declarative-equivalence.test.cjs | 102 ++++++++++++++++++ 6 files changed, 230 insertions(+) create mode 100644 .changeset/daring-deer-leap.md create mode 100644 src/adapter-declarative.cts create mode 100644 src/embedding-adapter.cts create mode 100644 tests/adapter-declarative-equivalence.test.cjs 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'); +});