@fougere/core 0.12.1-alpha.0 → 0.12.4-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 (261) hide show
  1. package/dist/CallIdentity.d.ts.map +1 -1
  2. package/dist/CallIdentity.js.map +1 -1
  3. package/dist/DomainCluster.d.ts.map +1 -1
  4. package/dist/DomainCluster.js +1 -1
  5. package/dist/DomainCluster.js.map +1 -1
  6. package/dist/EffectiveOperationModel.d.ts +1 -4
  7. package/dist/EffectiveOperationModel.d.ts.map +1 -1
  8. package/dist/EffectiveOperationModel.js +182 -173
  9. package/dist/EffectiveOperationModel.js.map +1 -1
  10. package/dist/FougereConfig.d.ts +5 -3
  11. package/dist/FougereConfig.d.ts.map +1 -1
  12. package/dist/FougereConfig.js +11 -13
  13. package/dist/FougereConfig.js.map +1 -1
  14. package/dist/FrondConfig.d.ts.map +1 -1
  15. package/dist/FrondConfig.js.map +1 -1
  16. package/dist/FrondDeclaration.d.ts.map +1 -1
  17. package/dist/FrondDeclaration.js +1 -5
  18. package/dist/FrondDeclaration.js.map +1 -1
  19. package/dist/Source.js +3 -3
  20. package/dist/Source.js.map +1 -1
  21. package/dist/SourceView.d.ts +0 -2
  22. package/dist/SourceView.d.ts.map +1 -1
  23. package/dist/boot/App.d.ts +2 -4
  24. package/dist/boot/App.d.ts.map +1 -1
  25. package/dist/boot/AppLifecycle.d.ts +5 -0
  26. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  27. package/dist/boot/AppLifecycle.js +19 -10
  28. package/dist/boot/AppLifecycle.js.map +1 -1
  29. package/dist/boot/AuthRuntime.d.ts +5 -12
  30. package/dist/boot/AuthRuntime.d.ts.map +1 -1
  31. package/dist/boot/AuthRuntime.js +2 -1
  32. package/dist/boot/AuthRuntime.js.map +1 -1
  33. package/dist/boot/CreateAppOptions.d.ts +0 -11
  34. package/dist/boot/CreateAppOptions.d.ts.map +1 -1
  35. package/dist/boot/Emissions.d.ts.map +1 -1
  36. package/dist/boot/Emissions.js +3 -6
  37. package/dist/boot/Emissions.js.map +1 -1
  38. package/dist/boot/Hosting.d.ts +2 -1
  39. package/dist/boot/Hosting.d.ts.map +1 -1
  40. package/dist/boot/ambient.als.d.ts.map +1 -1
  41. package/dist/boot/ambient.als.js.map +1 -1
  42. package/dist/boot/apply.d.ts.map +1 -1
  43. package/dist/boot/apply.js.map +1 -1
  44. package/dist/boot/arity.d.ts +13 -0
  45. package/dist/boot/arity.d.ts.map +1 -0
  46. package/dist/boot/arity.js +33 -0
  47. package/dist/boot/arity.js.map +1 -0
  48. package/dist/boot/bootstrap.d.ts.map +1 -1
  49. package/dist/boot/bootstrap.js +143 -265
  50. package/dist/boot/bootstrap.js.map +1 -1
  51. package/dist/boot/card.d.ts.map +1 -1
  52. package/dist/boot/card.js.map +1 -1
  53. package/dist/boot/frame.d.ts.map +1 -1
  54. package/dist/boot/frame.js.map +1 -1
  55. package/dist/boot/install.d.ts.map +1 -1
  56. package/dist/boot/install.js +105 -115
  57. package/dist/boot/install.js.map +1 -1
  58. package/dist/boot/ownership.d.ts.map +1 -1
  59. package/dist/boot/ownership.js.map +1 -1
  60. package/dist/boot/relations.d.ts +10 -0
  61. package/dist/boot/relations.d.ts.map +1 -1
  62. package/dist/boot/relations.js +35 -2
  63. package/dist/boot/relations.js.map +1 -1
  64. package/dist/boot/remote.d.ts.map +1 -1
  65. package/dist/boot/remote.js.map +1 -1
  66. package/dist/boot/seed.d.ts.map +1 -1
  67. package/dist/boot/seed.js +1 -1
  68. package/dist/boot/seed.js.map +1 -1
  69. package/dist/boot/statement-drift.d.ts +6 -0
  70. package/dist/boot/statement-drift.d.ts.map +1 -1
  71. package/dist/boot/statement-drift.js +19 -0
  72. package/dist/boot/statement-drift.js.map +1 -1
  73. package/dist/boot/together.d.ts.map +1 -1
  74. package/dist/boot/together.js.map +1 -1
  75. package/dist/builtin/Carry.d.ts.map +1 -1
  76. package/dist/builtin/Carry.js.map +1 -1
  77. package/dist/builtin/Logger.d.ts.map +1 -1
  78. package/dist/builtin/Logger.js +3 -1
  79. package/dist/builtin/Logger.js.map +1 -1
  80. package/dist/builtin/config.js.map +1 -1
  81. package/dist/contract.d.ts.map +1 -1
  82. package/dist/contract.js.map +1 -1
  83. package/dist/crypto/encoding.d.ts.map +1 -1
  84. package/dist/crypto/encoding.js.map +1 -1
  85. package/dist/crypto/node.d.ts.map +1 -1
  86. package/dist/crypto/node.js.map +1 -1
  87. package/dist/crypto/webcrypto.d.ts.map +1 -1
  88. package/dist/crypto/webcrypto.js.map +1 -1
  89. package/dist/descriptor/Fronds.d.ts +5 -0
  90. package/dist/descriptor/Fronds.d.ts.map +1 -1
  91. package/dist/descriptor/Fronds.js +8 -0
  92. package/dist/descriptor/Fronds.js.map +1 -1
  93. package/dist/descriptor/index.d.ts +1 -0
  94. package/dist/descriptor/index.d.ts.map +1 -1
  95. package/dist/descriptor/index.js +1 -0
  96. package/dist/descriptor/index.js.map +1 -1
  97. package/dist/dispatch/ArrayResult.d.ts.map +1 -1
  98. package/dist/dispatch/ArrayResult.js.map +1 -1
  99. package/dist/dispatch/Dispatcher.d.ts.map +1 -1
  100. package/dist/dispatch/Dispatcher.js.map +1 -1
  101. package/dist/dispatch/HandlerFacade.d.ts +6 -1
  102. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  103. package/dist/dispatch/HandlerFacade.js +11 -3
  104. package/dist/dispatch/HandlerFacade.js.map +1 -1
  105. package/dist/dispatch/InFlight.d.ts +3 -2
  106. package/dist/dispatch/InFlight.d.ts.map +1 -1
  107. package/dist/dispatch/InFlight.js +11 -1
  108. package/dist/dispatch/InFlight.js.map +1 -1
  109. package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
  110. package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
  111. package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
  112. package/dist/dispatch/RouteRegistry.js.map +1 -1
  113. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  114. package/dist/dispatch/StorageGuard.js +4 -1
  115. package/dist/dispatch/StorageGuard.js.map +1 -1
  116. package/dist/identity-keys.d.ts.map +1 -1
  117. package/dist/identity-keys.js.map +1 -1
  118. package/dist/index.d.ts +2 -4
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +2 -0
  121. package/dist/index.js.map +1 -1
  122. package/dist/loader.d.ts.map +1 -1
  123. package/dist/loader.js.map +1 -1
  124. package/dist/node.d.ts +1 -1
  125. package/dist/node.d.ts.map +1 -1
  126. package/dist/node.js +1 -1
  127. package/dist/node.js.map +1 -1
  128. package/dist/prefab/CrudConstructor.d.ts.map +1 -1
  129. package/dist/prefab/CrudConstructor.js.map +1 -1
  130. package/dist/prefab/collector.d.ts.map +1 -1
  131. package/dist/prefab/collector.js.map +1 -1
  132. package/dist/prefab/presenter.d.ts.map +1 -1
  133. package/dist/prefab/presenter.js.map +1 -1
  134. package/dist/storage/Comparison.d.ts +2 -2
  135. package/dist/storage/Comparison.d.ts.map +1 -1
  136. package/dist/storage/Comparison.js +3 -1
  137. package/dist/storage/Comparison.js.map +1 -1
  138. package/dist/storage/Storage.d.ts.map +1 -1
  139. package/dist/storage/Storage.js.map +1 -1
  140. package/dist/storage/Store.d.ts.map +1 -1
  141. package/dist/storage/Store.js +1 -1
  142. package/dist/storage/Store.js.map +1 -1
  143. package/dist/verify.d.ts.map +1 -1
  144. package/dist/verify.js.map +1 -1
  145. package/dist/wire/AppMiddleware.d.ts.map +1 -1
  146. package/dist/wire/AppMiddleware.js.map +1 -1
  147. package/dist/wire/Emit.d.ts +0 -2
  148. package/dist/wire/Emit.d.ts.map +1 -1
  149. package/dist/wire/Emit.js +1 -1
  150. package/dist/wire/Emit.js.map +1 -1
  151. package/dist/wire/Facade.d.ts +7 -0
  152. package/dist/wire/Facade.d.ts.map +1 -1
  153. package/dist/wire/Facade.js +13 -0
  154. package/dist/wire/Facade.js.map +1 -1
  155. package/dist/wire/FougereError.d.ts.map +1 -1
  156. package/dist/wire/FougereError.js.map +1 -1
  157. package/dist/wire/Invocation.d.ts.map +1 -1
  158. package/dist/wire/Invocation.js.map +1 -1
  159. package/dist/wire/OperationContract.d.ts +0 -4
  160. package/dist/wire/OperationContract.d.ts.map +1 -1
  161. package/dist/wire/OperationContract.js +1 -1
  162. package/dist/wire/OperationContract.js.map +1 -1
  163. package/dist/wire/OperationsMap.d.ts +0 -3
  164. package/dist/wire/OperationsMap.d.ts.map +1 -1
  165. package/dist/wire/OperationsMap.js +0 -5
  166. package/dist/wire/OperationsMap.js.map +1 -1
  167. package/dist/wire/RouteAddress.d.ts.map +1 -1
  168. package/dist/wire/RouteAddress.js.map +1 -1
  169. package/dist/wire/binding.d.ts.map +1 -1
  170. package/dist/wire/binding.js.map +1 -1
  171. package/dist/wire/card/IdentityCard.d.ts.map +1 -1
  172. package/dist/wire/card/IdentityCard.js.map +1 -1
  173. package/dist/wire/drift.d.ts.map +1 -1
  174. package/dist/wire/drift.js.map +1 -1
  175. package/dist/wire/http-error.d.ts.map +1 -1
  176. package/dist/wire/http-error.js.map +1 -1
  177. package/dist/wire/loggerMiddleware.d.ts.map +1 -1
  178. package/dist/wire/loggerMiddleware.js.map +1 -1
  179. package/package.json +5 -5
  180. package/src/CallIdentity.ts +6 -0
  181. package/src/DomainCluster.ts +2 -1
  182. package/src/EffectiveOperationModel.ts +236 -198
  183. package/src/FougereConfig.ts +17 -16
  184. package/src/FrondConfig.ts +2 -0
  185. package/src/FrondDeclaration.ts +1 -7
  186. package/src/Source.ts +3 -3
  187. package/src/SourceView.ts +0 -2
  188. package/src/boot/App.ts +2 -4
  189. package/src/boot/AppLifecycle.ts +22 -10
  190. package/src/boot/AuthRuntime.ts +6 -13
  191. package/src/boot/CreateAppOptions.ts +0 -11
  192. package/src/boot/Emissions.ts +7 -7
  193. package/src/boot/Hosting.ts +2 -1
  194. package/src/boot/ambient.als.ts +2 -0
  195. package/src/boot/apply.ts +1 -0
  196. package/src/boot/arity.ts +42 -0
  197. package/src/boot/bootstrap.ts +175 -300
  198. package/src/boot/card.ts +2 -0
  199. package/src/boot/frame.ts +6 -0
  200. package/src/boot/install.ts +123 -146
  201. package/src/boot/ownership.ts +1 -0
  202. package/src/boot/relations.ts +42 -10
  203. package/src/boot/remote.ts +1 -0
  204. package/src/boot/seed.ts +3 -5
  205. package/src/boot/statement-drift.ts +25 -0
  206. package/src/boot/together.ts +3 -0
  207. package/src/builtin/Carry.ts +1 -0
  208. package/src/builtin/Logger.ts +9 -1
  209. package/src/builtin/config.ts +1 -0
  210. package/src/contract.ts +1 -0
  211. package/src/crypto/encoding.ts +4 -0
  212. package/src/crypto/node.ts +2 -0
  213. package/src/crypto/webcrypto.ts +2 -0
  214. package/src/descriptor/Fronds.ts +12 -0
  215. package/src/descriptor/index.ts +1 -0
  216. package/src/dispatch/ArrayResult.ts +1 -0
  217. package/src/dispatch/Dispatcher.ts +1 -0
  218. package/src/dispatch/HandlerFacade.ts +16 -4
  219. package/src/dispatch/InFlight.ts +18 -2
  220. package/src/dispatch/LocalRoutePolicy.ts +1 -0
  221. package/src/dispatch/RouteRegistry.ts +4 -0
  222. package/src/dispatch/StorageGuard.ts +9 -1
  223. package/src/identity-keys.ts +2 -0
  224. package/src/index.ts +2 -4
  225. package/src/loader.ts +1 -0
  226. package/src/node.ts +1 -1
  227. package/src/prefab/CrudConstructor.ts +1 -11
  228. package/src/prefab/collector.ts +1 -0
  229. package/src/prefab/presenter.ts +1 -0
  230. package/src/storage/Comparison.ts +7 -4
  231. package/src/storage/Storage.ts +2 -0
  232. package/src/storage/Store.ts +9 -1
  233. package/src/verify.ts +2 -0
  234. package/src/wire/AppMiddleware.ts +2 -0
  235. package/src/wire/Emit.ts +1 -1
  236. package/src/wire/Facade.ts +17 -10
  237. package/src/wire/FougereError.ts +2 -0
  238. package/src/wire/Invocation.ts +2 -0
  239. package/src/wire/OperationContract.ts +2 -1
  240. package/src/wire/OperationsMap.ts +0 -10
  241. package/src/wire/RouteAddress.ts +2 -0
  242. package/src/wire/binding.ts +1 -0
  243. package/src/wire/card/IdentityCard.ts +1 -0
  244. package/src/wire/drift.ts +3 -0
  245. package/src/wire/http-error.ts +2 -0
  246. package/src/wire/loggerMiddleware.ts +1 -0
  247. package/dist/boot/AuthConfig.d.ts +0 -14
  248. package/dist/boot/AuthConfig.d.ts.map +0 -1
  249. package/dist/boot/AuthConfig.js +0 -2
  250. package/dist/boot/AuthConfig.js.map +0 -1
  251. package/dist/boot/AuthContext.d.ts +0 -12
  252. package/dist/boot/AuthContext.d.ts.map +0 -1
  253. package/dist/boot/AuthContext.js +0 -2
  254. package/dist/boot/AuthContext.js.map +0 -1
  255. package/dist/storage/ComparisonName.d.ts +0 -3
  256. package/dist/storage/ComparisonName.d.ts.map +0 -1
  257. package/dist/storage/ComparisonName.js +0 -2
  258. package/dist/storage/ComparisonName.js.map +0 -1
  259. package/src/boot/AuthConfig.ts +0 -14
  260. package/src/boot/AuthContext.ts +0 -12
  261. package/src/storage/ComparisonName.ts +0 -3
