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:
7
.changeset/daring-deer-leap.md
Normal file
7
.changeset/daring-deer-leap.md
Normal 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 -->
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
42
src/adapter-declarative.cts
Normal file
42
src/adapter-declarative.cts
Normal 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
74
src/embedding-adapter.cts
Normal 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.
|
||||
102
tests/adapter-declarative-equivalence.test.cjs
Normal file
102
tests/adapter-declarative-equivalence.test.cjs
Normal 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');
|
||||
});
|
||||
Reference in New Issue
Block a user