feat(#1680): ADR-1239 Phase C-1 — declarative embedding adapter + minimal HostIntegrationInterface [AC1] (#1802)

* 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.
This commit is contained in:
Tom Boucher
2026-06-28 02:12:06 -04:00
committed by GitHub
parent 4810bfe4df
commit c642ed0ec5
6 changed files with 230 additions and 0 deletions

View File

@@ -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.
<!-- docs-exempt: internal adapter infrastructure; no user-facing command/flag/config/schema/doc surface -->

View File

@@ -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",

View File

@@ -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',
],
},

View File

@@ -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);
},
});
}

74
src/embedding-adapter.cts Normal file
View File

@@ -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.

View File

@@ -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');
});