@fougere/core 0.16.8-alpha.0 → 0.17.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +1 -1
  2. package/dist/CallIdentity.js +2 -2
  3. package/dist/CallIdentity.js.map +1 -1
  4. package/dist/Declared.d.ts +2 -0
  5. package/dist/Declared.d.ts.map +1 -1
  6. package/dist/FrondDeclaration.d.ts.map +1 -1
  7. package/dist/FrondDeclaration.js +1 -0
  8. package/dist/FrondDeclaration.js.map +1 -1
  9. package/dist/boot/App.d.ts +4 -4
  10. package/dist/boot/App.d.ts.map +1 -1
  11. package/dist/boot/AppLifecycle.d.ts +2 -2
  12. package/dist/boot/AppLifecycle.js +3 -3
  13. package/dist/boot/AppLifecycle.js.map +1 -1
  14. package/dist/boot/bootstrap.d.ts.map +1 -1
  15. package/dist/boot/bootstrap.js +27 -28
  16. package/dist/boot/bootstrap.js.map +1 -1
  17. package/dist/boot/card.d.ts.map +1 -1
  18. package/dist/boot/card.js +30 -29
  19. package/dist/boot/card.js.map +1 -1
  20. package/dist/boot/install.d.ts +1 -1
  21. package/dist/boot/install.d.ts.map +1 -1
  22. package/dist/boot/install.js +15 -7
  23. package/dist/boot/install.js.map +1 -1
  24. package/dist/boot/peerOver.js +2 -2
  25. package/dist/boot/peerOver.js.map +1 -1
  26. package/dist/boot/ports.d.ts +2 -0
  27. package/dist/boot/ports.d.ts.map +1 -1
  28. package/dist/boot/ports.js +2 -0
  29. package/dist/boot/ports.js.map +1 -1
  30. package/dist/boot/remote.d.ts +7 -5
  31. package/dist/boot/remote.d.ts.map +1 -1
  32. package/dist/boot/remote.js +42 -29
  33. package/dist/boot/remote.js.map +1 -1
  34. package/dist/contract.d.ts +2 -2
  35. package/dist/contract.d.ts.map +1 -1
  36. package/dist/contract.js +3 -3
  37. package/dist/contract.js.map +1 -1
  38. package/dist/descriptor/ProviderEntry.d.ts +5 -0
  39. package/dist/descriptor/ProviderEntry.d.ts.map +1 -1
  40. package/dist/descriptor/bases.d.ts +7 -1
  41. package/dist/descriptor/bases.d.ts.map +1 -1
  42. package/dist/descriptor/bases.js +21 -5
  43. package/dist/descriptor/bases.js.map +1 -1
  44. package/dist/dispatch/ArgumentResolver.d.ts +7 -0
  45. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
  46. package/dist/dispatch/ArgumentResolver.js +45 -12
  47. package/dist/dispatch/ArgumentResolver.js.map +1 -1
  48. package/dist/dispatch/Dispatcher.js +1 -1
  49. package/dist/dispatch/Dispatcher.js.map +1 -1
  50. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  51. package/dist/dispatch/HandlerFacade.js +9 -8
  52. package/dist/dispatch/HandlerFacade.js.map +1 -1
  53. package/dist/dispatch/InFlight.d.ts +1 -1
  54. package/dist/dispatch/InFlight.d.ts.map +1 -1
  55. package/dist/dispatch/InFlight.js +2 -2
  56. package/dist/dispatch/InFlight.js.map +1 -1
  57. package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
  58. package/dist/dispatch/LocalRoutePolicy.js +7 -7
  59. package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
  60. package/dist/dispatch/PresenterExecutor.d.ts +1 -3
  61. package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
  62. package/dist/dispatch/PresenterExecutor.js +1 -7
  63. package/dist/dispatch/PresenterExecutor.js.map +1 -1
  64. package/dist/dispatch/Release.js +0 -2
  65. package/dist/dispatch/Release.js.map +1 -1
  66. package/dist/dispatch/RouteRegistry.d.ts +1 -1
  67. package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
  68. package/dist/dispatch/RouteRegistry.js +3 -3
  69. package/dist/dispatch/RouteRegistry.js.map +1 -1
  70. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  71. package/dist/dispatch/StorageGuard.js +3 -9
  72. package/dist/dispatch/StorageGuard.js.map +1 -1
  73. package/dist/dispatch/remoteRoutes.d.ts +1 -1
  74. package/dist/dispatch/remoteRoutes.d.ts.map +1 -1
  75. package/dist/dispatch/remoteRoutes.js +1 -1
  76. package/dist/dispatch/remoteRoutes.js.map +1 -1
  77. package/dist/dispatch/routeNotFound.d.ts.map +1 -1
  78. package/dist/dispatch/routeNotFound.js +6 -5
  79. package/dist/dispatch/routeNotFound.js.map +1 -1
  80. package/dist/dispatch/validateInput.d.ts +1 -1
  81. package/dist/dispatch/validateInput.d.ts.map +1 -1
  82. package/dist/dispatch/validateInput.js +1 -3
  83. package/dist/dispatch/validateInput.js.map +1 -1
  84. package/dist/entry/facade.d.ts +1 -1
  85. package/dist/entry/facade.d.ts.map +1 -1
  86. package/dist/entry/facade.js +2 -2
  87. package/dist/entry/facade.js.map +1 -1
  88. package/dist/entry/transport.js +1 -1
  89. package/dist/entry/transport.js.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/wire/CallRecord.d.ts +1 -1
  95. package/dist/wire/CallRecord.d.ts.map +1 -1
  96. package/dist/wire/FougereError.d.ts +8 -3
  97. package/dist/wire/FougereError.d.ts.map +1 -1
  98. package/dist/wire/FougereError.js +13 -4
  99. package/dist/wire/FougereError.js.map +1 -1
  100. package/dist/wire/FougereErrorOptions.d.ts +1 -1
  101. package/dist/wire/FougereErrorOptions.d.ts.map +1 -1
  102. package/dist/wire/FrondCall.d.ts +2 -2
  103. package/dist/wire/FrondCall.d.ts.map +1 -1
  104. package/dist/wire/OperationContext.d.ts +2 -2
  105. package/dist/wire/OperationContext.d.ts.map +1 -1
  106. package/dist/wire/RouteAddress.d.ts +1 -1
  107. package/dist/wire/RouteAddress.d.ts.map +1 -1
  108. package/dist/wire/RouteAddress.js +5 -5
  109. package/dist/wire/RouteAddress.js.map +1 -1
  110. package/dist/wire/RouteAddressInput.d.ts +1 -1
  111. package/dist/wire/RouteAddressInput.d.ts.map +1 -1
  112. package/dist/wire/RpcAnswer.d.ts +1 -1
  113. package/dist/wire/RpcAnswer.d.ts.map +1 -1
  114. package/dist/wire/RpcAnswer.js +1 -1
  115. package/dist/wire/RpcAnswer.js.map +1 -1
  116. package/dist/wire/SignedCall.d.ts +1 -1
  117. package/dist/wire/SignedCall.d.ts.map +1 -1
  118. package/dist/wire/StateShape.d.ts +1 -1
  119. package/dist/wire/StateShape.d.ts.map +1 -1
  120. package/dist/wire/StateShape.js +2 -2
  121. package/dist/wire/StateShape.js.map +1 -1
  122. package/dist/wire/card/IdentityCard.d.ts +10 -3
  123. package/dist/wire/card/IdentityCard.d.ts.map +1 -1
  124. package/dist/wire/card/IdentityCard.js +3 -1
  125. package/dist/wire/card/IdentityCard.js.map +1 -1
  126. package/dist/wire/drift.d.ts +2 -1
  127. package/dist/wire/drift.d.ts.map +1 -1
  128. package/dist/wire/drift.js +19 -16
  129. package/dist/wire/drift.js.map +1 -1
  130. package/dist/wire/http-error.js +2 -2
  131. package/dist/wire/http-error.js.map +1 -1
  132. package/package.json +3 -3
  133. package/src/CallIdentity.ts +2 -2
  134. package/src/Declared.ts +2 -0
  135. package/src/FrondDeclaration.ts +1 -0
  136. package/src/boot/App.ts +4 -4
  137. package/src/boot/AppLifecycle.ts +3 -3
  138. package/src/boot/bootstrap.ts +29 -30
  139. package/src/boot/card.ts +38 -29
  140. package/src/boot/install.ts +14 -8
  141. package/src/boot/peerOver.ts +2 -2
  142. package/src/boot/ports.ts +3 -0
  143. package/src/boot/remote.ts +52 -36
  144. package/src/contract.ts +3 -3
  145. package/src/descriptor/ProviderEntry.ts +5 -0
  146. package/src/descriptor/bases.ts +21 -5
  147. package/src/dispatch/ArgumentResolver.ts +49 -10
  148. package/src/dispatch/Dispatcher.ts +1 -1
  149. package/src/dispatch/HandlerFacade.ts +9 -10
  150. package/src/dispatch/InFlight.ts +2 -2
  151. package/src/dispatch/LocalRoutePolicy.ts +7 -7
  152. package/src/dispatch/PresenterExecutor.ts +0 -4
  153. package/src/dispatch/Release.ts +0 -2
  154. package/src/dispatch/RouteRegistry.ts +3 -3
  155. package/src/dispatch/StorageGuard.ts +3 -9
  156. package/src/dispatch/remoteRoutes.ts +2 -2
  157. package/src/dispatch/routeNotFound.ts +6 -5
  158. package/src/dispatch/validateInput.ts +0 -4
  159. package/src/entry/facade.ts +2 -2
  160. package/src/entry/transport.ts +1 -1
  161. package/src/index.ts +1 -1
  162. package/src/wire/CallRecord.ts +1 -1
  163. package/src/wire/FougereError.ts +16 -5
  164. package/src/wire/FougereErrorOptions.ts +1 -1
  165. package/src/wire/FrondCall.ts +2 -2
  166. package/src/wire/OperationContext.ts +2 -2
  167. package/src/wire/RouteAddress.ts +5 -5
  168. package/src/wire/RouteAddressInput.ts +1 -1
  169. package/src/wire/RpcAnswer.ts +1 -1
  170. package/src/wire/SignedCall.ts +1 -1
  171. package/src/wire/StateShape.ts +2 -2
  172. package/src/wire/card/IdentityCard.ts +9 -7
  173. package/src/wire/drift.ts +22 -27
  174. package/src/wire/http-error.ts +2 -2
