Resync hq ADR references (0044-0054 -> 0039-0049) #3

Merged
jschoubben merged 1 commits from feat/adr-ref-resync into main 2026-09-05 10:44:38 +00:00
8 changed files with 22 additions and 22 deletions
+5 -5
View File
@@ -3,7 +3,7 @@
The stable spine a Novox Mesh **module's own code builds against** — the tool it uses to plug
into the mesh, and nothing that belongs to a specific module or to another tier.
It earns its place by **rarely changing** (novox/hq [ADR 0044](https://git.novox.be/novox/hq)).
It earns its place by **rarely changing** (novox/hq [ADR 0039](https://git.novox.be/novox/hq)).
The test for anything here: *if editing it recompiles unrelated modules and it changes often, it
does not belong.* Per-module code (a service's API client, its tool implementations, its
create-a-resource adapter) lives **in the module**, never here — that coupling is exactly what
@@ -25,7 +25,7 @@ The loop is identical for postgres, redis, minio and umami: watch the grants dir
requested grant call the provider's adapter, write the sealed credential, handle withdrawal, and
keep converging (`watch`). That loop lives here. A **module provides only the adapter** — "create
a database", "create a umami site" — which is the volatile, per-service half and belongs in the
module. This is the ADR 0044 line drawn through provisioning: stable loop here, per-service create
module. This is the ADR 0039 line drawn through provisioning: stable loop here, per-service create
in the module.
### `tools` — the tool-serving harness
@@ -45,7 +45,7 @@ own resolved environment.
## What is NOT in it, and where it goes
- **A module's API client and its tool implementations** → in the module. (The Plex client, the
Umami client, `tools/plex.ts` — all module-local. ADR 0044.)
Umami client, `tools/plex.ts` — all module-local. ADR 0039.)
- **Delivery machinery** — build executor, bundler, dependency resolver, artifact manager, feature
handlers → **mesh-control**. It co-evolves with the pipeline.
- **Host synchronisers** — config-sync, ufw config, vhost generation, systemd, health checks →
@@ -83,5 +83,5 @@ the harness's.
Design and decisions are in [`novox/hq`](https://git.novox.be/novox/hq):
- `02-DECISIONS/0044-what-the-sdk-holds-and-refuses.md` — the stability rule this repository is
- `02-DECISIONS/0045-what-a-module-is.md` — a module, and why its per-module code lives in the module
- `02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md` — the stability rule this repository is
- `02-DECISIONS/0040-what-a-module-is.md` — a module, and why its per-module code lives in the module
+4 -4
View File
@@ -1,7 +1,7 @@
// The runtime shapes a module's own code touches — NOT the manifest schema, which the control
// plane owns and parses (in Go). These are what a running module receives and returns: a grant
// and its credentials, the mesh interface a provider and consumer both conform to, and the tool
// and event types. They change rarely and deliberately (novox/hq ADR 0044).
// and event types. They change rarely and deliberately (novox/hq ADR 0039).
/** A request for one instance of a provided resource, addressed to a provider. */
export interface Grant {
@@ -21,7 +21,7 @@ export interface Credential {
}
/**
* A mesh interface: the provider-neutral contract for a capability (novox/hq ADR 0045). Both a
* A mesh interface: the provider-neutral contract for a capability (novox/hq ADR 0040). Both a
* provider (which adapts its software to it) and a consumer (which depends on it, never on a
* provider) conform. `spec` names what a consumer may contribute; `credential` names what it
* receives. The interface is drawn at the consumer's real coupling: neutral where thin
@@ -45,7 +45,7 @@ export interface ToolDefinition {
}
/**
* The metadata that rides an event as AMQP headers (novox/hq ADR 0047). An event's identity and
* The metadata that rides an event as AMQP headers (novox/hq ADR 0042). An event's identity and
* provenance live here, not in the body, so a consumer — or the broker, or an audit tool — reads
* who/when/what without parsing the payload. An unknown `x-` header is ignored, not refused: an
* event is observed by parties that need not all understand every header.
@@ -70,7 +70,7 @@ export interface EventHeaders {
/**
* A message crossing the broker: a routing key and a JSON body, per-node addressed. For an event,
* `headers` carries the ADR 0047 metadata; plain request/reply transport leaves it absent.
* `headers` carries the ADR 0042 metadata; plain request/reply transport leaves it absent.
*/
export interface Envelope<T = unknown> {
readonly key: string;
+3 -3
View File
@@ -4,7 +4,7 @@
// credential — just the broker's topic routing). Declared as emits/consumes on the manifest so the
// mesh knows the event graph.
//
// Identity and provenance ride as AMQP headers, not in the body (novox/hq ADR 0047): a consumer —
// Identity and provenance ride as AMQP headers, not in the body (novox/hq ADR 0042): a consumer —
// or the broker, or an audit tool — reads who/when/what without parsing the payload, and the body
// is only the domain payload. This surface hides the header/exchange/queue mechanics; a module
// names a type and a body and never sees the wire.
@@ -44,7 +44,7 @@ export interface EmitOptions {
/**
* Emit an event. Source and node come from the environment the runtime set for the module
* (MESH_MODULE, MESH_NODE), so a module names only the type and the body; the sdk stamps the
* ADR 0047 headers (id, source, node, time) and the runtime rides them on the broker.
* ADR 0042 headers (id, source, node, time) and the runtime rides them on the broker.
*/
export async function emit<T>(type: string, body: T, opts: EmitOptions = {}): Promise<void> {
const source = process.env.MESH_MODULE ?? "unknown";
@@ -72,7 +72,7 @@ export async function on<T>(pattern: string, handler: (event: Event<T>) => Promi
});
}
/** Rebuild the Event a handler sees from a broker envelope's headers (ADR 0047) and body. */
/** Rebuild the Event a handler sees from a broker envelope's headers (ADR 0042) and body. */
function fromEnvelope<T>(env: Envelope<T>): Event<T> {
const h = env.headers ?? ({} as EventHeaders);
return {
+1 -1
View File
@@ -1,4 +1,4 @@
// @novox/mesh-sdk — the stable spine a module's own code builds against (novox/hq ADR 0044).
// @novox/mesh-sdk — the stable spine a module's own code builds against (novox/hq ADR 0039).
//
// Import the area you need directly (`@novox/mesh-sdk/tools`, `/provisioner`, …) or the whole
// surface from here. Everything specific to one module — its API client, its tool implementations,
+1 -1
View File
@@ -3,7 +3,7 @@
//
// There was a symmetric seal()/unseal() here, for a provider to seal a credential to a key before
// writing it. It is gone: a provider is handed the credential the mesh minted and seals nothing
// (novox/hq ADR 0053), the mesh's own secret delivery is asymmetric and belongs to the host, and
// (novox/hq ADR 0048), the mesh's own secret delivery is asymmetric and belongs to the host, and
// nothing else called it. The primitive left with the provisioner that was its only caller.
// --- semver ---
+2 -2
View File
@@ -2,9 +2,9 @@
// minio and umami: read the contributions the mesh delivered, bring each consumer's resource into
// being through the provider's adapter under the login and password the mesh minted, and withdraw
// what the mesh no longer asks for. A module writes ONLY the adapter — the per-service half — which
// is why this lives in the sdk and the adapter lives in the module (novox/hq ADR 0044).
// is why this lives in the sdk and the adapter lives in the module (novox/hq ADR 0039).
//
// **A provider creates the credential the mesh minted, and seals nothing (novox/hq ADR 0053).** The
// **A provider creates the credential the mesh minted, and seals nothing (novox/hq ADR 0048).** The
// control plane mints one password per consumer and seals it to this node; the host unseals it into
// the file the contribution names. The provider does not generate a password, does not seal one, and
// does not hand one back — the consumer already receives its copy through the mesh's own channel.
+3 -3
View File
@@ -1,7 +1,7 @@
// The tool-serving harness. A module declares its tools through registerModuleTools; a runtime
// (the mesh's per-node tool host) collects the registrations and serves them through the command
// surface. HOW a tool is declared and served is settled and lives here; the tools themselves, and
// the API client they call, live in the module (novox/hq ADR 0044).
// the API client they call, live in the module (novox/hq ADR 0039).
import type { ToolDefinition } from "../contracts/index.js";
import type { Broker } from "../messaging/index.js";
@@ -66,7 +66,7 @@ export function listTools(env: NodeJS.ProcessEnv = process.env): { module: strin
* other — two tools answering one name is a fault, not a race to resolve.
*/
export async function serveTools(broker: Broker, env: NodeJS.ProcessEnv = process.env): Promise<() => void> {
// Each tool is served on its own key, namespaced by its module (novox/hq ADR 0052): a caller
// Each tool is served on its own key, namespaced by its module (novox/hq ADR 0047): a caller
// invokes `<module>.<tool>`, only the module that serves it answers, and the module's account is
// scoped to serve.<module>.* — so one module cannot answer another's calls. The module is the
// namespace, so a tool name need only be unique within its module, not across the whole mesh.
@@ -90,7 +90,7 @@ export async function serveTools(broker: Broker, env: NodeJS.ProcessEnv = proces
};
}
/** The broker key a tool is served on and invoked by — the module namespaces the tool (ADR 0052). */
/** The broker key a tool is served on and invoked by — the module namespaces the tool (ADR 0047). */
export function toolKey(module: string, tool: string): string {
return `${module}.${tool}`;
}
+3 -3
View File
@@ -107,7 +107,7 @@ test("modules can SERVE: a real async tool, loaded and invoked over the broker",
const stop = await serveTools(broker, {});
// Invoke it the way a caller (mesh-control's command API) would — over the broker, by module and
// tool, each served on its own key `demo.create_site` (novox/hq ADR 0052).
// tool, each served on its own key `demo.create_site` (novox/hq ADR 0047).
const result = (await invokeTool(broker, "demo", "create_site", { domain: "my-app" })) as { snippet: string };
assert.match(result.snippet, /data-website-id="site-123"/);
@@ -120,7 +120,7 @@ test("modules can SERVE: a real async tool, loaded and invoked over the broker",
});
test("serving refuses one module exposing two tools of the same name", async () => {
// Two *modules* may share a tool name — each is served on its own `module.tool` key (ADR 0052).
// Two *modules* may share a tool name — each is served on its own `module.tool` key (ADR 0047).
// What is refused is one module exposing the same name twice, where the key would collide.
resetTools();
registerModuleTools("a", () => [
@@ -197,7 +197,7 @@ test("events: a module emits, a listener and the audit sink (#) both receive it,
assert.deepEqual(heard.map((e) => e.type), ["module.umami.site.created"]);
assert.deepEqual(audited.map((e) => e.type), ["module.umami.site.created", "module.plex.play.started"]);
// The metadata an audit trail needs is present — read back from the ADR 0047 headers, not the body.
// The metadata an audit trail needs is present — read back from the ADR 0042 headers, not the body.
const e = heard[0];
assert.equal(e.source, "umami");
assert.equal(e.node, "anchor");