ADR 0022 — Stable (service, action) identity on MethodDef
Context
Per-endpoint audit / observability keys a row on (service, action) —
service = the contract prefix, action = the endpoint key (e.g.
updatePartial). A consuming project doing exactly this could not obtain the
pair from stitchkit:
MethodDefcarried no identity. It hasmethod/path/toolName, but no service name and no endpoint key.implement()knows both (contract.meta.prefix+ the loop key) and dropped them.- Neither is derivable downstream.
path(e.g./tenants/:id/widgets/...) yields the prefix at best but never the action — the endpoint key is not a path segment.toolNameis absent on HTTP-only endpoints. - The lifecycle hooks already receive
MethodDef.beforeHandle/afterHandle/onErrortake(ctx, …, endpoint: MethodDef).afterHandleadditionally has the handlerresult— so it is the natural home for a rich mutation audit (input fromctx, output fromresult). The only missing piece there was the identity.
Decision
Add stable, first-class identity to MethodDef, populated by implement() and
implementRemote():
serviceName: string; // = contract prefix (the "service")
key: string; // = endpoint key (the "action")
- Required, not optional. Every
MethodDefis built byimplement/implementRemote, both of which always have both values — so optionality would be a false nullability forcing every reader to?? ''(a fallback this project forbids — Fail First). The two existing in-repoMethodDeftest literals were updated; there are no other construction sites. - This is not a domain model.
serviceName/keyare structural framework facts — the same category aspath,method,scope— that the framework already computes. Exposing them is “stop discarding identity we own,” not “model the domain” (ADR 0002 holds). - A consumer reads them in a hook (cast-free):
endpoint.serviceName,endpoint.key.
Alternatives considered
- Derive from
path/toolName. Rejected:actionis not in the path, andtoolNameis absent on HTTP-only endpoints — the pair cannot be recovered. - Stamp into
meta(the opaque bag from ADR 0021) and read it in a hook. Rejected:metais for facts the core does not model; (service, action) is structural identity the core does know — pushing it throughmetare-creates exactly the brittle side-channel ADR 0021 set out to retire. - Enrich
createAuditHook’sRequestEventwith (service, action) (stamp into the ALS request-context at dispatch). Deferred:createAuditHook’s HTTP path is a fetch wrapper decoupled from the dispatcher (ADR 0012) and records no response body — so a consumer auditingoutputis forced ontoafterHandleanyway, where this ADR’sMethodDefidentity already lands. Revisit only if a real consumer needs (service, action) on thecreateAuditHookpath without the handler output.
Consequences
- Any hook with a
MethodDef(and tool mounts viaMountableTool.method) can key audit / metrics on(serviceName, key)with noas, nometastamp, no path parsing. - Additive in spirit, but the fields are required — a pre-1.0 change with no
back-compat shim (consistent with the “no compatibility layer” rule). Only
internal
MethodDefliterals (two tests) needed updating. implementRemotenow also carriesmeta(it was dropping it) — same line.- The core still models no domain; it stops throwing away identity it already holds.