@@ -7,7 +7,7 @@ import type { Transport } from '../wire/Transport.js';
7
7
  export function createTransportEntry(dispatcher: DispatchPort, surface?: string): Transport {
8
8
  return (legacy, invocation) => dispatcher.dispatch(new Call(
9
9
  new RouteAddress({
10
- entity: legacy.entity,
10
+ address: legacy.address,
11
11
  operation: legacy.op,
12
12
  ...(surface !== undefined ? { surface } : {}),
13
13
  }),
package/src/index.ts CHANGED
@@ -62,7 +62,7 @@ export { FougereError, ErrorCode, validationErrorsOf } from './contract.js';
62
62
  export type { AppMiddleware } from './wire/AppMiddleware.js';
63
63
  export type { AppNext } from './wire/AppNext.js';
64
64
  export type { OperationContext } from './wire/OperationContext.js';
65
- export { assertIdentityCard, RPC_ENTITY, ENVELOPE_BYTES, maxBodyBytes, maxFrameBytes, setMaxBodyBytes } from './contract.js';
65
+ export { assertIdentityCard, RPC_ADDRESS, ENVELOPE_BYTES, maxBodyBytes, maxFrameBytes, setMaxBodyBytes } from './contract.js';
66
66
  export { createLocalRunner, createAppRunner } from './boot/runner.js';
67
67
 
68
68
  // A frond an app STATES, for a boot that will not scan. No disk, no `typescript` — which
@@ -5,7 +5,7 @@ export interface CallRecord {
5
5
  /** Monotonic, and the whole cursor protocol: a reader asks for what is above its own. */
6
6
  seq: number;
7
7
  frond?: string;
8
- entity: string;
8
+ address: string;
9
9
  operation: string;
10
10
  surface?: string;
11
11
  /** Known at `resolved`, so absent on a call that never found a route. */
@@ -14,24 +14,35 @@ import type { FougereErrorOptions } from './FougereErrorOptions.js';
14
14
  */
15
15
  export class FougereError<Code extends ErrorCode = ErrorCode> extends Error {
16
16
  readonly code: Code;
17
- readonly entity?: string;
18
- readonly operation?: string;
17
+ address?: string;
18
+ operation?: string;
19
19
  readonly details?: unknown;
20
20
 
21
21
  constructor(options: FougereErrorOptions<Code>) {
22
22
  super(options.message, { cause: options.cause });
23
23
  this.name = 'FougereError';
24
24
  this.code = options.code;
25
- this.entity = options.entity;
25
+ this.address = options.address;
26
26
  this.operation = options.operation;
27
27
  this.details = options.details;
28
28
  }
29
29
 
30
+ /**
31
+ * Where the call was refused. The facade a refusal crosses first is where it happened, so a
32
+ * facade further out leaves what a deeper one wrote.
33
+ */
34
+ at(address: string, operation: string): this {
35
+ this.address ??= address;
36
+ this.operation ??= operation;
37
+
38
+ return this;
39
+ }
40
+
30
41
  toJSON() {
31
42
  return {
32
43
  code: this.code,
33
44
  message: this.message,
34
- ...(this.entity && { entity: this.entity }),
45
+ ...(this.address && { address: this.address }),
35
46
  ...(this.operation && { operation: this.operation }),
36
47
  ...(this.details !== undefined && { details: this.details }),
37
48
  };
@@ -45,7 +56,7 @@ export class FougereError<Code extends ErrorCode = ErrorCode> extends Error {
45
56
  return new FougereError({
46
57
  code: known ? (raw.code as ErrorCode) : ErrorCode.INTERNAL_ERROR,
47
58
  message: typeof raw.message === 'string' ? raw.message : 'Unknown error',
48
- entity: typeof raw.entity === 'string' ? raw.entity : undefined,
59
+ address: typeof raw.address === 'string' ? raw.address : undefined,
49
60
  operation: typeof raw.operation === 'string' ? raw.operation : undefined,
50
61
  details: known ? raw.details : { originalCode: raw.code, details: raw.details },
51
62
  });
@@ -3,7 +3,7 @@ import type { ErrorCode } from './ErrorCode.js';
3
3
  export interface FougereErrorOptions<Code extends ErrorCode = ErrorCode> {
4
4
  code: Code;
5
5
  message: string;
6
- entity?: string;
6
+ address?: string;
7
7
  operation?: string;
8
8
  details?: unknown;
9
9
  cause?: unknown;
@@ -2,8 +2,8 @@
2
2
  export interface FrondCall {
3
3
  /** Frond name, when the caller knows it. Routing hint only. */
4
4
  frond?: string;
5
- /** Entity registration key (e.g. 'product'). */
6
- entity: string;
5
+ /** The address a handler answers at (e.g. 'product'). */
6
+ address: string;
7
7
  /** Façade operation name (e.g. 'findById', 'search'). */
8
8
  op: string;
9
9
  }
@@ -1,7 +1,7 @@
1
1
  // ── Types ───────────────────────────────────────
2
2
  export interface OperationContext {
3
- /** Entity name (e.g. 'product'). */
4
- entity: string;
3
+ /** The address the operation answers at — `checkout` in `checkout.pay`. */
4
+ address: string;
5
5
  /**
6
6
  * Which frond owns this operation — the unit that gets deployed, and therefore the
7
7
  * one a reader groups by first. Absent only where no frond claims the call.
@@ -14,13 +14,13 @@ function optional(value: string | undefined, name: string): string | undefined {
14
14
 
15
15
  /** Canonical identity of one callable operation. */
16
16
  export class RouteAddress {
17
- readonly entity: string;
17
+ readonly address: string;
18
18
  readonly operation: string;
19
19
  readonly surface?: string;
20
20
 
21
21
  /** Names one callable operation, and freezes it so a registry key cannot drift. */
22
22
  constructor(input: RouteAddressInput) {
23
- this.entity = required(input.entity, 'entity');
23
+ this.address = required(input.address, 'address');
24
24
  this.operation = required(input.operation, 'operation');
25
25
  this.surface = optional(input.surface, 'surface');
26
26
  Object.freeze(this);
@@ -28,7 +28,7 @@ export class RouteAddress {
28
28
 
29
29
  /** Serializes the three segments so a route can be looked up in a `Map`. */
30
30
  key(): string {
31
- return JSON.stringify([this.surface ?? null, this.entity, this.operation]);
31
+ return JSON.stringify([this.surface ?? null, this.address, this.operation]);
32
32
  }
33
33
 
34
34
  /** Compares two addresses by their key, so `surface` counts as much as the operation. */
@@ -40,13 +40,13 @@ export class RouteAddress {
40
40
  toString(): string {
41
41
  const audience = this.surface ? `${this.surface}/` : '';
42
42
 
43
- return `${audience}${this.entity}.${this.operation}`;
43
+ return `${audience}${this.address}.${this.operation}`;
44
44
  }
45
45
 
46
46
  /** Sends the address over the wire without `surface: undefined` riding along. */
47
47
  toJSON(): RouteAddressInput {
48
48
  return {
49
- entity: this.entity,
49
+ address: this.address,
50
50
  operation: this.operation,
51
51
  ...(this.surface !== undefined ? { surface: this.surface } : {}),
52
52
  };
@@ -1,5 +1,5 @@
1
1
  export interface RouteAddressInput {
2
- entity: string;
2
+ address: string;
3
3
  operation: string;
4
4
  surface?: string;
5
5
  }
@@ -1,7 +1,7 @@
1
1
  import type { InvocationContext } from './InvocationContext.js';
2
2
 
3
3
  /** Reserved namespace — calls the runner answers itself, never a façade. */
4
- export const RPC_ENTITY = 'rpc';
4
+ export const RPC_ADDRESS = 'rpc';
5
5
 
6
6
  /** What an `rpc` op answers — the facade for what the app says about ITSELF, never about a row. */
7
7
  export type RpcAnswer = (invocation: InvocationContext, surface?: string) => unknown;
@@ -1,6 +1,6 @@
1
1
  /** Everything a caller's envelope covers — the call as the sender meant it. */
2
2
  export interface SignedCall {
3
- entity: string;
3
+ address: string;
4
4
  op: string;
5
5
  params?: Record<string, unknown>;
6
6
  query?: Record<string, unknown>;
@@ -51,7 +51,7 @@ export class StateShape {
51
51
  */
52
52
  judge(
53
53
  state: Record<string, unknown>,
54
- entity: string,
54
+ address: string,
55
55
  operation: string,
56
56
  entered: Record<string, unknown> = {},
57
57
  ): Record<string, unknown> {
@@ -69,7 +69,7 @@ export class StateShape {
69
69
  message: errors.map((error) => `${dotted(error.path)}: ${error.message}`).join(', ')
70
70
  + (declared.length ? ` — this process declares ${declared.join(', ')}` : ' — this process declares no state member'),
71
71
  details: errors,
72
- entity,
72
+ address,
73
73
  operation,
74
74
  });
75
75
  }
@@ -7,18 +7,19 @@ import type { CardOp } from './CardOp.js';
7
7
  export interface IdentityCard {
8
8
  fronds: {
9
9
  name: string;
10
- facades: {
11
- name: string;
12
- ops: CardOp[];
13
- /** The shape stored under this name — **absent when nothing is**. */
14
- schema?: SchemaDescriptor;
15
- }[];
10
+ /** What the frond STORES — one schema per entity, under the entity's name. */
11
+ entities: { name: string; schema: SchemaDescriptor }[];
12
+ /**
13
+ * What the frond ANSWERS — one entry per address, each op carrying its own input and output.
14
+ * An output that IS an entity is titled with the entity's name.
15
+ */
16
+ facades: { name: string; ops: CardOp[] }[];
16
17
  /** The facts this frond ANNOUNCES — one entry per `Emit<T>` its handlers inject. */
17
18
  facts: { name: string; schema?: SchemaDescriptor }[];
18
19
  }[];
19
20
  }
20
21
 
21
- /** The shape a card must have to be walked — `fronds`, and each frond's `facades`. */
22
+ /** The shape a card must have to be walked — `fronds`, and each frond's `entities` and `facades`. */
22
23
  export function assertIdentityCard(value: unknown, source: string): IdentityCard {
23
24
  const card = value as IdentityCard | undefined;
24
25
  const fronds = Array.isArray(card?.fronds) ? card.fronds : undefined;
@@ -26,6 +27,7 @@ export function assertIdentityCard(value: unknown, source: string): IdentityCard
26
27
  for (const frond of fronds) {
27
28
  if (!frond || typeof frond.name !== 'string') throw cardRefusal(source, 'a frond with no name');
28
29
  if (!Array.isArray(frond.facades)) throw cardRefusal(source, `frond '${frond.name}' has no valid facades array`);
30
+ if (!Array.isArray(frond.entities)) throw cardRefusal(source, `frond '${frond.name}' has no valid entities array`);
29
31
  }
30
32
 
31
33
  return card as IdentityCard;
package/src/wire/drift.ts CHANGED
@@ -8,26 +8,18 @@ export interface CardDrift {
8
8
  missingDoors: string[];
9
9
  /** An operation the consumer calls that the facade no longer has. */
10
10
  missingOps: { facade: string; ops: string[] }[];
11
- /** A shape that moved under a facade the consumer still calls. */
12
- shapes: { facade: string; changes: Change[] }[];
11
+ /** What an operation the consumer still calls answers, and how it moved. */
12
+ shapes: { facade: string; op: string; changes: Change[] }[];
13
13
  /** A fact the consumer subscribes to whose shape moved, or that is gone. */
14
14
  facts: { fact: string; changes: Change[] | 'gone' }[];
15
15
  }
16
16
 
17
- /** Every facade of a card, by name. */
18
- /** What a door of that card answers, and the shape of the row it hands back. */
19
- interface Answering {
20
- ops: Set<string>;
21
- schema?: SchemaDescriptor;
22
- }
23
-
24
- function facadesOf(card: IdentityCard, frond: string): Map<string, Answering> {
25
- const found = new Map<string, Answering>();
17
+ /** Every facade of a card, by name, and what each of its operations answers. */
18
+ function facadesOf(card: IdentityCard, frond: string): Map<string, Map<string, SchemaDescriptor | undefined>> {
19
+ const found = new Map<string, Map<string, SchemaDescriptor | undefined>>();
26
20
  for (const one of card.fronds) {
27
21
  if (one.name !== frond) continue;
28
- for (const facade of one.facades) {
29
- found.set(facade.name, { ops: new Set(facade.ops.map((op) => op.name)), schema: facade.schema });
30
- }
22
+ for (const facade of one.facades) found.set(facade.name, new Map(facade.ops.map((op) => [op.name, op.output])));
31
23
  }
32
24
 
33
25
  return found;
@@ -53,29 +45,32 @@ export function driftOf(mine: IdentityCard, theirs: IdentityCard, frond: string)
53
45
  return drift;
54
46
  }
55
47
 
56
- /** A door that is gone, an operation it no longer answers, or a row that changed shape. */
48
+ /** A door that is gone, an operation it no longer answers, or an answer that changed shape. */
57
49
  function driftInDoors(
58
- mine: Map<string, Answering>,
59
- theirs: Map<string, Answering>,
50
+ mine: Map<string, Map<string, SchemaDescriptor | undefined>>,
51
+ theirs: Map<string, Map<string, SchemaDescriptor | undefined>>,
60
52
  drift: CardDrift,
61
53
  ): void {
62
- for (const [name, facade] of mine) {
54
+ for (const [name, ops] of mine) {
63
55
  const there = theirs.get(name);
64
56
  if (!there) {
65
57
  drift.missingDoors.push(name);
66
58
  continue;
67
59
  }
68
60
 
69
- const missing = [...facade.ops].filter((op) => !there.ops.has(op));
61
+ const missing = [...ops.keys()].filter((op) => !there.has(op));
70
62
  if (missing.length > 0) drift.missingOps.push({ facade: name, ops: missing.sort() });
71
63
 
72
- if (!facade.schema || !there.schema) continue;
64
+ for (const [op, output] of ops) {
65
+ const answered = there.get(op);
66
+ if (!output || !answered) continue;
73
67
 
74
- // `Card.diff` never guesses a rename — a field gone plus a field appeared lands in
75
- // `ambiguous`, and only a declaration settles it. Here nobody can declare one, so the
76
- // pair is reported as it is and a human reads it.
77
- const moved = Card.fromDescriptor(facade.schema).diff(Card.fromDescriptor(there.schema));
78
- if (moved.changes.length > 0) drift.shapes.push({ facade: name, changes: moved.changes });
68
+ // `Card.diff` never guesses a rename — a field gone plus a field appeared lands in
69
+ // `ambiguous`, and only a declaration settles it. Here nobody can declare one, so the
70
+ // pair is reported as it is and a human reads it.
71
+ const moved = Card.fromDescriptor(output).diff(Card.fromDescriptor(answered));
72
+ if (moved.changes.length > 0) drift.shapes.push({ facade: name, op, changes: moved.changes });
73
+ }
79
74
  }
80
75
  }
81
76
 
@@ -112,8 +107,8 @@ export function explain(drift: CardDrift): string[] {
112
107
  const lines: string[] = [];
113
108
  for (const facade of drift.missingDoors) lines.push(`${drift.frond}.${facade} — you call it, it is not served`);
114
109
  for (const { facade, ops } of drift.missingOps) lines.push(`${drift.frond}.${facade} — gone: ${ops.join(', ')}`);
115
- for (const { facade, changes } of drift.shapes) {
116
- for (const change of changes) lines.push(`${drift.frond}.${facade} — ${describe(change)}`);
110
+ for (const { facade, op, changes } of drift.shapes) {
111
+ for (const change of changes) lines.push(`${drift.frond}.${facade}.${op} — ${describe(change)}`);
117
112
  }
118
113
  for (const { fact, changes } of drift.facts) {
119
114
  if (changes === 'gone') { lines.push(`${fact} — you subscribe to it, it is no longer announced`); continue; }
@@ -38,13 +38,13 @@ function httpStatusFor(code: ErrorCode): number {
38
38
  /** Serialize an application error for an untrusted caller. */
39
39
  export function toPublicError(err: FougereError): ReturnType<FougereError['toJSON']> {
40
40
  if (err.code !== ErrorCode.INTERNAL_ERROR) return err.toJSON();
41
- const where = [err.entity, err.operation].filter(Boolean).join('.');
41
+ const where = [err.address, err.operation].filter(Boolean).join('.');
42
42
  log.error(`${where || 'internal'}: ${err.message}`, err.cause ?? err);
43
43
 
44
44
  return {
45
45
  code: ErrorCode.INTERNAL_ERROR,
46
46
  message: 'Internal error',
47
- ...(err.entity && { entity: err.entity }),
47
+ ...(err.address && { address: err.address }),
48
48
  ...(err.operation && { operation: err.operation }),
49
49
  };
50
50
  }