package/src/boot/card.ts CHANGED
@@ -37,6 +37,7 @@ export function identityCardOf(app: App, surface?: string): IdentityCard {
37
37
  const ops = facadeOps(app, address, surface);
38
38
  if (ops.length === 0) return [];
39
39
  const entity = byEntity.get(address);
40
+
40
41
  return [{
41
42
  name: address,
42
43
  ops,
@@ -47,6 +48,7 @@ export function identityCardOf(app: App, surface?: string): IdentityCard {
47
48
  /** What leaves on its own — the same list on every surface, deliberately. */
48
49
  facts: factsAnnouncedBy(frond.handlers).map((name) => {
49
50
  const entityClass = declared.get(name);
51
+
50
52
  return { name, ...(entityClass ? { schema: Card.fromSchema(entityClass, name).descriptor } : {}) };
51
53
  }),
52
54
  };
package/src/boot/frame.ts CHANGED
@@ -83,6 +83,7 @@ function recordCreate({ base, recorded, entity, key, journal }: Recording): void
83
83
  const row = await base.create.apply(this, args);
84
84
  const id = String(row[key]);
85
85
  journal.push({ what: `create ${entity}#${id}`, run: () => base.delete.call(this, id).then(() => undefined) });
86
+
86
87
  return row;
87
88
  };
88
89
  }
@@ -119,6 +120,7 @@ function recordUpdate({ base, recorded, entity, journal }: Recording): void {
119
120
  await base.update.call(this, id, pick(before, touched));
120
121
  },
121
122
  });
