fix(conformance): withCapture propagates inner brands + chain test + docstring fixes

This commit is contained in:
2026-05-12 23:00:50 +02:00
parent 046bd0829e
commit 83f135b5e1
4 changed files with 97 additions and 7 deletions

View File

@@ -17,9 +17,11 @@ type Brand = "__instrumented" | "__captured" | "__audited";
/**
* Attaches the brand as a non-enumerable property on the given function.
* Returns the same reference (no allocation). Idempotent: calling twice with
* the same brand throws because the property is non-configurable — wrappers
* never double-wrap, so this is the correct safety net.
* Returns the same reference (no allocation). Calling twice with the same
* brand on the same fn is a no-op (matching descriptors) — the engine silently
* accepts a redundant `defineProperty` call when every descriptor attribute
* matches the existing one. Redefining with different attributes (e.g. flipping
* `configurable`) would throw, but wrappers never do that.
*/
export function attachBrand<F extends object>(fn: F, brand: Brand): F {
Object.defineProperty(fn, brand, {

View File

@@ -1,9 +1,11 @@
/**
* Phantom-type brands attached at wrap time by `withSpan`, `withCapture`,
* and `withAudit`. Pure type-level — no runtime cost, no proxy, no
* `Object.assign`. The conformance system uses these as the type-level
* seam the binding signature checks; a use-case factory that hasn't been
* wrapped is not assignable to a `ProductionUseCase<...>` slot.
* and `withAudit`. The brand has a type-level form (this file) and a
* non-enumerable runtime counterpart (see `./brand-runtime.ts`). The runtime
* cost is one `Object.defineProperty` call per wrap. The conformance system
* uses these as the type-level seam the binding signature checks; a use-case
* factory that hasn't been wrapped is not assignable to a
* `ProductionUseCase<...>` slot.
*/
export type Instrumented<F> = F & { readonly __instrumented: true };
export type Captured<F> = F & { readonly __captured: true };

View File

@@ -28,6 +28,8 @@ export function withCapture<Args extends unknown[], R>(
tags: Record<string, string>,
fn: (...args: Args) => Promise<R>,
): Captured<(...args: Args) => Promise<R>> {
const PROPAGATED_BRANDS = ["__instrumented", "__audited"] as const;
const wrapped: (...args: Args) => Promise<R> = async (...args) => {
try {
return await fn(...args);
@@ -39,6 +41,14 @@ export function withCapture<Args extends unknown[], R>(
throw err;
}
};
// Propagate brands from the inner function (e.g. __audited from withAudit,
// __instrumented if already spanned) so the outermost binding carries all brands.
// __captured is omitted here because it is attached explicitly below.
for (const brand of PROPAGATED_BRANDS) {
if ((fn as unknown as Record<string, unknown>)[brand] === true) {
attachBrand(wrapped, brand);
}
}
attachBrand(wrapped, "__captured");
return wrapped as Captured<(...args: Args) => Promise<R>>;
}