123
+
122
124
  return row;
123
125
  };
124
126
  }
@@ -131,6 +133,7 @@ function recordDelete({ base, recorded, entity, journal }: Recording): void {
131
133
  if (removed && before) {
132
134
  journal.push({ what: `delete ${entity}#${id}`, run: () => base.create.call(this, before).then(() => undefined) });
133
135
  }
136
+
134
137
  return removed;
135
138
  };
136
139
  }
@@ -144,6 +147,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
144
147
  const undoUpsert = async function (this: unknown, rows: readonly Record<string, unknown>[]): Promise<Undo> {
145
148
  const ids = rows.map((row) => String(row[key]));
146
149
  const before = await base.findByKeys.call(this, ids);
150
+
147
151
  return {
148
152
  what: `upsert ${entity} (${ids.length} row(s))`,
149
153
  run: async () => {
@@ -163,6 +167,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
163
167
  const undo = await undoUpsert.call(this, [input]);
164
168
  const row = await upsert.call(this, input, ...rest);
165
169
  journal.push(undo);
170
+
166
171
  return row;
167
172
  };
168
173
  }
@@ -174,6 +179,7 @@ function recordUpserts({ base, recorded, entity, key, journal }: Recording, sche
174
179
  const undo = await undoUpsert.call(this, inputs);
175
180
  const written = await upsertAll.call(this, inputs, ...rest);
176
181
  journal.push(undo);
182
+
177
183
  return written;
178
184
  };
179
185
  }
@@ -21,6 +21,7 @@ import type { Diagnostic } from '../diagnostic.js';
21
21
  import { HandlerFacade } from '../dispatch/HandlerFacade.js';
22
22
  import { targetOf } from '../prefab/prefab.js';
23
23
  import { ownersOf, sharedNames, storageInUserCode, crudOnOwned } from './ownership.js';
24
+ import { refuseArityDrift } from './arity.js';
24
25
  import { StorageGuard } from '../dispatch/StorageGuard.js';
25
26
  import { portBindings, seamChains, wrapping, SEAMS } from './ports.js';
26
27
  import { contractsKeyOf, facadeKeyOf } from '../wire/Facade.js';
@@ -307,7 +308,7 @@ function registerProviders(
307
308
  // of: the container resolves a dep by NAME, so wrapping is a substituted key and
308
309
  // needs nothing of the container itself. The outermost answers under the port.
309
310
  let inner = nameOf(chain.at(-1)!);
310
- for (const wrapper of [...chain.slice(0, -1)].reverse()) {
311
+ for (const wrapper of chain.slice(0, -1).reverse()) {
311
312
  const deps = wrapper.deps.map((dep) => (dep === port ? inner : dep));
312
313
  inner = nameOf(wrapper);
313
314
  scope.register(inner, wrapper.ctor, { deps });
@@ -456,88 +457,51 @@ function buildFacadeInto(
456
457
  ));
457
458
  }
458
459
 
459
- export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
460
- const {
461
- container, routeRegistry, emissions, dispatcher, localDispatcher, effectiveByKey,
462
- boundPorts, refused, operationModel, entityByName, frondOf, contractsOf, getMiddlewares, use,
463
- log, options,
464
- } = assembly;
465
-
466
- // Declared remote: keep the scanned metadata (bridges route with it),
467
- // register nothing locally — resolve() falls through to the remote façade.
468
- if (options.remotes && frond.name in options.remotes) {
469
- log.child(frond.name).info('declared remote — not hosted locally');
470
- // Its facades answer elsewhere, but what they LISTEN to was read here.
471
- for (const handler of frond.handlers) {
472
- const key = facadeKeyOf(handler.address, handler.surface);
473
- const operations = operationModel.forHandler(handler);
474
- effectiveByKey.set(key, operations);
475
- emissions.note(contractsOf(operations), key);
476
-
477
- }
478
- return;
460
+ /** Its facades answer elsewhere, but what they LISTEN to is read here, and the bridges route with it. */
461
+ function noteRemoteFrond(frond: FrondDescriptor, { operationModel, effectiveByKey, emissions, contractsOf, log }: Assembly): void {
462
+ log.child(frond.name).info('declared remote — not hosted locally');
463
+ for (const handler of frond.handlers) {
464
+ const key = facadeKeyOf(handler.address, handler.surface);
465
+ const operations = operationModel.forHandler(handler);
466
+ effectiveByKey.set(key, operations);
467
+ emissions.note(contractsOf(operations), key);
479
468
  }
480
- // A child hangs off its parent, so everything the parent registered answers here too and
481
- // nothing else has to know: `ScopeContainer.resolve` already walks up. The boot installs
482
- // parents first, which is what makes the key below resolvable.
483
- const above = frond.extends ? container.resolve<Container>(`frond:${frond.extends}`) : container;
484
- const scope = above.createScope();
485
- const frondLog = log.child(frond.name);
486
- // The frond's own voice, a child of the APP logger and never of `log` — which is the
487
- // boot's, so a service's line would have claimed `boot:` long after the boot was over.
488
- scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
489
-
490
- // `reads:` is what makes a cross-source reader exist here, and the list IS its
491
- // environment — a source holding none of these is never opened. Registered under
492
- // the type's own name, which is the key `depKeyOf` already derives for a plain
493
- // parameter: `constructor(private reads: Reads)` and nothing else to say.
494
- // Declaring `reads:` with nothing to build the reader is a boot that ignores a
495
- // clause: the handler asking for `Reads` then dies at its first call, on a
496
- // container message that names neither the clause nor what is missing.
497
- await registerReads(frond, scope, entityByName, options.sourcesFactory, frondLog);
469
+ }
498
470
 
499
- // Who owns what, and the rule that makes owning mean something. Before anything is
500
- // registered, so a bad line is named by this refusal rather than by the container's.
471
+ /**
472
+ * Who owns what, and the rule that makes owning mean something — refused before anything is
473
+ * registered, so a bad line is named here rather than by the container.
474
+ */
475
+ function ownershipOf(frond: FrondDescriptor, { entityByName, refused }: Assembly): Map<string, string> {
501
476
  sharedNames(frond, refused);
477
+ refuseArityDrift(frond, refused);
502
478
  const owners = ownersOf(frond.providers, frond.name, refused);
503
479
  storageInUserCode(frond, owners, (entity) => entityByName.has(entity), refused);
504
480
  crudOnOwned(frond, owners, refused);
505
481
 
506
- const declared = registerProviders(frond, scope, options.ports, boundPorts, refused, frondLog);
507
- // An ancestor's links stand OUTSIDE this frond's own, the order `wrapping` reads — and
508
- // like a middleware, a link cannot be inherited through a container key, since a seam has
509
- // none: its realization is built rather than resolved.
510
- const seams = inheritedSeams(frond, assembly.seamsOf, declared);
511
- assembly.seamsOf.set(frond.name, seams);
482
+ return owners;
483
+ }
512
484
 
513
- // Register Storage for each entity — PascalCase type name (e.g. 'PostStorage')
514
- // When a handler declares Crud(Entity, Output), scope the storage via .output(Output)
515
- registerStorages(frond, assembly, scope, seams, owners, frondLog);
485
+ /** A frame is read from the same `deps` every port is: asking for one IS declaring it. */
486
+ function registerFramesOf(frond: FrondDescriptor, assembly: Assembly, scope: Container, frondLog: Logger): void {
487
+ const { entityByName, frondOf, options, hosting, refused } = assembly;
488
+ const wanted = [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors]
489
+ .flatMap((declared) => declared.deps.map((key) => ({ key, filePath: declared.filePath })));
490
+
491
+ registerFrames(scope, wanted, frond.providers, {
492
+ entityByName,
493
+ frondOf,
494
+ hostedHere: (name) => !(options.remotes && name in options.remotes),
495
+ storageFactory: options.storageFactory,
496
+ sourceOf: options.sourceOf,
497
+ transacts: options.transacts,
498
+ transacted: options.transacted,
499
+ hosting,
500
+ log: frondLog,
501
+ }, refused);
502
+ }
516
503
 
517
- // Frames, after the ORMs and before anything that may ask for one. A frame is read
518
- // from the same `deps` every other port is read from — asking for it IS declaring it,
519
- // so nothing is registered for a frame nobody wants.
520
- registerFrames(
521
- scope,
522
- [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors]
523
- .flatMap((d) => d.deps.map((key) => ({ key, filePath: d.filePath }))),
524
- frond.providers,
525
- {
526
- entityByName,
527
- frondOf,
528
- hostedHere: (name) => !(options.remotes && name in options.remotes),
529
- storageFactory: options.storageFactory,
530
- sourceOf: options.sourceOf,
531
- transacts: options.transacts,
532
- transacted: options.transacted,
533
- hosting: assembly.hosting,
534
- log: frondLog,
535
- },
536
- refused,
537
- );
538
-
539
- // Register presenters in scope — PascalCase type name (e.g. 'PostPresenter')
540
- const presenterMap = new Map(frond.presenters.map((p) => [p.entityName, p]));
504
+ function registerPresenters(frond: FrondDescriptor, scope: Container, frondLog: Logger): Map<string, PresenterEntry> {
541
505
  for (const presenter of frond.presenters) {
542
506
  scope.register(presenterKeyOf(presenter.entityName), presenter.ctor, { deps: presenter.deps });
543
507
  }
@@ -545,104 +509,117 @@ export async function installFrond(frond: FrondDescriptor, assembly: Assembly):
545
509
  frondLog.debug(`${frond.presenters.length} presenter(s): ${frond.presenters.map((p) => p.entityName).join(', ')}`);
546
510
  }
547
511
 
548
- // Register collectors in scope — PascalCase type name (e.g. 'UserCollector')
549
- const collectorTypeNames = new Set(frond.collectors.map((c) => c.typeName));
512
+ return new Map(frond.presenters.map((presenter) => [presenter.entityName, presenter]));
513
+ }
514
+
515
+ function registerCollectors(frond: FrondDescriptor, scope: Container, frondLog: Logger): Set<string> {
550
516
  for (const collector of frond.collectors) {
551
- const key = collectorKeyOf(collector.typeName);
552
- scope.register(key, collector.ctor, { deps: collector.deps });
517
+ scope.register(collectorKeyOf(collector.typeName), collector.ctor, { deps: collector.deps });
553
518
  }
554
519
  if (frond.collectors.length > 0) {
555
520
  frondLog.debug(`${frond.collectors.length} collector(s): ${frond.collectors.map((c) => c.typeName).join(', ')}`);
556
521
  }
557
522
 
558
- // Register middlewares in scope, then take them on. Resolved per call and never here:
559
- // a middleware asking for something request-scoped would otherwise be handed the one
560
- // instance the boot built — the same reason `getMiddlewares` is read at call time.
561
- registerMiddlewares(frond, scope, assembly, frondLog);
562
-
563
- // Build handler facades → registered in ROOT container (public contract)
564
- const defaultHandlers = frond.handlers.filter((h) => !h.surface);
565
- const surfaceHandlers = frond.handlers.filter((h) => h.surface);
566
- const defaultHandlerMap = new Map(defaultHandlers.map((h) => [h.address, h]));
523
+ return new Set(frond.collectors.map((collector) => collector.typeName));
524
+ }
567
525
 
568
- const building: Building = { frond, assembly, scope, collectorTypeNames, presenterMap, frondLog };
569
- const buildFacade = (
570
- entity: EntityEntry | undefined,
571
- handler: HandlerEntry,
572
- targetScope: Container,
573
- facadeKey: string,
574
- ) => buildFacadeInto(building, entity, handler, targetScope, facadeKey);
575
-
576
- // A presenter is about an entity — computed fields sit on a shape — so this walks
577
- // entities. Exposing the instance lazily; the bridge resolves it on first access.
578
- exposePresenters(frond, presenterMap, container, scope);
526
+ function sayFacade(building: Building, facadeKey: string, entity: EntityEntry | undefined): void {
527
+ const operations = Object.keys(building.assembly.container.resolve(facadeKey) as object).join(', ');
528
+ building.frondLog.debug(`${facadeKey} [${operations}]`
529
+ + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
530
+ }
579
531
 
580
- // A facade is about a handler, so this walks HANDLERS. It walked entities before,
581
- // which made an entity a precondition for being callable at all: a handler naming
582
- // none was scanned, then never built, and nothing said so.
583
- for (const handler of defaultHandlers) {
584
- // Two ways to know the subject, and the explicit one wins: `Crud(Item)` names the
585
- // entity it was built on, whatever the handler is called. Otherwise the handler's
586
- // own name is the only thing pointing at one — and pointing at nothing is legal.
587
- //
588
- // By NAME, not by identity: the scanner loads an entity through its own loader and
589
- // the handler imports it through the runtime's, so the same class arrives as two
590
- // objects. `===` compares module instances, which is not the question being asked.
532
+ /**
533
+ * A facade is about a handler, so this walks HANDLERS — pointing at no entity is legal. The
534
+ * subject is `Crud(Item)`'s when it names one, the handler's own name otherwise, compared by
535
+ * NAME: the scanner and the runtime load one class as two objects.
536
+ */
537
+ function buildDefaultFacades(building: Building, handlers: readonly HandlerEntry[]): void {
538
+ for (const handler of handlers) {
591
539
  const crudTarget = targetOf(handler.ctor);
592
540
  const subject = crudTarget?.name ? lowerFirst(crudTarget.name) : handler.address;
593
- const entity = frond.entities.find((e) => e.name === subject);
541
+ const entity = building.frond.entities.find((e) => e.name === subject);
594
542
  const facadeKey = facadeKeyOf(handler.address);
595
- buildFacade(entity, handler, scope, facadeKey);
596
- frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as object).join(', ')}]`
597
- + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
543
+ buildFacadeInto(building, entity, handler, building.scope, facadeKey);
544
+ sayFacade(building, facadeKey, entity);
598
545
  }
546
+ }
599
547
 
600
- // The dual, and it stays: a shape that declares no operation answers nothing. Said
601
- // once per entity rather than deduced from a silence.
602
- for (const entity of frond.entities) {
603
- if (!defaultHandlerMap.has(entity.name)) {
604
- frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
605
- }
548
+ /** A shape that declares no operation answers nothing — said once per entity rather than deduced from a silence. */
549
+ function sayUnhandledEntities(building: Building, handlers: readonly HandlerEntry[]): void {
550
+ const handled = new Set(handlers.map((handler) => handler.address));
551
+ for (const entity of building.frond.entities) {
552
+ if (!handled.has(entity.name)) building.frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
606
553
  }
554
+ }
607
555
 
608
- // Surface handlers — create sub-scope per surface handler with scoped storage
609
- //
610
- // Pointing at nothing is legal HERE TOO. This loop used to `continue` when no entity
611
- // carried the handler's name, so `handlers/public/SearchHandler.ts` with no `Search`
612
- // entity got no facade at all and no line saying why — while the very same handler at
613
- // the default surface is built and logged. One rule, both surfaces.
614
- for (const handler of surfaceHandlers) {
615
- const entity = frond.entities.find((e) => e.name === handler.address);
616
- const surfaceScope = scope.createScope();
617
-
618
- // Register scoped storage if output override differs from entity — under the REPOSITORY
619
- // key, which is what a Crud handler asks for, and under the port's own for a holder
620
- // that legitimately names it. Registering only the latter left a named surface with
621
- // no facade at all once the façade stopped spelling the storage.
622
- if (entity) registerSurfaceStorage(entity, handler, surfaceScope, assembly, frondLog);
556
+ /**
557
+ * One sub-scope per surface handler, holding the storage its view scopes — under the REPOSITORY
558
+ * key a Crud handler asks for. Pointing at no entity is legal here too: one rule, both surfaces.
559
+ */
560
+ function buildSurfaceFacades(building: Building, handlers: readonly HandlerEntry[]): void {
561
+ for (const handler of handlers) {
562
+ const entity = building.frond.entities.find((e) => e.name === handler.address);
563
+ const surfaceScope = building.scope.createScope();
564
+ if (entity) registerSurfaceStorage(entity, handler, surfaceScope, building.assembly, building.frondLog);
623
565
 
624
566
  const facadeKey = facadeKeyOf(handler.address, handler.surface);
625
- buildFacade(entity, handler, surfaceScope, facadeKey);
626
- frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey) as object).join(', ')}]`
627
- + (entity ? '' : ' — no entity of that name: no storage, no projection, no presenter'));
567
+ buildFacadeInto(building, entity, handler, surfaceScope, facadeKey);
568
+ sayFacade(building, facadeKey, entity);
628
569
  }
570
+ }
629
571
 
630
- // A named surface is closed, so what it contains is a fact worth stating.
631
- // Saying it at boot is the difference between a rule and a rule you can
632
- // check: an entity you meant to serve and never wrote a handler for is
633
- // absent HERE, in one line, instead of being discovered missing later.
634
- const surfaceNames = [...new Set(surfaceHandlers.map((h) => h.surface as string))].sort();
572
+ /** A named surface is closed, so what it serves — and what it leaves out — is said at boot, in one line. */
573
+ function sayWhatSurfacesServe(frond: FrondDescriptor, handlers: readonly HandlerEntry[], frondLog: Logger): void {
574
+ const surfaceNames = [...new Set(handlers.map((h) => h.surface as string))].sort();
635
575
  for (const surfaceName of surfaceNames) {
636
- const served = surfaceHandlers
637
- .filter((h) => h.surface === surfaceName)
638
- .map((h) => h.address)
639
- .sort();
576
+ const served = handlers.filter((h) => h.surface === surfaceName).map((h) => h.address).sort();
640
577
  const absent = frond.entities.map((e) => e.name).filter((n) => !served.includes(n));
641
578
  frondLog.info(
642
579
  `surface '${surfaceName}' — ${served.length} entit${served.length === 1 ? 'y' : 'ies'}: ${served.join(', ')}` +
643
580
  (absent.length > 0 ? ` (not served: ${absent.join(', ')})` : ''),
644
581
  );
645
582
  }
583
+ }
584
+
585
+ export async function installFrond(frond: FrondDescriptor, assembly: Assembly): Promise<void> {
586
+ const { container, entityByName, options, log } = assembly;
587
+
588
+ if (options.remotes && frond.name in options.remotes) {
589
+ noteRemoteFrond(frond, assembly);
590
+
591
+ return;
592
+ }
593
+
594
+ // A child hangs off its parent, so what the parent registered answers here too — parents are installed first.
595
+ const above = frond.extends ? container.resolve<Container>(`frond:${frond.extends}`) : container;
596
+ const scope = above.createScope();
597
+ const frondLog = log.child(frond.name);
598
+ // A child of the APP logger, never of the boot's, whose `boot:` would outlive the boot.
599
+ scope.registerValue('Logger', container.resolve<Logger>('Logger').child(frond.name));
600
+
601
+ await registerReads(frond, scope, entityByName, options.sourcesFactory, frondLog);
602
+ const owners = ownershipOf(frond, assembly);
603
+
604
+ const declared = registerProviders(frond, scope, options.ports, assembly.boundPorts, assembly.refused, frondLog);
605
+ // An ancestor's links stand OUTSIDE this frond's own; a seam has no container key to inherit through.
606
+ const seams = inheritedSeams(frond, assembly.seamsOf, declared);
607
+ assembly.seamsOf.set(frond.name, seams);
608
+ registerStorages(frond, assembly, scope, seams, owners, frondLog);
609
+ registerFramesOf(frond, assembly, scope, frondLog);
610
+
611
+ const presenterMap = registerPresenters(frond, scope, frondLog);
612
+ const collectorTypeNames = registerCollectors(frond, scope, frondLog);
613
+ registerMiddlewares(frond, scope, assembly, frondLog);
614
+ exposePresenters(frond, presenterMap, container, scope);
615
+
616
+ const building: Building = { frond, assembly, scope, collectorTypeNames, presenterMap, frondLog };
617
+ const defaultHandlers = frond.handlers.filter((h) => !h.surface);
618
+ const surfaceHandlers = frond.handlers.filter((h) => h.surface);
619
+ buildDefaultFacades(building, defaultHandlers);
620
+ sayUnhandledEntities(building, defaultHandlers);
621
+ buildSurfaceFacades(building, surfaceHandlers);
622
+ sayWhatSurfacesServe(frond, surfaceHandlers, frondLog);
646
623
 
647
624
  container.registerValue(`frond:${frond.name}`, scope);
648
625
  frondLog.info(`registered — ${frond.entities.length} entities, ${frond.handlers.length} handlers, ${frond.seeds.length} seeds`);
@@ -80,6 +80,7 @@ function builtOn(ctor: unknown): string[] {
80
80
  const owned = ownedBy(ctor);
81
81
  if (owned.length > 0) return owned.map((e) => lowerFirst((e as { name: string }).name));
82
82
  const target = targetOf(ctor) as { name?: string } | undefined;
83
+
83
84
  return target?.name ? [lowerFirst(target.name)] : [];
84
85
  }
85
86
 
@@ -4,7 +4,8 @@ import type { Dependent } from '../dispatch/Dependent.js';
4
4
  import type { RelationCheck } from '../dispatch/RelationCheck.js';
5
5
  import type { Hosting } from './Hosting.js';
6
6
  import type { Diagnostic } from '../diagnostic.js';
7
- import type { Releasing, Rows } from '../dispatch/Release.js';
7
+ import type { Releasing } from '../dispatch/Release.js';
8
+ import type { EntityEntry } from '../descriptor/EntityEntry.js';
8
9
 
9
10
  /**
10
11
  * Does a foreign key hold this reference? Both sides in ONE source that keeps relations — two
@@ -34,7 +35,7 @@ function referencesTo(target: string, hosting: Hosting): { entity: string; field
34
35
  for (const [entity, schema] of hosting.entities()) {
35
36
  for (const [field, declared] of Object.entries(schema.getFields())) {
36
37
  const role = Role.of(declared);
37
- if (role.isReference && role.target?.name && lowerFirst(role.target.name) === target) {
38
+ if (role.isReference() && role.target?.name && lowerFirst(role.target.name) === target) {
38
39
  found.push({ entity, field, role });
39
40
  }
40
41
  }
@@ -43,11 +44,6 @@ function referencesTo(target: string, hosting: Hosting): { entity: string; field
43
44
  return found;
44
45
  }
45
46
 
46
- /** What a target's storage has to answer for a key set to be judged in one read. */
47
- interface ByKeys {
48
- findByKeys(keys: readonly string[]): Promise<Map<string, unknown>>;
49
- }
50
-
51
47
  /**
52
48
  * The references a foreign key cannot hold, each as a read the guard makes before the write.
53
49
  *
@@ -58,7 +54,7 @@ export function heldBy(entity: SchemaView, name: string, hosting: Hosting): Rela
58
54
 
59
55
  for (const [field, declared] of Object.entries(entity.getFields())) {
60
56
  const role = Role.of(declared);
61
- if (!role.isReference || !role.target?.name) continue;
57
+ if (!role.isReference() || !role.target?.name) continue;
62
58
 
63
59
  const target = lowerFirst(role.target.name);
64
60
  if (keyed(name, target, hosting)) continue;
@@ -67,7 +63,7 @@ export function heldBy(entity: SchemaView, name: string, hosting: Hosting): Rela
67
63
  field,
68
64
  target,
69
65
  missing: async (keys) => {
70
- const storage = hosting.storageOf(target) as ByKeys | undefined;
66
+ const storage = hosting.storageOf(target);
71
67
  // Nothing here holds those rows — the process that does answers the same reading,
72
68
  // so the question crosses rather than being given up on.
73
69
  if (!storage) return hosting.peerOf(target)?.missing(target, keys) ?? [];
@@ -122,6 +118,42 @@ export function refuseUnwritableNull(entity: SchemaView, name: string, filePath:
122
118
  return refused;
123
119
  }
124
120
 
121
+ /**
122
+ * A `many()` whose target declares no `ref()` back. The key lives on the far row, so a
123
+ * relation neither side states can never be filled: SQL emits no column, GraphQL drops the
124
+ * field when it finds no reverse key, and nothing says why.
125
+ *
126
+ * Asked once every frond is installed, since the target may belong to another one — and left
127
+ * alone when it answers elsewhere, where its fields are declared and cannot be read here.
128
+ */
129
+ export function unpaired(entities: readonly EntityEntry[], hosting: Hosting): Diagnostic[] {
130
+ const refused: Diagnostic[] = [];
131
+
132
+ for (const entry of entities) {
133
+ for (const [field, declared] of Object.entries(entry.entityClass.getFields())) {
134
+ const role = Role.of(declared);
135
+ const target = role.isCollection() ? role.target?.name : undefined;
136
+ if (!target || !hosting.hostedHere(lowerFirst(target))) continue;
137
+
138
+ const held = referencesTo(lowerFirst(entry.name), hosting)
139
+ .some((one) => one.entity === lowerFirst(target));
140
+ if (held) continue;
141
+
142
+ refused.push({
143
+ severity: 'blocking',
144
+ code: 'many-without-reference',
145
+ filePath: entry.filePath,
146
+ subject: `${entry.name}.${field}`,
147
+ message: `${entry.entityClass.name}.${field} states many(${target}), and no field of `
148
+ + `${target} references ${entry.entityClass.name}: the relation is declared on neither `
149
+ + `side, so nothing can fill it. Declare the ref() on ${target}.`,
150
+ });
151
+ }
152
+ }
153
+
154
+ return refused;
155
+ }
156
+
125
157
  /** The checks nothing in this process can answer — asked once every frond has registered. */
126
158
  export function unheldAmong(checks: readonly RelationCheck[], hosting: Hosting): string[] {
127
159
  return checks
@@ -139,7 +171,7 @@ export function unheldAmong(checks: readonly RelationCheck[], hosting: Hosting):
139
171
  export function releasing(hosting: Hosting): Releasing {
140
172
  return {
141
173
  dependentsOf: (entity) => dependentsOf(entity, hosting),
142
- rowsOf: (entity) => hosting.storageOf(entity) as Rows | undefined,
174
+ rowsOf: (entity) => hosting.storageOf(entity),
143
175
  schemaOf: (entity) => hosting.entities().get(entity),
144
176
  peers: () => hosting.peers(),
145
177
  journal: (entity) => hosting.journal(entity),
@@ -54,6 +54,7 @@ export function createRemoteRouter(
54
54
  transports.set(url, transport);
55
55
  try {
56
56
  const answer = await transport({ entity: RPC_ENTITY, op: 'discover' }, Invocation.empty);
57
+
57
58
  // Judged below and not here: this catch means "unreachable, retry", and a
58
59
  // refusal thrown inside it would be swallowed into another silent retry.
59
60
  return { label, url, transport, answer };
package/src/boot/seed.ts CHANGED
@@ -47,7 +47,7 @@ function whatEachWaitsFor(
47
47
  for (const entity of frond.entities) {
48
48
  const targets = new Set<string>();
49
49
  for (const field of Object.values(entity.entityClass.getFields())) {
50
- if (!Role.of(field).isReference) continue;
50
+ if (!Role.of(field).isReference()) continue;
51
51
  const target = (Role.of(field).target as { name?: string }).name?.toLowerCase();
52
52
  if (target && target !== entity.name.toLowerCase()) targets.add(target);
53
53
  }
@@ -67,7 +67,7 @@ function whatEachWaitsFor(
67
67
  /** Where a seed writes, and what it may skip — resolved per entity. */
68
68
  interface SeedFacade {
69
69
  list(): Promise<unknown[]>;
70
- write(item: unknown): Promise<unknown>;
70
+ write(item: Record<string, unknown>): Promise<unknown>;
71
71
  }
72
72
 
73
73
  /**
@@ -126,9 +126,7 @@ function facadeFor(app: App, entityName: string): SeedFacade | undefined {
126
126
  };
127
127
  }
128
128
 
129
- const storage = app.storageFor(entityName) as
130
- | { list: () => Promise<unknown[]>; create: (input: unknown) => Promise<unknown> }
131
- | undefined;
129
+ const storage = app.storageFor(entityName);
132
130
  if (!storage) return undefined;
133
131
 
134
132
  return { list: () => storage.list(), write: (item) => storage.create(item) };
@@ -41,3 +41,28 @@ export function statementDrift(frond: FrondDescriptor, handler: HandlerEntry): D
41
41
 
42
42
  return found;
43
43
  }
44
+
45
+ /**
46
+ * A contract with no method under its name. The scan cannot produce one; a declaration can, and
47
+ * its first call used to fail as `instance[method] is not a function` — a server error for what
48
+ * is a typo in the declaration.
49
+ */
50
+ export function unansweredOperations(
51
+ frond: FrondDescriptor,
52
+ handler: HandlerEntry,
53
+ names: Iterable<string>,
54
+ ): Diagnostic[] {
55
+ const prototype = handler.ctor.prototype as Record<string, unknown>;
56
+
57
+ return [...names]
58
+ .filter((name) => typeof prototype[name] !== 'function')
59
+ .map((name) => ({
60
+ severity: 'blocking',
61
+ code: 'operation-without-method',
62
+ filePath: handler.filePath,
63
+ frond: frond.name,
64
+ subject: `${handler.ctor.name}.${name}`,
65
+ message: `${handler.ctor.name}.${name} is declared as an operation, and ${handler.ctor.name} `
66
+ + 'has no method of that name — its first call would fail as a server error.',
67
+ }));
68
+ }
@@ -154,13 +154,16 @@ async function inScope<R>(
154
154
  const storages = members.entities.map((member) => {
155
155
  const storage = wrap(factory(member.schema, member.name), member.name, member.schema);
156
156
  scope.registerValue(storageKeyOf(member.name), storage);
157
+
157
158
  return storage;
158
159
  });
159
160
  // Providers after every storage is in place: one may depend on another member's.
160
161
  const built = members.providers.map((provider) => {
161
162
  scope.register(provider.ctor.name, provider.ctor, { deps: provider.deps });
163
+
162
164
  return scope.resolve(provider.ctor.name);
163
165
  });
166
+
164
167
  return await fn(storages, built);
165
168
  } finally {
166
169
  await scope.dispose();
@@ -24,6 +24,7 @@ export class Carry {
24
24
  try {
25
25
  this.take(record);
26
26
  } catch { /* announcing never breaks logging, for the same reason a sink does not */ }
27
+
27
28
  return;
28
29
  }
29
30
  this.held.push(record);
@@ -12,6 +12,7 @@ let threshold: number = LEVELS[envLevel() ?? 'info'];
12
12
  /** The level the PROCESS was started with. */
13
13
  export function envLevel(): LogLevel | undefined {
14
14
  const raw = typeof process === 'undefined' ? undefined : process.env.FOUGERE_LOG_LEVEL;
15
+
15
16
  return raw !== undefined && raw in LEVELS ? (raw as LogLevel) : undefined;
16
17
  }
17
18
 
@@ -51,8 +52,10 @@ function supportsColor(): boolean {
51
52
  if (typeof process !== 'undefined') {
52
53
  if (process.env.NO_COLOR) return false;
53
54
  if (process.env.FORCE_COLOR === '1') return true;
55
+
54
56
  return process.stdout?.isTTY === true;
55
57
  }
58
+
56
59
  return false;
57
60
  }
58
61
 
@@ -62,6 +65,7 @@ function stamp(at: number | Date): string {
62
65
  const m = String(d.getMinutes()).padStart(2, '0');
63
66
  const s = String(d.getSeconds()).padStart(2, '0');
64
67
  const ms = String(d.getMilliseconds()).padStart(3, '0');
68
+
65
69
  return `${h}:${m}:${s}.${ms}`;
66
70
  }
67
71
 
@@ -92,7 +96,7 @@ export class Logger {
92
96
  level: level as LogRecord['level'], name: this.name, message: msg, args, at: Date.now(),
93
97
  };
94
98
 
95
- this.carry?.push(record);
99
+ this.carry?.push({ ...record, args: args.map(objectOf) });
96
100
 
97
101
  // The console ALWAYS, whoever else took the line. Skipping it once a destination
98
102
  // existed made `calls()` — a devtools ring that prints nothing — silence the
@@ -103,6 +107,10 @@ export class Logger {
103
107
  }
104
108
  }
105
109
 
110
+ /** A line travels as a fact, where `args` states `json()` — an object. */
111
+ const objectOf = (arg: unknown): object =>
112
+ typeof arg === 'object' && arg !== null && !Array.isArray(arg) ? arg : { value: arg };
113
+
106
114
  export function formatted(
107
115
  record: Rendered,
108
116
  color = supportsColor(),